Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4d213231c | ||
|
|
7ebbaa2784 | ||
|
|
553c1ef316 | ||
|
|
f75d36e1ff | ||
|
|
a86ded9ea6 | ||
|
|
c8174ab3f2 | ||
|
|
01d607d766 | ||
|
|
fba478dcca | ||
|
|
c4852bd488 | ||
|
|
64f14814bd | ||
|
|
7f54923acf | ||
|
|
0f1446b5d7 | ||
|
|
c0fa3ccfd9 | ||
|
|
ed5784d1f7 | ||
|
|
a4a8c2de58 | ||
|
|
0a7eb97d14 | ||
|
|
efd7462e35 | ||
|
|
345149dd6b | ||
|
|
95a9c04fa6 | ||
|
|
a7938a3b80 | ||
|
|
738f76c486 | ||
|
|
9f65cd447f | ||
|
|
eae5654f01 | ||
|
|
71927bedef |
No files matched your search
@@ -39,7 +39,7 @@ record_router = APIRouter(prefix="/vehicle-recharge-record", tags=["运输计划
|
|||||||
RECHARGE_TYPE_LABELS = {"etc": "ETC", "fuel": "油卡", "shanxia": "山峡", "bailian": "百链"}
|
RECHARGE_TYPE_LABELS = {"etc": "ETC", "fuel": "油卡", "shanxia": "山峡", "bailian": "百链"}
|
||||||
RECHARGE_APPLY_STATUS_LABELS = {
|
RECHARGE_APPLY_STATUS_LABELS = {
|
||||||
"draft": "草稿",
|
"draft": "草稿",
|
||||||
"pending": "待发放",
|
"pending": "发放中",
|
||||||
"in_approval": "审批中",
|
"in_approval": "审批中",
|
||||||
"rejected": "已驳回",
|
"rejected": "已驳回",
|
||||||
"disbursed": "已发放",
|
"disbursed": "已发放",
|
||||||
@@ -113,6 +113,7 @@ def _apply_list_kwargs(params: dict) -> dict:
|
|||||||
license_plate=_param_get(params, "licensePlate", "license_plate"),
|
license_plate=_param_get(params, "licensePlate", "license_plate"),
|
||||||
recharge_type=_param_get(params, "rechargeType", "recharge_type"),
|
recharge_type=_param_get(params, "rechargeType", "recharge_type"),
|
||||||
recharge_type_in=_param_get(params, "rechargeTypeIn", "recharge_type_in"),
|
recharge_type_in=_param_get(params, "rechargeTypeIn", "recharge_type_in"),
|
||||||
|
recharge_type_ne=_param_get(params, "rechargeTypeNe", "recharge_type_ne"),
|
||||||
status=_param_get(params, "status"),
|
status=_param_get(params, "status"),
|
||||||
status_in=_param_get(params, "statusIn", "status_in"),
|
status_in=_param_get(params, "statusIn", "status_in"),
|
||||||
apply_date_from=_param_get(params, "applyDateFrom", "apply_date_from"),
|
apply_date_from=_param_get(params, "applyDateFrom", "apply_date_from"),
|
||||||
@@ -139,6 +140,7 @@ def _disburse_list_kwargs(params: dict) -> dict:
|
|||||||
license_plate=_param_get(params, "licensePlate", "license_plate"),
|
license_plate=_param_get(params, "licensePlate", "license_plate"),
|
||||||
recharge_type=_param_get(params, "rechargeType", "recharge_type"),
|
recharge_type=_param_get(params, "rechargeType", "recharge_type"),
|
||||||
recharge_type_in=_param_get(params, "rechargeTypeIn", "recharge_type_in"),
|
recharge_type_in=_param_get(params, "rechargeTypeIn", "recharge_type_in"),
|
||||||
|
recharge_type_ne=_param_get(params, "rechargeTypeNe", "recharge_type_ne"),
|
||||||
apply_status=_param_get(params, "applyStatus", "apply_status"),
|
apply_status=_param_get(params, "applyStatus", "apply_status"),
|
||||||
apply_status_in=_param_get(params, "applyStatusIn", "apply_status_in"),
|
apply_status_in=_param_get(params, "applyStatusIn", "apply_status_in"),
|
||||||
apply_date_from=_param_get(params, "applyDateFrom", "apply_date_from"),
|
apply_date_from=_param_get(params, "applyDateFrom", "apply_date_from"),
|
||||||
@@ -170,6 +172,7 @@ def list_applies(
|
|||||||
request: Request,
|
request: Request,
|
||||||
q: Optional[str] = Query(default=None),
|
q: Optional[str] = Query(default=None),
|
||||||
recharge_type: Optional[str] = Query(default=None, alias="rechargeType"),
|
recharge_type: Optional[str] = Query(default=None, alias="rechargeType"),
|
||||||
|
recharge_type_ne: Optional[str] = Query(default=None, alias="rechargeTypeNe"),
|
||||||
status: Optional[str] = Query(default=None),
|
status: Optional[str] = Query(default=None),
|
||||||
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
|
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
|
||||||
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
|
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
|
||||||
@@ -185,6 +188,7 @@ def list_applies(
|
|||||||
request,
|
request,
|
||||||
q=q,
|
q=q,
|
||||||
rechargeType=recharge_type,
|
rechargeType=recharge_type,
|
||||||
|
rechargeTypeNe=recharge_type_ne,
|
||||||
status=status,
|
status=status,
|
||||||
applyDateFrom=apply_date_from,
|
applyDateFrom=apply_date_from,
|
||||||
applyDateTo=apply_date_to,
|
applyDateTo=apply_date_to,
|
||||||
@@ -233,6 +237,7 @@ RECHARGE_APPLY_EXPORT_COLUMNS = [
|
|||||||
class RechargeApplyExportBody(BaseModel):
|
class RechargeApplyExportBody(BaseModel):
|
||||||
q: Optional[str] = None
|
q: Optional[str] = None
|
||||||
recharge_type: Optional[str] = Field(default=None, alias="rechargeType")
|
recharge_type: Optional[str] = Field(default=None, alias="rechargeType")
|
||||||
|
recharge_type_ne: Optional[str] = Field(default=None, alias="rechargeTypeNe")
|
||||||
status: Optional[str] = None
|
status: Optional[str] = None
|
||||||
apply_date_from: Optional[date] = Field(default=None, alias="applyDateFrom")
|
apply_date_from: Optional[date] = Field(default=None, alias="applyDateFrom")
|
||||||
apply_date_to: Optional[date] = Field(default=None, alias="applyDateTo")
|
apply_date_to: Optional[date] = Field(default=None, alias="applyDateTo")
|
||||||
@@ -422,6 +427,7 @@ def delete_applies(
|
|||||||
def list_records(
|
def list_records(
|
||||||
q: Optional[str] = Query(default=None),
|
q: Optional[str] = Query(default=None),
|
||||||
recharge_type: Optional[str] = Query(default=None, alias="rechargeType"),
|
recharge_type: Optional[str] = Query(default=None, alias="rechargeType"),
|
||||||
|
recharge_type_ne: Optional[str] = Query(default=None, alias="rechargeTypeNe"),
|
||||||
recharge_date_from: Optional[date] = Query(default=None, alias="rechargeDateFrom"),
|
recharge_date_from: Optional[date] = Query(default=None, alias="rechargeDateFrom"),
|
||||||
recharge_date_to: Optional[date] = Query(default=None, alias="rechargeDateTo"),
|
recharge_date_to: Optional[date] = Query(default=None, alias="rechargeDateTo"),
|
||||||
gas_station: Optional[str] = Query(default=None, alias="gasStation"),
|
gas_station: Optional[str] = Query(default=None, alias="gasStation"),
|
||||||
@@ -438,6 +444,7 @@ def list_records(
|
|||||||
user,
|
user,
|
||||||
q=q,
|
q=q,
|
||||||
recharge_type=recharge_type,
|
recharge_type=recharge_type,
|
||||||
|
recharge_type_ne=recharge_type_ne,
|
||||||
recharge_date_from=recharge_date_from,
|
recharge_date_from=recharge_date_from,
|
||||||
recharge_date_to=recharge_date_to,
|
recharge_date_to=recharge_date_to,
|
||||||
gas_station=gas_station,
|
gas_station=gas_station,
|
||||||
@@ -455,6 +462,7 @@ def list_disburse_board(
|
|||||||
request: Request,
|
request: Request,
|
||||||
q: Optional[str] = Query(default=None),
|
q: Optional[str] = Query(default=None),
|
||||||
recharge_type: Optional[str] = Query(default=None, alias="rechargeType"),
|
recharge_type: Optional[str] = Query(default=None, alias="rechargeType"),
|
||||||
|
recharge_type_ne: Optional[str] = Query(default=None, alias="rechargeTypeNe"),
|
||||||
apply_status: Optional[str] = Query(default=None, alias="applyStatus"),
|
apply_status: Optional[str] = Query(default=None, alias="applyStatus"),
|
||||||
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
|
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
|
||||||
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
|
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
|
||||||
@@ -470,6 +478,7 @@ def list_disburse_board(
|
|||||||
request,
|
request,
|
||||||
q=q,
|
q=q,
|
||||||
rechargeType=recharge_type,
|
rechargeType=recharge_type,
|
||||||
|
rechargeTypeNe=recharge_type_ne,
|
||||||
applyStatus=apply_status,
|
applyStatus=apply_status,
|
||||||
applyDateFrom=apply_date_from,
|
applyDateFrom=apply_date_from,
|
||||||
applyDateTo=apply_date_to,
|
applyDateTo=apply_date_to,
|
||||||
@@ -513,6 +522,7 @@ RECHARGE_DISBURSE_EXPORT_COLUMNS = [
|
|||||||
class RechargeDisburseExportBody(BaseModel):
|
class RechargeDisburseExportBody(BaseModel):
|
||||||
q: Optional[str] = None
|
q: Optional[str] = None
|
||||||
recharge_type: Optional[str] = Field(default=None, alias="rechargeType")
|
recharge_type: Optional[str] = Field(default=None, alias="rechargeType")
|
||||||
|
recharge_type_ne: Optional[str] = Field(default=None, alias="rechargeTypeNe")
|
||||||
apply_status: Optional[str] = Field(default=None, alias="applyStatus")
|
apply_status: Optional[str] = Field(default=None, alias="applyStatus")
|
||||||
apply_date_from: Optional[date] = Field(default=None, alias="applyDateFrom")
|
apply_date_from: Optional[date] = Field(default=None, alias="applyDateFrom")
|
||||||
apply_date_to: Optional[date] = Field(default=None, alias="applyDateTo")
|
apply_date_to: Optional[date] = Field(default=None, alias="applyDateTo")
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ def _approval_list_kwargs(params: dict) -> dict:
|
|||||||
license_plate=_param_get(params, "licensePlate", "license_plate"),
|
license_plate=_param_get(params, "licensePlate", "license_plate"),
|
||||||
recharge_type=_param_get(params, "rechargeType", "recharge_type"),
|
recharge_type=_param_get(params, "rechargeType", "recharge_type"),
|
||||||
recharge_type_in=_param_get(params, "rechargeTypeIn", "recharge_type_in"),
|
recharge_type_in=_param_get(params, "rechargeTypeIn", "recharge_type_in"),
|
||||||
|
recharge_type_ne=_param_get(params, "rechargeTypeNe", "recharge_type_ne"),
|
||||||
status=_param_get(params, "status"),
|
status=_param_get(params, "status"),
|
||||||
status_in=_param_get(params, "statusIn", "status_in"),
|
status_in=_param_get(params, "statusIn", "status_in"),
|
||||||
apply_date_from=_param_get(params, "applyDateFrom", "apply_date_from"),
|
apply_date_from=_param_get(params, "applyDateFrom", "apply_date_from"),
|
||||||
@@ -61,6 +62,7 @@ def _approval_list_kwargs(params: dict) -> dict:
|
|||||||
@approval_router.get("/pending", response_model=VehicleRechargeApplyListResponse)
|
@approval_router.get("/pending", response_model=VehicleRechargeApplyListResponse)
|
||||||
def list_pending(
|
def list_pending(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
recharge_type_ne: Optional[str] = Query(default=None, alias="rechargeTypeNe"),
|
||||||
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
|
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
|
||||||
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
|
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
|
||||||
page: int = Query(default=1, ge=1),
|
page: int = Query(default=1, ge=1),
|
||||||
@@ -71,6 +73,7 @@ def list_pending(
|
|||||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||||
params = _merge_query(
|
params = _merge_query(
|
||||||
request,
|
request,
|
||||||
|
rechargeTypeNe=recharge_type_ne,
|
||||||
applyDateFrom=apply_date_from,
|
applyDateFrom=apply_date_from,
|
||||||
applyDateTo=apply_date_to,
|
applyDateTo=apply_date_to,
|
||||||
)
|
)
|
||||||
@@ -90,6 +93,7 @@ def list_pending(
|
|||||||
@approval_router.get("/done", response_model=VehicleRechargeApplyListResponse)
|
@approval_router.get("/done", response_model=VehicleRechargeApplyListResponse)
|
||||||
def list_done(
|
def list_done(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
recharge_type_ne: Optional[str] = Query(default=None, alias="rechargeTypeNe"),
|
||||||
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
|
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
|
||||||
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
|
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
|
||||||
page: int = Query(default=1, ge=1),
|
page: int = Query(default=1, ge=1),
|
||||||
@@ -100,6 +104,7 @@ def list_done(
|
|||||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||||
params = _merge_query(
|
params = _merge_query(
|
||||||
request,
|
request,
|
||||||
|
rechargeTypeNe=recharge_type_ne,
|
||||||
applyDateFrom=apply_date_from,
|
applyDateFrom=apply_date_from,
|
||||||
applyDateTo=apply_date_to,
|
applyDateTo=apply_date_to,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import List, Optional
|
|||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
RECHARGE_TYPES = {"etc", "fuel", "shanxia", "bailian"}
|
RECHARGE_TYPES = {"etc", "fuel", "shanxia", "bailian"}
|
||||||
APPLY_STATUSES = {"draft", "in_approval", "rejected", "disbursed", "cancelled"}
|
APPLY_STATUSES = {"draft", "in_approval", "pending", "rejected", "disbursed", "cancelled"}
|
||||||
NODE_ACTION_TYPES = {"approve", "disburse"}
|
NODE_ACTION_TYPES = {"approve", "disburse"}
|
||||||
APPROVAL_STATUSES = {"approved", "rejected", "disbursed"}
|
APPROVAL_STATUSES = {"approved", "rejected", "disbursed"}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ def _split_csv(raw: Optional[str]) -> List[str]:
|
|||||||
return [s.strip() for s in str(raw).split(",") if s.strip()]
|
return [s.strip() for s in str(raw).split(",") if s.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def _append_ne_filter(filters: list, col, raw: Optional[str]) -> None:
|
||||||
|
value = (raw or "").strip()
|
||||||
|
if value:
|
||||||
|
filters.append(col != value)
|
||||||
|
|
||||||
|
|
||||||
def _append_enum_filter(
|
def _append_enum_filter(
|
||||||
filters: list,
|
filters: list,
|
||||||
col,
|
col,
|
||||||
@@ -148,6 +154,7 @@ def build_recharge_apply_column_filters(
|
|||||||
license_plate: Optional[str] = None,
|
license_plate: Optional[str] = None,
|
||||||
recharge_type: Optional[str] = None,
|
recharge_type: Optional[str] = None,
|
||||||
recharge_type_in: Optional[str] = None,
|
recharge_type_in: Optional[str] = None,
|
||||||
|
recharge_type_ne: Optional[str] = None,
|
||||||
status: Optional[str] = None,
|
status: Optional[str] = None,
|
||||||
status_in: Optional[str] = None,
|
status_in: Optional[str] = None,
|
||||||
apply_date_from: DateLike = None,
|
apply_date_from: DateLike = None,
|
||||||
@@ -175,6 +182,7 @@ def build_recharge_apply_column_filters(
|
|||||||
single=recharge_type,
|
single=recharge_type,
|
||||||
multi=recharge_type_in,
|
multi=recharge_type_in,
|
||||||
)
|
)
|
||||||
|
_append_ne_filter(filters, PlVehicleRechargeApply.recharge_type, recharge_type_ne)
|
||||||
_append_enum_filter(
|
_append_enum_filter(
|
||||||
filters, PlVehicleRechargeApply.status, single=status, multi=status_in
|
filters, PlVehicleRechargeApply.status, single=status, multi=status_in
|
||||||
)
|
)
|
||||||
@@ -223,6 +231,7 @@ def build_recharge_disburse_board_column_filters(
|
|||||||
license_plate: Optional[str] = None,
|
license_plate: Optional[str] = None,
|
||||||
recharge_type: Optional[str] = None,
|
recharge_type: Optional[str] = None,
|
||||||
recharge_type_in: Optional[str] = None,
|
recharge_type_in: Optional[str] = None,
|
||||||
|
recharge_type_ne: Optional[str] = None,
|
||||||
apply_status: Optional[str] = None,
|
apply_status: Optional[str] = None,
|
||||||
apply_status_in: Optional[str] = None,
|
apply_status_in: Optional[str] = None,
|
||||||
apply_date_from: DateLike = None,
|
apply_date_from: DateLike = None,
|
||||||
@@ -256,6 +265,7 @@ def build_recharge_disburse_board_column_filters(
|
|||||||
single=recharge_type,
|
single=recharge_type,
|
||||||
multi=recharge_type_in,
|
multi=recharge_type_in,
|
||||||
)
|
)
|
||||||
|
_append_ne_filter(filters, PlVehicleRechargeApply.recharge_type, recharge_type_ne)
|
||||||
_append_enum_filter(
|
_append_enum_filter(
|
||||||
filters,
|
filters,
|
||||||
PlVehicleRechargeApply.status,
|
PlVehicleRechargeApply.status,
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ def count_recharge_approval_pending(db: Session, tenant_no: str, user_id: str) -
|
|||||||
.filter(
|
.filter(
|
||||||
PlVehicleRechargeApply.tenant_no == tenant_no,
|
PlVehicleRechargeApply.tenant_no == tenant_no,
|
||||||
PlVehicleRechargeApply.del_flag == 0,
|
PlVehicleRechargeApply.del_flag == 0,
|
||||||
PlVehicleRechargeApply.status == "in_approval",
|
PlVehicleRechargeApply.status.in_(("in_approval", "pending")),
|
||||||
BxWorkflowNode.tenant_no == tenant_no,
|
BxWorkflowNode.tenant_no == tenant_no,
|
||||||
BxWorkflowNode.del_flag == 0,
|
BxWorkflowNode.del_flag == 0,
|
||||||
BxWorkflowNode.approver_id == user_id,
|
BxWorkflowNode.approver_id == user_id,
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ def _current_node(
|
|||||||
user: DbUserContext,
|
user: DbUserContext,
|
||||||
tenant_no: str,
|
tenant_no: str,
|
||||||
) -> BxWorkflowNode:
|
) -> BxWorkflowNode:
|
||||||
if apply.status != "in_approval" or not apply.current_node:
|
if apply.status not in ("in_approval", "pending") or not apply.current_node:
|
||||||
raise HTTPException(status_code=400, detail="申请单不在审批中")
|
raise HTTPException(status_code=400, detail="申请单不在可审批/发放状态")
|
||||||
node = (
|
node = (
|
||||||
db.query(BxWorkflowNode)
|
db.query(BxWorkflowNode)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -119,6 +119,7 @@ def list_pending(
|
|||||||
license_plate: Optional[str] = None,
|
license_plate: Optional[str] = None,
|
||||||
recharge_type: Optional[str] = None,
|
recharge_type: Optional[str] = None,
|
||||||
recharge_type_in: Optional[str] = None,
|
recharge_type_in: Optional[str] = None,
|
||||||
|
recharge_type_ne: Optional[str] = None,
|
||||||
status: Optional[str] = None,
|
status: Optional[str] = None,
|
||||||
status_in: Optional[str] = None,
|
status_in: Optional[str] = None,
|
||||||
apply_date_from=None,
|
apply_date_from=None,
|
||||||
@@ -144,7 +145,7 @@ def list_pending(
|
|||||||
.filter(
|
.filter(
|
||||||
PlVehicleRechargeApply.tenant_no == tenant_no,
|
PlVehicleRechargeApply.tenant_no == tenant_no,
|
||||||
PlVehicleRechargeApply.del_flag == 0,
|
PlVehicleRechargeApply.del_flag == 0,
|
||||||
PlVehicleRechargeApply.status == "in_approval",
|
PlVehicleRechargeApply.status.in_(("in_approval", "pending")),
|
||||||
BxWorkflowNode.tenant_no == tenant_no,
|
BxWorkflowNode.tenant_no == tenant_no,
|
||||||
BxWorkflowNode.del_flag == 0,
|
BxWorkflowNode.del_flag == 0,
|
||||||
BxWorkflowNode.approver_id == user.id,
|
BxWorkflowNode.approver_id == user.id,
|
||||||
@@ -155,6 +156,7 @@ def list_pending(
|
|||||||
license_plate=license_plate,
|
license_plate=license_plate,
|
||||||
recharge_type=recharge_type,
|
recharge_type=recharge_type,
|
||||||
recharge_type_in=recharge_type_in,
|
recharge_type_in=recharge_type_in,
|
||||||
|
recharge_type_ne=recharge_type_ne,
|
||||||
status=status,
|
status=status,
|
||||||
status_in=status_in,
|
status_in=status_in,
|
||||||
apply_date_from=apply_date_from,
|
apply_date_from=apply_date_from,
|
||||||
@@ -192,6 +194,7 @@ def list_done(
|
|||||||
license_plate: Optional[str] = None,
|
license_plate: Optional[str] = None,
|
||||||
recharge_type: Optional[str] = None,
|
recharge_type: Optional[str] = None,
|
||||||
recharge_type_in: Optional[str] = None,
|
recharge_type_in: Optional[str] = None,
|
||||||
|
recharge_type_ne: Optional[str] = None,
|
||||||
status: Optional[str] = None,
|
status: Optional[str] = None,
|
||||||
status_in: Optional[str] = None,
|
status_in: Optional[str] = None,
|
||||||
apply_date_from=None,
|
apply_date_from=None,
|
||||||
@@ -233,6 +236,7 @@ def list_done(
|
|||||||
license_plate=license_plate,
|
license_plate=license_plate,
|
||||||
recharge_type=recharge_type,
|
recharge_type=recharge_type,
|
||||||
recharge_type_in=recharge_type_in,
|
recharge_type_in=recharge_type_in,
|
||||||
|
recharge_type_ne=recharge_type_ne,
|
||||||
status=status,
|
status=status,
|
||||||
status_in=status_in,
|
status_in=status_in,
|
||||||
apply_date_from=apply_date_from,
|
apply_date_from=apply_date_from,
|
||||||
@@ -290,7 +294,11 @@ def approve_apply(
|
|||||||
opinion=opinion,
|
opinion=opinion,
|
||||||
now=now,
|
now=now,
|
||||||
)
|
)
|
||||||
apply.current_node = nodes[idx + 1].id
|
next_node = nodes[idx + 1]
|
||||||
|
apply.current_node = next_node.id
|
||||||
|
next_action = (next_node.action_type or "approve").strip() or "approve"
|
||||||
|
if next_action == "disburse":
|
||||||
|
apply.status = "pending"
|
||||||
apply.update_by = user.id
|
apply.update_by = user.id
|
||||||
apply.update_time = now
|
apply.update_time = now
|
||||||
sync_approval_notifications(
|
sync_approval_notifications(
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ def _resolve_apply_for_record(
|
|||||||
detail="审批中的申请请在「ETC/油卡充值审批」中完成发放登记",
|
detail="审批中的申请请在「ETC/油卡充值审批」中完成发放登记",
|
||||||
)
|
)
|
||||||
if apply.status != "pending":
|
if apply.status != "pending":
|
||||||
raise HTTPException(status_code=400, detail="仅待发放申请可关联")
|
raise HTTPException(status_code=400, detail="仅发放中申请可关联")
|
||||||
existing = (
|
existing = (
|
||||||
db.query(PlVehicleRechargeRecord)
|
db.query(PlVehicleRechargeRecord)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -513,6 +513,7 @@ def list_applies(
|
|||||||
license_plate: Optional[str] = None,
|
license_plate: Optional[str] = None,
|
||||||
recharge_type: Optional[str] = None,
|
recharge_type: Optional[str] = None,
|
||||||
recharge_type_in: Optional[str] = None,
|
recharge_type_in: Optional[str] = None,
|
||||||
|
recharge_type_ne: Optional[str] = None,
|
||||||
status: Optional[str] = None,
|
status: Optional[str] = None,
|
||||||
status_in: Optional[str] = None,
|
status_in: Optional[str] = None,
|
||||||
apply_date_from: Optional[date] = None,
|
apply_date_from: Optional[date] = None,
|
||||||
@@ -555,6 +556,7 @@ def list_applies(
|
|||||||
license_plate=license_plate,
|
license_plate=license_plate,
|
||||||
recharge_type=recharge_type,
|
recharge_type=recharge_type,
|
||||||
recharge_type_in=recharge_type_in,
|
recharge_type_in=recharge_type_in,
|
||||||
|
recharge_type_ne=recharge_type_ne,
|
||||||
status=status,
|
status=status,
|
||||||
status_in=status_in,
|
status_in=status_in,
|
||||||
apply_date_from=apply_date_from,
|
apply_date_from=apply_date_from,
|
||||||
@@ -854,6 +856,7 @@ def list_records(
|
|||||||
*,
|
*,
|
||||||
q: Optional[str] = None,
|
q: Optional[str] = None,
|
||||||
recharge_type: Optional[str] = None,
|
recharge_type: Optional[str] = None,
|
||||||
|
recharge_type_ne: Optional[str] = None,
|
||||||
recharge_date_from: Optional[date] = None,
|
recharge_date_from: Optional[date] = None,
|
||||||
recharge_date_to: Optional[date] = None,
|
recharge_date_to: Optional[date] = None,
|
||||||
gas_station: Optional[str] = None,
|
gas_station: Optional[str] = None,
|
||||||
@@ -875,6 +878,10 @@ def list_records(
|
|||||||
)
|
)
|
||||||
if recharge_type:
|
if recharge_type:
|
||||||
query = query.filter(PlVehicleRechargeRecord.recharge_type == recharge_type)
|
query = query.filter(PlVehicleRechargeRecord.recharge_type == recharge_type)
|
||||||
|
if recharge_type_ne and str(recharge_type_ne).strip():
|
||||||
|
query = query.filter(
|
||||||
|
PlVehicleRechargeRecord.recharge_type != str(recharge_type_ne).strip()
|
||||||
|
)
|
||||||
if recharge_date_from:
|
if recharge_date_from:
|
||||||
query = query.filter(PlVehicleRechargeRecord.recharge_date >= recharge_date_from)
|
query = query.filter(PlVehicleRechargeRecord.recharge_date >= recharge_date_from)
|
||||||
if recharge_date_to:
|
if recharge_date_to:
|
||||||
@@ -937,6 +944,7 @@ def list_disburse_board(
|
|||||||
license_plate: Optional[str] = None,
|
license_plate: Optional[str] = None,
|
||||||
recharge_type: Optional[str] = None,
|
recharge_type: Optional[str] = None,
|
||||||
recharge_type_in: Optional[str] = None,
|
recharge_type_in: Optional[str] = None,
|
||||||
|
recharge_type_ne: Optional[str] = None,
|
||||||
apply_status: Optional[str] = None,
|
apply_status: Optional[str] = None,
|
||||||
apply_status_in: Optional[str] = None,
|
apply_status_in: Optional[str] = None,
|
||||||
apply_date_from: Optional[date] = None,
|
apply_date_from: Optional[date] = None,
|
||||||
@@ -997,6 +1005,7 @@ def list_disburse_board(
|
|||||||
license_plate=license_plate,
|
license_plate=license_plate,
|
||||||
recharge_type=recharge_type,
|
recharge_type=recharge_type,
|
||||||
recharge_type_in=recharge_type_in,
|
recharge_type_in=recharge_type_in,
|
||||||
|
recharge_type_ne=recharge_type_ne,
|
||||||
apply_status=apply_status,
|
apply_status=apply_status,
|
||||||
apply_status_in=apply_status_in,
|
apply_status_in=apply_status_in,
|
||||||
apply_date_from=apply_date_from,
|
apply_date_from=apply_date_from,
|
||||||
@@ -1124,7 +1133,7 @@ def update_record(
|
|||||||
),
|
),
|
||||||
nodes[-1] if nodes else None,
|
nodes[-1] if nodes else None,
|
||||||
)
|
)
|
||||||
old_apply.status = "in_approval"
|
old_apply.status = "pending"
|
||||||
old_apply.current_node = disburse.id if disburse else None
|
old_apply.current_node = disburse.id if disburse else None
|
||||||
else:
|
else:
|
||||||
old_apply.status = "pending"
|
old_apply.status = "pending"
|
||||||
@@ -1193,7 +1202,7 @@ def delete_records(
|
|||||||
),
|
),
|
||||||
nodes[-1] if nodes else None,
|
nodes[-1] if nodes else None,
|
||||||
)
|
)
|
||||||
apply.status = "in_approval"
|
apply.status = "pending"
|
||||||
apply.current_node = disburse.id if disburse else None
|
apply.current_node = disburse.id if disburse else None
|
||||||
else:
|
else:
|
||||||
apply.status = "pending"
|
apply.status = "pending"
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ def test_recharge_approval_badge_counts_current_approver_pending(monkeypatch):
|
|||||||
add_apply("a3", status="in_approval", current_node="node-other")
|
add_apply("a3", status="in_approval", current_node="node-other")
|
||||||
add_apply("a4", status="rejected", current_node="node-me")
|
add_apply("a4", status="rejected", current_node="node-me")
|
||||||
add_apply("a5", status="draft", current_node=None)
|
add_apply("a5", status="draft", current_node=None)
|
||||||
|
add_apply("a6", status="pending", current_node="node-me")
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
@@ -89,4 +90,4 @@ def test_recharge_approval_badge_counts_current_approver_pending(monkeypatch):
|
|||||||
user_id="u-me",
|
user_id="u-me",
|
||||||
menu_codes=[MENU_CODE_RECHARGE_APPROVAL],
|
menu_codes=[MENU_CODE_RECHARGE_APPROVAL],
|
||||||
)
|
)
|
||||||
assert counts[MENU_CODE_RECHARGE_APPROVAL] == 2
|
assert counts[MENU_CODE_RECHARGE_APPROVAL] == 3
|
||||||
@@ -119,6 +119,20 @@ def test_maintenance_process_board_filters():
|
|||||||
assert "handler_id" in joined
|
assert "handler_id" in joined
|
||||||
|
|
||||||
|
|
||||||
|
def test_recharge_apply_type_ne_filter():
|
||||||
|
filters = build_recharge_apply_column_filters(recharge_type_ne="etc")
|
||||||
|
assert len(filters) == 1
|
||||||
|
sql = _compile(filters[0])
|
||||||
|
assert "recharge_type" in sql
|
||||||
|
assert "!=" in sql or "<>" in sql
|
||||||
|
assert "etc" in sql
|
||||||
|
|
||||||
|
|
||||||
|
def test_recharge_disburse_board_type_ne_filter():
|
||||||
|
filters = build_recharge_disburse_board_column_filters(recharge_type_ne="etc")
|
||||||
|
assert any("recharge_type" in _compile(f) for f in filters)
|
||||||
|
|
||||||
|
|
||||||
def test_empty():
|
def test_empty():
|
||||||
assert build_recharge_apply_column_filters() == []
|
assert build_recharge_apply_column_filters() == []
|
||||||
assert build_recharge_disburse_board_column_filters() == []
|
assert build_recharge_disburse_board_column_filters() == []
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from app.schemas.pl.vehicle_recharge import APPLY_STATUSES, NODE_ACTION_TYPES, R
|
|||||||
|
|
||||||
|
|
||||||
def test_apply_statuses_include_workflow_states():
|
def test_apply_statuses_include_workflow_states():
|
||||||
assert {"draft", "in_approval", "rejected", "disbursed", "cancelled"} <= APPLY_STATUSES
|
assert {"draft", "in_approval", "pending", "rejected", "disbursed", "cancelled"} <= APPLY_STATUSES
|
||||||
assert {"approve", "disburse"} <= NODE_ACTION_TYPES
|
assert {"approve", "disburse"} <= NODE_ACTION_TYPES
|
||||||
assert {"etc", "fuel"} <= RECHARGE_TYPES
|
assert {"etc", "fuel"} <= RECHARGE_TYPES
|
||||||
|
|
||||||
@@ -98,6 +98,40 @@ def _seed_workflow(db) -> tuple[str, str, str]:
|
|||||||
return wf_id, node_gm, node_fin
|
return wf_id, node_gm, node_fin
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_three_node_workflow(db) -> tuple[str, str, str, str]:
|
||||||
|
wf_id = "wf-3"
|
||||||
|
n1, n2, n3 = "node-a1", "node-a2", "node-dis"
|
||||||
|
db.add(
|
||||||
|
BxWorkflowDef(
|
||||||
|
id=wf_id,
|
||||||
|
biz_type="vehicle_recharge",
|
||||||
|
wf_name="三级充值流程",
|
||||||
|
status=1,
|
||||||
|
tenant_no="t1",
|
||||||
|
create_dept_id="d1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for nid, name, order, uid, uname, action in (
|
||||||
|
(n1, "一审", 1, "u-a1", "一审", "approve"),
|
||||||
|
(n2, "二审", 2, "u-a2", "二审", "approve"),
|
||||||
|
(n3, "发放", 3, "u-fin", "财务", "disburse"),
|
||||||
|
):
|
||||||
|
db.add(
|
||||||
|
BxWorkflowNode(
|
||||||
|
id=nid,
|
||||||
|
wf_id=wf_id,
|
||||||
|
node_name=name,
|
||||||
|
node_order=order,
|
||||||
|
approver_id=uid,
|
||||||
|
approver_name=uname,
|
||||||
|
action_type=action,
|
||||||
|
tenant_no="t1",
|
||||||
|
create_dept_id="d1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return wf_id, n1, n2, n3
|
||||||
|
|
||||||
|
|
||||||
def _patch_scope(monkeypatch, *modules) -> None:
|
def _patch_scope(monkeypatch, *modules) -> None:
|
||||||
for mod in modules:
|
for mod in modules:
|
||||||
monkeypatch.setattr(mod, "apply_pl_scope", lambda query, *_a, **_k: query)
|
monkeypatch.setattr(mod, "apply_pl_scope", lambda query, *_a, **_k: query)
|
||||||
@@ -272,7 +306,7 @@ def test_approve_advances_to_next_node(monkeypatch):
|
|||||||
result = approval_svc.approve_apply(
|
result = approval_svc.approve_apply(
|
||||||
db, "t1", _user("u-gm", "总经理"), "apply-1", opinion="同意"
|
db, "t1", _user("u-gm", "总经理"), "apply-1", opinion="同意"
|
||||||
)
|
)
|
||||||
assert result.status == "in_approval"
|
assert result.status == "pending"
|
||||||
assert result.current_node == node_fin
|
assert result.current_node == node_fin
|
||||||
assert any(a.approval_status == "approved" for a in result.approvals)
|
assert any(a.approval_status == "approved" for a in result.approvals)
|
||||||
assert len(result.approval_flow) == 2
|
assert len(result.approval_flow) == 2
|
||||||
@@ -285,6 +319,79 @@ def test_approve_advances_to_next_node(monkeypatch):
|
|||||||
assert result.approval_history[0].approval_status == "approved"
|
assert result.approval_history[0].approval_status == "approved"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mid_approve_stays_in_approval_final_approve_sets_pending(monkeypatch):
|
||||||
|
from app.services import pl_vehicle_recharge_approval_service as approval_svc
|
||||||
|
from app.services import pl_vehicle_recharge_service as svc
|
||||||
|
|
||||||
|
db = _make_session()
|
||||||
|
_seed_vehicle(db)
|
||||||
|
wf_id, n1, n2, n3 = _seed_three_node_workflow(db)
|
||||||
|
db.add(
|
||||||
|
PlVehicleRechargeApply(
|
||||||
|
id="apply-1",
|
||||||
|
apply_no="YK2026092601",
|
||||||
|
license_plate="鄂A00001",
|
||||||
|
recharge_type="fuel",
|
||||||
|
apply_date=date(2026, 9, 26),
|
||||||
|
applicant_id="u-applicant",
|
||||||
|
status="in_approval",
|
||||||
|
wf_id=wf_id,
|
||||||
|
current_node=n1,
|
||||||
|
approval_round=1,
|
||||||
|
tenant_no="t1",
|
||||||
|
create_dept_id="d1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
_patch_scope(monkeypatch, svc)
|
||||||
|
|
||||||
|
mid = approval_svc.approve_apply(db, "t1", _user("u-a1", "一审"), "apply-1", opinion="ok")
|
||||||
|
assert mid.status == "in_approval"
|
||||||
|
assert mid.current_node == n2
|
||||||
|
|
||||||
|
final = approval_svc.approve_apply(db, "t1", _user("u-a2", "二审"), "apply-1", opinion="ok")
|
||||||
|
assert final.status == "pending"
|
||||||
|
assert final.current_node == n3
|
||||||
|
|
||||||
|
|
||||||
|
def test_disburse_accepts_pending_status(monkeypatch):
|
||||||
|
from app.services import pl_vehicle_recharge_approval_service as approval_svc
|
||||||
|
from app.services import pl_vehicle_recharge_service as svc
|
||||||
|
from app.schemas.pl.vehicle_recharge import VehicleRechargeRecordCreate
|
||||||
|
|
||||||
|
db = _make_session()
|
||||||
|
_seed_vehicle(db)
|
||||||
|
wf_id, _gm, node_fin = _seed_workflow(db)
|
||||||
|
db.add(
|
||||||
|
PlVehicleRechargeApply(
|
||||||
|
id="apply-1",
|
||||||
|
apply_no="ETC2026092601",
|
||||||
|
license_plate="鄂A00001",
|
||||||
|
recharge_type="etc",
|
||||||
|
apply_date=date(2026, 9, 26),
|
||||||
|
applicant_id="u-applicant",
|
||||||
|
status="pending",
|
||||||
|
wf_id=wf_id,
|
||||||
|
current_node=node_fin,
|
||||||
|
approval_round=1,
|
||||||
|
tenant_no="t1",
|
||||||
|
create_dept_id="d1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
_patch_scope(monkeypatch, svc)
|
||||||
|
payload = VehicleRechargeRecordCreate(
|
||||||
|
rechargeDate=date(2026, 9, 26),
|
||||||
|
rechargeType="etc",
|
||||||
|
amount=Decimal("50.00"),
|
||||||
|
applyId="apply-1",
|
||||||
|
)
|
||||||
|
result = approval_svc.disburse_apply(
|
||||||
|
db, "t1", _user("u-fin", "财务"), "apply-1", payload
|
||||||
|
)
|
||||||
|
assert result.status == "disbursed"
|
||||||
|
|
||||||
|
|
||||||
def test_reject_sets_rejected_and_appends_history(monkeypatch):
|
def test_reject_sets_rejected_and_appends_history(monkeypatch):
|
||||||
from app.services import pl_vehicle_recharge_approval_service as approval_svc
|
from app.services import pl_vehicle_recharge_approval_service as approval_svc
|
||||||
from app.services import pl_vehicle_recharge_service as svc
|
from app.services import pl_vehicle_recharge_service as svc
|
||||||
@@ -508,3 +615,49 @@ def test_withdraw_rejects_non_in_approval(monkeypatch):
|
|||||||
with pytest.raises(HTTPException) as ei:
|
with pytest.raises(HTTPException) as ei:
|
||||||
svc.withdraw_apply(db, "t1", _user("u-applicant"), "a-wd3")
|
svc.withdraw_apply(db, "t1", _user("u-applicant"), "a-wd3")
|
||||||
assert ei.value.status_code == 400
|
assert ei.value.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_record_with_workflow_reverts_to_pending(monkeypatch):
|
||||||
|
from app.services import pl_vehicle_recharge_service as svc
|
||||||
|
|
||||||
|
db = _make_session()
|
||||||
|
_seed_vehicle(db)
|
||||||
|
wf_id, _gm, node_fin = _seed_workflow(db)
|
||||||
|
db.add(
|
||||||
|
PlVehicleRechargeApply(
|
||||||
|
id="apply-1",
|
||||||
|
apply_no="ETC2026092601",
|
||||||
|
license_plate="鄂A00001",
|
||||||
|
recharge_type="etc",
|
||||||
|
apply_date=date(2026, 9, 26),
|
||||||
|
applicant_id="u-applicant",
|
||||||
|
status="disbursed",
|
||||||
|
wf_id=wf_id,
|
||||||
|
current_node=None,
|
||||||
|
approval_round=1,
|
||||||
|
tenant_no="t1",
|
||||||
|
create_by="u-applicant",
|
||||||
|
create_dept_id="d1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
db.add(
|
||||||
|
PlVehicleRechargeRecord(
|
||||||
|
id="rec-1",
|
||||||
|
recharge_order_no="RO1",
|
||||||
|
recharge_date=date(2026, 9, 26),
|
||||||
|
recharge_type="etc",
|
||||||
|
amount=Decimal("10.00"),
|
||||||
|
apply_id="apply-1",
|
||||||
|
license_plate="鄂A00001",
|
||||||
|
tenant_no="t1",
|
||||||
|
create_by="u-fin",
|
||||||
|
create_dept_id="d1",
|
||||||
|
del_flag=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
_patch_scope(monkeypatch, svc)
|
||||||
|
svc.delete_records(db, "t1", _user("u-fin", "财务"), ["rec-1"])
|
||||||
|
apply = db.query(PlVehicleRechargeApply).filter_by(id="apply-1").one()
|
||||||
|
assert apply.status == "pending"
|
||||||
|
assert apply.current_node == node_fin
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
-- 油卡充值与 ETC 充值菜单拆分,并按已有油卡菜单同步角色。
|
||||||
|
-- MySQL 5.7 / utf8mb4。可重复执行。仅租户 shanxia2.0 的有效菜单。
|
||||||
|
-- 不移动 PL_VEH_FUEL_CARD,车油卡信息仍挂在油卡分组下。
|
||||||
|
-- 不改 docs/sql/shanxia2.0.sql 全量快照;快照仍是本迁移之前的数据,不要为这条脚本重导整库。
|
||||||
|
-- 审批菜单 PL_VEH_RECHARGE_APPROVAL 在快照与迁移中没有 F 按钮,本脚本不为 ETC 审批补按钮。
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
START TRANSACTION;
|
||||||
|
|
||||||
|
-- A. 原「ETC/油卡」分组改为油卡,path 让给下面新建的 ETC 分组。
|
||||||
|
UPDATE `sys_menu`
|
||||||
|
SET `menu_name` = '油卡充值管理',
|
||||||
|
`path` = 'fuel-recharge',
|
||||||
|
`remark` = '油卡充值分组',
|
||||||
|
`update_time` = NOW()
|
||||||
|
WHERE `id` = 'PL_GROUP_VEH_RECHARGE'
|
||||||
|
AND `tenant_no` = 'shanxia2.0'
|
||||||
|
AND `del_flag` = 0;
|
||||||
|
|
||||||
|
UPDATE `sys_menu`
|
||||||
|
SET `menu_name` = '油卡充值申请', `update_time` = NOW()
|
||||||
|
WHERE `id` = 'PL_VEH_RECHARGE_APPLY'
|
||||||
|
AND `tenant_no` = 'shanxia2.0'
|
||||||
|
AND `del_flag` = 0;
|
||||||
|
|
||||||
|
UPDATE `sys_menu`
|
||||||
|
SET `menu_name` = '油卡充值审批', `update_time` = NOW()
|
||||||
|
WHERE `id` = 'PL_VEH_RECHARGE_APPROVAL'
|
||||||
|
AND `tenant_no` = 'shanxia2.0'
|
||||||
|
AND `del_flag` = 0;
|
||||||
|
|
||||||
|
UPDATE `sys_menu`
|
||||||
|
SET `menu_name` = '油卡充值发放管理', `update_time` = NOW()
|
||||||
|
WHERE `id` = 'PL_VEH_RECHARGE_RECORD'
|
||||||
|
AND `tenant_no` = 'shanxia2.0'
|
||||||
|
AND `del_flag` = 0;
|
||||||
|
|
||||||
|
UPDATE `sys_menu`
|
||||||
|
SET `menu_name` = '油卡充值审批人设置', `update_time` = NOW()
|
||||||
|
WHERE `id` = 'PL_VEH_RECHARGE_WF'
|
||||||
|
AND `tenant_no` = 'shanxia2.0'
|
||||||
|
AND `del_flag` = 0;
|
||||||
|
|
||||||
|
-- B. ETC 分组插在油卡分组原位置:先记下油卡 order_num,再把车辆管理下
|
||||||
|
-- order_num >= 该值的有效同级(含油卡本身,以及加油登记、维保等)全部 +1,
|
||||||
|
-- 然后用记下的序号插入 ETC。这样 ETC 紧挨在油卡之前,且不会和油卡同号。
|
||||||
|
-- MySQL 5.7 的 UPDATE 不能在子查询里再读 sys_menu,所以序号先放进用户变量。
|
||||||
|
-- 仅当 ETC 分组尚不存在、且车辆管理与油卡分组都有效时才顺延;重复执行不再 +1。
|
||||||
|
-- 请在同一会话整文件执行,@oil_order 等用户变量不能跨连接。
|
||||||
|
SET @etc_group_exists := (
|
||||||
|
SELECT COUNT(*) FROM `sys_menu` WHERE `id` = 'PL_GROUP_VEH_ETC_RECHARGE'
|
||||||
|
);
|
||||||
|
SET @vehicle_exists := (
|
||||||
|
SELECT COUNT(*) FROM `sys_menu`
|
||||||
|
WHERE `id` = 'PL_GROUP_VEHICLE' AND `tenant_no` = 'shanxia2.0' AND `del_flag` = 0
|
||||||
|
);
|
||||||
|
SET @oil_order := (
|
||||||
|
SELECT `order_num` FROM `sys_menu`
|
||||||
|
WHERE `id` = 'PL_GROUP_VEH_RECHARGE' AND `tenant_no` = 'shanxia2.0' AND `del_flag` = 0
|
||||||
|
LIMIT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE `sys_menu`
|
||||||
|
SET `order_num` = `order_num` + 1,
|
||||||
|
`update_time` = NOW()
|
||||||
|
WHERE `parent_id` = 'PL_GROUP_VEHICLE'
|
||||||
|
AND `tenant_no` = 'shanxia2.0'
|
||||||
|
AND `del_flag` = 0
|
||||||
|
AND @etc_group_exists = 0
|
||||||
|
AND @vehicle_exists = 1
|
||||||
|
AND @oil_order IS NOT NULL
|
||||||
|
AND `order_num` >= @oil_order;
|
||||||
|
|
||||||
|
INSERT INTO `sys_menu`
|
||||||
|
(`id`,`menu_name`,`parent_id`,`menu_type`,`path`,`component`,`perms`,
|
||||||
|
`is_frame`,`visible`,`status`,`tenant_no`,`order_num`,`remark`,`create_by`,`create_dept_id`)
|
||||||
|
SELECT 'PL_GROUP_VEH_ETC_RECHARGE', 'ETC充值管理', vehicle.id, 'M', 'etc-recharge',
|
||||||
|
NULL, NULL, 1, '1', '1', vehicle.tenant_no, COALESCE(@oil_order, 1),
|
||||||
|
'ETC充值分组', 'system', ''
|
||||||
|
FROM `sys_menu` vehicle
|
||||||
|
WHERE vehicle.id = 'PL_GROUP_VEHICLE'
|
||||||
|
AND vehicle.tenant_no = 'shanxia2.0'
|
||||||
|
AND vehicle.del_flag = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_menu` existing WHERE existing.id = 'PL_GROUP_VEH_ETC_RECHARGE'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- C. 四个页面菜单。审批人设置复用油卡流程权限,因此只按 id 判重,不能按 perms 判重。
|
||||||
|
INSERT INTO `sys_menu`
|
||||||
|
(`id`,`menu_name`,`parent_id`,`menu_type`,`path`,`component`,`perms`,
|
||||||
|
`is_frame`,`visible`,`status`,`tenant_no`,`order_num`,`create_by`,`create_dept_id`)
|
||||||
|
SELECT item.id, item.menu_name, parent.id, 'C', item.path, item.component, item.perms,
|
||||||
|
1, '1', '1', parent.tenant_no, item.order_num, 'system', ''
|
||||||
|
FROM `sys_menu` parent
|
||||||
|
JOIN (
|
||||||
|
SELECT 'PL_ETC_VEH_RECHARGE_APPLY' AS id, 'ETC充值申请' AS menu_name,
|
||||||
|
'etc-vehicle-recharge-apply' AS path, 'pl/etc-vehicle-recharge-apply/index' AS component,
|
||||||
|
'pl:etc-vehicle-recharge-apply:list' AS perms, 1 AS order_num
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'PL_ETC_VEH_RECHARGE_APPROVAL', 'ETC充值审批',
|
||||||
|
'etc-vehicle-recharge-approval', 'pl/etc-vehicle-recharge-approval/index',
|
||||||
|
'pl:etc-vehicle-recharge-approval:list', 2
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'PL_ETC_VEH_RECHARGE_RECORD', 'ETC发放管理',
|
||||||
|
'etc-vehicle-recharge-record', 'pl/etc-vehicle-recharge-record/index',
|
||||||
|
'pl:etc-vehicle-recharge-record:list', 3
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'PL_ETC_VEH_RECHARGE_WF', 'ETC充值审批人设置',
|
||||||
|
'etc-vehicle-recharge-workflow', 'pl/etc-vehicle-recharge-workflow/index',
|
||||||
|
'pl:vehicle-recharge-workflow:list', 4
|
||||||
|
) item
|
||||||
|
WHERE parent.id = 'PL_GROUP_VEH_ETC_RECHARGE'
|
||||||
|
AND parent.tenant_no = 'shanxia2.0'
|
||||||
|
AND parent.del_flag = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_menu` existing WHERE existing.id = item.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- D. 申请按钮,对照 PL_VRA_Q/A/E/R/C。
|
||||||
|
INSERT INTO `sys_menu`
|
||||||
|
(`id`,`menu_name`,`parent_id`,`menu_type`,`path`,`component`,`perms`,
|
||||||
|
`is_frame`,`visible`,`status`,`tenant_no`,`order_num`,`create_by`,`create_dept_id`)
|
||||||
|
SELECT btn.id, btn.menu_name, parent.id, 'F', '', '', btn.perms,
|
||||||
|
1, '1', '1', parent.tenant_no, btn.order_num, 'system', ''
|
||||||
|
FROM `sys_menu` parent
|
||||||
|
JOIN (
|
||||||
|
SELECT 'PL_ETC_VRA_Q' AS id, '查询' AS menu_name, 'pl:etc-vehicle-recharge-apply:query' AS perms, 1 AS order_num
|
||||||
|
UNION ALL SELECT 'PL_ETC_VRA_A', '新增', 'pl:etc-vehicle-recharge-apply:add', 2
|
||||||
|
UNION ALL SELECT 'PL_ETC_VRA_E', '编辑', 'pl:etc-vehicle-recharge-apply:edit', 3
|
||||||
|
UNION ALL SELECT 'PL_ETC_VRA_R', '删除', 'pl:etc-vehicle-recharge-apply:remove', 4
|
||||||
|
UNION ALL SELECT 'PL_ETC_VRA_C', '作废', 'pl:etc-vehicle-recharge-apply:cancel', 5
|
||||||
|
) btn
|
||||||
|
WHERE parent.id = 'PL_ETC_VEH_RECHARGE_APPLY'
|
||||||
|
AND parent.tenant_no = 'shanxia2.0'
|
||||||
|
AND parent.del_flag = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_menu` existing WHERE existing.id = btn.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- E. 发放按钮,对照 PL_VRR_Q/A/E/R。审批侧没有对应 F 按钮,这里不补。
|
||||||
|
INSERT INTO `sys_menu`
|
||||||
|
(`id`,`menu_name`,`parent_id`,`menu_type`,`path`,`component`,`perms`,
|
||||||
|
`is_frame`,`visible`,`status`,`tenant_no`,`order_num`,`create_by`,`create_dept_id`)
|
||||||
|
SELECT btn.id, btn.menu_name, parent.id, 'F', '', '', btn.perms,
|
||||||
|
1, '1', '1', parent.tenant_no, btn.order_num, 'system', ''
|
||||||
|
FROM `sys_menu` parent
|
||||||
|
JOIN (
|
||||||
|
SELECT 'PL_ETC_VRR_Q' AS id, '查询' AS menu_name, 'pl:etc-vehicle-recharge-record:query' AS perms, 1 AS order_num
|
||||||
|
UNION ALL SELECT 'PL_ETC_VRR_A', '新增', 'pl:etc-vehicle-recharge-record:add', 2
|
||||||
|
UNION ALL SELECT 'PL_ETC_VRR_E', '编辑', 'pl:etc-vehicle-recharge-record:edit', 3
|
||||||
|
UNION ALL SELECT 'PL_ETC_VRR_R', '删除', 'pl:etc-vehicle-recharge-record:remove', 4
|
||||||
|
) btn
|
||||||
|
WHERE parent.id = 'PL_ETC_VEH_RECHARGE_RECORD'
|
||||||
|
AND parent.tenant_no = 'shanxia2.0'
|
||||||
|
AND parent.del_flag = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_menu` existing WHERE existing.id = btn.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- G. 角色同步。拥有油卡页面(del_flag=0)且角色仍有效的,授予对等 ETC 页面、
|
||||||
|
-- 其下 F 按钮,以及最多三层祖先(ETC 分组、车辆管理、Web 根)。
|
||||||
|
-- 同一条 INSERT 里用 DISTINCT,避免一个角色同时拥有多个油卡页面时把祖先插重。
|
||||||
|
-- 已有任意 role_menu 行(含 del_flag=1)则跳过:不把软删除改回有效,也不另插一条把授权加回来。
|
||||||
|
INSERT INTO `sys_role_menu`
|
||||||
|
(`id`,`role_id`,`menu_id`,`tenant_no`,`create_by`,`create_dept_id`)
|
||||||
|
SELECT UUID(), grant_row.role_id, grant_row.menu_id, grant_row.tenant_no, 'system', ''
|
||||||
|
FROM (
|
||||||
|
SELECT DISTINCT rm.role_id, target.id AS menu_id, rm.tenant_no
|
||||||
|
FROM (
|
||||||
|
SELECT 'PL_VEH_RECHARGE_APPLY' AS source_id, 'PL_ETC_VEH_RECHARGE_APPLY' AS target_id
|
||||||
|
UNION ALL SELECT 'PL_VEH_RECHARGE_APPROVAL', 'PL_ETC_VEH_RECHARGE_APPROVAL'
|
||||||
|
UNION ALL SELECT 'PL_VEH_RECHARGE_RECORD', 'PL_ETC_VEH_RECHARGE_RECORD'
|
||||||
|
UNION ALL SELECT 'PL_VEH_RECHARGE_WF', 'PL_ETC_VEH_RECHARGE_WF'
|
||||||
|
) mapping
|
||||||
|
JOIN `sys_role_menu` rm
|
||||||
|
ON rm.menu_id = mapping.source_id
|
||||||
|
AND rm.tenant_no = 'shanxia2.0'
|
||||||
|
AND rm.del_flag = 0
|
||||||
|
JOIN `sys_role` r
|
||||||
|
ON r.id = rm.role_id
|
||||||
|
AND r.tenant_no = rm.tenant_no
|
||||||
|
AND r.del_flag = 0
|
||||||
|
AND r.status = '1'
|
||||||
|
JOIN `sys_menu` child
|
||||||
|
ON child.id = mapping.target_id
|
||||||
|
AND child.tenant_no = rm.tenant_no
|
||||||
|
AND child.del_flag = 0
|
||||||
|
LEFT JOIN `sys_menu` p1
|
||||||
|
ON p1.id = child.parent_id AND p1.tenant_no = rm.tenant_no AND p1.del_flag = 0
|
||||||
|
LEFT JOIN `sys_menu` p2
|
||||||
|
ON p2.id = p1.parent_id AND p2.tenant_no = rm.tenant_no AND p2.del_flag = 0
|
||||||
|
LEFT JOIN `sys_menu` p3
|
||||||
|
ON p3.id = p2.parent_id AND p3.tenant_no = rm.tenant_no AND p3.del_flag = 0
|
||||||
|
LEFT JOIN `sys_menu` btn
|
||||||
|
ON btn.parent_id = child.id
|
||||||
|
AND btn.tenant_no = rm.tenant_no
|
||||||
|
AND btn.del_flag = 0
|
||||||
|
AND btn.menu_type = 'F'
|
||||||
|
JOIN `sys_menu` target
|
||||||
|
ON target.tenant_no = rm.tenant_no
|
||||||
|
AND target.del_flag = 0
|
||||||
|
AND target.id IN (child.id, p1.id, p2.id, p3.id, btn.id)
|
||||||
|
) grant_row
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_role_menu` existing
|
||||||
|
WHERE existing.role_id = grant_row.role_id
|
||||||
|
AND existing.menu_id = grant_row.menu_id
|
||||||
|
AND existing.tenant_no = grant_row.tenant_no
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
# 证件到期提醒跳转编辑弹窗 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 在证件到期提醒列表增加「维护证件」操作列,按车辆/人员及自有/三方跳到基础信息库对应页并自动打开编辑弹窗。
|
||||||
|
|
||||||
|
**Architecture:** 纯函数根据主体类型与归属字段解析目标 path;提醒页 get 详情后 `router.push({ path, query: { editId } })`;车辆/自有车辆/人员页监听 `editId` 调用现有 `handleEdit`,成功后 `replace` 清除 query。后端不动。
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3 / TypeScript / vue-router / Element Plus;`node:test` + `tsx`。
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-11-12-document-reminder-edit-jump-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
| File | Responsibility |
|
||||||
|
| --- | --- |
|
||||||
|
| `web-admin/src/utils/pl-document-reminder-jump.ts` | 解析目标 path;错误文案常量 |
|
||||||
|
| `web-admin/src/utils/pl-document-reminder-jump.test.ts` | 路径解析单测 |
|
||||||
|
| `web-admin/src/composables/useRouteEditIdOpen.ts` | 监听 `editId`、调用 opener、清 query |
|
||||||
|
| `web-admin/src/views/pl/inventory/document-reminder.vue` | 操作列 + 跳转 |
|
||||||
|
| `web-admin/src/views/pl/vehicle/index.vue` | 接入 `editId` 打开编辑 |
|
||||||
|
| `web-admin/src/views/pl/self-vehicle/index.vue` | 同上 |
|
||||||
|
| `web-admin/src/views/pl/personnel/index.vue` | 同上(覆盖 self / carrier 两路由) |
|
||||||
|
|
||||||
|
说明:提交步骤在已获用户授权时执行;否则暂存改动待授权再 commit。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 路径解析纯函数 + 单测
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web-admin/src/utils/pl-document-reminder-jump.ts`
|
||||||
|
- Create: `web-admin/src/utils/pl-document-reminder-jump.test.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败测试**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { describe, it } from 'node:test'
|
||||||
|
import {
|
||||||
|
resolveDocumentReminderEditPath,
|
||||||
|
DOCUMENT_REMINDER_JUMP_UNSUPPORTED
|
||||||
|
} from './pl-document-reminder-jump'
|
||||||
|
|
||||||
|
describe('证件提醒跳转路径', () => {
|
||||||
|
it('车辆按 vehicleType 分流自有与三方', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveDocumentReminderEditPath({ ownerType: 'vehicle', vehicleType: 'self' }),
|
||||||
|
'/vehicle-mgmt/base-info/self-vehicle'
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
resolveDocumentReminderEditPath({ ownerType: 'vehicle', vehicleType: 'carrier' }),
|
||||||
|
'/vehicle-mgmt/base-info/vehicle'
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
resolveDocumentReminderEditPath({ ownerType: 'vehicle', vehicleType: '' }),
|
||||||
|
'/vehicle-mgmt/base-info/vehicle'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('人员按 personnelAffiliation 分流', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveDocumentReminderEditPath({
|
||||||
|
ownerType: 'personnel',
|
||||||
|
personnelAffiliation: 'self'
|
||||||
|
}),
|
||||||
|
'/vehicle-mgmt/base-info/personnel'
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
resolveDocumentReminderEditPath({
|
||||||
|
ownerType: 'personnel',
|
||||||
|
personnelAffiliation: 'carrier'
|
||||||
|
}),
|
||||||
|
'/vehicle-mgmt/base-info/personnel-carrier'
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
resolveDocumentReminderEditPath({
|
||||||
|
ownerType: 'personnel',
|
||||||
|
personnelAffiliation: ''
|
||||||
|
}),
|
||||||
|
'/vehicle-mgmt/base-info/personnel'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('不支持的主体返回 null', () => {
|
||||||
|
assert.equal(resolveDocumentReminderEditPath({ ownerType: 'carrier' }), null)
|
||||||
|
assert.equal(resolveDocumentReminderEditPath({ ownerType: '' }), null)
|
||||||
|
assert.equal(DOCUMENT_REMINDER_JUMP_UNSUPPORTED, '暂不支持该主体类型的证件维护跳转')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测确认失败**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web-admin && ./node_modules/.bin/tsx --test src/utils/pl-document-reminder-jump.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL(模块不存在)
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const DOCUMENT_REMINDER_JUMP_UNSUPPORTED = '暂不支持该主体类型的证件维护跳转'
|
||||||
|
|
||||||
|
export type ReminderJumpInput = {
|
||||||
|
ownerType?: string | null
|
||||||
|
vehicleType?: string | null
|
||||||
|
personnelAffiliation?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDocumentReminderEditPath(input: ReminderJumpInput): string | null {
|
||||||
|
const ownerType = String(input.ownerType || '').trim()
|
||||||
|
if (ownerType === 'vehicle') {
|
||||||
|
return String(input.vehicleType || '').trim() === 'self'
|
||||||
|
? '/vehicle-mgmt/base-info/self-vehicle'
|
||||||
|
: '/vehicle-mgmt/base-info/vehicle'
|
||||||
|
}
|
||||||
|
if (ownerType === 'personnel') {
|
||||||
|
return String(input.personnelAffiliation || '').trim() === 'carrier'
|
||||||
|
? '/vehicle-mgmt/base-info/personnel-carrier'
|
||||||
|
: '/vehicle-mgmt/base-info/personnel'
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 跑测确认通过**
|
||||||
|
|
||||||
|
同 Step 2,Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit(需授权)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add web-admin/src/utils/pl-document-reminder-jump.ts \
|
||||||
|
web-admin/src/utils/pl-document-reminder-jump.test.ts
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(web-admin): add document reminder jump path resolver
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: `useRouteEditIdOpen` composable
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web-admin/src/composables/useRouteEditIdOpen.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 实现 composable**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听 route.query.editId,调用 openById;结束后清除 editId,避免刷新重复弹窗。
|
||||||
|
* openById 应自行处理加载失败提示。
|
||||||
|
*/
|
||||||
|
export function useRouteEditIdOpen(openById: (id: string) => void | Promise<void>) {
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
async function consumeEditId() {
|
||||||
|
const raw = route.query.editId
|
||||||
|
const id = Array.isArray(raw) ? String(raw[0] || '') : String(raw || '')
|
||||||
|
if (!id.trim()) return
|
||||||
|
try {
|
||||||
|
await openById(id.trim())
|
||||||
|
} finally {
|
||||||
|
if (route.query.editId == null) return
|
||||||
|
const query = { ...route.query }
|
||||||
|
delete query.editId
|
||||||
|
await router.replace({ path: route.path, query })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.editId,
|
||||||
|
() => {
|
||||||
|
void consumeEditId()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit(需授权)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add web-admin/src/composables/useRouteEditIdOpen.ts
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(web-admin): add useRouteEditIdOpen for deep-link edit dialogs
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: 提醒列表操作列与跳转
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web-admin/src/views/pl/inventory/document-reminder.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 模板 — 在 `</ListTable>` 前增加操作列**
|
||||||
|
|
||||||
|
在状态/备注列之后、`</ListTable>` 之前插入:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-table-column label="操作" fixed="right" min-width="140" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:loading="maintainLoadingId === row.id"
|
||||||
|
@click="handleMaintainCert(row)"
|
||||||
|
>
|
||||||
|
<el-icon><EditPen /></el-icon>维护证件
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: script — 导入与跳转逻辑**
|
||||||
|
|
||||||
|
1. 图标:`import { CircleCheck, Filter, EditPen } from '@element-plus/icons-vue'`
|
||||||
|
2. API:`import { inventoryApi, vehicleApi, personnelApi, type DocumentReminder } from '@/api/pl'`
|
||||||
|
3. `import { useRouter } from 'vue-router'`(若尚未)
|
||||||
|
4. `import { pickBxData } from '@/utils/http/bx'`
|
||||||
|
5. `import { resolveDocumentReminderEditPath, DOCUMENT_REMINDER_JUMP_UNSUPPORTED } from '@/utils/pl-document-reminder-jump'`
|
||||||
|
6. `import type { VehicleDetail, PersonnelDetail } from '@/api/pl'`(若类型从同文件导出则合并 import)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const router = useRouter()
|
||||||
|
const maintainLoadingId = ref<string | null>(null)
|
||||||
|
|
||||||
|
async function handleMaintainCert(row: DocumentReminder) {
|
||||||
|
const ownerId = String(row.ownerId || '').trim()
|
||||||
|
const ownerType = String(row.ownerType || '').trim()
|
||||||
|
if (!ownerId || !ownerType) {
|
||||||
|
ElMessage.warning('缺少主体信息,无法跳转维护')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
maintainLoadingId.value = row.id
|
||||||
|
try {
|
||||||
|
let path: string | null = null
|
||||||
|
if (ownerType === 'vehicle') {
|
||||||
|
const res = await vehicleApi.get(ownerId)
|
||||||
|
const detail = pickBxData<VehicleDetail>(res) || (res as any)?.data || res
|
||||||
|
const vehicleType = String((detail as VehicleDetail)?.vehicleType || '')
|
||||||
|
path = resolveDocumentReminderEditPath({ ownerType, vehicleType })
|
||||||
|
} else if (ownerType === 'personnel') {
|
||||||
|
const res = await personnelApi.get(ownerId)
|
||||||
|
const detail = pickBxData<PersonnelDetail>(res) || (res as any)?.data || res
|
||||||
|
const personnelAffiliation = String(
|
||||||
|
(detail as PersonnelDetail)?.personnelAffiliation || ''
|
||||||
|
)
|
||||||
|
path = resolveDocumentReminderEditPath({ ownerType, personnelAffiliation })
|
||||||
|
} else {
|
||||||
|
ElMessage.warning(DOCUMENT_REMINDER_JUMP_UNSUPPORTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!path) {
|
||||||
|
ElMessage.warning(DOCUMENT_REMINDER_JUMP_UNSUPPORTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await router.push({ path, query: { editId: ownerId } })
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('无法打开主体档案,请确认数据存在且有权限')
|
||||||
|
} finally {
|
||||||
|
maintainLoadingId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
确保页面已导入 `ElMessage`(项目若自动导入则无需手写)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit(需授权)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add web-admin/src/views/pl/inventory/document-reminder.vue
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(web-admin): add maintain-cert action on document reminder list
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: 车辆 / 自有车辆页响应 `editId`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web-admin/src/views/pl/vehicle/index.vue`
|
||||||
|
- Modify: `web-admin/src/views/pl/self-vehicle/index.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: vehicle/index.vue**
|
||||||
|
|
||||||
|
在 `handleEdit` 定义之后加入:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { useRouteEditIdOpen } from '@/composables/useRouteEditIdOpen'
|
||||||
|
|
||||||
|
useRouteEditIdOpen(async (id) => {
|
||||||
|
await handleEdit({ id } as Vehicle)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
确认 `handleEdit` 仅依赖 `row.id` 拉详情(现有实现用 `vehicleApi.get(row.id)`)。若 TypeScript 抱怨,使用 `as Vehicle` 或最小对象即可。
|
||||||
|
|
||||||
|
- [ ] **Step 2: self-vehicle/index.vue**
|
||||||
|
|
||||||
|
同样在 `handleEdit` 之后:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { useRouteEditIdOpen } from '@/composables/useRouteEditIdOpen'
|
||||||
|
|
||||||
|
useRouteEditIdOpen(async (id) => {
|
||||||
|
await handleEdit({ id } as Vehicle)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
(若自有车页 Vehicle 类型同名,用该页已有类型。)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit(需授权)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add web-admin/src/views/pl/vehicle/index.vue \
|
||||||
|
web-admin/src/views/pl/self-vehicle/index.vue
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(web-admin): open vehicle edit dialog from editId query
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 人员页响应 `editId`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web-admin/src/views/pl/personnel/index.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 接入 composable**
|
||||||
|
|
||||||
|
在 `handleEdit` 之后:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { useRouteEditIdOpen } from '@/composables/useRouteEditIdOpen'
|
||||||
|
|
||||||
|
useRouteEditIdOpen(async (id) => {
|
||||||
|
await handleEdit({ id } as Personnel)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
该组件同时挂 `personnel` 与 `personnel-carrier`,两路由都会响应 `editId`。提醒页已按 affiliation 选对 path;若进错页,`handleEdit`/列表数据范围会失败——保持现有错误提示即可,必要时在 `openById` 外包一层 try/catch 用 `ElMessage.error`。
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit(需授权)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add web-admin/src/views/pl/personnel/index.vue
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(web-admin): open personnel edit dialog from editId query
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 验证
|
||||||
|
|
||||||
|
**Files:** 无必须改动。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 单元测试**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web-admin && ./node_modules/.bin/tsx --test src/utils/pl-document-reminder-jump.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 2: 手工冒烟(需登录)**
|
||||||
|
|
||||||
|
1. 打开 `/#/pl/document-reminder-module/document-reminder`
|
||||||
|
2. 点车辆行「维护证件」→ 进入对应自有/三方车辆页且编辑弹窗打开,证件可编辑
|
||||||
|
3. 点人员行同理
|
||||||
|
4. 刷新目标页不重复弹窗(query 已清)
|
||||||
|
5. 原「处理」按钮仍可用
|
||||||
|
|
||||||
|
- [ ] **Step 3: 记录未跑项**
|
||||||
|
|
||||||
|
若无法登录 UI,在交付说明写明仅完成单测与代码接入,UI 冒烟待用户验证。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spec coverage
|
||||||
|
|
||||||
|
| 设计项 | 任务 |
|
||||||
|
| --- | --- |
|
||||||
|
| 操作列「维护证件」 | Task 3 |
|
||||||
|
| 按类型/归属分流 path | Task 1 + 3 |
|
||||||
|
| `editId` 打开编辑并清 query | Task 2 + 4 + 5 |
|
||||||
|
| 后端不动 / 不自动 resolve | 全程遵守 |
|
||||||
|
| 仅 Web | 不改 miniprogram |
|
||||||
|
|
||||||
|
## Placeholder scan
|
||||||
|
|
||||||
|
无 TBD;路径、文件、测试命令、代码块已钉死。
|
||||||
@@ -0,0 +1,543 @@
|
|||||||
|
# ETC / 油卡充值管理拆分 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Web 端将混合的「ETC/油卡充值管理」拆成并列的【ETC充值管理】与【油卡充值管理】;前端复制页面并按类型过滤,后端共用接口并新增 `rechargeTypeNe`;审批流程与审批人配置保持一套。
|
||||||
|
|
||||||
|
**Architecture:** 后端在现有 list/export/approval/board 过滤链增加 `recharge_type_ne`;油卡前端固定传 `rechargeTypeNe=etc`,ETC 前端固定传 `rechargeType=etc` 并复制四套页面。原分组 path 改为 `fuel-recharge`,新建 `etc-recharge` 分组;顶部 `PlRechargeSummary` 仅油卡路径显示。工作流 `biz_type=vehicle_recharge` 不拆。
|
||||||
|
|
||||||
|
**Tech Stack:** FastAPI / SQLAlchemy;Vue 3 / TypeScript / Element Plus;pytest;`node:test` + `tsx`;MySQL 菜单迁移 SQL。
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-11-12-pl-etc-fuel-recharge-split-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
| File | Responsibility |
|
||||||
|
| --- | --- |
|
||||||
|
| `backend/app/services/pl/vehicle_recharge_list_filters.py` | `recharge_type_ne` → `col != value` |
|
||||||
|
| `backend/app/services/pl_vehicle_recharge_service.py` | `list_applies` / `list_disburse_board`(及 list_records 若已接类型)透传 ne |
|
||||||
|
| `backend/app/services/pl_vehicle_recharge_approval_service.py` | pending/done 透传 ne |
|
||||||
|
| `backend/app/api/pl/vehicle_recharge.py` | Query/export body 增加 `rechargeTypeNe` |
|
||||||
|
| `backend/app/api/pl/vehicle_recharge_approval.py` | pending/done 增加 `rechargeTypeNe` |
|
||||||
|
| `backend/tests/test_pl_vehicle_ops_list_filters.py` | ne 过滤单测 |
|
||||||
|
| `web-admin/src/utils/pl-recharge-summary.ts` | 仅 `fuel-recharge` 显示统计 |
|
||||||
|
| `web-admin/src/utils/pl-recharge-summary.test.ts` | 路由断言更新 |
|
||||||
|
| `web-admin/src/views/pl/vehicle-recharge-{apply,approval,record,workflow}/` | 油卡侧:排除 etc、文案、默认类型 |
|
||||||
|
| `web-admin/src/views/pl/etc-vehicle-recharge-{apply,approval,record,workflow}/` | ETC 复制页:固定 etc、无油卡字段、新 table-key |
|
||||||
|
| `web-admin/src/router/modules/pl.ts` | 分组 path、标题、ETC 子路由、旧 URL redirect |
|
||||||
|
| `docs/sql/migrations/2026-11-12-pl-etc-fuel-recharge-split-menus.sql` | 改名、改 path、新建 ETC 菜单、角色同步 |
|
||||||
|
| `web-admin/src/components/core/layouts/art-page-content/index.vue` | 确认 summary 仍指向油卡申请 menuCode(一般无需改逻辑) |
|
||||||
|
|
||||||
|
**说明:**
|
||||||
|
|
||||||
|
- 「车油卡信息」`PL_VEH_FUEL_CARD` 继续留在油卡分组下(现挂在原充值分组),不迁入 ETC。
|
||||||
|
- 写入接口本期不按域拆分;类型由前端固定。
|
||||||
|
- 提交前须有用户明确授权再 `git commit`;下列 Commit 步骤在已授权时执行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 后端 `rechargeTypeNe` 过滤
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/services/pl/vehicle_recharge_list_filters.py`
|
||||||
|
- Modify: `backend/tests/test_pl_vehicle_ops_list_filters.py`
|
||||||
|
- Modify: `backend/app/services/pl_vehicle_recharge_service.py`
|
||||||
|
- Modify: `backend/app/services/pl_vehicle_recharge_approval_service.py`
|
||||||
|
- Modify: `backend/app/api/pl/vehicle_recharge.py`
|
||||||
|
- Modify: `backend/app/api/pl/vehicle_recharge_approval.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败单测**
|
||||||
|
|
||||||
|
在 `test_pl_vehicle_ops_list_filters.py` 追加:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_recharge_apply_type_ne_filter():
|
||||||
|
filters = build_recharge_apply_column_filters(recharge_type_ne="etc")
|
||||||
|
assert len(filters) == 1
|
||||||
|
sql = _compile(filters[0])
|
||||||
|
assert "recharge_type" in sql
|
||||||
|
assert "!=" in sql or "<>" in sql
|
||||||
|
assert "etc" in sql
|
||||||
|
|
||||||
|
|
||||||
|
def test_recharge_disburse_board_type_ne_filter():
|
||||||
|
filters = build_recharge_disburse_board_column_filters(recharge_type_ne="etc")
|
||||||
|
assert any("recharge_type" in _compile(f) for f in filters)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测确认失败**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && source venv/bin/activate
|
||||||
|
pytest tests/test_pl_vehicle_ops_list_filters.py::test_recharge_apply_type_ne_filter tests/test_pl_vehicle_ops_list_filters.py::test_recharge_disburse_board_type_ne_filter -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL(未知参数或无 ne 条件)
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现 filter 层**
|
||||||
|
|
||||||
|
在 `vehicle_recharge_list_filters.py` 增加:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _append_ne_filter(filters: list, col, raw: Optional[str]) -> None:
|
||||||
|
value = (raw or "").strip()
|
||||||
|
if value:
|
||||||
|
filters.append(col != value)
|
||||||
|
```
|
||||||
|
|
||||||
|
`build_recharge_apply_column_filters` / `build_recharge_disburse_board_column_filters` 增加参数 `recharge_type_ne: Optional[str] = None`,在现有 `_append_enum_filter(... recharge_type ...)` 之后调用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_append_ne_filter(filters, PlVehicleRechargeApply.recharge_type, recharge_type_ne)
|
||||||
|
```
|
||||||
|
|
||||||
|
(发放看板同样用 `PlVehicleRechargeApply.recharge_type`。)
|
||||||
|
|
||||||
|
- [ ] **Step 4: 透传 service + API**
|
||||||
|
|
||||||
|
对以下调用链增加同名可选参数并向下传递(alias `rechargeTypeNe`):
|
||||||
|
|
||||||
|
1. `list_applies`(service + `_apply_list_kwargs` / export 解析 + `list_applies` Query)
|
||||||
|
2. `list_disburse_board`(及 export)
|
||||||
|
3. `list_records`(若当前已支持 `recharge_type`,一并加 ne;否则仅保证 apply/board/approval)
|
||||||
|
4. approval `list_pending` / `list_done`(`vehicle_recharge_approval.py` 的 `_param_get` 与 Query)
|
||||||
|
|
||||||
|
示例(API Query):
|
||||||
|
|
||||||
|
```python
|
||||||
|
recharge_type_ne: Optional[str] = Query(default=None, alias="rechargeTypeNe"),
|
||||||
|
```
|
||||||
|
|
||||||
|
export body Field 同样加 `rechargeTypeNe`。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 跑测确认通过**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && source venv/bin/activate
|
||||||
|
pytest tests/test_pl_vehicle_ops_list_filters.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit(需用户授权)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/services/pl/vehicle_recharge_list_filters.py \
|
||||||
|
backend/app/services/pl_vehicle_recharge_service.py \
|
||||||
|
backend/app/services/pl_vehicle_recharge_approval_service.py \
|
||||||
|
backend/app/api/pl/vehicle_recharge.py \
|
||||||
|
backend/app/api/pl/vehicle_recharge_approval.py \
|
||||||
|
backend/tests/test_pl_vehicle_ops_list_filters.py
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(pl): add rechargeTypeNe filter for vehicle recharge lists
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: 顶部统计仅油卡路由
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web-admin/src/utils/pl-recharge-summary.ts`
|
||||||
|
- Modify: `web-admin/src/utils/pl-recharge-summary.test.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 更新失败测试**
|
||||||
|
|
||||||
|
替换 `isRechargeSummaryRoute` 相关用例为:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('只对油卡充值业务子页显示,ETC 与其它车辆页不显示', () => {
|
||||||
|
assert.equal(
|
||||||
|
isRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-apply'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-record'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-approval'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-workflow'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-apply'),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/vehicle-recharge-apply'),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
assert.equal(isRechargeSummaryRoute('/vehicle-mgmt/vehicle-fuel-card'), false)
|
||||||
|
assert.equal(isRechargeSummaryRoute('/vehicle-mgmt/vehicle'), false)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测确认失败**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web-admin && ./node_modules/.bin/tsx --test src/utils/pl-recharge-summary.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL(旧路径仍为 true / 新 fuel 路径未匹配)
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function isRechargeSummaryRoute(path: string) {
|
||||||
|
return /\/fuel-recharge\/vehicle-recharge-(apply|approval|record|workflow)\/?$/.test(
|
||||||
|
path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
确认 `art-page-content` 仍用 `menuCode === 'pl:vehicle-recharge-apply:list'` 解析跳转(油卡申请 perms 不变),一般无需改文件。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 跑测确认通过**
|
||||||
|
|
||||||
|
同 Step 2,Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit(需用户授权)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add web-admin/src/utils/pl-recharge-summary.ts web-admin/src/utils/pl-recharge-summary.test.ts
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
fix(web-admin): show recharge summary only on fuel-recharge routes
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: 油卡侧原页面改造(排除 ETC)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web-admin/src/views/pl/vehicle-recharge-apply/index.vue`
|
||||||
|
- Modify: `web-admin/src/views/pl/vehicle-recharge-approval/index.vue`
|
||||||
|
- Modify: `web-admin/src/views/pl/vehicle-recharge-record/index.vue`
|
||||||
|
- Modify: `web-admin/src/views/pl/vehicle-recharge-workflow/index.vue`
|
||||||
|
- Modify: `web-admin/src/views/pl/vehicle-recharge-apply/columnFilterDefs.ts`(若枚举选项在此定义)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 申请页强制排除 etc**
|
||||||
|
|
||||||
|
在 `buildListQueryParams`(约 L715)末尾增加:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
params.rechargeTypeNe = 'etc'
|
||||||
|
return params
|
||||||
|
```
|
||||||
|
|
||||||
|
若用户列筛选选了 `rechargeType` 且包含 `etc`,以 ne 为准仍排除 etc(后端 `!=` 与 `IN` 同时生效时需确认 SQLAlchemy 为 AND)。为避免矛盾:油卡页 `enumOptionsFor('rechargeType')` **过滤掉** `etc` 选项;表单 `<el-option>` 同样不渲染 `etc`。
|
||||||
|
|
||||||
|
默认新建类型:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// handleAdd / formData 初始
|
||||||
|
formData.rechargeType = 'fuel'
|
||||||
|
```
|
||||||
|
|
||||||
|
文案:文件头注释、导出 filename、提示里的「ETC/油卡」改为「油卡」(审批跳转文案等同理)。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 审批页 / 发放页同样传 `rechargeTypeNe=etc`**
|
||||||
|
|
||||||
|
在各自组装 list/export/board 参数处增加 `rechargeTypeNe: 'etc'`;充值类型筛选项去掉 etc;文案改为油卡。
|
||||||
|
|
||||||
|
发放页中类似:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
ElMessage.warning('审批中的申请请到「油卡充值审批」菜单完成发放登记')
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 审批人设置页**
|
||||||
|
|
||||||
|
顶部 `wf-info-bar` 旁或下方增加一行提示(简短即可):
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<span class="form-tip">ETC 与油卡共用同一套审批人配置</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
不改 `biz_type` / API。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 本地点验(开发服)**
|
||||||
|
|
||||||
|
打开 `/#/vehicle-mgmt/fuel-recharge/vehicle-recharge-apply`(路由 Task 5 完成前可暂用旧 path 带 `rechargeTypeNe` 联调)。确认列表无 etc 行;新建无 ETC 选项。
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit(需用户授权)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add web-admin/src/views/pl/vehicle-recharge-apply \
|
||||||
|
web-admin/src/views/pl/vehicle-recharge-approval \
|
||||||
|
web-admin/src/views/pl/vehicle-recharge-record \
|
||||||
|
web-admin/src/views/pl/vehicle-recharge-workflow
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
fix(web-admin): restrict legacy recharge pages to non-ETC types
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: 复制并改造 ETC 四套页面
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web-admin/src/views/pl/etc-vehicle-recharge-apply/`(含 `index.vue`、`columnFilterDefs.ts`)
|
||||||
|
- Create: `web-admin/src/views/pl/etc-vehicle-recharge-approval/index.vue`
|
||||||
|
- Create: `web-admin/src/views/pl/etc-vehicle-recharge-record/index.vue`
|
||||||
|
- Create: `web-admin/src/views/pl/etc-vehicle-recharge-workflow/index.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 复制目录**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web-admin/src/views/pl
|
||||||
|
cp -R vehicle-recharge-apply etc-vehicle-recharge-apply
|
||||||
|
cp -R vehicle-recharge-approval etc-vehicle-recharge-approval
|
||||||
|
cp -R vehicle-recharge-record etc-vehicle-recharge-record
|
||||||
|
cp -R vehicle-recharge-workflow etc-vehicle-recharge-workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: ETC 申请页改造清单(在复制后的文件上改)**
|
||||||
|
|
||||||
|
1. 注释与标题文案 →「ETC充值申请」等。
|
||||||
|
2. `buildListQueryParams`:删除 `rechargeTypeNe`;固定 `params.rechargeType = 'etc'`(若用户又选了类型筛选,仍强制 etc,或隐藏类型筛选项)。
|
||||||
|
3. 表单:充值类型固定 `etc`,可用只读展示或隐藏 select 并在提交时写死;去掉油卡卡号相关表单项与 `useVehicleFuelCard` 展示(ETC 不需要)。
|
||||||
|
4. `table-key`、`usePersistedListColumns` 的 localStorage key、导出列定义 key → 加 `etc-` 前缀,例如 `pl-etc-vehicle-recharge-apply`、`pl-etc-vehicle-recharge-apply-list-columns-v2`。
|
||||||
|
5. `columnFilterDefs` 中若 import 路径指向旧目录,改为相对本目录或 `@/views/pl/etc-vehicle-recharge-apply/columnFilterDefs`。
|
||||||
|
6. 权限指令 / `menuCode` 若页面内有硬编码,改为 ETC 新 perms(与菜单 SQL 一致,见 Task 6):`pl:etc-vehicle-recharge-apply:*`。
|
||||||
|
7. 统计刷新:可保留 `mittBus.emit('rechargeSummaryRefresh')`(无害);ETC 路由本就不显示 summary。
|
||||||
|
|
||||||
|
- [ ] **Step 3: ETC 审批 / 发放页**
|
||||||
|
|
||||||
|
- 列表参数固定 `rechargeType=etc`。
|
||||||
|
- 文案「ETC充值审批」「ETC发放管理」。
|
||||||
|
- 新 `table-key` / 列偏好 key。
|
||||||
|
- 发放页跳转提示改为「ETC充值审批」。
|
||||||
|
- 隐藏或固定类型筛选为 etc;无油卡卡号列亦可保留显示为 —。
|
||||||
|
|
||||||
|
- [ ] **Step 4: ETC 审批人设置页**
|
||||||
|
|
||||||
|
- 仍调用现有工作流 API / `vehicle_recharge` biz_type(与油卡页相同 service 调用)。
|
||||||
|
- 增加共用说明文案。
|
||||||
|
- `table-key` 改为 `pl-etc-vehicle-recharge-workflow`(仅前端偏好隔离)。
|
||||||
|
- 页面内 perms 复用 `pl:vehicle-recharge-workflow:*`(与设计一致)。
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit(需用户授权)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add web-admin/src/views/pl/etc-vehicle-recharge-apply \
|
||||||
|
web-admin/src/views/pl/etc-vehicle-recharge-approval \
|
||||||
|
web-admin/src/views/pl/etc-vehicle-recharge-record \
|
||||||
|
web-admin/src/views/pl/etc-vehicle-recharge-workflow
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(web-admin): add dedicated ETC vehicle recharge pages
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 静态路由与 redirect
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web-admin/src/router/modules/pl.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 原分组改为油卡**
|
||||||
|
|
||||||
|
将 `PL_GROUP_VEH_RECHARGE`:
|
||||||
|
|
||||||
|
- `path: 'etc-recharge'` → `'fuel-recharge'`
|
||||||
|
- `meta.title` → `'油卡充值管理'`
|
||||||
|
- 子菜单 title:`油卡充值申请` / `油卡充值审批` / `油卡充值发放管理` / `油卡充值审批人设置`
|
||||||
|
- **保留** `PL_VEH_FUEL_CARD`(车油卡信息)在此分组下
|
||||||
|
- component 仍指向原 `pl/vehicle-recharge-*`
|
||||||
|
|
||||||
|
- [ ] **Step 2: 新增 ETC 分组(插在油卡分组旁)**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
id: 'PL_GROUP_VEH_ETC_RECHARGE',
|
||||||
|
name: 'PlVehicleEtcRechargeGroup',
|
||||||
|
path: 'etc-recharge',
|
||||||
|
meta: { title: 'ETC充值管理', keepAlive: false },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'PL_ETC_VEH_RECHARGE_APPLY',
|
||||||
|
name: 'PlEtcVehicleRechargeApply',
|
||||||
|
path: 'etc-vehicle-recharge-apply',
|
||||||
|
component: '/pl/etc-vehicle-recharge-apply/index',
|
||||||
|
meta: {
|
||||||
|
title: 'ETC充值申请',
|
||||||
|
keepAlive: false,
|
||||||
|
menuCode: 'pl:etc-vehicle-recharge-apply:list'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PL_ETC_VEH_RECHARGE_APPROVAL',
|
||||||
|
name: 'PlEtcVehicleRechargeApproval',
|
||||||
|
path: 'etc-vehicle-recharge-approval',
|
||||||
|
component: '/pl/etc-vehicle-recharge-approval/index',
|
||||||
|
meta: {
|
||||||
|
title: 'ETC充值审批',
|
||||||
|
keepAlive: false,
|
||||||
|
menuCode: 'pl:etc-vehicle-recharge-approval:list'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PL_ETC_VEH_RECHARGE_RECORD',
|
||||||
|
name: 'PlEtcVehicleRechargeRecord',
|
||||||
|
path: 'etc-vehicle-recharge-record',
|
||||||
|
component: '/pl/etc-vehicle-recharge-record/index',
|
||||||
|
meta: {
|
||||||
|
title: 'ETC发放管理',
|
||||||
|
keepAlive: false,
|
||||||
|
menuCode: 'pl:etc-vehicle-recharge-record:list'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PL_ETC_VEH_RECHARGE_WF',
|
||||||
|
name: 'PlEtcVehicleRechargeWorkflow',
|
||||||
|
path: 'etc-vehicle-recharge-workflow',
|
||||||
|
component: '/pl/etc-vehicle-recharge-workflow/index',
|
||||||
|
meta: {
|
||||||
|
title: 'ETC充值审批人设置',
|
||||||
|
keepAlive: false,
|
||||||
|
menuCode: 'pl:vehicle-recharge-workflow:list'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 更新隐藏 redirect**
|
||||||
|
|
||||||
|
所有原指向 `/vehicle-mgmt/etc-recharge/vehicle-recharge-*` 的 redirect,改为:
|
||||||
|
|
||||||
|
`/vehicle-mgmt/fuel-recharge/vehicle-recharge-*`
|
||||||
|
|
||||||
|
(书签落到油卡侧。)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit(需用户授权)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add web-admin/src/router/modules/pl.ts
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(web-admin): split etc and fuel recharge router groups
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 菜单迁移 SQL + 角色同步
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/sql/migrations/2026-11-12-pl-etc-fuel-recharge-split-menus.sql`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 编写可重复执行的迁移**
|
||||||
|
|
||||||
|
要点(MySQL 5.7,`SET NAMES utf8mb4;` + 事务):
|
||||||
|
|
||||||
|
1. 更新原分组与子菜单名称:
|
||||||
|
- `PL_GROUP_VEH_RECHARGE`:`menu_name='油卡充值管理'`,`path='fuel-recharge'`,remark 更新
|
||||||
|
- 原子菜单名改为「油卡充值申请/审批/发放管理/审批人设置」
|
||||||
|
2. 插入 ETC 分组(`parent_id=PL_GROUP_VEHICLE`,`path='etc-recharge'`,`order_num` 建议小于油卡分组以便并列靠前,或紧挨油卡):
|
||||||
|
- id: `PL_GROUP_VEH_ETC_RECHARGE`
|
||||||
|
3. 插入 4 个 C 菜单(component / path / perms 与 Task 5 一致):
|
||||||
|
- Apply/Approval/Record:新 perms `pl:etc-vehicle-recharge-*:list`
|
||||||
|
- WF:`perms='pl:vehicle-recharge-workflow:list'`(复用)
|
||||||
|
4. 为 Apply/Approval/Record 复制对应 F 按钮菜单(对照现有 `PL_VRA_*` / `PL_VRR_*` / 审批按钮),id 前缀 `PL_ETC_…`,perms 换新前缀。
|
||||||
|
5. 角色同步:对已拥有对应油卡菜单(如 `PL_VEH_RECHARGE_APPLY`)的角色,插入 ETC 对等菜单及祖先(模式参考 `docs/sql/migrations/2026-10-25-pl-vehicle-fuel-card.sql` 的 `sys_role_menu` 段);`NOT EXISTS` 防重复;不恢复管理员已软删授权。
|
||||||
|
6. WF 菜单:对已有 `PL_VEH_RECHARGE_WF` 授权的角色补 `PL_ETC_VEH_RECHARGE_WF` + 分组祖先。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在本地库执行并抽查**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用项目既有 mysql 方式执行该 migration;执行后:
|
||||||
|
# SELECT id,menu_name,path,component,perms FROM sys_menu
|
||||||
|
# WHERE id LIKE 'PL_%RECHARGE%' OR parent_id IN ('PL_GROUP_VEH_RECHARGE','PL_GROUP_VEH_ETC_RECHARGE');
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 两组菜单并存;油卡 path=`fuel-recharge`;ETC 指向 `pl/etc-vehicle-recharge-*`。
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit(需用户授权)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/sql/migrations/2026-11-12-pl-etc-fuel-recharge-split-menus.sql
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
chore(sql): split ETC and fuel recharge menus with role sync
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: 联调验收与回归
|
||||||
|
|
||||||
|
**Files:** 无必须改动;按需修漏网文案 / 参数。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 后端过滤回归**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && source venv/bin/activate
|
||||||
|
pytest tests/test_pl_vehicle_ops_list_filters.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 2: 前端 summary 单测**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web-admin && ./node_modules/.bin/tsx --test src/utils/pl-recharge-summary.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 3: 类型检查(可选但推荐)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web-admin && pnpm build:typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 与本改动相关的错误为 0
|
||||||
|
|
||||||
|
- [ ] **Step 4: 手工验收对照设计「验收」清单**
|
||||||
|
|
||||||
|
1. 侧栏并列【ETC充值管理】【油卡充值管理】,ETC 4 子菜单;油卡含原 4 项 +「车油卡信息」。
|
||||||
|
2. ETC 列表仅 etc;新建固定 ETC;无顶部统计。
|
||||||
|
3. 油卡列表无 etc;有顶部统计;卡片跳转进油卡申请。
|
||||||
|
4. 两边审批人设置改同一套节点。
|
||||||
|
5. 旧 URL `/#/vehicle-mgmt/etc-recharge/vehicle-recharge-apply` redirect 到 fuel 路径。
|
||||||
|
6. 小程序未改。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 若有修复,再提交(需用户授权)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spec coverage(自检)
|
||||||
|
|
||||||
|
| 设计要求 | 任务 |
|
||||||
|
| --- | --- |
|
||||||
|
| 并列两模块 + 子菜单 | Task 5–6 |
|
||||||
|
| 油卡排除 etc、界面现状 | Task 3 |
|
||||||
|
| ETC 复制页 + 固定 etc + 无统计 | Task 2、4 |
|
||||||
|
| 后端不整拆 + `rechargeTypeNe` | Task 1 |
|
||||||
|
| 审批/审批人保持一套 + 双入口 | Task 3–4 workflow |
|
||||||
|
| 仅 Web | 全程不改 miniprogram |
|
||||||
|
| 旧 URL redirect | Task 5 |
|
||||||
|
| 角色默认同步 | Task 6 |
|
||||||
|
| 车油卡信息归属油卡组 | Task 5–6 明确保留 |
|
||||||
|
|
||||||
|
## Placeholder scan
|
||||||
|
|
||||||
|
无 TBD/TODO;油卡/ETC 路径、菜单 id、perms、测试命令已钉死。
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
# 充值申请「发放中」状态 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 末级审批通过进入发放节点后,申请状态变为 `pending`,Web 展示为「发放中」;待办/徽章/发放登记与撤销回退一并兼容。
|
||||||
|
|
||||||
|
**Architecture:** 油卡/ETC 共用后端状态机。`approve_apply` 在下一节点为 `disburse` 时写入 `pending`;`_current_node` / `list_pending` / 菜单徽章同时接受 `in_approval` 与 `pending`;撤销发放有流程时回退 `pending`。前端仅改充值申请文案与筛选项(尿素等其它业务不动)。
|
||||||
|
|
||||||
|
**Tech Stack:** FastAPI / SQLAlchemy、pytest、Vue 3 + Element Plus(web-admin)
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-11-12-pl-recharge-pending-disburse-status-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| `backend/app/schemas/pl/vehicle_recharge.py` | `APPLY_STATUSES` 纳入 `pending` |
|
||||||
|
| `backend/app/services/pl_vehicle_recharge_approval_service.py` | 审批推进切 `pending`;待办/当前节点准入 |
|
||||||
|
| `backend/app/services/pl_vehicle_recharge_service.py` | 撤销/换绑回退;关联提示文案 |
|
||||||
|
| `backend/app/services/pl_menu_badge_service.py` | 徽章计数含 `pending` |
|
||||||
|
| `backend/app/api/pl/vehicle_recharge.py` | 导出状态标签「发放中」 |
|
||||||
|
| `backend/tests/test_pl_vehicle_recharge_workflow.py` | 状态机单测 |
|
||||||
|
| `backend/tests/test_pl_menu_badge_recharge_approval.py` | 徽章含 pending |
|
||||||
|
| `web-admin/src/utils/pl-display.ts` | 充值申请 `pending` → 发放中 |
|
||||||
|
| `web-admin/src/views/pl/vehicle-recharge-apply/columnFilterDefs.ts` | 筛选项 |
|
||||||
|
| `web-admin/src/views/pl/etc-vehicle-recharge-apply/columnFilterDefs.ts` | 筛选项 |
|
||||||
|
| `web-admin/src/views/pl/vehicle-recharge-record/index.vue` | 撤销确认文案 |
|
||||||
|
| `web-admin/src/views/pl/etc-vehicle-recharge-record/index.vue` | 撤销确认文案 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Schema + 审批推进写 pending(TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/schemas/pl/vehicle_recharge.py`
|
||||||
|
- Modify: `backend/app/services/pl_vehicle_recharge_approval_service.py`
|
||||||
|
- Modify: `backend/tests/test_pl_vehicle_recharge_workflow.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 改现有断言并加失败用例**
|
||||||
|
|
||||||
|
在 `test_apply_statuses_include_workflow_states` 中让集合包含 `pending`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
assert {"draft", "in_approval", "pending", "rejected", "disbursed", "cancelled"} <= APPLY_STATUSES
|
||||||
|
```
|
||||||
|
|
||||||
|
将 `test_approve_advances_to_next_node` 中通过后断言改为(默认种子流程下一节点是发放):
|
||||||
|
|
||||||
|
```python
|
||||||
|
assert result.status == "pending"
|
||||||
|
assert result.current_node == node_fin
|
||||||
|
```
|
||||||
|
|
||||||
|
新增三级流程用例(一级通过仍审批中,二级通过变发放中):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _seed_three_node_workflow(db) -> tuple[str, str, str, str]:
|
||||||
|
wf_id = "wf-3"
|
||||||
|
n1, n2, n3 = "node-a1", "node-a2", "node-dis"
|
||||||
|
db.add(
|
||||||
|
BxWorkflowDef(
|
||||||
|
id=wf_id,
|
||||||
|
biz_type="vehicle_recharge",
|
||||||
|
wf_name="三级充值流程",
|
||||||
|
status=1,
|
||||||
|
tenant_no="t1",
|
||||||
|
create_dept_id="d1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for nid, name, order, uid, uname, action in (
|
||||||
|
(n1, "一审", 1, "u-a1", "一审", "approve"),
|
||||||
|
(n2, "二审", 2, "u-a2", "二审", "approve"),
|
||||||
|
(n3, "发放", 3, "u-fin", "财务", "disburse"),
|
||||||
|
):
|
||||||
|
db.add(
|
||||||
|
BxWorkflowNode(
|
||||||
|
id=nid,
|
||||||
|
wf_id=wf_id,
|
||||||
|
node_name=name,
|
||||||
|
node_order=order,
|
||||||
|
approver_id=uid,
|
||||||
|
approver_name=uname,
|
||||||
|
action_type=action,
|
||||||
|
tenant_no="t1",
|
||||||
|
create_dept_id="d1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return wf_id, n1, n2, n3
|
||||||
|
|
||||||
|
|
||||||
|
def test_mid_approve_stays_in_approval_final_approve_sets_pending(monkeypatch):
|
||||||
|
from app.services import pl_vehicle_recharge_approval_service as approval_svc
|
||||||
|
from app.services import pl_vehicle_recharge_service as svc
|
||||||
|
|
||||||
|
db = _make_session()
|
||||||
|
_seed_vehicle(db)
|
||||||
|
wf_id, n1, n2, n3 = _seed_three_node_workflow(db)
|
||||||
|
db.add(
|
||||||
|
PlVehicleRechargeApply(
|
||||||
|
id="apply-1",
|
||||||
|
apply_no="YK2026092601",
|
||||||
|
license_plate="鄂A00001",
|
||||||
|
recharge_type="fuel",
|
||||||
|
apply_date=date(2026, 9, 26),
|
||||||
|
applicant_id="u-applicant",
|
||||||
|
status="in_approval",
|
||||||
|
wf_id=wf_id,
|
||||||
|
current_node=n1,
|
||||||
|
approval_round=1,
|
||||||
|
tenant_no="t1",
|
||||||
|
create_dept_id="d1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
_patch_scope(monkeypatch, svc)
|
||||||
|
|
||||||
|
mid = approval_svc.approve_apply(db, "t1", _user("u-a1", "一审"), "apply-1", opinion="ok")
|
||||||
|
assert mid.status == "in_approval"
|
||||||
|
assert mid.current_node == n2
|
||||||
|
|
||||||
|
final = approval_svc.approve_apply(db, "t1", _user("u-a2", "二审"), "apply-1", opinion="ok")
|
||||||
|
assert final.status == "pending"
|
||||||
|
assert final.current_node == n3
|
||||||
|
```
|
||||||
|
|
||||||
|
新增:`pending` 状态下可发放:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_disburse_accepts_pending_status(monkeypatch):
|
||||||
|
from app.services import pl_vehicle_recharge_approval_service as approval_svc
|
||||||
|
from app.services import pl_vehicle_recharge_service as svc
|
||||||
|
from app.schemas.pl.vehicle_recharge import VehicleRechargeRecordCreate
|
||||||
|
|
||||||
|
db = _make_session()
|
||||||
|
_seed_vehicle(db)
|
||||||
|
wf_id, _gm, node_fin = _seed_workflow(db)
|
||||||
|
db.add(
|
||||||
|
PlVehicleRechargeApply(
|
||||||
|
id="apply-1",
|
||||||
|
apply_no="ETC2026092601",
|
||||||
|
license_plate="鄂A00001",
|
||||||
|
recharge_type="etc",
|
||||||
|
apply_date=date(2026, 9, 26),
|
||||||
|
applicant_id="u-applicant",
|
||||||
|
status="pending",
|
||||||
|
wf_id=wf_id,
|
||||||
|
current_node=node_fin,
|
||||||
|
approval_round=1,
|
||||||
|
tenant_no="t1",
|
||||||
|
create_dept_id="d1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
_patch_scope(monkeypatch, svc)
|
||||||
|
payload = VehicleRechargeRecordCreate(
|
||||||
|
rechargeDate=date(2026, 9, 26),
|
||||||
|
rechargeType="etc",
|
||||||
|
amount=Decimal("50.00"),
|
||||||
|
applyId="apply-1",
|
||||||
|
)
|
||||||
|
result = approval_svc.disburse_apply(
|
||||||
|
db, "t1", _user("u-fin", "财务"), "apply-1", payload
|
||||||
|
)
|
||||||
|
assert result.status == "disbursed"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测确认失败**
|
||||||
|
|
||||||
|
Run(在 `backend/`,已激活项目 venv):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_pl_vehicle_recharge_workflow.py::test_apply_statuses_include_workflow_states \
|
||||||
|
tests/test_pl_vehicle_recharge_workflow.py::test_approve_advances_to_next_node \
|
||||||
|
tests/test_pl_vehicle_recharge_workflow.py::test_mid_approve_stays_in_approval_final_approve_sets_pending \
|
||||||
|
tests/test_pl_vehicle_recharge_workflow.py::test_disburse_accepts_pending_status -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL(`pending` 不在集合 / 状态仍为 `in_approval` / `申请单不在审批中`)
|
||||||
|
|
||||||
|
- [ ] **Step 3: 最小实现**
|
||||||
|
|
||||||
|
`vehicle_recharge.py` schema:
|
||||||
|
|
||||||
|
```python
|
||||||
|
APPLY_STATUSES = {"draft", "in_approval", "pending", "rejected", "disbursed", "cancelled"}
|
||||||
|
```
|
||||||
|
|
||||||
|
`pl_vehicle_recharge_approval_service.py` 中 `_current_node`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if apply.status not in ("in_approval", "pending") or not apply.current_node:
|
||||||
|
raise HTTPException(status_code=400, detail="申请单不在可审批/发放状态")
|
||||||
|
```
|
||||||
|
|
||||||
|
`list_pending` 过滤:
|
||||||
|
|
||||||
|
```python
|
||||||
|
PlVehicleRechargeApply.status.in_(("in_approval", "pending")),
|
||||||
|
```
|
||||||
|
|
||||||
|
`approve_apply` 在推进节点后:
|
||||||
|
|
||||||
|
```python
|
||||||
|
next_node = nodes[idx + 1]
|
||||||
|
apply.current_node = next_node.id
|
||||||
|
next_action = (next_node.action_type or "approve").strip() or "approve"
|
||||||
|
if next_action == "disburse":
|
||||||
|
apply.status = "pending"
|
||||||
|
```
|
||||||
|
|
||||||
|
(勿改中间 approve 的 status。)
|
||||||
|
|
||||||
|
- [ ] **Step 4: 再跑 Step 2 命令**
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/schemas/pl/vehicle_recharge.py \
|
||||||
|
backend/app/services/pl_vehicle_recharge_approval_service.py \
|
||||||
|
backend/tests/test_pl_vehicle_recharge_workflow.py
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(pl): set recharge apply to pending when entering disburse node
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: 徽章计数含 pending
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/services/pl_menu_badge_service.py`
|
||||||
|
- Modify: `backend/tests/test_pl_menu_badge_recharge_approval.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 扩展徽章测试**
|
||||||
|
|
||||||
|
在现有 `add_apply` 种子后增加:
|
||||||
|
|
||||||
|
```python
|
||||||
|
add_apply("a6", status="pending", current_node="node-me")
|
||||||
|
```
|
||||||
|
|
||||||
|
并将断言改为:
|
||||||
|
|
||||||
|
```python
|
||||||
|
assert counts[MENU_CODE_RECHARGE_APPROVAL] == 3
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测确认失败**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_pl_menu_badge_recharge_approval.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL(仍为 2)
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现**
|
||||||
|
|
||||||
|
`count_recharge_approval_pending` 中:
|
||||||
|
|
||||||
|
```python
|
||||||
|
PlVehicleRechargeApply.status.in_(("in_approval", "pending")),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 再跑测**
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/services/pl_menu_badge_service.py \
|
||||||
|
backend/tests/test_pl_menu_badge_recharge_approval.py
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
fix(pl): include pending recharge applies in approval menu badge
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: 撤销发放回退为 pending + 文案「发放中」
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/services/pl_vehicle_recharge_service.py`(`delete_records` / 换绑旧申请回退两处:约 1136、1205 行,有 `wf_id` 时现为 `in_approval`)
|
||||||
|
- Modify: `backend/app/api/pl/vehicle_recharge.py`(`RECHARGE_APPLY_STATUS_LABELS`)
|
||||||
|
- Modify: `backend/app/services/pl_vehicle_recharge_service.py`(`仅待发放申请可关联` → `仅发放中申请可关联`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写回退单测(可放在 workflow 测试文件)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_delete_record_with_workflow_reverts_to_pending(monkeypatch):
|
||||||
|
from app.services import pl_vehicle_recharge_service as svc
|
||||||
|
|
||||||
|
db = _make_session()
|
||||||
|
_seed_vehicle(db)
|
||||||
|
wf_id, _gm, node_fin = _seed_workflow(db)
|
||||||
|
db.add(
|
||||||
|
PlVehicleRechargeApply(
|
||||||
|
id="apply-1",
|
||||||
|
apply_no="ETC2026092601",
|
||||||
|
license_plate="鄂A00001",
|
||||||
|
recharge_type="etc",
|
||||||
|
apply_date=date(2026, 9, 26),
|
||||||
|
applicant_id="u-applicant",
|
||||||
|
status="disbursed",
|
||||||
|
wf_id=wf_id,
|
||||||
|
current_node=None,
|
||||||
|
approval_round=1,
|
||||||
|
tenant_no="t1",
|
||||||
|
create_by="u-applicant",
|
||||||
|
create_dept_id="d1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
db.add(
|
||||||
|
PlVehicleRechargeRecord(
|
||||||
|
id="rec-1",
|
||||||
|
recharge_order_no="RO1",
|
||||||
|
recharge_date=date(2026, 9, 26),
|
||||||
|
recharge_type="etc",
|
||||||
|
amount=Decimal("10.00"),
|
||||||
|
apply_id="apply-1",
|
||||||
|
license_plate="鄂A00001",
|
||||||
|
tenant_no="t1",
|
||||||
|
create_by="u-fin",
|
||||||
|
create_dept_id="d1",
|
||||||
|
del_flag=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
_patch_scope(monkeypatch, svc)
|
||||||
|
svc.delete_records(db, "t1", _user("u-fin", "财务"), ["rec-1"])
|
||||||
|
apply = db.query(PlVehicleRechargeApply).filter_by(id="apply-1").one()
|
||||||
|
assert apply.status == "pending"
|
||||||
|
assert apply.current_node == node_fin
|
||||||
|
```
|
||||||
|
|
||||||
|
若 `PlVehicleRechargeRecord` 构造缺必填字段,按模型补齐后保持断言不变。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测确认失败**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_pl_vehicle_recharge_workflow.py::test_delete_record_with_workflow_reverts_to_pending -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL(仍为 `in_approval`)
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现回退与文案**
|
||||||
|
|
||||||
|
两处有 wf 回退改为:
|
||||||
|
|
||||||
|
```python
|
||||||
|
old_apply.status = "pending" # 或 apply.status = "pending"
|
||||||
|
```
|
||||||
|
|
||||||
|
API 标签:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"pending": "发放中",
|
||||||
|
```
|
||||||
|
|
||||||
|
服务提示:
|
||||||
|
|
||||||
|
```python
|
||||||
|
detail="仅发放中申请可关联",
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 跑回退测 + 相关 workflow 测**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_pl_vehicle_recharge_workflow.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/services/pl_vehicle_recharge_service.py \
|
||||||
|
backend/app/api/pl/vehicle_recharge.py \
|
||||||
|
backend/tests/test_pl_vehicle_recharge_workflow.py
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
fix(pl): revert disbursed recharge apply to pending and rename label
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Web 文案与筛选项
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web-admin/src/utils/pl-display.ts`(仅 `getPlRechargeApplyStatusLabel`,**不要**改 `getPlUreaApplyStatusLabel`)
|
||||||
|
- Modify: `web-admin/src/views/pl/vehicle-recharge-apply/columnFilterDefs.ts`
|
||||||
|
- Modify: `web-admin/src/views/pl/etc-vehicle-recharge-apply/columnFilterDefs.ts`
|
||||||
|
- Modify: `web-admin/src/views/pl/vehicle-recharge-record/index.vue`
|
||||||
|
- Modify: `web-admin/src/views/pl/etc-vehicle-recharge-record/index.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 改充值状态文案**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
pending: '发放中',
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 筛选项插入 pending(建议放在审批中之后)**
|
||||||
|
|
||||||
|
油卡与 ETC 的 `RECHARGE_STATUS_OPTIONS`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{ value: 'draft', label: '草稿' },
|
||||||
|
{ value: 'in_approval', label: '审批中' },
|
||||||
|
{ value: 'pending', label: '发放中' },
|
||||||
|
{ value: 'rejected', label: '已驳回' },
|
||||||
|
{ value: 'disbursed', label: '已发放' },
|
||||||
|
{ value: 'cancelled', label: '已作废' }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 撤销确认文案**
|
||||||
|
|
||||||
|
两处 record 页:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'确定撤销该发放登记吗?关联申请将回退为发放中。'
|
||||||
|
```
|
||||||
|
|
||||||
|
全文检索 `web-admin` 内充值相关「待发放」字样,仅改充值申请/发放语境;跳过尿素、运费、备用金。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 静态核对**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在 web-admin/
|
||||||
|
rg "待发放" src/utils/pl-display.ts src/views/pl/vehicle-recharge-apply src/views/pl/etc-vehicle-recharge-apply src/views/pl/vehicle-recharge-record src/views/pl/etc-vehicle-recharge-record
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 上述路径无残留「待发放」(尿素等其它目录可仍有)
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add web-admin/src/utils/pl-display.ts \
|
||||||
|
web-admin/src/views/pl/vehicle-recharge-apply/columnFilterDefs.ts \
|
||||||
|
web-admin/src/views/pl/etc-vehicle-recharge-apply/columnFilterDefs.ts \
|
||||||
|
web-admin/src/views/pl/vehicle-recharge-record/index.vue \
|
||||||
|
web-admin/src/views/pl/etc-vehicle-recharge-record/index.vue
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(web-admin): show recharge pending status as 发放中
|
||||||
|
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 回归验证
|
||||||
|
|
||||||
|
**Files:** 无新文件
|
||||||
|
|
||||||
|
- [ ] **Step 1: 后端相关测试全跑**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_pl_vehicle_recharge_workflow.py \
|
||||||
|
tests/test_pl_menu_badge_recharge_approval.py \
|
||||||
|
tests/test_pl_vehicle_recharge_summary.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 2: 手工冒烟清单(记结果,未跑勿称通过)**
|
||||||
|
|
||||||
|
1. 两级审批 + 发放节点:一审后「审批中」;二审后「发放中」;发放后「已发放」
|
||||||
|
2. 发放节点人审批待办仍可见并发放成功
|
||||||
|
3. 申请列表可用「发放中」筛选
|
||||||
|
4. 油卡、ETC 各至少一单(或同后端断言后单侧冒烟 + 说明)
|
||||||
|
|
||||||
|
- [ ] **Step 3: 若用户要求再 push;默认本任务不 push**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spec coverage check
|
||||||
|
|
||||||
|
| Spec 项 | Task |
|
||||||
|
| --- | --- |
|
||||||
|
| 末级 approve → pending | Task 1 |
|
||||||
|
| 中间仍 in_approval | Task 1 |
|
||||||
|
| 待办/准入认 pending | Task 1 |
|
||||||
|
| 徽章含 pending | Task 2 |
|
||||||
|
| 撤销回退 pending | Task 3 |
|
||||||
|
| 文案「发放中」 | Task 3–4 |
|
||||||
|
| 筛选项 | Task 4 |
|
||||||
|
| 不做历史刷库 / 不改尿素文案 | 明确不做 |
|
||||||
|
| 回归 | Task 5 |
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# 证件到期提醒本页维护证件弹窗(Web)
|
||||||
|
|
||||||
|
## 背景与问题
|
||||||
|
|
||||||
|
Web【证件到期提醒】展示车辆、人员证件过期信息。用户需要补录/更新证件时,若整页跳到【基础信息库】再返回,连续处理多条很麻烦。
|
||||||
|
|
||||||
|
## 已确认决策
|
||||||
|
|
||||||
|
| 项 | 结论 |
|
||||||
|
| --- | --- |
|
||||||
|
| 交互(方案 A) | 在提醒页点「维护证件」,**本页弹出**与基础信息库同款编辑弹窗;关闭后仍停在提醒列表 |
|
||||||
|
| 车辆弹窗 | 复用 `PlVehicleFormDialog`;按 `vehicleType` 传 `self` / `carrier` |
|
||||||
|
| 人员弹窗 | 复用 `PlPersonnelFormDialog`;按 `personnelAffiliation` 传 `self` / `carrier` |
|
||||||
|
| 打开前 | `get` 主体详情以判定归属;缺 ID / 失败则提示,不弹窗 |
|
||||||
|
| 保存后 | 刷新提醒列表与菜单角标;**不**自动将提醒标为已处理 |
|
||||||
|
| 整页跳转 | 提醒页「维护证件」**不再** `router.push` 离开本页 |
|
||||||
|
| `editId` 深链 | 车辆/人员档案页已有的 `editId` 能力可保留(书签/外链),与本页弹窗无关 |
|
||||||
|
| 后端 | 不改 |
|
||||||
|
| 范围 | 仅 Web |
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
**包含**
|
||||||
|
|
||||||
|
- `document-reminder.vue`:操作列改为打开本页 `PlVehicleFormDialog` / `PlPersonnelFormDialog`
|
||||||
|
- `pl-document-reminder-jump.ts`:增加表单模式解析(或复用归属判定);路径函数可保留给深链/兼容
|
||||||
|
|
||||||
|
**不包含**
|
||||||
|
|
||||||
|
- 小程序
|
||||||
|
- 保存后自动 resolve 提醒
|
||||||
|
- 抽离档案列表页内联大表单(已有独立 dialog 组件则直接复用)
|
||||||
|
|
||||||
|
## 前端设计
|
||||||
|
|
||||||
|
### 提醒页
|
||||||
|
|
||||||
|
1. 模板挂载:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<PlVehicleFormDialog
|
||||||
|
v-model="vehicleFormVisible"
|
||||||
|
:vehicle-type="vehicleFormType"
|
||||||
|
:vehicle-id="editingOwnerId"
|
||||||
|
@saved="handleOwnerFormSaved"
|
||||||
|
/>
|
||||||
|
<PlPersonnelFormDialog
|
||||||
|
v-model="personnelFormVisible"
|
||||||
|
:affiliation="personnelFormAffiliation"
|
||||||
|
:personnel-id="editingOwnerId"
|
||||||
|
@saved="handleOwnerFormSaved"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. `handleMaintainCert`:校验 → get → `resolveDocumentReminderFormMode` → 设置 `editingOwnerId` 与 type/affiliation → `vehicleFormVisible` 或 `personnelFormVisible = true`。
|
||||||
|
3. `@saved`:`fetchData()` + `refreshMenuBadges()`;成功提示可选。
|
||||||
|
4. 文案:「缺少主体信息,无法维护」等,不再提「跳转」。
|
||||||
|
|
||||||
|
### 模式解析
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type ReminderFormMode =
|
||||||
|
| { kind: 'vehicle'; vehicleType: 'self' | 'carrier' }
|
||||||
|
| { kind: 'personnel'; affiliation: 'self' | 'carrier' }
|
||||||
|
| null
|
||||||
|
```
|
||||||
|
|
||||||
|
- vehicle + vehicleType self → self,否则 carrier
|
||||||
|
- personnel + affiliation carrier → carrier,否则 self
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
1. 提醒页点「维护证件」不离开当前路由,弹出编辑框(含证件)。
|
||||||
|
2. 关窗后列表仍在,可立刻点下一条。
|
||||||
|
3. 保存后列表/角标刷新;提醒状态仍须手动「处理」。
|
||||||
|
4. 车辆自有/三方、人员山峡/三方弹窗标题与校验与基础信息库一致。
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
用户确认方案 A(本页弹窗)。实现于 `V3.0`。
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# ETC / 油卡充值管理菜单拆分(Web)
|
||||||
|
|
||||||
|
## 背景与问题
|
||||||
|
|
||||||
|
Web【车辆管理】下现有【ETC/油卡充值管理】(路由前缀 `/vehicle-mgmt/etc-recharge/`)将 ETC 与油卡(及山峡、百链等非 ETC 类型)混在同一套申请 / 审批 / 发放 / 审批人设置页面中。布局层在匹配充值路由时还会展示 `PlRechargeSummary` 顶部统计,该统计面向油卡场景,不适合挂在独立 ETC 模块上。
|
||||||
|
|
||||||
|
用户希望:ETC 与油卡分成两个并列模块,前端可独立演进;后端表与接口暂不整模块拆分;审批流程与审批人配置继续共用现有 `biz_type=vehicle_recharge`。
|
||||||
|
|
||||||
|
## 已确认决策
|
||||||
|
|
||||||
|
| 项 | 结论 |
|
||||||
|
| --- | --- |
|
||||||
|
| 产品结构 | 并列【ETC充值管理】与【油卡充值管理】(原名改名) |
|
||||||
|
| ETC 子菜单 | 申请、审批、发放管理、审批人设置 |
|
||||||
|
| 油卡侧 | 界面保持现状;列表等不再展示 `rechargeType=etc` |
|
||||||
|
| ETC 顶部统计 | ETC 分组展示 `PlRechargeSummary`(仅 ETC 信息);油卡分组仅展示加油卡信息(验收调整) |
|
||||||
|
| 前端实现 | 复制现有 4 个页面目录再改,便于后续各自演进 |
|
||||||
|
| 后端实现 | 不拆服务/表;列表与写入靠类型过滤/约束;小改即可 |
|
||||||
|
| 审批流程 / 审批人 | 保持现状,仍一套 `vehicle_recharge`;两边菜单都进同一配置(方案 A) |
|
||||||
|
| 修改端 | 仅 Web 管理端;小程序不改 |
|
||||||
|
| 非 ETC 类型归属 | `fuel` / `shanxia` / `bailian` 等非 `etc` 均留在油卡模块 |
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
**包含**
|
||||||
|
|
||||||
|
- `web-admin`:菜单与路由、页面复制与类型固化、顶部统计路由判定、文案与筛选
|
||||||
|
- `backend`:列表/导出/看板/审批查询落实类型过滤(见后端设计);不拆接口与表
|
||||||
|
- `docs/sql/migrations/`:菜单改名、新建 ETC 分组与子菜单、角色菜单同步;旧 path 由路由 redirect 兼容
|
||||||
|
|
||||||
|
**不包含**
|
||||||
|
|
||||||
|
- 小程序 / 移动 H5
|
||||||
|
- 拆分审批流程 `biz_type`、复制工作流定义、在途单迁移
|
||||||
|
- 拆分数据表或新建独立后端模块
|
||||||
|
- 本期不要求油卡与 ETC 审批人可分别配置(后续若需要再单独立项)
|
||||||
|
|
||||||
|
## 菜单与路由
|
||||||
|
|
||||||
|
### 目标结构(均挂在【车辆管理】`PL_GROUP_VEHICLE` 下)
|
||||||
|
|
||||||
|
1. **【ETC充值管理】**(新建分组)
|
||||||
|
- ETC充值申请
|
||||||
|
- ETC充值审批
|
||||||
|
- ETC发放管理
|
||||||
|
- ETC充值审批人设置
|
||||||
|
2. **【油卡充值管理】**(原 `PL_GROUP_VEH_RECHARGE` 改名)
|
||||||
|
- 油卡充值申请 / 审批 / 发放管理 / 审批人设置(原子菜单改名)
|
||||||
|
|
||||||
|
### 路径约定
|
||||||
|
|
||||||
|
| 模块 | 分组 path | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ETC | `etc-recharge` | 语义与产品名一致;组件指向**复制后的** ETC 页面 |
|
||||||
|
| 油卡 | `fuel-recharge` | 原分组 path 由 `etc-recharge` 改为 `fuel-recharge` |
|
||||||
|
|
||||||
|
完整路径(菜单 SQL 与此对齐):
|
||||||
|
|
||||||
|
- ETC:`/#/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-{apply|approval|record|workflow}`
|
||||||
|
- 油卡:`/#/vehicle-mgmt/fuel-recharge/vehicle-recharge-{apply|approval|record|workflow}`
|
||||||
|
|
||||||
|
### 兼容
|
||||||
|
|
||||||
|
- `web-admin/src/router/modules/pl.ts` 中现有指向 `/vehicle-mgmt/etc-recharge/vehicle-recharge-*` 的 redirect,改为指向 **油卡** 新路径 `/vehicle-mgmt/fuel-recharge/vehicle-recharge-*`(保持书签可用且落到油卡侧)。
|
||||||
|
- 若另有文档/外链写死旧地址,实现计划中列出并改 redirect 或文档。
|
||||||
|
|
||||||
|
### 权限
|
||||||
|
|
||||||
|
- 油卡:保留现有菜单 id 与 `perms`(如 `pl:vehicle-recharge-apply:list`),仅改展示名与分组 path/component 若需要。
|
||||||
|
- ETC:新建菜单 id 与独立 `perms`(如 `pl:etc-vehicle-recharge-apply:list` 等),便于日后分开授权。
|
||||||
|
- 迁移 SQL:对已拥有原充值相关菜单的角色,同步挂上对应 ETC 菜单,保证上线后默认两边都可用;之后可由管理员单独收回。
|
||||||
|
- 审批人设置:ETC / 油卡各一条菜单,component 分别指向复制页与原页,均调用同一工作流 API / `biz_type=vehicle_recharge`。ETC 菜单的 `perms` **复用**现有 `pl:vehicle-recharge-workflow:*`,避免配置权被拆成两套。
|
||||||
|
|
||||||
|
## 前端设计
|
||||||
|
|
||||||
|
### 页面复制
|
||||||
|
|
||||||
|
在 `web-admin/src/views/pl/` 复制:
|
||||||
|
|
||||||
|
| 原路径 | ETC 复制目标(示例名) |
|
||||||
|
| --- | --- |
|
||||||
|
| `vehicle-recharge-apply/` | `etc-vehicle-recharge-apply/` |
|
||||||
|
| `vehicle-recharge-approval/` | `etc-vehicle-recharge-approval/` |
|
||||||
|
| `vehicle-recharge-record/` | `etc-vehicle-recharge-record/` |
|
||||||
|
| `vehicle-recharge-workflow/` | `etc-vehicle-recharge-workflow/`(复制后仅改文案/说明;仍调用同一工作流 API) |
|
||||||
|
|
||||||
|
油卡侧继续使用原目录,做「排除 etc」与文案改名。审批人设置菜单的 `component` 分别指向上述两套页面,避免一边改提示文案影响另一边;业务数据仍是同一 `biz_type`。
|
||||||
|
|
||||||
|
`columnFilterDefs`、本地 `table-key` / 列偏好 localStorage key:ETC 页使用**新 key**,避免与油卡列配置互相污染。
|
||||||
|
|
||||||
|
API 封装:优先继续共用 `web-admin/src/api/pl` 现有 `vehicleRecharge*Api`,在页面请求参数中带类型约束;不为本期再复制一套 API 模块,除非出现仅 ETC 需要的入参。
|
||||||
|
|
||||||
|
### 类型与交互
|
||||||
|
|
||||||
|
**ETC 页**
|
||||||
|
|
||||||
|
- 列表 / 导出 / 待办 / 已办 / 发放看板:固定只查 `rechargeType=etc`(或后端等价过滤)。
|
||||||
|
- 新增/编辑:充值类型固定为 `etc`,不提供油卡/山峡/百链选项;隐藏油卡卡号等仅油卡字段。
|
||||||
|
- 文案、导出文件名、空态、跳转提示改为「ETC…」。
|
||||||
|
- **不展示**顶部 `PlRechargeSummary`。
|
||||||
|
|
||||||
|
**油卡页**
|
||||||
|
|
||||||
|
- 列表 / 导出 / 审批 / 发放:**排除** `rechargeType=etc`;保留 `fuel` / `shanxia` / `bailian` 等现有非 ETC 类型与筛选(筛选中去掉或禁用单独再选 ETC,避免空结果误导)。
|
||||||
|
- 顶部统计:**保留**(仍走现有 summary 组件与接口)。
|
||||||
|
- 其余交互、按钮、审批发放流程与现网一致。
|
||||||
|
|
||||||
|
### 顶部统计路由
|
||||||
|
|
||||||
|
`isRechargeSummaryRoute`(`web-admin/src/utils/pl-recharge-summary.ts`)及 `art-page-content` 中 `showRechargeSummary` / `rechargeApplyPath`:
|
||||||
|
|
||||||
|
- 仅对**油卡**充值相关 path 为 true(例如匹配 `fuel-recharge` 下的 apply/approval/record/workflow)。
|
||||||
|
- ETC 路径一律不展示。
|
||||||
|
- `rechargeApplyPath` 解析菜单 code 时指向油卡申请菜单,保证统计卡片跳转仍进入油卡申请页。
|
||||||
|
|
||||||
|
相关单测(`pl-recharge-summary.test.ts`)同步改断言。
|
||||||
|
|
||||||
|
### 审批人设置(方案 A)
|
||||||
|
|
||||||
|
- 两菜单入口都打开同一业务配置(`biz_type=vehicle_recharge`)。
|
||||||
|
- UI 可增加简短说明:「ETC 与油卡共用同一套审批人配置」,降低「改了一边另一边不变」的误解。
|
||||||
|
- 流程显示名可暂保持「ETC/油卡充值审批流程」,本期不强制改名。
|
||||||
|
|
||||||
|
## 后端设计
|
||||||
|
|
||||||
|
### 查询
|
||||||
|
|
||||||
|
现有列表已支持 `rechargeType` / `rechargeTypeIn`(见 `vehicle_recharge` API 与 `vehicle_recharge_list_filters`)。本期落实:
|
||||||
|
|
||||||
|
- ETC 前端固定传 `rechargeType=etc`。
|
||||||
|
- 油卡「非 etc」过滤:**采用新增可选查询参数 `rechargeTypeNe=etc`**(filter 层 `!=`),避免字典新增类型时维护白名单;保留现有 `rechargeType` / `rechargeTypeIn` 兼容。
|
||||||
|
- 审批 pending/done、发放看板、导出与列表使用同一套类型约束,避免一侧漏过滤。
|
||||||
|
|
||||||
|
### 写入约束
|
||||||
|
|
||||||
|
- 本期**不**引入域请求头,**不**按菜单拆 create/update 接口。
|
||||||
|
- 类型正确性由前端保证:ETC 页只提交 `etc`;油卡页表单选项与默认值不含 `etc`。
|
||||||
|
- 后端 create/update 维持现有字段校验即可;防串改列为后续增强,不阻塞本期验收。
|
||||||
|
|
||||||
|
### 审批与工作流
|
||||||
|
|
||||||
|
- `_BIZ_TYPE_VEHICLE_RECHARGE = "vehicle_recharge"` 不变。
|
||||||
|
- 不新增 workflow 定义,不做在途迁移。
|
||||||
|
- summary 接口可保持现行为(油卡统计);不强制为 ETC 提供第二套 summary。
|
||||||
|
|
||||||
|
## 数据与兼容
|
||||||
|
|
||||||
|
- 表结构不改;历史 `etc` 单据仅在 ETC 菜单可见,非 etc 仅在油卡菜单可见。
|
||||||
|
- 小程序仍使用原混合入口与文案;可能仍能看到/提交两类数据,与 Web 分栏并存,接受为已知差异,后续另项处理。
|
||||||
|
|
||||||
|
## UI 规范
|
||||||
|
|
||||||
|
- 沿用现页模式(复杂业务列表)与 Fresh Mint;不新造布局与主色。
|
||||||
|
- 复制页保持与原页相同的 panel / 工具栏 / 表格 / 弹窗结构;仅改类型范围、文案与统计显隐。
|
||||||
|
- 菜单标题变更后,若申请页有 `panelTitleBarTextRules` 注册需求,按 Web AGENTS 补齐完整文案。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
1. 侧栏可见并列【ETC充值管理】【油卡充值管理】,各含 4 个子菜单(文案正确)。
|
||||||
|
2. ETC 申请/审批/发放仅出现 `rechargeType=etc` 数据;新建只能选/固定 ETC。
|
||||||
|
3. 油卡对应页面不出现 ETC 数据;`fuel` / `shanxia` / `bailian` 仍可见可操作(在权限与数据允许时)。
|
||||||
|
4. ETC 各子路由无顶部充值统计;油卡各子路由仍有统计,且卡片跳转进油卡申请。
|
||||||
|
5. 两边「审批人设置」打开后编辑的是同一套节点;保存一侧,另一侧刷新可见。
|
||||||
|
6. 旧 URL `/vehicle-mgmt/etc-recharge/vehicle-recharge-apply` 等 redirect 到油卡新路径且可用。
|
||||||
|
7. 原已授权充值菜单的角色,上线后默认可进 ETC 新菜单(迁移 SQL 生效后)。
|
||||||
|
8. 小程序行为与菜单未改。
|
||||||
|
|
||||||
|
## 风险
|
||||||
|
|
||||||
|
- 前端双份页面:同类 Bug 可能需改两处;接受为独立演进的代价。
|
||||||
|
- 审批人双入口同配置:需文案提示,否则易误解为已拆分。
|
||||||
|
- 小程序仍混合:与 Web 不一致,需在发布说明中写明。
|
||||||
|
- `rechargeTypeNe` 需在 list/export/approval/board 全路径接通,漏接会导致某一页仍混入 ETC。
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
会话中已确认:复制前端、后端不整拆、审批配置保持现状且双菜单进同一配置(A)、仅 Web。实现计划:`docs/superpowers/plans/2026-11-12-pl-etc-fuel-recharge-split.md`。
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# 充值申请「发放中」状态(审批通过后)
|
||||||
|
|
||||||
|
## 背景与问题
|
||||||
|
|
||||||
|
车辆充值(油卡 / ETC 共用后端)走审批流程时,单据从提交起一直是 `in_approval`(审批中),直到发放登记才变为 `disbursed`(已发放)。
|
||||||
|
|
||||||
|
用户配置多级审批人后,期望:
|
||||||
|
|
||||||
|
- 中间审批级等待处理:仍显示「审批中」
|
||||||
|
- **末级审批人通过、进入发放节点后**:显示「发放中」
|
||||||
|
- 发放登记完成后:显示「已发放」
|
||||||
|
|
||||||
|
系统里已有 `pending` 枚举(原前端文案「待发放」),但有工作流时主路径几乎不会写入该状态;发放撤销在有流程时还会回退为 `in_approval`。申请列表筛选项也未暴露该状态。
|
||||||
|
|
||||||
|
**文案约定(已确认)**:充值申请 `pending` 的对外名称统一为 **「发放中」**(不再用「待发放」)。状态码仍为 `pending`,不改库字段。尿素 / 运费等其它业务的「待发放」文案不在本期范围。
|
||||||
|
|
||||||
|
## 已确认决策
|
||||||
|
|
||||||
|
| 项 | 结论 |
|
||||||
|
| --- | --- |
|
||||||
|
| 切换时机 | 末级 `approve` 节点通过后,若下一节点为 `disburse`(发放登记),将 `status` 设为 `pending` |
|
||||||
|
| 展示文案 | `pending` → **发放中**(Web `getPlRechargeApplyStatusLabel`、导出/后端 `RECHARGE_APPLY_STATUS_LABELS`、筛选项、相关提示语) |
|
||||||
|
| 中间审批 | 仍保持 `in_approval` |
|
||||||
|
| 发放完成 | 仍为 `disbursed` |
|
||||||
|
| 撤销发放 | 有工作流时回退为 `pending`(停在发放节点),不再回退为 `in_approval`;提示改为「回退为发放中」 |
|
||||||
|
| 无工作流路径 | 维持现有 `pending` 语义不变,仅文案改为「发放中」 |
|
||||||
|
| 历史数据 | 本期**不做**批量刷库;已停在发放节点但仍为 `in_approval` 的单据继续可在审批待办发放,列表可能仍显示「审批中」直至被处理 |
|
||||||
|
| 范围 | 油卡 + ETC 共用后端一并生效;Web 列表文案/筛选项同步;小程序不单独改(若共用展示映射则自然带上) |
|
||||||
|
| 不另拆 | 不新建独立「发放中」菜单;发放仍在现有「充值审批」待办中对发放节点操作 |
|
||||||
|
|
||||||
|
## 目标状态机(有工作流)
|
||||||
|
|
||||||
|
```text
|
||||||
|
draft / rejected
|
||||||
|
└─提交─► in_approval # 当前节点为 approve*
|
||||||
|
│
|
||||||
|
├─中间 approve 通过──► in_approval(推进 current_node)
|
||||||
|
│
|
||||||
|
└─末级 approve 通过且下一节点为 disburse
|
||||||
|
─► pending # 发放中;current_node = 发放节点
|
||||||
|
│
|
||||||
|
└─发放登记─► disbursed
|
||||||
|
|
||||||
|
任意审批节点驳回 ─► rejected
|
||||||
|
撤回(规则不变)─► draft
|
||||||
|
撤销发放登记 ─► pending(有 wf)或 pending(无 wf,保持现状)
|
||||||
|
```
|
||||||
|
|
||||||
|
\* 流程末节点必须仍是 `disburse`(现有约束:`approve_apply` 不得收尾)。
|
||||||
|
|
||||||
|
## 后端改动要点
|
||||||
|
|
||||||
|
### 1. 状态常量与文案
|
||||||
|
|
||||||
|
- `APPLY_STATUSES` 正式纳入 `pending`(schema / 单测断言同步)
|
||||||
|
- `RECHARGE_APPLY_STATUS_LABELS["pending"]`:`待发放` → `发放中`
|
||||||
|
- 关联申请错误提示等用户可见「待发放」字样同步改为「发放中」
|
||||||
|
|
||||||
|
### 2. `approve_apply`
|
||||||
|
|
||||||
|
通过当前 `approve` 节点后:
|
||||||
|
|
||||||
|
- `apply.current_node = nodes[idx + 1].id`(不变)
|
||||||
|
- 若下一节点 `action_type == "disburse"`:`apply.status = "pending"`
|
||||||
|
- 否则:保持 `in_approval`
|
||||||
|
|
||||||
|
### 3. 待办与发放登记准入
|
||||||
|
|
||||||
|
当前 `_current_node` / `list_pending` / 菜单徽章只认 `status == "in_approval"`。改为认:
|
||||||
|
|
||||||
|
`status in ("in_approval", "pending")`
|
||||||
|
|
||||||
|
且仍要求 `current_node` 存在、当前用户为该节点审批人。这样「发放中」单据继续出现在审批待办,发放人可完成登记。
|
||||||
|
|
||||||
|
错误文案可区分「不在可处理状态」,但行为上两者同等可办。
|
||||||
|
|
||||||
|
### 4. 撤销 / 换绑发放记录回退
|
||||||
|
|
||||||
|
`delete_records` / 换绑旧申请回退时:有 `wf_id` 时改为 `status = "pending"` + `current_node = 发放节点`(与文案「回退为发放中」一致);无流程仍 `pending`。
|
||||||
|
|
||||||
|
### 5. 发放登记页关联申请
|
||||||
|
|
||||||
|
现有 `_resolve_apply_for_record`:拒绝 `in_approval`、仅允许 `pending`。与新状态机一致——走审批发放应走审批菜单的 `disburse_apply`;记录页手动关联仍只认 `pending` 单。无需为发放节点旁路放开记录页。
|
||||||
|
|
||||||
|
### 6. 看板 / 汇总
|
||||||
|
|
||||||
|
`pl_vehicle_recharge_summary_service` 已将 `pending` 与 `in_approval` 计入 pending 类统计,保持即可;回归确认油卡看板数字仍合理。
|
||||||
|
|
||||||
|
## 前端改动要点
|
||||||
|
|
||||||
|
- `getPlRechargeApplyStatusLabel`:`pending` → `发放中`(仅充值申请映射;尿素等其它模块不动)
|
||||||
|
- 油卡 / ETC 申请列表列头筛选项:增加 `{ value: 'pending', label: '发放中' }`
|
||||||
|
- 发放页撤销确认等「回退为待发放」→「回退为发放中」
|
||||||
|
- 发放页提示「审批中请到审批菜单发放」逻辑可保留(`in_approval`);`pending` 仍可按现有关联逻辑处理
|
||||||
|
- 审批页「待我审批」Tab 不改名(含审批 + 发放节点待办)
|
||||||
|
|
||||||
|
## 测试与验收
|
||||||
|
|
||||||
|
**后端单测(优先)**
|
||||||
|
|
||||||
|
- 两级 approve + 末节点 disburse:一级通过后仍 `in_approval`;二级通过后 `pending` 且 `current_node` 为发放节点
|
||||||
|
- `list_pending` / 徽章计数包含 `pending` 且当前节点审批人匹配的单据
|
||||||
|
- `disburse_apply` 在 `pending` 状态下成功 → `disbursed`
|
||||||
|
- 撤销发放回退为 `pending`(有流程)
|
||||||
|
- 导出/标签映射:`pending` 文案为「发放中」
|
||||||
|
|
||||||
|
**手工验收**
|
||||||
|
|
||||||
|
- 申请列表:二级通过后状态显示「发放中」,可按「发放中」筛选
|
||||||
|
- 发放节点处理人在审批待办仍能看到并完成发放 →「已发放」
|
||||||
|
- 油卡与 ETC 模块各走一单冒烟
|
||||||
|
|
||||||
|
## 明确不做
|
||||||
|
|
||||||
|
- 历史 `in_approval` + 已在发放节点的数据迁移脚本
|
||||||
|
- 拆分发放独立菜单 / 独立待办 API
|
||||||
|
- 修改流程配置 UI(仍要求末节点为发放登记)
|
||||||
|
- 小程序专项改造
|
||||||
|
- 尿素 / 运费 / 备用金等其它业务「待发放」文案改名
|
||||||
|
|
||||||
|
## 风险
|
||||||
|
|
||||||
|
- 若漏改徽章或 `list_pending` 状态条件,会出现「列表已是发放中但待办/角标消失」;实现时与 `approve_apply` 同批改、同批测。
|
||||||
|
- 历史停在发放节点的 `in_approval` 单据列表文案仍为「审批中」,验收时需知悉;若后续需要可另开刷数任务。
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# 单包工程也须声明 packages,否则 pnpm 9+ 报 “packages field missing or empty”
|
||||||
|
packages:
|
||||||
|
- '.'
|
||||||
|
|
||||||
allowBuilds:
|
allowBuilds:
|
||||||
'@parcel/watcher': true
|
'@parcel/watcher': true
|
||||||
'@tailwindcss/oxide': true
|
'@tailwindcss/oxide': true
|
||||||
|
|||||||
@@ -9,15 +9,15 @@
|
|||||||
<article class="fuel-plate-card recharge-card recharge-notice">
|
<article class="fuel-plate-card recharge-card recharge-notice">
|
||||||
<div class="recharge-notice-head"> <div class="plate">消息通知</div><em>最新</em> </div>
|
<div class="recharge-notice-head"> <div class="plate">消息通知</div><em>最新</em> </div>
|
||||||
<div class="recharge-notice-list">
|
<div class="recharge-notice-list">
|
||||||
<div v-for="notice in summary.notices" :key="notice.id" class="recharge-notice-item">
|
<div v-for="notice in displayedNotices" :key="notice.id" class="recharge-notice-item">
|
||||||
<time :datetime="notice.occurredAt">{{ notice.occurredAt.slice(0, 10) }}</time>
|
<time :datetime="notice.occurredAt">{{ notice.occurredAt.slice(0, 10) }}</time>
|
||||||
<span
|
<span
|
||||||
>{{ notice.actorName }}{{ notice.roleName ? `(${notice.roleName})` : ''
|
>{{ notice.actorName }}{{ notice.roleName ? `(${notice.roleName})` : ''
|
||||||
}}{{ notice.action }}{{ notice.licensePlate
|
}}{{ notice.action }}{{ notice.licensePlate
|
||||||
}}{{ notice.rechargeType === 'etc' ? 'ETC' : '加油卡' }}充值;</span
|
}}{{ noticeTypeLabel(notice.rechargeType) }}充值;</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!summary.notices.length" class="recharge-notice-item muted">暂无消息</div>
|
<div v-if="!displayedNotices.length" class="recharge-notice-item muted">暂无消息</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<article
|
<article
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
>{{ statusLabels[vehicle.vehicleStatus] || vehicle.vehicleStatus || '—' }}</el-tag
|
>{{ statusLabels[vehicle.vehicleStatus] || vehicle.vehicleStatus || '—' }}</el-tag
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="recharge-split">
|
<div class="recharge-split" :class="{ 'is-single': kinds.length === 1 }">
|
||||||
<div v-for="kind in kinds" :key="kind.value" class="recharge-pane">
|
<div v-for="kind in kinds" :key="kind.value" class="recharge-pane">
|
||||||
<div class="recharge-pane-h">
|
<div class="recharge-pane-h">
|
||||||
<h4>{{ kind.label }}</h4>
|
<h4>{{ kind.label }}</h4>
|
||||||
@@ -94,10 +94,17 @@
|
|||||||
} from '@/api/pl/recharge-summary'
|
} from '@/api/pl/recharge-summary'
|
||||||
import { pickBxData } from '@/utils/http/bx'
|
import { pickBxData } from '@/utils/http/bx'
|
||||||
import { formatPlVehiclePlateDisplay } from '@/utils/pl-display'
|
import { formatPlVehiclePlateDisplay } from '@/utils/pl-display'
|
||||||
import { rechargeComparison, rechargeSummaryQuery } from '@/utils/pl-recharge-summary'
|
import {
|
||||||
|
rechargeComparison,
|
||||||
|
rechargeSummaryQuery,
|
||||||
|
type RechargeSummaryMode
|
||||||
|
} from '@/utils/pl-recharge-summary'
|
||||||
import mittBus from '@/utils/sys/mittBus'
|
import mittBus from '@/utils/sys/mittBus'
|
||||||
|
|
||||||
const props = defineProps<{ applyPath: string }>()
|
const props = withDefaults(
|
||||||
|
defineProps<{ applyPath: string; mode?: RechargeSummaryMode }>(),
|
||||||
|
{ mode: 'fuel' }
|
||||||
|
)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const summary = ref<RechargeSummary | null>(null)
|
const summary = ref<RechargeSummary | null>(null)
|
||||||
@@ -107,12 +114,22 @@
|
|||||||
vehicle.vehicleType === 'self' && !['油桶', '叉车'].includes(vehicle.licensePlate.trim())
|
vehicle.vehicleType === 'self' && !['油桶', '叉车'].includes(vehicle.licensePlate.trim())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
const displayedNotices = computed(() => {
|
||||||
|
const notices = summary.value?.notices || []
|
||||||
|
if (props.mode === 'etc') return notices.filter((n) => n.rechargeType === 'etc')
|
||||||
|
return notices.filter((n) => n.rechargeType !== 'etc')
|
||||||
|
})
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref(false)
|
const error = ref(false)
|
||||||
const kinds = [
|
const kindDefs = [
|
||||||
{ value: 'etc', label: 'ETC' },
|
{ value: 'etc', label: 'ETC' },
|
||||||
{ value: 'fuel', label: '加油卡' }
|
{ value: 'fuel', label: '加油卡' }
|
||||||
] as const
|
] as const
|
||||||
|
const kinds = computed(() =>
|
||||||
|
kindDefs.filter((kind) =>
|
||||||
|
props.mode === 'etc' ? kind.value === 'etc' : kind.value === 'fuel'
|
||||||
|
)
|
||||||
|
)
|
||||||
const statusLabels: Record<string, string> = {
|
const statusLabels: Record<string, string> = {
|
||||||
available: '可用',
|
available: '可用',
|
||||||
maintaining: '维修中',
|
maintaining: '维修中',
|
||||||
@@ -122,6 +139,11 @@
|
|||||||
Number(value).toLocaleString('zh-CN', { maximumFractionDigits: 0 })
|
Number(value).toLocaleString('zh-CN', { maximumFractionDigits: 0 })
|
||||||
let requestVersion = 0
|
let requestVersion = 0
|
||||||
|
|
||||||
|
function noticeTypeLabel(rechargeType: string) {
|
||||||
|
if (rechargeType === 'etc') return 'ETC'
|
||||||
|
return '加油卡'
|
||||||
|
}
|
||||||
|
|
||||||
function metrics(amounts: RechargeAmounts) {
|
function metrics(amounts: RechargeAmounts) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -186,11 +186,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.recharge-grid {
|
.recharge-grid {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
/* 单栏(油卡/ETC 分开展示):固定一行 5 张 */
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.recharge-card {
|
.recharge-card {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
padding: 12px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recharge-split {
|
.recharge-split {
|
||||||
@@ -199,6 +201,10 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recharge-split.is-single {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.recharge-pane {
|
.recharge-pane {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
@@ -14,7 +14,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PlRechargeSummary v-if="!isFullPage && showRechargeSummary" :apply-path="rechargeApplyPath" />
|
<PlRechargeSummary
|
||||||
|
v-if="!isFullPage && showRechargeSummary && rechargeSummaryMode"
|
||||||
|
:apply-path="rechargeApplyPath"
|
||||||
|
:mode="rechargeSummaryMode"
|
||||||
|
/>
|
||||||
<ArtGroupLeafTabs v-if="!isFullPage && showGroupLeafTabs" />
|
<ArtGroupLeafTabs v-if="!isFullPage && showGroupLeafTabs" />
|
||||||
|
|
||||||
<!-- 勿强制 minHeight:calc(100vh-…),否则门户布局会叠出多余纵向滚动条 -->
|
<!-- 勿强制 minHeight:calc(100vh-…),否则门户布局会叠出多余纵向滚动条 -->
|
||||||
@@ -71,7 +75,11 @@
|
|||||||
import { layoutOutletCacheKey, resolveLeafGroup } from '@/utils/router/leaf-group'
|
import { layoutOutletCacheKey, resolveLeafGroup } from '@/utils/router/leaf-group'
|
||||||
import ArtGroupLeafTabs from '@/components/core/layouts/art-group-leaf-tabs/index.vue'
|
import ArtGroupLeafTabs from '@/components/core/layouts/art-group-leaf-tabs/index.vue'
|
||||||
import PlRechargeSummary from '@/components/business/pl-recharge-summary/index.vue'
|
import PlRechargeSummary from '@/components/business/pl-recharge-summary/index.vue'
|
||||||
import { isRechargeSummaryRoute } from '@/utils/pl-recharge-summary'
|
import {
|
||||||
|
isRechargeSummaryRoute,
|
||||||
|
rechargeSummaryModeFromPath,
|
||||||
|
type RechargeSummaryMode
|
||||||
|
} from '@/utils/pl-recharge-summary'
|
||||||
|
|
||||||
defineOptions({ name: 'ArtPageContent' })
|
defineOptions({ name: 'ArtPageContent' })
|
||||||
|
|
||||||
@@ -93,12 +101,29 @@
|
|||||||
const showGroupLeafTabs = computed(() => (leafGroup.value?.leaves.length || 0) > 1)
|
const showGroupLeafTabs = computed(() => (leafGroup.value?.leaves.length || 0) > 1)
|
||||||
|
|
||||||
const leafGroupId = computed(() => leafGroup.value?.groupId || '')
|
const leafGroupId = computed(() => leafGroup.value?.groupId || '')
|
||||||
const rechargeApplyPath = computed(() =>
|
const rechargeSummaryMode = computed<RechargeSummaryMode | null>(() => {
|
||||||
leafGroup.value?.leaves.find((leaf) => leaf.meta.menuCode === 'pl:vehicle-recharge-apply:list')?.path || ''
|
const fromRoute = rechargeSummaryModeFromPath(route.path)
|
||||||
|
if (fromRoute) return fromRoute
|
||||||
|
for (const leaf of leafGroup.value?.leaves || []) {
|
||||||
|
const mode = rechargeSummaryModeFromPath(leaf.path)
|
||||||
|
if (mode) return mode
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
const rechargeApplyMenuCode = computed(() =>
|
||||||
|
rechargeSummaryMode.value === 'etc'
|
||||||
|
? 'pl:etc-vehicle-recharge-apply:list'
|
||||||
|
: 'pl:vehicle-recharge-apply:list'
|
||||||
)
|
)
|
||||||
const showRechargeSummary = computed(() =>
|
const rechargeApplyPath = computed(
|
||||||
isRechargeSummaryRoute(route.path) ||
|
() =>
|
||||||
!!leafGroup.value?.leaves.some((leaf) => isRechargeSummaryRoute(leaf.path))
|
leafGroup.value?.leaves.find((leaf) => leaf.meta.menuCode === rechargeApplyMenuCode.value)
|
||||||
|
?.path || ''
|
||||||
|
)
|
||||||
|
const showRechargeSummary = computed(
|
||||||
|
() =>
|
||||||
|
isRechargeSummaryRoute(route.path) ||
|
||||||
|
!!leafGroup.value?.leaves.some((leaf) => isRechargeSummaryRoute(leaf.path))
|
||||||
)
|
)
|
||||||
|
|
||||||
const useGroupOutletCache = computed(
|
const useGroupOutletCache = computed(
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听 route.query.editId,调用 openById;结束后清除 editId,避免刷新重复弹窗。
|
||||||
|
* openById 应自行处理加载失败提示。
|
||||||
|
*/
|
||||||
|
export function useRouteEditIdOpen(openById: (id: string) => void | Promise<void>) {
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
async function consumeEditId() {
|
||||||
|
const raw = route.query.editId
|
||||||
|
const id = Array.isArray(raw) ? String(raw[0] || '') : String(raw || '')
|
||||||
|
if (!id.trim()) return
|
||||||
|
try {
|
||||||
|
await openById(id.trim())
|
||||||
|
} finally {
|
||||||
|
if (route.query.editId == null) return
|
||||||
|
const query = { ...route.query }
|
||||||
|
delete query.editId
|
||||||
|
await router.replace({ path: route.path, query })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.editId,
|
||||||
|
() => {
|
||||||
|
void consumeEditId()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -411,57 +411,57 @@ export const plRoutes: AppRouteRecord[] = [
|
|||||||
id: 'PL_REDIRECT_VEH_RECHARGE_APPLY',
|
id: 'PL_REDIRECT_VEH_RECHARGE_APPLY',
|
||||||
name: 'PlVehicleRechargeApplyRedirect',
|
name: 'PlVehicleRechargeApplyRedirect',
|
||||||
path: 'vehicle-recharge-apply',
|
path: 'vehicle-recharge-apply',
|
||||||
redirect: '/vehicle-mgmt/etc-recharge/vehicle-recharge-apply',
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-apply',
|
||||||
meta: { title: 'ETC/油卡充值申请', isHide: true }
|
meta: { title: '油卡充值申请', isHide: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PL_REDIRECT_VEH_RECHARGE_APPLY_NESTED',
|
id: 'PL_REDIRECT_VEH_RECHARGE_APPLY_NESTED',
|
||||||
name: 'PlVehicleRechargeApplyNestedRedirect',
|
name: 'PlVehicleRechargeApplyNestedRedirect',
|
||||||
path: 'vehicle-mgmt/vehicle-recharge-apply',
|
path: 'vehicle-mgmt/vehicle-recharge-apply',
|
||||||
redirect: '/vehicle-mgmt/etc-recharge/vehicle-recharge-apply',
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-apply',
|
||||||
meta: { title: 'ETC/油卡充值申请', isHide: true }
|
meta: { title: '油卡充值申请', isHide: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PL_REDIRECT_VEH_RECHARGE_RECORD',
|
id: 'PL_REDIRECT_VEH_RECHARGE_RECORD',
|
||||||
name: 'PlVehicleRechargeRecordRedirect',
|
name: 'PlVehicleRechargeRecordRedirect',
|
||||||
path: 'vehicle-recharge-record',
|
path: 'vehicle-recharge-record',
|
||||||
redirect: '/vehicle-mgmt/etc-recharge/vehicle-recharge-record',
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-record',
|
||||||
meta: { title: 'ETC/油卡充值发放管理', isHide: true }
|
meta: { title: '油卡充值发放管理', isHide: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PL_REDIRECT_VEH_RECHARGE_RECORD_NESTED',
|
id: 'PL_REDIRECT_VEH_RECHARGE_RECORD_NESTED',
|
||||||
name: 'PlVehicleRechargeRecordNestedRedirect',
|
name: 'PlVehicleRechargeRecordNestedRedirect',
|
||||||
path: 'vehicle-mgmt/vehicle-recharge-record',
|
path: 'vehicle-mgmt/vehicle-recharge-record',
|
||||||
redirect: '/vehicle-mgmt/etc-recharge/vehicle-recharge-record',
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-record',
|
||||||
meta: { title: 'ETC/油卡充值发放管理', isHide: true }
|
meta: { title: '油卡充值发放管理', isHide: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PL_REDIRECT_VEH_RECHARGE_APPROVAL',
|
id: 'PL_REDIRECT_VEH_RECHARGE_APPROVAL',
|
||||||
name: 'PlVehicleRechargeApprovalRedirect',
|
name: 'PlVehicleRechargeApprovalRedirect',
|
||||||
path: 'vehicle-recharge-approval',
|
path: 'vehicle-recharge-approval',
|
||||||
redirect: '/vehicle-mgmt/etc-recharge/vehicle-recharge-approval',
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-approval',
|
||||||
meta: { title: 'ETC/油卡充值审批', isHide: true }
|
meta: { title: '油卡充值审批', isHide: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PL_REDIRECT_VEH_RECHARGE_APPROVAL_NESTED',
|
id: 'PL_REDIRECT_VEH_RECHARGE_APPROVAL_NESTED',
|
||||||
name: 'PlVehicleRechargeApprovalNestedRedirect',
|
name: 'PlVehicleRechargeApprovalNestedRedirect',
|
||||||
path: 'vehicle-mgmt/vehicle-recharge-approval',
|
path: 'vehicle-mgmt/vehicle-recharge-approval',
|
||||||
redirect: '/vehicle-mgmt/etc-recharge/vehicle-recharge-approval',
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-approval',
|
||||||
meta: { title: 'ETC/油卡充值审批', isHide: true }
|
meta: { title: '油卡充值审批', isHide: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PL_REDIRECT_VEH_RECHARGE_WORKFLOW',
|
id: 'PL_REDIRECT_VEH_RECHARGE_WORKFLOW',
|
||||||
name: 'PlVehicleRechargeWorkflowRedirect',
|
name: 'PlVehicleRechargeWorkflowRedirect',
|
||||||
path: 'vehicle-recharge-workflow',
|
path: 'vehicle-recharge-workflow',
|
||||||
redirect: '/vehicle-mgmt/etc-recharge/vehicle-recharge-workflow',
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-workflow',
|
||||||
meta: { title: 'ETC/油卡充值审批人设置', isHide: true }
|
meta: { title: '油卡充值审批人设置', isHide: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PL_REDIRECT_VEH_RECHARGE_WORKFLOW_NESTED',
|
id: 'PL_REDIRECT_VEH_RECHARGE_WORKFLOW_NESTED',
|
||||||
name: 'PlVehicleRechargeWorkflowNestedRedirect',
|
name: 'PlVehicleRechargeWorkflowNestedRedirect',
|
||||||
path: 'vehicle-mgmt/vehicle-recharge-workflow',
|
path: 'vehicle-mgmt/vehicle-recharge-workflow',
|
||||||
redirect: '/vehicle-mgmt/etc-recharge/vehicle-recharge-workflow',
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-workflow',
|
||||||
meta: { title: 'ETC/油卡充值审批人设置', isHide: true }
|
meta: { title: '油卡充值审批人设置', isHide: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PL_REDIRECT_VEH_MAINT_APPLY',
|
id: 'PL_REDIRECT_VEH_MAINT_APPLY',
|
||||||
@@ -566,15 +566,15 @@ export const plRoutes: AppRouteRecord[] = [
|
|||||||
{
|
{
|
||||||
id: 'PL_GROUP_VEH_RECHARGE',
|
id: 'PL_GROUP_VEH_RECHARGE',
|
||||||
name: 'PlVehicleRechargeGroup',
|
name: 'PlVehicleRechargeGroup',
|
||||||
path: 'etc-recharge',
|
path: 'fuel-recharge',
|
||||||
meta: { title: 'ETC/油卡充值管理', keepAlive: false },
|
meta: { title: '油卡充值管理', keepAlive: false },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'PL_VEH_RECHARGE_APPLY',
|
id: 'PL_VEH_RECHARGE_APPLY',
|
||||||
name: 'PlVehicleRechargeApply',
|
name: 'PlVehicleRechargeApply',
|
||||||
path: 'vehicle-recharge-apply',
|
path: 'vehicle-recharge-apply',
|
||||||
component: '/pl/vehicle-recharge-apply/index',
|
component: '/pl/vehicle-recharge-apply/index',
|
||||||
meta: { title: 'ETC/油卡充值申请', keepAlive: false, menuCode: 'pl:vehicle-recharge-apply:list' }
|
meta: { title: '油卡充值申请', keepAlive: false, menuCode: 'pl:vehicle-recharge-apply:list' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PL_VEH_RECHARGE_APPROVAL',
|
id: 'PL_VEH_RECHARGE_APPROVAL',
|
||||||
@@ -582,7 +582,7 @@ export const plRoutes: AppRouteRecord[] = [
|
|||||||
path: 'vehicle-recharge-approval',
|
path: 'vehicle-recharge-approval',
|
||||||
component: '/pl/vehicle-recharge-approval/index',
|
component: '/pl/vehicle-recharge-approval/index',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'ETC/油卡充值审批',
|
title: '油卡充值审批',
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
menuCode: 'pl:vehicle-recharge-approval:list'
|
menuCode: 'pl:vehicle-recharge-approval:list'
|
||||||
}
|
}
|
||||||
@@ -592,7 +592,7 @@ export const plRoutes: AppRouteRecord[] = [
|
|||||||
name: 'PlVehicleRechargeRecord',
|
name: 'PlVehicleRechargeRecord',
|
||||||
path: 'vehicle-recharge-record',
|
path: 'vehicle-recharge-record',
|
||||||
component: '/pl/vehicle-recharge-record/index',
|
component: '/pl/vehicle-recharge-record/index',
|
||||||
meta: { title: 'ETC/油卡充值发放管理', keepAlive: false, menuCode: 'pl:vehicle-recharge-record:list' }
|
meta: { title: '油卡充值发放管理', keepAlive: false, menuCode: 'pl:vehicle-recharge-record:list' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PL_VEH_RECHARGE_WF',
|
id: 'PL_VEH_RECHARGE_WF',
|
||||||
@@ -600,7 +600,7 @@ export const plRoutes: AppRouteRecord[] = [
|
|||||||
path: 'vehicle-recharge-workflow',
|
path: 'vehicle-recharge-workflow',
|
||||||
component: '/pl/vehicle-recharge-workflow/index',
|
component: '/pl/vehicle-recharge-workflow/index',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'ETC/油卡充值审批人设置',
|
title: '油卡充值审批人设置',
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
menuCode: 'pl:vehicle-recharge-workflow:list'
|
menuCode: 'pl:vehicle-recharge-workflow:list'
|
||||||
}
|
}
|
||||||
@@ -614,6 +614,86 @@ export const plRoutes: AppRouteRecord[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'PL_GROUP_VEH_ETC_RECHARGE',
|
||||||
|
name: 'PlVehicleEtcRechargeGroup',
|
||||||
|
path: 'etc-recharge',
|
||||||
|
meta: { title: 'ETC充值管理', keepAlive: false },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'PL_ETC_VEH_RECHARGE_APPLY',
|
||||||
|
name: 'PlEtcVehicleRechargeApply',
|
||||||
|
path: 'etc-vehicle-recharge-apply',
|
||||||
|
component: '/pl/etc-vehicle-recharge-apply/index',
|
||||||
|
meta: {
|
||||||
|
title: 'ETC充值申请',
|
||||||
|
keepAlive: false,
|
||||||
|
menuCode: 'pl:etc-vehicle-recharge-apply:list'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PL_ETC_VEH_RECHARGE_APPROVAL',
|
||||||
|
name: 'PlEtcVehicleRechargeApproval',
|
||||||
|
path: 'etc-vehicle-recharge-approval',
|
||||||
|
component: '/pl/etc-vehicle-recharge-approval/index',
|
||||||
|
meta: {
|
||||||
|
title: 'ETC充值审批',
|
||||||
|
keepAlive: false,
|
||||||
|
menuCode: 'pl:etc-vehicle-recharge-approval:list'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PL_ETC_VEH_RECHARGE_RECORD',
|
||||||
|
name: 'PlEtcVehicleRechargeRecord',
|
||||||
|
path: 'etc-vehicle-recharge-record',
|
||||||
|
component: '/pl/etc-vehicle-recharge-record/index',
|
||||||
|
meta: {
|
||||||
|
title: 'ETC发放管理',
|
||||||
|
keepAlive: false,
|
||||||
|
menuCode: 'pl:etc-vehicle-recharge-record:list'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PL_ETC_VEH_RECHARGE_WF',
|
||||||
|
name: 'PlEtcVehicleRechargeWorkflow',
|
||||||
|
path: 'etc-vehicle-recharge-workflow',
|
||||||
|
component: '/pl/etc-vehicle-recharge-workflow/index',
|
||||||
|
meta: {
|
||||||
|
title: 'ETC充值审批人设置',
|
||||||
|
keepAlive: false,
|
||||||
|
menuCode: 'pl:vehicle-recharge-workflow:list'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PL_REDIRECT_ETC_GROUP_OLD_APPLY',
|
||||||
|
name: 'PlEtcGroupOldApplyRedirect',
|
||||||
|
path: 'vehicle-recharge-apply',
|
||||||
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-apply',
|
||||||
|
meta: { title: '油卡充值申请', isHide: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PL_REDIRECT_ETC_GROUP_OLD_APPROVAL',
|
||||||
|
name: 'PlEtcGroupOldApprovalRedirect',
|
||||||
|
path: 'vehicle-recharge-approval',
|
||||||
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-approval',
|
||||||
|
meta: { title: '油卡充值审批', isHide: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PL_REDIRECT_ETC_GROUP_OLD_RECORD',
|
||||||
|
name: 'PlEtcGroupOldRecordRedirect',
|
||||||
|
path: 'vehicle-recharge-record',
|
||||||
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-record',
|
||||||
|
meta: { title: '油卡充值发放管理', isHide: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PL_REDIRECT_ETC_GROUP_OLD_WORKFLOW',
|
||||||
|
name: 'PlEtcGroupOldWorkflowRedirect',
|
||||||
|
path: 'vehicle-recharge-workflow',
|
||||||
|
redirect: '/vehicle-mgmt/fuel-recharge/vehicle-recharge-workflow',
|
||||||
|
meta: { title: '油卡充值审批人设置', isHide: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'PL_GROUP_VEH_FUEL',
|
id: 'PL_GROUP_VEH_FUEL',
|
||||||
name: 'PlVehicleFuelGroup',
|
name: 'PlVehicleFuelGroup',
|
||||||
|
|||||||
@@ -661,7 +661,7 @@ export function buildFuelRegisterCopyText(row: {
|
|||||||
export function getPlRechargeApplyStatusLabel(status: string) {
|
export function getPlRechargeApplyStatusLabel(status: string) {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
draft: '草稿',
|
draft: '草稿',
|
||||||
pending: '待发放',
|
pending: '发放中',
|
||||||
in_approval: '审批中',
|
in_approval: '审批中',
|
||||||
rejected: '已驳回',
|
rejected: '已驳回',
|
||||||
disbursed: '已发放',
|
disbursed: '已发放',
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { describe, it } from 'node:test'
|
||||||
|
import {
|
||||||
|
resolveDocumentReminderEditPath,
|
||||||
|
resolveDocumentReminderFormMode,
|
||||||
|
DOCUMENT_REMINDER_JUMP_UNSUPPORTED
|
||||||
|
} from './pl-document-reminder-jump'
|
||||||
|
|
||||||
|
describe('证件提醒表单模式与路径', () => {
|
||||||
|
it('车辆按 vehicleType 解析弹窗类型与路径', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveDocumentReminderFormMode({ ownerType: 'vehicle', vehicleType: 'self' }),
|
||||||
|
{ kind: 'vehicle', vehicleType: 'self' }
|
||||||
|
)
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveDocumentReminderFormMode({ ownerType: 'vehicle', vehicleType: 'carrier' }),
|
||||||
|
{ kind: 'vehicle', vehicleType: 'carrier' }
|
||||||
|
)
|
||||||
|
assert.deepEqual(resolveDocumentReminderFormMode({ ownerType: 'vehicle', vehicleType: '' }), {
|
||||||
|
kind: 'vehicle',
|
||||||
|
vehicleType: 'carrier'
|
||||||
|
})
|
||||||
|
assert.equal(
|
||||||
|
resolveDocumentReminderEditPath({ ownerType: 'vehicle', vehicleType: 'self' }),
|
||||||
|
'/vehicle-mgmt/base-info/self-vehicle'
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
resolveDocumentReminderEditPath({ ownerType: 'vehicle', vehicleType: 'carrier' }),
|
||||||
|
'/vehicle-mgmt/base-info/vehicle'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('人员按 personnelAffiliation 解析弹窗归属与路径', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveDocumentReminderFormMode({
|
||||||
|
ownerType: 'personnel',
|
||||||
|
personnelAffiliation: 'self'
|
||||||
|
}),
|
||||||
|
{ kind: 'personnel', affiliation: 'self' }
|
||||||
|
)
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveDocumentReminderFormMode({
|
||||||
|
ownerType: 'personnel',
|
||||||
|
personnelAffiliation: 'carrier'
|
||||||
|
}),
|
||||||
|
{ kind: 'personnel', affiliation: 'carrier' }
|
||||||
|
)
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveDocumentReminderFormMode({
|
||||||
|
ownerType: 'personnel',
|
||||||
|
personnelAffiliation: ''
|
||||||
|
}),
|
||||||
|
{ kind: 'personnel', affiliation: 'self' }
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
resolveDocumentReminderEditPath({
|
||||||
|
ownerType: 'personnel',
|
||||||
|
personnelAffiliation: 'carrier'
|
||||||
|
}),
|
||||||
|
'/vehicle-mgmt/base-info/personnel-carrier'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('不支持的主体返回 null', () => {
|
||||||
|
assert.equal(resolveDocumentReminderFormMode({ ownerType: 'carrier' }), null)
|
||||||
|
assert.equal(resolveDocumentReminderEditPath({ ownerType: '' }), null)
|
||||||
|
assert.equal(DOCUMENT_REMINDER_JUMP_UNSUPPORTED, '暂不支持该主体类型的证件维护跳转')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export const DOCUMENT_REMINDER_JUMP_UNSUPPORTED = '暂不支持该主体类型的证件维护跳转'
|
||||||
|
|
||||||
|
export type ReminderJumpInput = {
|
||||||
|
ownerType?: string | null
|
||||||
|
vehicleType?: string | null
|
||||||
|
personnelAffiliation?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReminderFormMode =
|
||||||
|
| { kind: 'vehicle'; vehicleType: 'self' | 'carrier' }
|
||||||
|
| { kind: 'personnel'; affiliation: 'self' | 'carrier' }
|
||||||
|
|
||||||
|
/** 提醒页本页弹窗:解析车辆/人员表单锁定类型。 */
|
||||||
|
export function resolveDocumentReminderFormMode(
|
||||||
|
input: ReminderJumpInput
|
||||||
|
): ReminderFormMode | null {
|
||||||
|
const ownerType = String(input.ownerType || '').trim()
|
||||||
|
if (ownerType === 'vehicle') {
|
||||||
|
return {
|
||||||
|
kind: 'vehicle',
|
||||||
|
vehicleType: String(input.vehicleType || '').trim() === 'self' ? 'self' : 'carrier'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ownerType === 'personnel') {
|
||||||
|
return {
|
||||||
|
kind: 'personnel',
|
||||||
|
affiliation:
|
||||||
|
String(input.personnelAffiliation || '').trim() === 'carrier' ? 'carrier' : 'self'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 可选深链:档案页 path(提醒主流程已改为本页弹窗)。 */
|
||||||
|
export function resolveDocumentReminderEditPath(input: ReminderJumpInput): string | null {
|
||||||
|
const mode = resolveDocumentReminderFormMode(input)
|
||||||
|
if (!mode) return null
|
||||||
|
if (mode.kind === 'vehicle') {
|
||||||
|
return mode.vehicleType === 'self'
|
||||||
|
? '/vehicle-mgmt/base-info/self-vehicle'
|
||||||
|
: '/vehicle-mgmt/base-info/vehicle'
|
||||||
|
}
|
||||||
|
return mode.affiliation === 'carrier'
|
||||||
|
? '/vehicle-mgmt/base-info/personnel-carrier'
|
||||||
|
: '/vehicle-mgmt/base-info/personnel'
|
||||||
|
}
|
||||||
@@ -3,7 +3,10 @@ import { describe, it } from 'node:test'
|
|||||||
import {
|
import {
|
||||||
rechargeComparison,
|
rechargeComparison,
|
||||||
rechargeSummaryQuery,
|
rechargeSummaryQuery,
|
||||||
isRechargeSummaryRoute
|
isRechargeSummaryRoute,
|
||||||
|
isFuelRechargeSummaryRoute,
|
||||||
|
isEtcRechargeSummaryRoute,
|
||||||
|
rechargeSummaryModeFromPath
|
||||||
} from './pl-recharge-summary'
|
} from './pl-recharge-summary'
|
||||||
|
|
||||||
describe('充值统计卡片', () => {
|
describe('充值统计卡片', () => {
|
||||||
@@ -30,10 +33,60 @@ describe('充值统计卡片', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('只对充值业务子页显示,其他车辆页面不显示', () => {
|
it('油卡分组子页识别为 fuel 模式', () => {
|
||||||
assert.equal(isRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/vehicle-recharge-apply'), true)
|
assert.equal(
|
||||||
assert.equal(isRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/vehicle-recharge-record'), true)
|
isFuelRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-apply'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isFuelRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-record'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isFuelRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-approval'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isFuelRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-workflow'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(rechargeSummaryModeFromPath('/vehicle-mgmt/fuel-recharge/vehicle-recharge-apply'), 'fuel')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ETC 分组子页识别为 etc 模式并展示顶部统计', () => {
|
||||||
|
assert.equal(
|
||||||
|
isEtcRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-apply'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isEtcRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-approval'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isEtcRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-record'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isEtcRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-workflow'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
isRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-apply'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
rechargeSummaryModeFromPath('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-apply'),
|
||||||
|
'etc'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('旧路径与其它车辆页不显示充值统计', () => {
|
||||||
|
assert.equal(
|
||||||
|
isRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/vehicle-recharge-apply'),
|
||||||
|
false
|
||||||
|
)
|
||||||
assert.equal(isRechargeSummaryRoute('/vehicle-mgmt/vehicle-fuel-card'), false)
|
assert.equal(isRechargeSummaryRoute('/vehicle-mgmt/vehicle-fuel-card'), false)
|
||||||
assert.equal(isRechargeSummaryRoute('/vehicle-mgmt/vehicle'), false)
|
assert.equal(isRechargeSummaryRoute('/vehicle-mgmt/vehicle'), false)
|
||||||
|
assert.equal(rechargeSummaryModeFromPath('/vehicle-mgmt/vehicle'), null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -13,10 +13,30 @@ export function rechargeComparison(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RechargeSummaryMode = 'fuel' | 'etc'
|
||||||
|
|
||||||
export function rechargeSummaryQuery(plate: string, kind: 'etc' | 'fuel') {
|
export function rechargeSummaryQuery(plate: string, kind: 'etc' | 'fuel') {
|
||||||
return { rechargePlate: plate, rechargeKind: kind }
|
return { rechargePlate: plate, rechargeKind: kind }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 油卡充值分组下的申请/审批/发放/审批人设置 */
|
||||||
|
export function isFuelRechargeSummaryRoute(path: string) {
|
||||||
|
return /\/fuel-recharge\/vehicle-recharge-(apply|approval|record|workflow)\/?$/.test(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ETC 充值分组下的申请/审批/发放/审批人设置 */
|
||||||
|
export function isEtcRechargeSummaryRoute(path: string) {
|
||||||
|
return /\/etc-recharge\/etc-vehicle-recharge-(apply|approval|record|workflow)\/?$/.test(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否展示顶部充值统计(油卡或 ETC 业务子页) */
|
||||||
export function isRechargeSummaryRoute(path: string) {
|
export function isRechargeSummaryRoute(path: string) {
|
||||||
return /\/vehicle-recharge-(apply|approval|record|workflow)\/?$/.test(path)
|
return isFuelRechargeSummaryRoute(path) || isEtcRechargeSummaryRoute(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前路由对应的统计模式;非充值业务页返回 null */
|
||||||
|
export function rechargeSummaryModeFromPath(path: string): RechargeSummaryMode | null {
|
||||||
|
if (isEtcRechargeSummaryRoute(path)) return 'etc'
|
||||||
|
if (isFuelRechargeSummaryRoute(path)) return 'fuel'
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
@@ -512,6 +512,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { fetchCustomerOptions } from '@/api/crm/customer'
|
import { fetchCustomerOptions } from '@/api/crm/customer'
|
||||||
|
import { listContract } from '@/api/crm/contract'
|
||||||
|
import type { ContractVO } from '@/api/crm/contract/types'
|
||||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||||
import MpWebviewShell from '@/layouts/MpWebviewShell.vue'
|
import MpWebviewShell from '@/layouts/MpWebviewShell.vue'
|
||||||
import MpBusinessHeader from '@/components/mp/MpBusinessHeader.vue'
|
import MpBusinessHeader from '@/components/mp/MpBusinessHeader.vue'
|
||||||
@@ -540,11 +542,14 @@
|
|||||||
} from '@/views/pl/quota-cert/model'
|
} from '@/views/pl/quota-cert/model'
|
||||||
import {
|
import {
|
||||||
bindingsFromPurchaseCerts,
|
bindingsFromPurchaseCerts,
|
||||||
contractsForCustomer,
|
|
||||||
exactCustomerMatches,
|
exactCustomerMatches,
|
||||||
matchPurchaseCert,
|
matchPurchaseCert,
|
||||||
scopeProductsFromLines
|
scopeProductsFromLines
|
||||||
} from '@/views/pl/quota-cert/contract-binder-rules'
|
} from '@/views/pl/quota-cert/contract-binder-rules'
|
||||||
|
import {
|
||||||
|
certArchiveContractListQuery,
|
||||||
|
filterAvailableBindingContracts
|
||||||
|
} from '@/views/pl/quota-cert/cert-archive-contract-query'
|
||||||
import { getProductUnitLabel } from '@/utils/crm-product-display'
|
import { getProductUnitLabel } from '@/utils/crm-product-display'
|
||||||
import { useDictStore } from '@/store/modules/dict'
|
import { useDictStore } from '@/store/modules/dict'
|
||||||
import { showCrmConfirmDialog } from '@/utils/mp-crm-confirm'
|
import { showCrmConfirmDialog } from '@/utils/mp-crm-confirm'
|
||||||
@@ -928,19 +933,39 @@
|
|||||||
if (current === epoch) ElMessage.warning('客户匹配失败,请手工选择')
|
if (current === epoch) ElMessage.warning('客户匹配失败,请手工选择')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function toContractOption(row: ContractVO): ContractOption {
|
||||||
|
return {
|
||||||
|
id: String(row.id),
|
||||||
|
contractNo: row.contractNo || '',
|
||||||
|
contractName: row.contractName || '',
|
||||||
|
contractType: row.contractType,
|
||||||
|
customerId: row.customerId != null ? String(row.customerId) : '',
|
||||||
|
customerName: row.customerName,
|
||||||
|
status: row.status,
|
||||||
|
signDate: row.signDate || null
|
||||||
|
}
|
||||||
|
}
|
||||||
async function searchContracts(keyword = '') {
|
async function searchContracts(keyword = '') {
|
||||||
const current = epoch
|
const current = epoch
|
||||||
|
const query = certArchiveContractListQuery(bindingCustomer.value.id, keyword)
|
||||||
|
if (!query) {
|
||||||
|
contractOptions.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const rows = await quotaCertApi.contracts(keyword)
|
const result = await listContract(query)
|
||||||
if (current !== epoch) return
|
if (current !== epoch) return
|
||||||
const taken = new Set(
|
const taken = (form.value.contractBindings || []).map(
|
||||||
(form.value.contractBindings || []).map((row: CertificateContractBinding) => row.contractId)
|
(row: CertificateContractBinding) => row.contractId
|
||||||
)
|
)
|
||||||
contractOptions.value = contractsForCustomer(rows, bindingCustomer.value).filter(
|
contractOptions.value = filterAvailableBindingContracts(result.rows || [], taken).map(
|
||||||
(row) => !taken.has(row.id)
|
toContractOption
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
if (current === epoch) contractOptions.value = []
|
if (current === epoch) {
|
||||||
|
contractOptions.value = []
|
||||||
|
ElMessage.warning('合同加载失败,请重试')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function openContracts() {
|
function openContracts() {
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import type { ColumnFilterDef } from '@/composables/useClientColumnFilters'
|
||||||
|
|
||||||
|
const RECHARGE_TYPE_OPTIONS = [{ value: 'etc', label: 'ETC' }]
|
||||||
|
|
||||||
|
const RECHARGE_STATUS_OPTIONS = [
|
||||||
|
{ value: 'draft', label: '草稿' },
|
||||||
|
{ value: 'in_approval', label: '审批中' },
|
||||||
|
{ value: 'pending', label: '发放中' },
|
||||||
|
{ value: 'rejected', label: '已驳回' },
|
||||||
|
{ value: 'disbursed', label: '已发放' },
|
||||||
|
{ value: 'cancelled', label: '已作废' }
|
||||||
|
]
|
||||||
|
|
||||||
|
/** ETC 申请列表列头漏斗(服务端)。 */
|
||||||
|
export const plVehicleRechargeApplyColumnFilterDefs: ColumnFilterDef[] = [
|
||||||
|
{ prop: 'applyNo', label: '申请单号', type: 'text', queryKey: 'applyNo' },
|
||||||
|
{ prop: 'licensePlate', label: '车牌号', type: 'text', queryKey: 'licensePlate' },
|
||||||
|
{
|
||||||
|
prop: 'rechargeType',
|
||||||
|
label: '充值类型',
|
||||||
|
type: 'enum',
|
||||||
|
queryKey: 'rechargeType',
|
||||||
|
enumOptions: RECHARGE_TYPE_OPTIONS
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'applyDate',
|
||||||
|
label: '申请日期',
|
||||||
|
type: 'datetime',
|
||||||
|
queryKeyFrom: 'applyDateFrom',
|
||||||
|
queryKeyTo: 'applyDateTo'
|
||||||
|
},
|
||||||
|
{ prop: 'odometerKm', label: '期初公里数', type: 'number', queryKeyPrefix: 'odometerKm' },
|
||||||
|
{ prop: 'applicantName', label: '申请人', type: 'text', queryKey: 'applicantName' },
|
||||||
|
{ prop: 'remark', label: '备注', type: 'text', queryKey: 'remark' },
|
||||||
|
{
|
||||||
|
prop: 'status',
|
||||||
|
label: '状态',
|
||||||
|
type: 'enum',
|
||||||
|
queryKey: 'status',
|
||||||
|
enumOptions: RECHARGE_STATUS_OPTIONS
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
type: 'datetime',
|
||||||
|
queryKeyFrom: 'createTimeFrom',
|
||||||
|
queryKeyTo: 'createTimeTo'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/** ETC 审批列表列头漏斗(服务端;在申请 defs 基础上增加当前节点)。 */
|
||||||
|
export const plVehicleRechargeApprovalColumnFilterDefs: ColumnFilterDef[] = [
|
||||||
|
...plVehicleRechargeApplyColumnFilterDefs.filter((d) => d.prop !== 'createTime'),
|
||||||
|
{ prop: 'currentNodeName', label: '当前节点', type: 'text', queryKey: 'currentNodeName' },
|
||||||
|
{
|
||||||
|
prop: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
type: 'datetime',
|
||||||
|
queryKeyFrom: 'createTimeFrom',
|
||||||
|
queryKeyTo: 'createTimeTo'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/** ETC 发放看板列头漏斗(服务端)。 */
|
||||||
|
export const plVehicleRechargeDisburseColumnFilterDefs: ColumnFilterDef[] = [
|
||||||
|
{ prop: 'applyNo', label: '申请单号', type: 'text', queryKey: 'applyNo' },
|
||||||
|
{
|
||||||
|
prop: 'applyDate',
|
||||||
|
label: '申请日期',
|
||||||
|
type: 'datetime',
|
||||||
|
queryKeyFrom: 'applyDateFrom',
|
||||||
|
queryKeyTo: 'applyDateTo'
|
||||||
|
},
|
||||||
|
{ prop: 'applicantName', label: '申请人', type: 'text', queryKey: 'applicantName' },
|
||||||
|
{ prop: 'odometerKm', label: '期初公里数', type: 'number', queryKeyPrefix: 'odometerKm' },
|
||||||
|
{
|
||||||
|
prop: 'applyStatus',
|
||||||
|
label: '申请状态',
|
||||||
|
type: 'enum',
|
||||||
|
queryKey: 'applyStatus',
|
||||||
|
enumOptions: RECHARGE_STATUS_OPTIONS
|
||||||
|
},
|
||||||
|
{ prop: 'licensePlate', label: '车牌号', type: 'text', queryKey: 'licensePlate' },
|
||||||
|
{
|
||||||
|
prop: 'rechargeType',
|
||||||
|
label: '充值类型',
|
||||||
|
type: 'enum',
|
||||||
|
queryKey: 'rechargeType',
|
||||||
|
enumOptions: RECHARGE_TYPE_OPTIONS
|
||||||
|
},
|
||||||
|
{ prop: 'rechargeOrderNo', label: '充值订单号', type: 'text', queryKey: 'rechargeOrderNo' },
|
||||||
|
{
|
||||||
|
prop: 'rechargeDate',
|
||||||
|
label: '充值日期',
|
||||||
|
type: 'datetime',
|
||||||
|
queryKeyFrom: 'rechargeDateFrom',
|
||||||
|
queryKeyTo: 'rechargeDateTo'
|
||||||
|
},
|
||||||
|
{ prop: 'cardNo', label: '卡号', type: 'text', queryKey: 'cardNo' },
|
||||||
|
{ prop: 'gasStation', label: '加油站/渠道', type: 'text', queryKey: 'gasStation' },
|
||||||
|
{ prop: 'amount', label: '金额(元)', type: 'number', queryKeyPrefix: 'amount' },
|
||||||
|
{
|
||||||
|
prop: 'recordCreateByName',
|
||||||
|
label: '登记人',
|
||||||
|
type: 'text',
|
||||||
|
queryKey: 'recordCreateByName'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/** ETC 审批人设置(客户端)。与油卡共用同一套节点筛选定义。 */
|
||||||
|
export const plVehicleRechargeWorkflowColumnFilterDefs: ColumnFilterDef[] = [
|
||||||
|
{ prop: 'nodeName', label: '节点名称', type: 'text' },
|
||||||
|
{
|
||||||
|
prop: 'actionType',
|
||||||
|
label: '节点动作',
|
||||||
|
type: 'enum',
|
||||||
|
enumOptions: [
|
||||||
|
{ value: 'approve', label: '审批' },
|
||||||
|
{ value: 'disburse', label: '发放登记' }
|
||||||
|
],
|
||||||
|
getValue: (row) => row.actionType || row.action_type || 'approve'
|
||||||
|
},
|
||||||
|
{ prop: 'nodeOrder', label: '节点顺序', type: 'number' },
|
||||||
|
{ prop: 'approverName', label: '审批人', type: 'text' },
|
||||||
|
{
|
||||||
|
prop: 'configStatus',
|
||||||
|
label: '配置状态',
|
||||||
|
type: 'enum',
|
||||||
|
enumOptions: [
|
||||||
|
{ value: 'configured', label: '已配置' },
|
||||||
|
{ value: 'unconfigured', label: '未配置' }
|
||||||
|
],
|
||||||
|
getValue: (row) => ((row.approverId ?? row.approver_id) ? 'configured' : 'unconfigured')
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,835 @@
|
|||||||
|
<!-- ETC充值审批(待办/已办) -->
|
||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<section class="panel">
|
||||||
|
<PlPlanTypeTabs
|
||||||
|
:model-value="activeTab"
|
||||||
|
:tabs="APPROVAL_TABS"
|
||||||
|
@change="handleTabChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="list-toolbar">
|
||||||
|
<div class="list-toolbar-row">
|
||||||
|
<span class="list-toolbar-selected">已选 {{ selectedRows.length }} 项</span>
|
||||||
|
<el-button
|
||||||
|
v-if="hasActiveFilters"
|
||||||
|
class="btn-ghost"
|
||||||
|
@click="clearAllFilters"
|
||||||
|
>
|
||||||
|
<el-icon><Filter /></el-icon>清空列过滤
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="activeTab === 'pending'"
|
||||||
|
class="btn-main"
|
||||||
|
type="primary"
|
||||||
|
:disabled="!canApproveSelected"
|
||||||
|
@click="openHandleDialog(selectedRows[0])"
|
||||||
|
>
|
||||||
|
<el-icon><Check /></el-icon>处理
|
||||||
|
</el-button>
|
||||||
|
<el-button class="btn-ghost" :disabled="selectedRows.length !== 1" @click="openView(selectedRows[0])">
|
||||||
|
<el-icon><View /></el-icon>查看
|
||||||
|
</el-button>
|
||||||
|
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-body list-wrap">
|
||||||
|
<ListTable
|
||||||
|
table-key="pl-etc-vehicle-recharge-approval"
|
||||||
|
class="list-table"
|
||||||
|
:data="tableData"
|
||||||
|
v-loading="loading"
|
||||||
|
stripe
|
||||||
|
@selection-change="(rows) => (selectedRows = rows)"
|
||||||
|
@row-dblclick="openView"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="48" align="center" />
|
||||||
|
<el-table-column type="index" label="序号" width="68" align="center" />
|
||||||
|
<el-table-column prop="applyNo" label="申请单号" min-width="160" show-overflow-tooltip>
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">申请单号</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="applyNo"
|
||||||
|
label="申请单号"
|
||||||
|
type="text"
|
||||||
|
:is-active="isFilterActive('applyNo')"
|
||||||
|
:model-value="filters.applyNo || null"
|
||||||
|
@update:model-value="(v) => setFilterState('applyNo', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link type="primary" @click="openView(row)">{{ row.applyNo }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="licensePlate" label="车牌号" min-width="160" show-overflow-tooltip align="center">
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">车牌号</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="licensePlate"
|
||||||
|
label="车牌号"
|
||||||
|
type="text"
|
||||||
|
:is-active="isFilterActive('licensePlate')"
|
||||||
|
:model-value="filters.licensePlate || null"
|
||||||
|
@update:model-value="(v) => setFilterState('licensePlate', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatPlVehiclePlateDisplay(row.licensePlate, undefined, vehicleTrailerLookup) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="rechargeType" label="充值类型" min-width="100" align="center">
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">充值类型</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="rechargeType"
|
||||||
|
label="充值类型"
|
||||||
|
type="enum"
|
||||||
|
:enum-options="enumOptionsFor('rechargeType')"
|
||||||
|
:is-active="isFilterActive('rechargeType')"
|
||||||
|
:model-value="filters.rechargeType || null"
|
||||||
|
@update:model-value="(v) => setFilterState('rechargeType', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getPlRechargeTypeTagType(row.rechargeType)" size="small">
|
||||||
|
{{ getPlRechargeTypeLabel(row.rechargeType) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="applyDate" label="申请日期" min-width="120" align="center">
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">申请日期</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="applyDate"
|
||||||
|
label="申请日期"
|
||||||
|
type="datetime"
|
||||||
|
:is-active="isFilterActive('applyDate')"
|
||||||
|
:model-value="filters.applyDate || null"
|
||||||
|
@update:model-value="(v) => setFilterState('applyDate', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="odometerKm" label="期初公里数" min-width="110" align="center">
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">期初公里数</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="odometerKm"
|
||||||
|
label="期初公里数"
|
||||||
|
type="number"
|
||||||
|
:is-active="isFilterActive('odometerKm')"
|
||||||
|
:model-value="filters.odometerKm || null"
|
||||||
|
@update:model-value="(v) => setFilterState('odometerKm', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">{{ row.odometerKm ?? '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center">
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">申请人</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="applicantName"
|
||||||
|
label="申请人"
|
||||||
|
type="text"
|
||||||
|
:is-active="isFilterActive('applicantName')"
|
||||||
|
:model-value="filters.applicantName || null"
|
||||||
|
@update:model-value="(v) => setFilterState('applicantName', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="attachmentUrls" label="附件" min-width="220" align="left" header-align="left">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<PlPlanDocumentAttachmentCell
|
||||||
|
:attachments="toRechargeAttachmentItems(row.attachmentUrls)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="remark"
|
||||||
|
label="备注"
|
||||||
|
min-width="160"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">备注</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="remark"
|
||||||
|
label="备注"
|
||||||
|
type="text"
|
||||||
|
:is-active="isFilterActive('remark')"
|
||||||
|
:model-value="filters.remark || null"
|
||||||
|
@update:model-value="(v) => setFilterState('remark', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="currentNodeName" label="当前节点" min-width="120" show-overflow-tooltip>
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">当前节点</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="currentNodeName"
|
||||||
|
label="当前节点"
|
||||||
|
type="text"
|
||||||
|
:is-active="isFilterActive('currentNodeName')"
|
||||||
|
:model-value="filters.currentNodeName || null"
|
||||||
|
@update:model-value="(v) => setFilterState('currentNodeName', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" min-width="100" align="center">
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">状态</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="status"
|
||||||
|
label="状态"
|
||||||
|
type="enum"
|
||||||
|
:enum-options="enumOptionsFor('status')"
|
||||||
|
:is-active="isFilterActive('status')"
|
||||||
|
:model-value="filters.status || null"
|
||||||
|
@update:model-value="(v) => setFilterState('status', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getPlRechargeApplyStatusTagType(row.status)" size="small">
|
||||||
|
{{ getPlRechargeApplyStatusLabel(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center">
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">创建时间</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="createTime"
|
||||||
|
label="创建时间"
|
||||||
|
type="datetime"
|
||||||
|
:is-active="isFilterActive('createTime')"
|
||||||
|
:model-value="filters.createTime || null"
|
||||||
|
@update:model-value="(v) => setFilterState('createTime', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">{{ formatPlDateTime(row.createTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</ListTable>
|
||||||
|
<div class="pagination list-pager-bar">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pager.page"
|
||||||
|
v-model:page-size="pager.pageSize"
|
||||||
|
:total="pager.total"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
@size-change="fetchData"
|
||||||
|
@current-change="fetchData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<SystemViewDialog
|
||||||
|
v-model:visible="viewVisible"
|
||||||
|
:title="viewDetail?.applyNo || '申请详情'"
|
||||||
|
:sections="viewSections"
|
||||||
|
width="960px"
|
||||||
|
modal-class="order-module-dialog-mask"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<template v-if="activeTab === 'pending' && viewDetail" #hero-actions>
|
||||||
|
<el-button type="primary" @click="openHandleDialog(viewDetail)">
|
||||||
|
<el-icon><Check /></el-icon>处理
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template #approval-flow>
|
||||||
|
<BxApprovalFlowTabs
|
||||||
|
section-title="审批记录"
|
||||||
|
:nodes="viewApprovalFlowNodes"
|
||||||
|
:history="viewApprovalHistoryItems"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #apply-attachments>
|
||||||
|
<AttachmentUploadField
|
||||||
|
:model-value="viewDetail?.attachmentUrls || ''"
|
||||||
|
disabled
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #record-attachments>
|
||||||
|
<AttachmentUploadField
|
||||||
|
:model-value="viewDetail?.record?.attachmentUrls || ''"
|
||||||
|
disabled
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</SystemViewDialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="handleVisible"
|
||||||
|
:title="handleDetail?.applyNo || '审批处理'"
|
||||||
|
width="1280px"
|
||||||
|
align-center
|
||||||
|
class="dialog-form-layout"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-form label-position="top" class="dialog-form">
|
||||||
|
<!-- 上方:审批信息 -->
|
||||||
|
<div class="handle-section">
|
||||||
|
<div class="handle-section__title">审批信息</div>
|
||||||
|
<div class="form-grid-4">
|
||||||
|
<el-form-item label="当前节点">
|
||||||
|
<el-input :model-value="handleDetail?.currentNodeName || '—'" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="节点动作">
|
||||||
|
<el-input :model-value="isDisburseNode ? '发放登记' : '审批'" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="isDisburseNode">
|
||||||
|
<el-form-item label="充值日期" required>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="disburseForm.rechargeDate"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="充值金额" required>
|
||||||
|
<el-input-number
|
||||||
|
v-model="disburseForm.amount"
|
||||||
|
:min="0.01"
|
||||||
|
:precision="2"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="卡号">
|
||||||
|
<el-input v-model="disburseForm.cardNo" placeholder="卡号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="加油站/渠道">
|
||||||
|
<el-select
|
||||||
|
v-model="disburseForm.gasStation"
|
||||||
|
allow-create
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
placeholder="请选择或输入"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option v-for="s in PL_GAS_STATION_PRESETS" :key="s" :label="s" :value="s" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" class="form-col-span-half">
|
||||||
|
<el-input v-model="opinion" type="textarea" :rows="2" placeholder="选填;驳回时请填写原因" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="发放附件" class="form-col-span-2">
|
||||||
|
<AttachmentUploadField
|
||||||
|
ref="disburseAttachmentRef"
|
||||||
|
v-model="disburseForm.attachmentUrls"
|
||||||
|
folder="pl/vehicle-recharge/record"
|
||||||
|
accept="image/*,.pdf"
|
||||||
|
:limit="5"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form-item v-else label="备注" class="form-col-span-2">
|
||||||
|
<el-input
|
||||||
|
v-model="opinion"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="默认同意;驳回时请说明原因"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 充值申请详情(只读信息元素,对齐查看弹窗) -->
|
||||||
|
<div class="handle-section">
|
||||||
|
<div class="handle-section__title">ETC充值申请详情</div>
|
||||||
|
<ViewKvGrid :fields="handleApplyDetailFields" :columns="4" />
|
||||||
|
<div class="handle-apply-attachments">
|
||||||
|
<div class="handle-apply-attachments__label">附件</div>
|
||||||
|
<AttachmentUploadField
|
||||||
|
:model-value="handleDetail?.attachmentUrls || ''"
|
||||||
|
disabled
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 审批记录 -->
|
||||||
|
<div class="handle-section">
|
||||||
|
<BxApprovalFlowTabs
|
||||||
|
section-title="审批记录"
|
||||||
|
:nodes="handleApprovalFlowNodes"
|
||||||
|
:history="handleApprovalHistoryItems"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button class="btn-ghost" @click="handleVisible = false">
|
||||||
|
<el-icon><Close /></el-icon>取消
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" class="btn-ghost" :loading="submitting" @click="handleReject">
|
||||||
|
<el-icon><CloseBold /></el-icon>驳回
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="!isDisburseNode"
|
||||||
|
type="primary"
|
||||||
|
class="btn-main"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="handleApprove"
|
||||||
|
>
|
||||||
|
<el-icon><Check /></el-icon>通过
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="primary"
|
||||||
|
class="btn-main"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="handleDisburse"
|
||||||
|
>
|
||||||
|
<el-icon><Check /></el-icon>确认发放
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import mittBus from '@/utils/sys/mittBus'
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { Check, Close, CloseBold, Filter, View } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||||
|
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
|
||||||
|
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||||
|
import PlPlanDocumentAttachmentCell from '@/components/business/pl-plan-document-attachment-cell/index.vue'
|
||||||
|
import PlPlanTypeTabs from '@/components/business/pl-plan-type-tabs/index.vue'
|
||||||
|
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||||
|
import ViewKvGrid from '@/components/business/system-view-dialog/ViewKvGrid.vue'
|
||||||
|
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||||
|
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
|
||||||
|
import { useServerColumnFilters } from '@/composables/useServerColumnFilters'
|
||||||
|
import { plVehicleRechargeApprovalColumnFilterDefs } from '@/views/pl/etc-vehicle-recharge-apply/columnFilterDefs'
|
||||||
|
import {
|
||||||
|
vehicleApi,
|
||||||
|
vehicleRechargeApplyApi,
|
||||||
|
vehicleRechargeApprovalApi,
|
||||||
|
type PlanDocumentAttachmentItem,
|
||||||
|
type Vehicle,
|
||||||
|
type VehicleRechargeApply
|
||||||
|
} from '@/api/pl'
|
||||||
|
import { pickBxPageResult } from '@/utils/http/bx'
|
||||||
|
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
|
||||||
|
import {
|
||||||
|
buildVehicleTrailerLookup,
|
||||||
|
formatPlDateTime,
|
||||||
|
formatPlVehiclePlateDisplay,
|
||||||
|
getPlRechargeApplyStatusLabel,
|
||||||
|
getPlRechargeApplyStatusTagType,
|
||||||
|
getPlRechargeTypeLabel,
|
||||||
|
getPlRechargeTypeTagType,
|
||||||
|
mapVehicleRechargeApprovalFlowToNodes,
|
||||||
|
PL_GAS_STATION_PRESETS
|
||||||
|
} from '@/utils/pl-display'
|
||||||
|
import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
||||||
|
import { useMenuStore } from '@/store/modules/menu'
|
||||||
|
|
||||||
|
const {
|
||||||
|
filters,
|
||||||
|
hasActiveFilters,
|
||||||
|
setFilterState,
|
||||||
|
clearAllFilters,
|
||||||
|
isFilterActive,
|
||||||
|
toQueryParams
|
||||||
|
} = useServerColumnFilters(plVehicleRechargeApprovalColumnFilterDefs, {
|
||||||
|
onChange: () => {
|
||||||
|
pager.page = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function enumOptionsFor(prop: string) {
|
||||||
|
const def = plVehicleRechargeApprovalColumnFilterDefs.find((d) => d.prop === prop)
|
||||||
|
return (def?.enumOptions || []).map((o) => ({ ...o, count: 0 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const APPROVAL_TABS = [
|
||||||
|
{ label: '待我审批', name: 'pending' },
|
||||||
|
{ label: '我已办', name: 'done' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const menuStore = useMenuStore()
|
||||||
|
|
||||||
|
async function refreshListAndBadges() {
|
||||||
|
await fetchData()
|
||||||
|
void refreshMenuBadges(menuStore.menuList)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_APPROVAL_OPINION = '同意'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const activeTab = ref<'pending' | 'done'>('pending')
|
||||||
|
const tableData = ref<VehicleRechargeApply[]>([])
|
||||||
|
const selectedRows = ref<VehicleRechargeApply[]>([])
|
||||||
|
const pager = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||||
|
const vehicleOptions = ref<Vehicle[]>([])
|
||||||
|
const vehicleTrailerLookup = computed(() => buildVehicleTrailerLookup(vehicleOptions.value))
|
||||||
|
|
||||||
|
const viewVisible = ref(false)
|
||||||
|
const viewDetail = ref<VehicleRechargeApply | null>(null)
|
||||||
|
const handleVisible = ref(false)
|
||||||
|
const handleDetail = ref<VehicleRechargeApply | null>(null)
|
||||||
|
let viewRequestId = 0
|
||||||
|
let handleRequestId = 0
|
||||||
|
watch(viewVisible, (visible) => {
|
||||||
|
if (!visible) viewRequestId += 1
|
||||||
|
}, { flush: 'sync' })
|
||||||
|
watch(handleVisible, (visible) => {
|
||||||
|
if (!visible) handleRequestId += 1
|
||||||
|
}, { flush: 'sync' })
|
||||||
|
const opinion = ref(DEFAULT_APPROVAL_OPINION)
|
||||||
|
const disburseForm = reactive({
|
||||||
|
rechargeDate: '',
|
||||||
|
amount: null as number | null,
|
||||||
|
cardNo: '',
|
||||||
|
gasStation: '',
|
||||||
|
attachmentUrls: ''
|
||||||
|
})
|
||||||
|
const disburseAttachmentRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||||
|
|
||||||
|
const canApproveSelected = computed(
|
||||||
|
() => activeTab.value === 'pending' && selectedRows.value.length === 1
|
||||||
|
)
|
||||||
|
const isDisburseNode = computed(
|
||||||
|
() => (handleDetail.value?.currentNodeActionType || 'approve') === 'disburse'
|
||||||
|
)
|
||||||
|
|
||||||
|
const viewApprovalFlowNodes = computed(() =>
|
||||||
|
mapVehicleRechargeApprovalFlowToNodes(viewDetail.value?.approvalFlow || [])
|
||||||
|
)
|
||||||
|
const viewApprovalHistoryItems = computed(() =>
|
||||||
|
mapVehicleRechargeApprovalFlowToNodes(viewDetail.value?.approvalHistory || [], {
|
||||||
|
withRoundPrefix: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleApprovalFlowNodes = computed(() =>
|
||||||
|
mapVehicleRechargeApprovalFlowToNodes(handleDetail.value?.approvalFlow || [])
|
||||||
|
)
|
||||||
|
const handleApprovalHistoryItems = computed(() =>
|
||||||
|
mapVehicleRechargeApprovalFlowToNodes(handleDetail.value?.approvalHistory || [], {
|
||||||
|
withRoundPrefix: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleApplyDetailFields = computed((): ViewField[] => {
|
||||||
|
const d = handleDetail.value
|
||||||
|
if (!d) return []
|
||||||
|
return [
|
||||||
|
{ label: '申请单号', value: d.applyNo },
|
||||||
|
{ label: '申请人', value: d.applicantName || '—' },
|
||||||
|
{
|
||||||
|
label: '车牌号',
|
||||||
|
value: formatPlVehiclePlateDisplay(d.licensePlate, undefined, vehicleTrailerLookup.value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '充值类型',
|
||||||
|
value: getPlRechargeTypeLabel(d.rechargeType),
|
||||||
|
tagType: getPlRechargeTypeTagType(d.rechargeType)
|
||||||
|
},
|
||||||
|
{ label: '申请日期', value: d.applyDate },
|
||||||
|
{ label: '期初公里数', value: d.odometerKm != null ? String(d.odometerKm) : '—' },
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
value: getPlRechargeApplyStatusLabel(d.status),
|
||||||
|
tagType: getPlRechargeApplyStatusTagType(d.status)
|
||||||
|
},
|
||||||
|
{ label: '创建时间', value: formatPlDateTime(d.createTime) },
|
||||||
|
{ label: '备注', value: d.remark || '—', span: true }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
function toRechargeAttachmentItems(raw?: string | null): PlanDocumentAttachmentItem[] {
|
||||||
|
return String(raw || '')
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((url, index) => ({
|
||||||
|
url,
|
||||||
|
label: `附件${index + 1}`
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetOpinionForDetail(detail: VehicleRechargeApply | null) {
|
||||||
|
const disburse = (detail?.currentNodeActionType || 'approve') === 'disburse'
|
||||||
|
opinion.value = disburse ? '' : DEFAULT_APPROVAL_OPINION
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewSections = computed(() => {
|
||||||
|
const d = viewDetail.value
|
||||||
|
if (!d) return []
|
||||||
|
const record = d.record
|
||||||
|
const sections: Array<Record<string, unknown>> = [
|
||||||
|
{
|
||||||
|
title: '申请信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '申请单号', value: d.applyNo },
|
||||||
|
{
|
||||||
|
label: '车牌号',
|
||||||
|
value: formatPlVehiclePlateDisplay(d.licensePlate, undefined, vehicleTrailerLookup.value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '充值类型',
|
||||||
|
value: getPlRechargeTypeLabel(d.rechargeType),
|
||||||
|
tagType: getPlRechargeTypeTagType(d.rechargeType)
|
||||||
|
},
|
||||||
|
{ label: '申请日期', value: d.applyDate },
|
||||||
|
{ label: '期初公里数', value: d.odometerKm != null ? String(d.odometerKm) : '—' },
|
||||||
|
{ label: '申请人', value: d.applicantName || '—' },
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
value: getPlRechargeApplyStatusLabel(d.status),
|
||||||
|
tagType: getPlRechargeApplyStatusTagType(d.status)
|
||||||
|
},
|
||||||
|
{ label: '当前节点', value: d.currentNodeName || '—' },
|
||||||
|
{ label: '备注', value: d.remark || '—', span: 2 as const }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ title: '申请附件', fields: [], slot: 'apply-attachments' },
|
||||||
|
{ title: '', fields: [], slot: 'approval-flow' }
|
||||||
|
]
|
||||||
|
if (record) {
|
||||||
|
sections.push({
|
||||||
|
title: '发放信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '充值订单号', value: record.rechargeOrderNo || '—', long: false },
|
||||||
|
{ label: '充值日期', value: record.rechargeDate || '—' },
|
||||||
|
{
|
||||||
|
label: '充值金额',
|
||||||
|
value: record.amount != null ? `${Number(record.amount).toFixed(2)} 元` : '—'
|
||||||
|
},
|
||||||
|
{ label: '卡号', value: record.cardNo || '—', long: false, fullSpan: false },
|
||||||
|
{ label: '加油站/渠道', value: record.gasStation || '—' },
|
||||||
|
{ label: '发放人', value: record.createByName || '—' },
|
||||||
|
{ label: '备注', value: record.remark || '—', span: true }
|
||||||
|
],
|
||||||
|
slotTitle: '发放附件',
|
||||||
|
slot: 'record-attachments'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
sections.push({
|
||||||
|
title: '发放信息',
|
||||||
|
fields: [{ label: '发放信息', value: '暂无', span: true }]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return sections
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadVehicleOptions() {
|
||||||
|
try {
|
||||||
|
vehicleOptions.value = await fetchAllPagedRecords<Vehicle>(async (page, pageSize) => {
|
||||||
|
const res = await vehicleApi.list({ page, pageSize })
|
||||||
|
return pickBxPageResult<Vehicle>(res)
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
vehicleOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const api = activeTab.value === 'pending' ? vehicleRechargeApprovalApi.pending : vehicleRechargeApprovalApi.done
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
page: pager.page,
|
||||||
|
pageSize: pager.pageSize,
|
||||||
|
...toQueryParams()
|
||||||
|
}
|
||||||
|
delete params.rechargeTypeNe
|
||||||
|
delete params.rechargeTypeIn
|
||||||
|
params.rechargeType = 'etc'
|
||||||
|
const res = await api(params)
|
||||||
|
const data: any = (res as any)?.data ?? res
|
||||||
|
tableData.value = data?.items || []
|
||||||
|
pager.total = data?.total || 0
|
||||||
|
mittBus.emit('rechargeSummaryRefresh')
|
||||||
|
selectedRows.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabChange(name: string) {
|
||||||
|
activeTab.value = name === 'done' ? 'done' : 'pending'
|
||||||
|
pager.page = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openView(row: VehicleRechargeApply) {
|
||||||
|
const requestId = ++viewRequestId
|
||||||
|
viewDetail.value = row
|
||||||
|
viewVisible.value = true
|
||||||
|
try {
|
||||||
|
const res = await vehicleRechargeApplyApi.get(row.id)
|
||||||
|
if (requestId === viewRequestId && viewVisible.value) viewDetail.value = (res as any)?.data ?? res
|
||||||
|
} catch {
|
||||||
|
// keep list row as fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openHandleDialog(row: VehicleRechargeApply) {
|
||||||
|
const requestId = ++handleRequestId
|
||||||
|
handleDetail.value = row
|
||||||
|
resetOpinionForDetail(row)
|
||||||
|
disburseForm.rechargeDate = new Date().toISOString().slice(0, 10)
|
||||||
|
disburseForm.amount = null
|
||||||
|
disburseForm.cardNo = ''
|
||||||
|
disburseForm.gasStation = ''
|
||||||
|
disburseForm.attachmentUrls = ''
|
||||||
|
handleVisible.value = true
|
||||||
|
try {
|
||||||
|
const res = await vehicleRechargeApplyApi.get(row.id)
|
||||||
|
if (requestId !== handleRequestId || !handleVisible.value) return
|
||||||
|
handleDetail.value = (res as any)?.data ?? res
|
||||||
|
resetOpinionForDetail(handleDetail.value)
|
||||||
|
} catch {
|
||||||
|
// keep list row as fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleApprove() {
|
||||||
|
if (!handleDetail.value) return
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await vehicleRechargeApprovalApi.approve(handleDetail.value.id, {
|
||||||
|
opinion: opinion.value.trim() || DEFAULT_APPROVAL_OPINION
|
||||||
|
})
|
||||||
|
ElMessage.success('已通过')
|
||||||
|
handleVisible.value = false
|
||||||
|
viewVisible.value = false
|
||||||
|
await refreshListAndBadges()
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReject() {
|
||||||
|
if (!handleDetail.value) return
|
||||||
|
const text = opinion.value.trim()
|
||||||
|
if (!text || text === DEFAULT_APPROVAL_OPINION) {
|
||||||
|
ElMessage.warning('驳回时请填写备注')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm('确定驳回该申请吗?', '提示', { type: 'warning' })
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await vehicleRechargeApprovalApi.reject(handleDetail.value.id, { opinion: text })
|
||||||
|
ElMessage.success('已驳回')
|
||||||
|
handleVisible.value = false
|
||||||
|
viewVisible.value = false
|
||||||
|
await refreshListAndBadges()
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDisburse() {
|
||||||
|
if (!handleDetail.value) return
|
||||||
|
if (!disburseForm.rechargeDate || !disburseForm.amount || disburseForm.amount <= 0) {
|
||||||
|
ElMessage.warning('请填写充值日期和金额')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (disburseAttachmentRef.value?.hasUploading?.()) {
|
||||||
|
ElMessage.warning('附件上传中,请稍候')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await vehicleRechargeApprovalApi.disburse(handleDetail.value.id, {
|
||||||
|
applyId: handleDetail.value.id,
|
||||||
|
licensePlate: handleDetail.value.licensePlate,
|
||||||
|
rechargeType: 'etc',
|
||||||
|
rechargeDate: disburseForm.rechargeDate,
|
||||||
|
amount: disburseForm.amount,
|
||||||
|
cardNo: disburseForm.cardNo || undefined,
|
||||||
|
gasStation: disburseForm.gasStation || undefined,
|
||||||
|
attachmentUrls: disburseForm.attachmentUrls || undefined
|
||||||
|
})
|
||||||
|
ElMessage.success('发放登记成功')
|
||||||
|
handleVisible.value = false
|
||||||
|
viewVisible.value = false
|
||||||
|
await refreshListAndBadges()
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadVehicleOptions()
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.handle-section {
|
||||||
|
& + & {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--mint-border, #e2e8f0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--mint-text, #1e293b);
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 3px;
|
||||||
|
height: 14px;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: -2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--primary, #10b981);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-apply-attachments {
|
||||||
|
margin-top: 14px;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--mint-text-muted, #64748b);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 四列栅格中占两列(如:加油站右侧的备注) */
|
||||||
|
.form-grid-4 > .form-col-span-half {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,663 @@
|
|||||||
|
<!-- ETC充值审批人设置 -->
|
||||||
|
<!-- eslint-disable vue/no-v-model-argument -->
|
||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<section class="panel">
|
||||||
|
<div class="wf-info-bar">
|
||||||
|
<span><b>流程名称:</b>{{ wfDef.wfName || '-' }}</span>
|
||||||
|
<span>
|
||||||
|
<b>状态:</b>
|
||||||
|
<el-tag :type="statusTagType(wfDef.status)" size="small">
|
||||||
|
{{ statusLabel(wfDef.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
<span class="form-tip">ETC 与油卡共用同一套审批人配置</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-toolbar">
|
||||||
|
<div class="list-toolbar-row">
|
||||||
|
<ListQuickSearchInput
|
||||||
|
v-model="quickSearchKeyword"
|
||||||
|
placeholder="节点名称/审批人"
|
||||||
|
@search="handleQuickSearch"
|
||||||
|
@clear="handleQuickSearchClear"
|
||||||
|
/>
|
||||||
|
<el-button class="btn-main" type="primary" @click="openAddDialog">
|
||||||
|
<el-icon><Plus /></el-icon>新增节点
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
class="btn-ghost"
|
||||||
|
:disabled="selectedRows.length !== 1"
|
||||||
|
@click="handleToolbarEdit"
|
||||||
|
>
|
||||||
|
<el-icon><EditPen /></el-icon>编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
class="btn-ghost"
|
||||||
|
type="danger"
|
||||||
|
:disabled="selectedRows.length !== 1 || !canDeleteByCreator(selectedRows[0])"
|
||||||
|
:title="selectedRows.length === 1 && !canDeleteByCreator(selectedRows[0]) ? CREATOR_DELETE_TIP : ''"
|
||||||
|
:loading="loadingMap.rowDelete"
|
||||||
|
@click="handleToolbarDelete"
|
||||||
|
>
|
||||||
|
<el-icon><Delete /></el-icon>删除
|
||||||
|
</el-button>
|
||||||
|
<el-button class="btn-ghost" :disabled="selectedRows.length !== 1" @click="handleToolbarMoveUp">
|
||||||
|
<el-icon><ArrowUp /></el-icon>上移
|
||||||
|
</el-button>
|
||||||
|
<el-button class="btn-ghost" :disabled="selectedRows.length !== 1" @click="handleToolbarMoveDown">
|
||||||
|
<el-icon><ArrowDown /></el-icon>下移
|
||||||
|
</el-button>
|
||||||
|
<el-button class="btn-ghost" :loading="loadingMap.save" @click="saveNodes">
|
||||||
|
<el-icon><Upload /></el-icon>保存配置
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<ListToolbarEndActions
|
||||||
|
:loading="loading"
|
||||||
|
:column-defs="listColumnDefs"
|
||||||
|
v-model:visible-columns="visibleColumns"
|
||||||
|
v-model:check-all="checkAll"
|
||||||
|
:is-indeterminate="isIndeterminate"
|
||||||
|
@refresh="loadWfDef"
|
||||||
|
@reset-columns="resetToDefault"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-body list-wrap">
|
||||||
|
<div class="list-table-viewport-anchor">
|
||||||
|
<ListTable
|
||||||
|
table-key="pl-etc-vehicle-recharge-workflow"
|
||||||
|
class="list-table"
|
||||||
|
:data="filteredNodes"
|
||||||
|
v-loading="loading"
|
||||||
|
stripe
|
||||||
|
@selection-change="onSelectionChange"
|
||||||
|
@row-dblclick="openEditDialog"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="48" align="center" header-align="center" />
|
||||||
|
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||||||
|
<el-table-column
|
||||||
|
prop="nodeName"
|
||||||
|
label="节点名称"
|
||||||
|
min-width="180"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">节点名称</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="nodeName"
|
||||||
|
label="节点名称"
|
||||||
|
type="text"
|
||||||
|
:is-active="isFilterActive('nodeName')"
|
||||||
|
:model-value="filters.nodeName || null"
|
||||||
|
@update:model-value="(v) => setFilterState('nodeName', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="actionType"
|
||||||
|
label="节点动作"
|
||||||
|
min-width="120"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">节点动作</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="actionType"
|
||||||
|
label="节点动作"
|
||||||
|
type="enum"
|
||||||
|
:enum-options="collectUniqueOptions('actionType')"
|
||||||
|
:is-active="isFilterActive('actionType')"
|
||||||
|
:model-value="filters.actionType || null"
|
||||||
|
@update:model-value="(v) => setFilterState('actionType', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="(row.actionType || row.action_type || 'approve') === 'disburse' ? 'success' : 'primary'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ (row.actionType || row.action_type || 'approve') === 'disburse' ? '发放登记' : '审批' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
v-if="isColumnVisible('nodeOrder')"
|
||||||
|
prop="nodeOrder"
|
||||||
|
label="节点顺序"
|
||||||
|
min-width="100"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">节点顺序</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="nodeOrder"
|
||||||
|
label="节点顺序"
|
||||||
|
type="number"
|
||||||
|
:is-active="isFilterActive('nodeOrder')"
|
||||||
|
:model-value="filters.nodeOrder || null"
|
||||||
|
@update:model-value="(v) => setFilterState('nodeOrder', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
v-if="isColumnVisible('approverName')"
|
||||||
|
prop="approverName"
|
||||||
|
label="审批人"
|
||||||
|
min-width="120"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">审批人</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="approverName"
|
||||||
|
label="审批人"
|
||||||
|
type="text"
|
||||||
|
:is-active="isFilterActive('approverName')"
|
||||||
|
:model-value="filters.approverName || null"
|
||||||
|
@update:model-value="(v) => setFilterState('approverName', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
v-if="isColumnVisible('configStatus')"
|
||||||
|
prop="configStatus"
|
||||||
|
label="配置状态"
|
||||||
|
min-width="110"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="col-header-with-filter">
|
||||||
|
<span class="col-header-with-filter__label">配置状态</span>
|
||||||
|
<ColumnHeaderFilter
|
||||||
|
prop="configStatus"
|
||||||
|
label="配置状态"
|
||||||
|
type="enum"
|
||||||
|
:enum-options="collectUniqueOptions('configStatus')"
|
||||||
|
:is-active="isFilterActive('configStatus')"
|
||||||
|
:model-value="filters.configStatus || null"
|
||||||
|
@update:model-value="(v) => setFilterState('configStatus', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="(row.approverId ?? row.approver_id) ? 'success' : 'warning'" size="small">
|
||||||
|
{{ (row.approverId ?? row.approver_id) ? '已配置' : '未配置' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</ListTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination list-pager-bar">
|
||||||
|
<div class="list-pager-selected">
|
||||||
|
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogs.addNode"
|
||||||
|
title="新增节点"
|
||||||
|
width="560px"
|
||||||
|
class="dialog-form-layout"
|
||||||
|
align-center
|
||||||
|
destroy-on-close
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="addFormRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-position="top"
|
||||||
|
class="bx-dialog-form"
|
||||||
|
scroll-to-error
|
||||||
|
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||||
|
>
|
||||||
|
<div class="form-grid-2">
|
||||||
|
<el-form-item label="节点名称" prop="nodeName">
|
||||||
|
<el-input v-model="formData.nodeName" placeholder="请输入节点名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="节点顺序">
|
||||||
|
<el-input-number v-model="formData.nodeOrder" controls-position="right" :min="1" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="节点动作" prop="actionType">
|
||||||
|
<el-select v-model="formData.actionType" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="审批" value="approve" />
|
||||||
|
<el-option label="发放登记" value="disburse" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="审批人" style="grid-column: 1 / -1">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.approverId"
|
||||||
|
placeholder="请选择审批人"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleApproverChange"
|
||||||
|
>
|
||||||
|
<el-option v-for="u in userOptions" :key="u.id" :label="u.displayName" :value="u.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button class="btn-ghost" @click="dialogs.addNode = false">
|
||||||
|
<el-icon><Close /></el-icon>取消
|
||||||
|
</el-button>
|
||||||
|
<el-button class="btn-main" type="primary" :loading="loadingMap.addNodeSubmit" @click="submitAddNode">
|
||||||
|
<el-icon><Check /></el-icon>确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogs.editNode"
|
||||||
|
title="编辑节点"
|
||||||
|
width="560px"
|
||||||
|
class="dialog-form-layout"
|
||||||
|
align-center
|
||||||
|
destroy-on-close
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="editFormRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-position="top"
|
||||||
|
class="bx-dialog-form"
|
||||||
|
scroll-to-error
|
||||||
|
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||||
|
>
|
||||||
|
<div class="form-grid-2">
|
||||||
|
<el-form-item label="节点名称" prop="nodeName">
|
||||||
|
<el-input v-model="formData.nodeName" placeholder="请输入节点名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="节点顺序">
|
||||||
|
<el-input-number v-model="formData.nodeOrder" controls-position="right" :min="1" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="节点动作" prop="actionType">
|
||||||
|
<el-select v-model="formData.actionType" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="审批" value="approve" />
|
||||||
|
<el-option label="发放登记" value="disburse" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="审批人" style="grid-column: 1 / -1">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.approverId"
|
||||||
|
placeholder="请选择审批人"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleApproverChange"
|
||||||
|
>
|
||||||
|
<el-option v-for="u in userOptions" :key="u.id" :label="u.displayName" :value="u.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button class="btn-ghost" @click="dialogs.editNode = false">
|
||||||
|
<el-icon><Close /></el-icon>取消
|
||||||
|
</el-button>
|
||||||
|
<el-button class="btn-main" type="primary" :loading="loadingMap.editNodeSubmit" @click="submitEditNode">
|
||||||
|
<el-icon><Check /></el-icon>确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, computed, onMounted } from 'vue'
|
||||||
|
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
|
||||||
|
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||||
|
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||||
|
import { useClientColumnFilters } from '@/composables/useClientColumnFilters'
|
||||||
|
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||||
|
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import type { FormInstance } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
EditPen,
|
||||||
|
Delete,
|
||||||
|
Close,
|
||||||
|
Check,
|
||||||
|
Upload,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { workflowApi } from '@/api/bx'
|
||||||
|
import { fetchUserOptions } from '@/api/admin'
|
||||||
|
import { statusLabel, statusTagType } from '@/utils/bx/format'
|
||||||
|
import { plVehicleRechargeWorkflowColumnFilterDefs } from '@/views/pl/etc-vehicle-recharge-apply/columnFilterDefs'
|
||||||
|
|
||||||
|
type DialogKey = 'addNode' | 'editNode'
|
||||||
|
|
||||||
|
const loadingMap = reactive<Record<string, boolean>>({})
|
||||||
|
const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||||
|
addNode: false,
|
||||||
|
editNode: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const quickSearchKeyword = ref('')
|
||||||
|
const appliedQuickSearch = ref('')
|
||||||
|
|
||||||
|
const listColumnDefs = [
|
||||||
|
{ prop: 'nodeOrder', label: '节点顺序' },
|
||||||
|
{ prop: 'approverName', label: '审批人' },
|
||||||
|
{ prop: 'configStatus', label: '配置状态' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const {
|
||||||
|
visibleColumns,
|
||||||
|
checkAll,
|
||||||
|
isIndeterminate,
|
||||||
|
isColumnVisible,
|
||||||
|
resetToDefault
|
||||||
|
} = usePersistedListColumns('pl-etc-vehicle-recharge-workflow-list-columns', listColumnDefs)
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const selectedRows = ref<Record<string, any>[]>([])
|
||||||
|
const wfDef = ref<Record<string, any>>({})
|
||||||
|
const allNodes = computed(() => wfDef.value.nodes || [])
|
||||||
|
const quickFilteredNodes = computed(() => {
|
||||||
|
const kw = appliedQuickSearch.value.trim().toLowerCase()
|
||||||
|
if (!kw) return allNodes.value
|
||||||
|
return allNodes.value.filter(
|
||||||
|
(n: any) =>
|
||||||
|
String(n.nodeName || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(kw) ||
|
||||||
|
String(n.approverName || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(kw)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
filters,
|
||||||
|
filteredRows: filteredNodes,
|
||||||
|
setFilterState,
|
||||||
|
isFilterActive,
|
||||||
|
collectUniqueOptions
|
||||||
|
} = useClientColumnFilters(quickFilteredNodes, plVehicleRechargeWorkflowColumnFilterDefs)
|
||||||
|
const nodes = allNodes
|
||||||
|
const userOptions = ref<
|
||||||
|
{ id: string; realName: string; username: string; roles: string; displayName: string }[]
|
||||||
|
>([])
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
nodeName: '',
|
||||||
|
nodeOrder: 1,
|
||||||
|
actionType: 'approve' as 'approve' | 'disburse',
|
||||||
|
approverId: null as string | null,
|
||||||
|
approverName: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
nodeName: [{ required: true, message: '节点名称不能为空', trigger: 'blur' }],
|
||||||
|
actionType: [{ required: true, message: '请选择节点动作', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFormRef = ref<FormInstance>()
|
||||||
|
const editFormRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
try {
|
||||||
|
const res = await fetchUserOptions()
|
||||||
|
const data: any = (res as any)?.data ?? res ?? []
|
||||||
|
userOptions.value = (Array.isArray(data) ? data : []).map((u: any) => {
|
||||||
|
const name = u.label || u.nickName || ''
|
||||||
|
const account = u.userNo || ''
|
||||||
|
return {
|
||||||
|
id: String(u.value ?? u.id ?? ''),
|
||||||
|
realName: name,
|
||||||
|
username: account,
|
||||||
|
roles: '',
|
||||||
|
displayName: account ? `${name}(${account})` : name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
userOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWfDef() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await workflowApi.getByBizType('vehicle_recharge')
|
||||||
|
wfDef.value = res.data ?? {}
|
||||||
|
} catch {
|
||||||
|
wfDef.value = {}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadWfDef()
|
||||||
|
loadUsers()
|
||||||
|
})
|
||||||
|
|
||||||
|
function onSelectionChange(rows: Record<string, any>[]) {
|
||||||
|
selectedRows.value = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuickSearch() {
|
||||||
|
appliedQuickSearch.value = quickSearchKeyword.value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuickSearchClear() {
|
||||||
|
quickSearchKeyword.value = ''
|
||||||
|
appliedQuickSearch.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToolbarEdit() {
|
||||||
|
if (selectedRows.value.length !== 1) {
|
||||||
|
ElMessage.warning('请先勾选一条节点')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
openEditDialog(selectedRows.value[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToolbarDelete() {
|
||||||
|
if (selectedRows.value.length !== 1) {
|
||||||
|
ElMessage.warning('请先勾选一条节点')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deleteNode(selectedRows.value[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToolbarMoveUp() {
|
||||||
|
if (selectedRows.value.length !== 1) return
|
||||||
|
moveUp(selectedRows.value[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToolbarMoveDown() {
|
||||||
|
if (selectedRows.value.length !== 1) return
|
||||||
|
moveDown(selectedRows.value[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNodePayload(list: Record<string, any>[]) {
|
||||||
|
return list.map((n, i) => ({
|
||||||
|
id: n.id != null ? String(n.id) : undefined,
|
||||||
|
nodeName: n.nodeName,
|
||||||
|
nodeOrder: i + 1,
|
||||||
|
actionType: n.actionType ?? n.action_type ?? 'approve',
|
||||||
|
approverId: n.approverId ?? n.approver_id ?? null,
|
||||||
|
approverName: n.approverName ?? n.approver_name ?? ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveNodes() {
|
||||||
|
loadingMap.save = true
|
||||||
|
try {
|
||||||
|
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(nodes.value))
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
await loadWfDef()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e.message || '保存失败')
|
||||||
|
} finally {
|
||||||
|
loadingMap.save = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAddDialog() {
|
||||||
|
Object.assign(formData, {
|
||||||
|
nodeName: '',
|
||||||
|
nodeOrder: nodes.value.length + 1,
|
||||||
|
actionType: 'approve',
|
||||||
|
approverId: null,
|
||||||
|
approverName: ''
|
||||||
|
})
|
||||||
|
dialogs.addNode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDialog(node: Record<string, any>) {
|
||||||
|
Object.assign(formData, {
|
||||||
|
nodeName: node.nodeName,
|
||||||
|
nodeOrder: node.nodeOrder,
|
||||||
|
actionType: (node.actionType ?? node.action_type ?? 'approve') as 'approve' | 'disburse',
|
||||||
|
approverId: node.approverId ?? node.approver_id ?? null,
|
||||||
|
approverName: node.approverName ?? node.approver_name ?? ''
|
||||||
|
})
|
||||||
|
dialogs.editNode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleApproverChange(userId: string | null) {
|
||||||
|
if (userId == null) {
|
||||||
|
formData.approverName = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const found = userOptions.value.find((u) => u.id === userId)
|
||||||
|
formData.approverName = found ? found.realName : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAddNode() {
|
||||||
|
const valid = await addFormRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
loadingMap.addNodeSubmit = true
|
||||||
|
try {
|
||||||
|
const newNodes = [
|
||||||
|
...nodes.value,
|
||||||
|
{
|
||||||
|
nodeName: formData.nodeName,
|
||||||
|
nodeOrder: formData.nodeOrder,
|
||||||
|
actionType: formData.actionType,
|
||||||
|
approverId: formData.approverId,
|
||||||
|
approverName: formData.approverName
|
||||||
|
}
|
||||||
|
]
|
||||||
|
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(newNodes))
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
dialogs.addNode = false
|
||||||
|
await loadWfDef()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e.message || '新增失败')
|
||||||
|
} finally {
|
||||||
|
loadingMap.addNodeSubmit = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitEditNode() {
|
||||||
|
const valid = await editFormRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
loadingMap.editNodeSubmit = true
|
||||||
|
try {
|
||||||
|
const newNodes = nodes.value.map((n: Record<string, any>) =>
|
||||||
|
n.nodeOrder === formData.nodeOrder
|
||||||
|
? {
|
||||||
|
...n,
|
||||||
|
nodeName: formData.nodeName,
|
||||||
|
actionType: formData.actionType,
|
||||||
|
approverId: formData.approverId,
|
||||||
|
approverName: formData.approverName
|
||||||
|
}
|
||||||
|
: n
|
||||||
|
)
|
||||||
|
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(newNodes))
|
||||||
|
ElMessage.success('编辑成功')
|
||||||
|
dialogs.editNode = false
|
||||||
|
await loadWfDef()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e.message || '编辑失败')
|
||||||
|
} finally {
|
||||||
|
loadingMap.editNodeSubmit = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteNode(node: Record<string, any>) {
|
||||||
|
if (!canDeleteByCreator(node)) {
|
||||||
|
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm(`确定要删除节点「${node.nodeName}」吗?`, '删除确认', {
|
||||||
|
confirmButtonText: '确定删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
loadingMap.rowDelete = true
|
||||||
|
try {
|
||||||
|
const newNodes = nodes.value.filter((n: Record<string, any>) => n.id !== node.id)
|
||||||
|
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(newNodes))
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
await loadWfDef()
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e !== 'cancel') ElMessage.error(e.message || '删除失败')
|
||||||
|
} finally {
|
||||||
|
loadingMap.rowDelete = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveUp(node: Record<string, any>) {
|
||||||
|
const idx = nodes.value.findIndex((n: Record<string, any>) => n.id === node.id)
|
||||||
|
if (idx <= 0) return
|
||||||
|
const arr = [...nodes.value]
|
||||||
|
const temp = arr[idx]
|
||||||
|
arr[idx] = arr[idx - 1]
|
||||||
|
arr[idx - 1] = temp
|
||||||
|
arr.forEach((n: Record<string, any>, i: number) => (n.nodeOrder = i + 1))
|
||||||
|
try {
|
||||||
|
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(arr))
|
||||||
|
ElMessage.success('上移成功')
|
||||||
|
await loadWfDef()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e.message || '上移失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveDown(node: Record<string, any>) {
|
||||||
|
const arr = [...nodes.value]
|
||||||
|
const idx = arr.findIndex((n: Record<string, any>) => n.id === node.id)
|
||||||
|
if (idx < 0 || idx >= arr.length - 1) return
|
||||||
|
const temp = arr[idx]
|
||||||
|
arr[idx] = arr[idx + 1]
|
||||||
|
arr[idx + 1] = temp
|
||||||
|
arr.forEach((n: Record<string, any>, i: number) => (n.nodeOrder = i + 1))
|
||||||
|
try {
|
||||||
|
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(arr))
|
||||||
|
ElMessage.success('下移成功')
|
||||||
|
await loadWfDef()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e.message || '下移失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -219,6 +219,18 @@
|
|||||||
min-width="160"
|
min-width="160"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
|
<el-table-column label="操作" fixed="right" min-width="140" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:loading="maintainLoadingId === row.id"
|
||||||
|
@click="handleMaintainCert(row)"
|
||||||
|
>
|
||||||
|
<el-icon><EditPen /></el-icon>维护证件
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</ListTable>
|
</ListTable>
|
||||||
</div>
|
</div>
|
||||||
<div ref="docReminderBelowRef">
|
<div ref="docReminderBelowRef">
|
||||||
@@ -230,6 +242,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<PlVehicleFormDialog
|
||||||
|
v-model="vehicleFormVisible"
|
||||||
|
:vehicle-type="vehicleFormType"
|
||||||
|
:vehicle-id="editingOwnerId"
|
||||||
|
@saved="handleOwnerFormSaved"
|
||||||
|
/>
|
||||||
|
<PlPersonnelFormDialog
|
||||||
|
v-model="personnelFormVisible"
|
||||||
|
:affiliation="personnelFormAffiliation"
|
||||||
|
:personnel-id="editingOwnerId"
|
||||||
|
@saved="handleOwnerFormSaved"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -238,8 +263,22 @@ import ListQuickSearchInput from '@/components/business/list-quick-search-input/
|
|||||||
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
|
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
|
||||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||||
import { CircleCheck, Filter } from '@element-plus/icons-vue'
|
import PlVehicleFormDialog from '@/components/business/pl-vehicle-form-dialog/index.vue'
|
||||||
import { inventoryApi, type DocumentReminder } from '@/api/pl'
|
import PlPersonnelFormDialog from '@/components/business/pl-personnel-form-dialog/index.vue'
|
||||||
|
import { CircleCheck, Filter, EditPen } from '@element-plus/icons-vue'
|
||||||
|
import {
|
||||||
|
inventoryApi,
|
||||||
|
vehicleApi,
|
||||||
|
personnelApi,
|
||||||
|
type DocumentReminder,
|
||||||
|
type VehicleDetail,
|
||||||
|
type PersonnelDetail
|
||||||
|
} from '@/api/pl'
|
||||||
|
import { pickBxData } from '@/utils/http/bx'
|
||||||
|
import {
|
||||||
|
resolveDocumentReminderFormMode,
|
||||||
|
DOCUMENT_REMINDER_JUMP_UNSUPPORTED
|
||||||
|
} from '@/utils/pl-document-reminder-jump'
|
||||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||||
import { useServerColumnFilters } from '@/composables/useServerColumnFilters'
|
import { useServerColumnFilters } from '@/composables/useServerColumnFilters'
|
||||||
import { useColumnSort } from '@/composables/useColumnSort'
|
import { useColumnSort } from '@/composables/useColumnSort'
|
||||||
@@ -330,6 +369,13 @@ const { tableHeight, recalc: recalcTableHeight } = useListTableViewportHeight({
|
|||||||
tableRef: docReminderTableRef
|
tableRef: docReminderTableRef
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const maintainLoadingId = ref<string | null>(null)
|
||||||
|
const vehicleFormVisible = ref(false)
|
||||||
|
const personnelFormVisible = ref(false)
|
||||||
|
const vehicleFormType = ref<'self' | 'carrier'>('carrier')
|
||||||
|
const personnelFormAffiliation = ref<'self' | 'carrier'>('self')
|
||||||
|
const editingOwnerId = ref<string | null>(null)
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const resolveLoading = ref(false)
|
const resolveLoading = ref(false)
|
||||||
const reminderList = ref<DocumentReminder[]>([])
|
const reminderList = ref<DocumentReminder[]>([])
|
||||||
@@ -445,6 +491,59 @@ function handleToolbarResolve() {
|
|||||||
handleResolve(selectedRow.value)
|
handleResolve(selectedRow.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleOwnerFormSaved() {
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMaintainCert(row: DocumentReminder) {
|
||||||
|
const ownerId = String(row.ownerId || '').trim()
|
||||||
|
const ownerType = String(row.ownerType || '').trim()
|
||||||
|
if (!ownerId || !ownerType) {
|
||||||
|
ElMessage.warning('缺少主体信息,无法维护证件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
maintainLoadingId.value = row.id
|
||||||
|
try {
|
||||||
|
let mode = null as ReturnType<typeof resolveDocumentReminderFormMode>
|
||||||
|
if (ownerType === 'vehicle') {
|
||||||
|
const res = await vehicleApi.get(ownerId)
|
||||||
|
const detail = pickBxData<VehicleDetail>(res) || (res as any)?.data || res
|
||||||
|
const vehicleType = String((detail as VehicleDetail)?.vehicleType || '')
|
||||||
|
mode = resolveDocumentReminderFormMode({ ownerType, vehicleType })
|
||||||
|
} else if (ownerType === 'personnel') {
|
||||||
|
const res = await personnelApi.get(ownerId)
|
||||||
|
const detail = pickBxData<PersonnelDetail>(res) || (res as any)?.data || res
|
||||||
|
const personnelAffiliation = String(
|
||||||
|
(detail as PersonnelDetail)?.personnelAffiliation || ''
|
||||||
|
)
|
||||||
|
mode = resolveDocumentReminderFormMode({ ownerType, personnelAffiliation })
|
||||||
|
} else {
|
||||||
|
ElMessage.warning(DOCUMENT_REMINDER_JUMP_UNSUPPORTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!mode) {
|
||||||
|
ElMessage.warning(DOCUMENT_REMINDER_JUMP_UNSUPPORTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
editingOwnerId.value = ownerId
|
||||||
|
vehicleFormVisible.value = false
|
||||||
|
personnelFormVisible.value = false
|
||||||
|
await nextTick()
|
||||||
|
if (mode.kind === 'vehicle') {
|
||||||
|
vehicleFormType.value = mode.vehicleType
|
||||||
|
vehicleFormVisible.value = true
|
||||||
|
} else {
|
||||||
|
personnelFormAffiliation.value = mode.affiliation
|
||||||
|
personnelFormVisible.value = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('无法打开主体档案,请确认数据存在且有权限')
|
||||||
|
} finally {
|
||||||
|
maintainLoadingId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
nextTick(() => recalcTableHeight())
|
nextTick(() => recalcTableHeight())
|
||||||
|
|||||||
@@ -562,6 +562,7 @@ import { useColumnFacetOptions } from '@/composables/useColumnFacetOptions'
|
|||||||
import { plPersonnelColumnFilterDefs } from '@/views/pl/personnel/columnFilterDefs'
|
import { plPersonnelColumnFilterDefs } from '@/views/pl/personnel/columnFilterDefs'
|
||||||
import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
|
import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
|
||||||
import { useListMediaActive } from '@/composables/useListMediaActive'
|
import { useListMediaActive } from '@/composables/useListMediaActive'
|
||||||
|
import { useRouteEditIdOpen } from '@/composables/useRouteEditIdOpen'
|
||||||
import { pickBxPageResult } from '@/utils/http/bx'
|
import { pickBxPageResult } from '@/utils/http/bx'
|
||||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime } from '@/utils/format'
|
||||||
@@ -1172,6 +1173,10 @@ async function handleEdit(row: Personnel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useRouteEditIdOpen(async (id) => {
|
||||||
|
await handleEdit({ id } as Personnel)
|
||||||
|
})
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
const valid = await formRef.value.validate().catch(() => false)
|
const valid = await formRef.value.validate().catch(() => false)
|
||||||
if (!valid || !validateCerts()) return
|
if (!valid || !validateCerts()) return
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { test } from 'node:test'
|
||||||
|
import {
|
||||||
|
certArchiveContractListQuery,
|
||||||
|
filterAvailableBindingContracts
|
||||||
|
} from './cert-archive-contract-query'
|
||||||
|
|
||||||
|
test('证件建档合同查询必须带客户 ID,不能只靠前端再过滤最近合同', () => {
|
||||||
|
assert.deepEqual(certArchiveContractListQuery('cust-1', ' HT-01 '), {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
keyword: 'HT-01',
|
||||||
|
customerId: 'cust-1',
|
||||||
|
excludeStatus: 'invalid',
|
||||||
|
includeProducts: false
|
||||||
|
})
|
||||||
|
assert.equal(certArchiveContractListQuery(''), null)
|
||||||
|
assert.equal(certArchiveContractListQuery(' '), null)
|
||||||
|
assert.deepEqual(certArchiveContractListQuery('cust-2', ' ').keyword, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('已关联合同与不可选状态不进入添加列表', () => {
|
||||||
|
const rows = [
|
||||||
|
{ id: 'a', status: 'approved' },
|
||||||
|
{ id: 'b', status: 'executing' },
|
||||||
|
{ id: 'c', status: 'draft' },
|
||||||
|
{ id: 'd', status: 'approved' },
|
||||||
|
{ id: '', status: 'approved' }
|
||||||
|
]
|
||||||
|
assert.deepEqual(
|
||||||
|
filterAvailableBindingContracts(rows, ['d', '']).map((item) => item.id),
|
||||||
|
['a', 'b']
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/** 证件建档添加合同时的 CRM 列表查询与本地过滤。 */
|
||||||
|
|
||||||
|
export interface CertArchiveContractListQuery {
|
||||||
|
pageNum: number
|
||||||
|
pageSize: number
|
||||||
|
keyword?: string
|
||||||
|
customerId: string
|
||||||
|
excludeStatus: 'invalid'
|
||||||
|
includeProducts: false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已选客户后按客户 ID 分页查询,避免只取最近合同再前端过滤。 */
|
||||||
|
export function certArchiveContractListQuery(
|
||||||
|
customerId: string,
|
||||||
|
keyword = ''
|
||||||
|
): CertArchiveContractListQuery | null {
|
||||||
|
const id = String(customerId || '').trim()
|
||||||
|
if (!id) return null
|
||||||
|
const text = keyword.trim()
|
||||||
|
return {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
keyword: text || undefined,
|
||||||
|
customerId: id,
|
||||||
|
excludeStatus: 'invalid',
|
||||||
|
includeProducts: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_STATUSES = ['approved', 'executing']
|
||||||
|
|
||||||
|
/** 去掉已关联合同和不在可选状态内的行。 */
|
||||||
|
export function filterAvailableBindingContracts<T extends { id?: string | number; status?: string }>(
|
||||||
|
rows: T[],
|
||||||
|
takenIds: Iterable<string>,
|
||||||
|
selectableStatuses: string[] = DEFAULT_STATUSES
|
||||||
|
): T[] {
|
||||||
|
const taken = new Set(
|
||||||
|
[...takenIds].map((id) => String(id || '').trim()).filter(Boolean)
|
||||||
|
)
|
||||||
|
return rows.filter((row) => {
|
||||||
|
const id = String(row.id || '').trim()
|
||||||
|
if (!id || taken.has(id)) return false
|
||||||
|
if (selectableStatuses.length && !selectableStatuses.includes(String(row.status || ''))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -556,6 +556,7 @@ import SystemViewDialog, {
|
|||||||
} from '@/components/business/system-view-dialog/index.vue'
|
} from '@/components/business/system-view-dialog/index.vue'
|
||||||
import { loadPersonnelWithCerts } from '@/composables/usePlVehicleViewDetail'
|
import { loadPersonnelWithCerts } from '@/composables/usePlVehicleViewDetail'
|
||||||
import { useListMediaActive } from '@/composables/useListMediaActive'
|
import { useListMediaActive } from '@/composables/useListMediaActive'
|
||||||
|
import { useRouteEditIdOpen } from '@/composables/useRouteEditIdOpen'
|
||||||
import { pickBxData, pickBxPageResult } from '@/utils/http/bx'
|
import { pickBxData, pickBxPageResult } from '@/utils/http/bx'
|
||||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime } from '@/utils/format'
|
||||||
@@ -1248,6 +1249,10 @@ async function handleEdit(row: Vehicle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useRouteEditIdOpen(async (id) => {
|
||||||
|
await handleEdit({ id } as Vehicle)
|
||||||
|
})
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
const valid = await formRef.value.validate().catch(() => false)
|
const valid = await formRef.value.validate().catch(() => false)
|
||||||
if (!valid || !validateSubTables()) return
|
if (!valid || !validateSubTables()) return
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { ColumnFilterDef } from '@/composables/useClientColumnFilters'
|
import type { ColumnFilterDef } from '@/composables/useClientColumnFilters'
|
||||||
|
|
||||||
const RECHARGE_TYPE_OPTIONS = [
|
const RECHARGE_TYPE_OPTIONS = [
|
||||||
{ value: 'etc', label: 'ETC' },
|
|
||||||
{ value: 'fuel', label: '油卡' },
|
{ value: 'fuel', label: '油卡' },
|
||||||
{ value: 'shanxia', label: '山峡' },
|
{ value: 'shanxia', label: '山峡' },
|
||||||
{ value: 'bailian', label: '百链' }
|
{ value: 'bailian', label: '百链' }
|
||||||
@@ -10,12 +9,13 @@ const RECHARGE_TYPE_OPTIONS = [
|
|||||||
const RECHARGE_STATUS_OPTIONS = [
|
const RECHARGE_STATUS_OPTIONS = [
|
||||||
{ value: 'draft', label: '草稿' },
|
{ value: 'draft', label: '草稿' },
|
||||||
{ value: 'in_approval', label: '审批中' },
|
{ value: 'in_approval', label: '审批中' },
|
||||||
|
{ value: 'pending', label: '发放中' },
|
||||||
{ value: 'rejected', label: '已驳回' },
|
{ value: 'rejected', label: '已驳回' },
|
||||||
{ value: 'disbursed', label: '已发放' },
|
{ value: 'disbursed', label: '已发放' },
|
||||||
{ value: 'cancelled', label: '已作废' }
|
{ value: 'cancelled', label: '已作废' }
|
||||||
]
|
]
|
||||||
|
|
||||||
/** ETC 申请列表列头漏斗(服务端)。 */
|
/** 油卡申请列表列头漏斗(服务端)。 */
|
||||||
export const plVehicleRechargeApplyColumnFilterDefs: ColumnFilterDef[] = [
|
export const plVehicleRechargeApplyColumnFilterDefs: ColumnFilterDef[] = [
|
||||||
{ prop: 'applyNo', label: '申请单号', type: 'text', queryKey: 'applyNo' },
|
{ prop: 'applyNo', label: '申请单号', type: 'text', queryKey: 'applyNo' },
|
||||||
{ prop: 'licensePlate', label: '车牌号', type: 'text', queryKey: 'licensePlate' },
|
{ prop: 'licensePlate', label: '车牌号', type: 'text', queryKey: 'licensePlate' },
|
||||||
@@ -52,7 +52,7 @@ export const plVehicleRechargeApplyColumnFilterDefs: ColumnFilterDef[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
/** ETC 审批列表列头漏斗(服务端;在申请 defs 基础上增加当前节点)。 */
|
/** 油卡审批列表列头漏斗(服务端;在申请 defs 基础上增加当前节点)。 */
|
||||||
export const plVehicleRechargeApprovalColumnFilterDefs: ColumnFilterDef[] = [
|
export const plVehicleRechargeApprovalColumnFilterDefs: ColumnFilterDef[] = [
|
||||||
...plVehicleRechargeApplyColumnFilterDefs.filter((d) => d.prop !== 'createTime'),
|
...plVehicleRechargeApplyColumnFilterDefs.filter((d) => d.prop !== 'createTime'),
|
||||||
{ prop: 'currentNodeName', label: '当前节点', type: 'text', queryKey: 'currentNodeName' },
|
{ prop: 'currentNodeName', label: '当前节点', type: 'text', queryKey: 'currentNodeName' },
|
||||||
@@ -65,7 +65,7 @@ export const plVehicleRechargeApprovalColumnFilterDefs: ColumnFilterDef[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
/** ETC 发放看板列头漏斗(服务端)。 */
|
/** 油卡发放看板列头漏斗(服务端)。 */
|
||||||
export const plVehicleRechargeDisburseColumnFilterDefs: ColumnFilterDef[] = [
|
export const plVehicleRechargeDisburseColumnFilterDefs: ColumnFilterDef[] = [
|
||||||
{ prop: 'applyNo', label: '申请单号', type: 'text', queryKey: 'applyNo' },
|
{ prop: 'applyNo', label: '申请单号', type: 'text', queryKey: 'applyNo' },
|
||||||
{
|
{
|
||||||
@@ -111,7 +111,7 @@ export const plVehicleRechargeDisburseColumnFilterDefs: ColumnFilterDef[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
/** ETC 审批人设置(客户端)。 */
|
/** 油卡审批人设置(客户端)。 */
|
||||||
export const plVehicleRechargeWorkflowColumnFilterDefs: ColumnFilterDef[] = [
|
export const plVehicleRechargeWorkflowColumnFilterDefs: ColumnFilterDef[] = [
|
||||||
{ prop: 'nodeName', label: '节点名称', type: 'text' },
|
{ prop: 'nodeName', label: '节点名称', type: 'text' },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- ETC/油卡充值申请 -->
|
<!-- 油卡充值申请 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
@@ -369,7 +369,6 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="充值类型" prop="rechargeType">
|
<el-form-item label="充值类型" prop="rechargeType">
|
||||||
<el-select v-model="formData.rechargeType" placeholder="请选择" style="width: 100%">
|
<el-select v-model="formData.rechargeType" placeholder="请选择" style="width: 100%">
|
||||||
<el-option label="ETC" value="etc" />
|
|
||||||
<el-option label="油卡" value="fuel" />
|
<el-option label="油卡" value="fuel" />
|
||||||
<el-option label="山峡" value="shanxia" />
|
<el-option label="山峡" value="shanxia" />
|
||||||
<el-option label="百链" value="bailian" />
|
<el-option label="百链" value="bailian" />
|
||||||
@@ -716,6 +715,7 @@ function buildListQueryParams(): Record<string, any> {
|
|||||||
const params: Record<string, any> = { ...toQueryParams(), ...toSortQueryParams() }
|
const params: Record<string, any> = { ...toQueryParams(), ...toSortQueryParams() }
|
||||||
const keyword = quickSearchKeyword.value.trim()
|
const keyword = quickSearchKeyword.value.trim()
|
||||||
if (keyword) params.q = keyword
|
if (keyword) params.q = keyword
|
||||||
|
params.rechargeTypeNe = 'etc'
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,7 +768,7 @@ const formData = reactive({
|
|||||||
applyNo: '',
|
applyNo: '',
|
||||||
applicantId: '',
|
applicantId: '',
|
||||||
licensePlate: '',
|
licensePlate: '',
|
||||||
rechargeType: 'etc' as 'etc' | 'fuel' | 'shanxia' | 'bailian',
|
rechargeType: 'fuel' as 'etc' | 'fuel' | 'shanxia' | 'bailian',
|
||||||
applyDate: '',
|
applyDate: '',
|
||||||
odometerKm: null as number | null,
|
odometerKm: null as number | null,
|
||||||
attachmentUrls: '',
|
attachmentUrls: '',
|
||||||
@@ -1004,7 +1004,7 @@ function resetForm() {
|
|||||||
formData.applyNo = ''
|
formData.applyNo = ''
|
||||||
formData.applicantId = ''
|
formData.applicantId = ''
|
||||||
formData.licensePlate = ''
|
formData.licensePlate = ''
|
||||||
formData.rechargeType = 'etc'
|
formData.rechargeType = 'fuel'
|
||||||
formData.applyDate = ''
|
formData.applyDate = ''
|
||||||
formData.odometerKm = null
|
formData.odometerKm = null
|
||||||
formData.attachmentUrls = ''
|
formData.attachmentUrls = ''
|
||||||
@@ -1244,7 +1244,9 @@ function applySummaryFilter() {
|
|||||||
if (typeof plate !== 'string' || !['etc', 'fuel'].includes(String(kind))) return
|
if (typeof plate !== 'string' || !['etc', 'fuel'].includes(String(kind))) return
|
||||||
Object.keys(filters).forEach((key) => delete filters[key])
|
Object.keys(filters).forEach((key) => delete filters[key])
|
||||||
filters.licensePlate = { text: plate }
|
filters.licensePlate = { text: plate }
|
||||||
filters.rechargeType = { text: '', selected: [String(kind)] }
|
if (String(kind) !== 'etc') {
|
||||||
|
filters.rechargeType = { text: '', selected: [String(kind)] }
|
||||||
|
}
|
||||||
quickSearchKeyword.value = ''
|
quickSearchKeyword.value = ''
|
||||||
pagination.page = 1
|
pagination.page = 1
|
||||||
const query = { ...route.query }
|
const query = { ...route.query }
|
||||||
|
|||||||
@@ -686,7 +686,8 @@
|
|||||||
const res = await api({
|
const res = await api({
|
||||||
page: pager.page,
|
page: pager.page,
|
||||||
pageSize: pager.pageSize,
|
pageSize: pager.pageSize,
|
||||||
...toQueryParams()
|
...toQueryParams(),
|
||||||
|
rechargeTypeNe: 'etc'
|
||||||
})
|
})
|
||||||
const data: any = (res as any)?.data ?? res
|
const data: any = (res as any)?.data ?? res
|
||||||
tableData.value = data?.items || []
|
tableData.value = data?.items || []
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- ETC/油卡充值发放管理 -->
|
<!-- 油卡充值发放管理 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
@@ -460,7 +460,6 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="充值类型" prop="rechargeType">
|
<el-form-item label="充值类型" prop="rechargeType">
|
||||||
<el-select v-model="formData.rechargeType" placeholder="请选择" style="width: 100%">
|
<el-select v-model="formData.rechargeType" placeholder="请选择" style="width: 100%">
|
||||||
<el-option label="ETC" value="etc" />
|
|
||||||
<el-option label="油卡" value="fuel" />
|
<el-option label="油卡" value="fuel" />
|
||||||
<el-option label="山峡" value="shanxia" />
|
<el-option label="山峡" value="shanxia" />
|
||||||
<el-option label="百链" value="bailian" />
|
<el-option label="百链" value="bailian" />
|
||||||
@@ -727,6 +726,7 @@ function buildListQueryParams(): Record<string, any> {
|
|||||||
const params: Record<string, any> = { ...toQueryParams(), ...toSortQueryParams() }
|
const params: Record<string, any> = { ...toQueryParams(), ...toSortQueryParams() }
|
||||||
const keyword = quickSearchKeyword.value.trim()
|
const keyword = quickSearchKeyword.value.trim()
|
||||||
if (keyword) params.q = keyword
|
if (keyword) params.q = keyword
|
||||||
|
params.rechargeTypeNe = 'etc'
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -780,7 +780,7 @@ const formData = reactive({
|
|||||||
rechargeOrderNo: '',
|
rechargeOrderNo: '',
|
||||||
applyId: '',
|
applyId: '',
|
||||||
licensePlate: '',
|
licensePlate: '',
|
||||||
rechargeType: 'etc' as 'etc' | 'fuel' | 'shanxia' | 'bailian',
|
rechargeType: 'fuel' as 'etc' | 'fuel' | 'shanxia' | 'bailian',
|
||||||
rechargeDate: '',
|
rechargeDate: '',
|
||||||
cardNo: '',
|
cardNo: '',
|
||||||
gasStation: '',
|
gasStation: '',
|
||||||
@@ -985,7 +985,7 @@ function resetForm() {
|
|||||||
formData.rechargeOrderNo = ''
|
formData.rechargeOrderNo = ''
|
||||||
formData.applyId = ''
|
formData.applyId = ''
|
||||||
formData.licensePlate = ''
|
formData.licensePlate = ''
|
||||||
formData.rechargeType = 'etc'
|
formData.rechargeType = 'fuel'
|
||||||
formData.rechargeDate = ''
|
formData.rechargeDate = ''
|
||||||
formData.cardNo = ''
|
formData.cardNo = ''
|
||||||
formData.gasStation = ''
|
formData.gasStation = ''
|
||||||
@@ -997,7 +997,7 @@ function resetForm() {
|
|||||||
|
|
||||||
function handleDisburse(row: VehicleRechargeDisburseBoardItem) {
|
function handleDisburse(row: VehicleRechargeDisburseBoardItem) {
|
||||||
if (row.applyStatus === 'in_approval') {
|
if (row.applyStatus === 'in_approval') {
|
||||||
ElMessage.warning('审批中的申请请到「ETC/油卡充值审批」菜单完成发放登记')
|
ElMessage.warning('审批中的申请请到「油卡充值审批」菜单完成发放登记')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resetForm()
|
resetForm()
|
||||||
@@ -1148,7 +1148,7 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
async function handleDelete(row: VehicleRechargeDisburseBoardItem) {
|
async function handleDelete(row: VehicleRechargeDisburseBoardItem) {
|
||||||
if (!row.recordId) return
|
if (!row.recordId) return
|
||||||
await ElMessageBox.confirm('确定撤销该发放登记吗?关联申请将回退为待发放。', '提示', { type: 'warning' })
|
await ElMessageBox.confirm('确定撤销该发放登记吗?关联申请将回退为发放中。', '提示', { type: 'warning' })
|
||||||
deleteLoadingMap[row.recordId] = true
|
deleteLoadingMap[row.recordId] = true
|
||||||
try {
|
try {
|
||||||
await vehicleRechargeRecordApi.delete(row.recordId)
|
await vehicleRechargeRecordApi.delete(row.recordId)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- 工作流配置(ETC/油卡充值) -->
|
<!-- 工作流配置(油卡充值) -->
|
||||||
<!-- eslint-disable vue/no-v-model-argument -->
|
<!-- eslint-disable vue/no-v-model-argument -->
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
{{ statusLabel(wfDef.status) }}
|
{{ statusLabel(wfDef.status) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="form-tip">ETC 与油卡共用同一套审批人配置</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="list-toolbar">
|
<div class="list-toolbar">
|
||||||
|
|||||||
@@ -570,6 +570,7 @@ import SystemViewDialog, {
|
|||||||
type ViewSection
|
type ViewSection
|
||||||
} from '@/components/business/system-view-dialog/index.vue'
|
} from '@/components/business/system-view-dialog/index.vue'
|
||||||
import { useListMediaActive } from '@/composables/useListMediaActive'
|
import { useListMediaActive } from '@/composables/useListMediaActive'
|
||||||
|
import { useRouteEditIdOpen } from '@/composables/useRouteEditIdOpen'
|
||||||
import { pickBxData, pickBxPageResult } from '@/utils/http/bx'
|
import { pickBxData, pickBxPageResult } from '@/utils/http/bx'
|
||||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime } from '@/utils/format'
|
||||||
@@ -1303,6 +1304,10 @@ async function handleEdit(row: Vehicle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useRouteEditIdOpen(async (id) => {
|
||||||
|
await handleEdit({ id } as Vehicle)
|
||||||
|
})
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
const valid = await formRef.value.validate().catch(() => false)
|
const valid = await formRef.value.validate().catch(() => false)
|
||||||
if (!valid || !validateSubTables()) return
|
if (!valid || !validateSubTables()) return
|
||||||
|
|||||||
Reference in new issue
Block a user