提交
This commit is contained in:
1 parent
e5ba8f097a
commit
62cfb8fbbf
24 files changed
+3323
-249
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
25041
|
||||
68508
|
||||
@@ -409,6 +409,133 @@ async def submit_collection_protocol_audit(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/protocol-payment-details")
|
||||
async def list_payment_details(
|
||||
page: int = Query(1, ge=1),
|
||||
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||
payStatus: str = Query("dfj", alias="payStatus"),
|
||||
keyword: str = Query("", alias="keyword"),
|
||||
transferType: str = Query("", alias="transferType"),
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
items, total = cp_svc.list_payment_details_page(
|
||||
db,
|
||||
page=page,
|
||||
page_size=pageSize,
|
||||
pay_status=payStatus,
|
||||
keyword=keyword or None,
|
||||
transfer_type=transferType or None,
|
||||
tenant_no=tenant_no,
|
||||
)
|
||||
tp = cp_svc.total_pages(total, pageSize)
|
||||
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
||||
|
||||
|
||||
class BatchDetailIdsBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
detail_ids: List[str] = Field(..., alias="detailIds", min_length=1)
|
||||
|
||||
|
||||
class RefundFinanceSubmitBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
refund_amount: float = Field(..., alias="refundAmount", gt=0)
|
||||
refund_remark: Optional[str] = Field(None, alias="refundRemark")
|
||||
refund_attachments: Optional[str] = Field(None, alias="refundAttachments")
|
||||
|
||||
|
||||
class BatchRefundFinanceSubmitBody(RefundFinanceSubmitBody):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
detail_ids: List[str] = Field(..., alias="detailIds", min_length=1)
|
||||
|
||||
|
||||
@router.post("/protocol-payment-details/{detail_id}/submit-receipt-finance")
|
||||
async def post_submit_receipt_finance(
|
||||
detail_id: str,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
try:
|
||||
cp_svc.submit_receipt_finance(
|
||||
db,
|
||||
detail_id=detail_id,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user.get("user_id"),
|
||||
)
|
||||
return success_response(None, message="提交成功")
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
|
||||
|
||||
@router.post("/protocol-payment-details/submit-receipt-finance-batch")
|
||||
async def post_submit_receipt_finance_batch(
|
||||
body: BatchDetailIdsBody,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
try:
|
||||
count = cp_svc.batch_submit_receipt_finance(
|
||||
db,
|
||||
detail_ids=body.detail_ids,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user.get("user_id"),
|
||||
)
|
||||
return success_response({"count": count}, message=f"已成功提交 {count} 条")
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
|
||||
|
||||
@router.post("/protocol-payment-details/{detail_id}/submit-refund-finance")
|
||||
async def post_submit_refund_finance(
|
||||
detail_id: str,
|
||||
body: RefundFinanceSubmitBody,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
try:
|
||||
cp_svc.submit_refund_finance(
|
||||
db,
|
||||
detail_id=detail_id,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user.get("user_id"),
|
||||
refund_amount=body.refund_amount,
|
||||
refund_remark=body.refund_remark,
|
||||
refund_attachments=body.refund_attachments,
|
||||
)
|
||||
return success_response(None, message="提交成功")
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
|
||||
|
||||
@router.post("/protocol-payment-details/submit-refund-finance-batch")
|
||||
async def post_submit_refund_finance_batch(
|
||||
body: BatchRefundFinanceSubmitBody,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
try:
|
||||
count = cp_svc.batch_submit_refund_finance(
|
||||
db,
|
||||
detail_ids=body.detail_ids,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user.get("user_id"),
|
||||
refund_amount=body.refund_amount,
|
||||
refund_remark=body.refund_remark,
|
||||
refund_attachments=body.refund_attachments,
|
||||
)
|
||||
return success_response({"count": count}, message=f"已成功提交 {count} 条")
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
|
||||
|
||||
@router.get("/protocol-payment-details/receipt-confirm")
|
||||
async def list_receipt_confirm(
|
||||
page: int = Query(1, ge=1),
|
||||
@@ -422,10 +549,35 @@ async def list_receipt_confirm(
|
||||
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
||||
|
||||
|
||||
@router.get("/protocol-payment-details/receipt-confirmed")
|
||||
async def list_receipt_confirmed(
|
||||
page: int = Query(1, ge=1),
|
||||
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
items, total = cp_svc.list_receipt_confirmed_details(
|
||||
db, page=page, page_size=pageSize, tenant_no=tenant_no
|
||||
)
|
||||
tp = cp_svc.total_pages(total, pageSize)
|
||||
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
||||
|
||||
|
||||
class FinanceConfirmBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
finance_confirm_attachments: Optional[str] = Field(None, alias="financeConfirmAttachments")
|
||||
finance_remark: Optional[str] = Field(None, alias="financeRemark")
|
||||
|
||||
|
||||
class RefundFinanceConfirmBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
refund_finance_confirm_attachments: Optional[str] = Field(
|
||||
None, alias="refundFinanceConfirmAttachments"
|
||||
)
|
||||
refund_finance_remark: Optional[str] = Field(None, alias="refundFinanceRemark")
|
||||
|
||||
|
||||
@router.post("/protocol-payment-details/{detail_id}/receipt-confirm")
|
||||
@@ -443,6 +595,7 @@ async def post_receipt_confirm(
|
||||
tenant_no=tenant_no,
|
||||
user_id=user.get("user_id"),
|
||||
finance_confirm_attachments=body.finance_confirm_attachments,
|
||||
finance_remark=body.finance_remark,
|
||||
)
|
||||
return success_response(None, message="确认成功")
|
||||
except ValueError as e:
|
||||
@@ -467,10 +620,25 @@ async def list_refund_confirm(
|
||||
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
||||
|
||||
|
||||
@router.get("/protocol-payment-details/refund-confirmed")
|
||||
async def list_refund_confirmed(
|
||||
page: int = Query(1, ge=1),
|
||||
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
items, total = cp_svc.list_refund_confirmed_details(
|
||||
db, page=page, page_size=pageSize, tenant_no=tenant_no
|
||||
)
|
||||
tp = cp_svc.total_pages(total, pageSize)
|
||||
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
||||
|
||||
|
||||
@router.post("/protocol-payment-details/{detail_id}/refund-confirm")
|
||||
async def post_refund_confirm(
|
||||
detail_id: str,
|
||||
body: FinanceConfirmBody,
|
||||
body: RefundFinanceConfirmBody,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -481,33 +649,10 @@ async def post_refund_confirm(
|
||||
detail_id=detail_id,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user.get("user_id"),
|
||||
finance_confirm_attachments=body.finance_confirm_attachments,
|
||||
refund_finance_confirm_attachments=body.refund_finance_confirm_attachments,
|
||||
refund_finance_remark=body.refund_finance_remark,
|
||||
)
|
||||
return success_response(None, message="确认成功")
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 分期预警
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/protocol-payment-details/installment-warning")
|
||||
async def list_installment_warning(
|
||||
page: int = Query(1, ge=1),
|
||||
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||
warningType: str = Query("all", alias="warningType"),
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
items, total = cp_svc.list_installment_warning(
|
||||
db,
|
||||
page=page,
|
||||
page_size=pageSize,
|
||||
warning_type=warningType,
|
||||
tenant_no=tenant_no,
|
||||
)
|
||||
tp = cp_svc.total_pages(total, pageSize)
|
||||
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
||||
@@ -166,6 +166,13 @@ STATIC_MENU_TREE = [
|
||||
"component": "/cell-storage/protocol/index",
|
||||
"meta": {"title": "采集协议单", "keepAlive": True},
|
||||
},
|
||||
{
|
||||
"id": "50000000000000000000000000000007",
|
||||
"name": "CellStoragePaymentInfo",
|
||||
"path": "payment-info",
|
||||
"component": "/cell-storage/payment-info/index",
|
||||
"meta": {"title": "缴费信息", "keepAlive": True},
|
||||
},
|
||||
{
|
||||
"id": "50000000000000000000000000000003",
|
||||
"name": "CellStorageProtocolAudit",
|
||||
@@ -178,21 +185,14 @@ STATIC_MENU_TREE = [
|
||||
"name": "CellStoragePaymentConfirm",
|
||||
"path": "payment-confirm",
|
||||
"component": "/cell-storage/payment-confirm/index",
|
||||
"meta": {"title": "采集协议单收款确认(财务确认)", "keepAlive": True},
|
||||
"meta": {"title": "缴费确认(财务)", "keepAlive": True},
|
||||
},
|
||||
{
|
||||
"id": "50000000000000000000000000000005",
|
||||
"name": "CellStorageRefundConfirm",
|
||||
"path": "refund-confirm",
|
||||
"component": "/cell-storage/refund-confirm/index",
|
||||
"meta": {"title": "采集协议单退费确认(财务确认)", "keepAlive": True},
|
||||
},
|
||||
{
|
||||
"id": "50000000000000000000000000000006",
|
||||
"name": "CellStorageInstallmentWarning",
|
||||
"path": "installment-warning",
|
||||
"component": "/cell-storage/installment-warning/index",
|
||||
"meta": {"title": "分期缴费用户预警模块", "keepAlive": True},
|
||||
"meta": {"title": "退费确认(财务)", "keepAlive": True},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -178,6 +178,14 @@ def urls_to_attachment_list(
|
||||
return result
|
||||
|
||||
|
||||
def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
|
||||
ids = list({x for x in user_ids if x})
|
||||
if not ids:
|
||||
return {}
|
||||
rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
|
||||
return {u.id: u.nick_name or "" for u in rows}
|
||||
|
||||
|
||||
def _protocol_to_dict(p: BizCollectionProtocol) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": p.id,
|
||||
@@ -226,21 +234,39 @@ def _detail_to_dict(
|
||||
*,
|
||||
protocol_no: str = "",
|
||||
collection_protocol_id: str = "",
|
||||
protocol_status: str = "",
|
||||
pay_mode: str = "",
|
||||
mother_name: str = "",
|
||||
mother_id_card: str = "",
|
||||
newborn_name: str = "",
|
||||
newborn_id_card: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": d.id,
|
||||
"paymentId": d.payment_id,
|
||||
"collectionProtocolId": collection_protocol_id,
|
||||
"protocolNo": protocol_no,
|
||||
"protocolStatus": protocol_status,
|
||||
"payMode": pay_mode,
|
||||
"motherName": mother_name,
|
||||
"motherIdCard": mother_id_card,
|
||||
"newbornName": newborn_name,
|
||||
"newbornIdCard": newborn_id_card,
|
||||
"transferType": d.transfer_type or "",
|
||||
"dueAmount": _dec(d.due_amount),
|
||||
"transferAmount": _dec(d.transfer_amount),
|
||||
"transferTime": _fmt_dt(d.transfer_time),
|
||||
"transferAttachments": d.transfer_attachments or "",
|
||||
"financeConfirmAttachments": d.finance_confirm_attachments or "",
|
||||
"financeRemark": d.finance_remark or "",
|
||||
"refundFinanceConfirmAttachments": d.refund_finance_confirm_attachments or "",
|
||||
"refundFinanceRemark": d.refund_finance_remark or "",
|
||||
"payStatus": d.pay_status or "",
|
||||
"installmentNo": d.installment_no,
|
||||
"installmentDueDate": (d.installment_due_date or "").strip() if d.installment_due_date else "",
|
||||
"refundAmount": _dec(d.refund_amount),
|
||||
"refundRemark": d.refund_remark or "",
|
||||
"refundAttachments": d.refund_attachments or "",
|
||||
"tenantNo": d.tenant_no,
|
||||
"orderNum": d.order_num if d.order_num is not None else 0,
|
||||
"remark": d.remark or "",
|
||||
@@ -567,10 +593,39 @@ def get_protocol_detail_bundle(
|
||||
ci = get_collection_info(db, protocol_id, tenant_no)
|
||||
s_rows = list_samples(db, protocol_id, tenant_no)
|
||||
sc = get_storage_cert(db, protocol_id, tenant_no)
|
||||
out: Dict[str, Any] = {"protocol": _protocol_to_dict(p)}
|
||||
protocol_dict = _protocol_to_dict(p)
|
||||
ci_dict = _collection_info_to_dict(ci) if ci else None
|
||||
user_ids: List[str] = []
|
||||
if protocol_dict.get("collectorUserId"):
|
||||
user_ids.append(str(protocol_dict["collectorUserId"]))
|
||||
if ci_dict and ci_dict.get("salesUserId"):
|
||||
user_ids.append(str(ci_dict["salesUserId"]))
|
||||
user_name_map = _user_nick_name_map(db, user_ids)
|
||||
protocol_dict["collectorUserName"] = user_name_map.get(
|
||||
str(protocol_dict.get("collectorUserId") or ""), ""
|
||||
)
|
||||
if ci_dict:
|
||||
ci_dict["salesUserName"] = user_name_map.get(str(ci_dict.get("salesUserId") or ""), "")
|
||||
region_id = str(ci_dict.get("regionId") or "")
|
||||
if region_id:
|
||||
r = (
|
||||
db.query(BizCollectionRegion.region_name)
|
||||
.filter(BizCollectionRegion.id == region_id)
|
||||
.first()
|
||||
)
|
||||
ci_dict["regionName"] = r.region_name if r else ""
|
||||
hospital_id = str(ci_dict.get("hospitalId") or "")
|
||||
if hospital_id:
|
||||
h = (
|
||||
db.query(BizCollectionHospital.hospital_name)
|
||||
.filter(BizCollectionHospital.id == hospital_id)
|
||||
.first()
|
||||
)
|
||||
ci_dict["hospitalName"] = h.hospital_name if h else ""
|
||||
out: Dict[str, Any] = {"protocol": protocol_dict}
|
||||
out["payment"] = _payment_to_dict(pay) if pay else None
|
||||
out["partyCustomer"] = _party_customer_to_dict(pc) if pc else None
|
||||
out["collectionInfo"] = _collection_info_to_dict(ci) if ci else None
|
||||
out["collectionInfo"] = ci_dict
|
||||
transport_rows = list_sample_transports(db, protocol_id, tenant_no)
|
||||
transport_by_sample = {t.sample_id: _transport_to_dict(t) for t in transport_rows}
|
||||
samples_out: List[Dict[str, Any]] = []
|
||||
@@ -591,6 +646,15 @@ def get_protocol_detail_bundle(
|
||||
collection_protocol_id=protocol_id,
|
||||
)
|
||||
item["attachments"] = urls_to_attachment_list(d.transfer_attachments, db, tenant_no)
|
||||
item["financeConfirmAttachmentFiles"] = urls_to_attachment_list(
|
||||
d.finance_confirm_attachments, db, tenant_no
|
||||
)
|
||||
item["refundFinanceConfirmAttachmentFiles"] = urls_to_attachment_list(
|
||||
d.refund_finance_confirm_attachments, db, tenant_no
|
||||
)
|
||||
item["refundAttachmentFiles"] = urls_to_attachment_list(
|
||||
d.refund_attachments, db, tenant_no
|
||||
)
|
||||
details_out.append(item)
|
||||
out["paymentDetails"] = details_out
|
||||
else:
|
||||
@@ -697,7 +761,7 @@ def _detail_query_with_protocol(
|
||||
pay_status_filter: str,
|
||||
):
|
||||
return (
|
||||
db.query(BizProtocolPaymentDetail, BizProtocolPayment, BizCollectionProtocol.protocol_no)
|
||||
db.query(BizProtocolPaymentDetail, BizProtocolPayment, BizCollectionProtocol)
|
||||
.join(BizProtocolPayment, BizProtocolPaymentDetail.payment_id == BizProtocolPayment.id)
|
||||
.join(BizCollectionProtocol, BizProtocolPayment.collection_protocol_id == BizCollectionProtocol.id)
|
||||
.filter(
|
||||
@@ -710,6 +774,40 @@ def _detail_query_with_protocol(
|
||||
)
|
||||
|
||||
|
||||
def _rows_to_payment_detail_dicts(
|
||||
db: Session,
|
||||
rows: List[Any],
|
||||
tenant_no: str,
|
||||
*,
|
||||
post_process: Optional[Any] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""缴费明细行 → 前端字典(含协议、客户、转账附件)。"""
|
||||
protocol_ids = [
|
||||
pay.collection_protocol_id for _, pay, _ in rows if pay and pay.collection_protocol_id
|
||||
]
|
||||
pc_map = _party_customer_map_by_protocol_ids(db, protocol_ids)
|
||||
out: List[Dict[str, Any]] = []
|
||||
for d, pay, prot in rows:
|
||||
cpid = pay.collection_protocol_id if pay else ""
|
||||
pc = pc_map.get(cpid)
|
||||
item = _detail_to_dict(
|
||||
d,
|
||||
protocol_no=(prot.protocol_no if prot else "") or "",
|
||||
collection_protocol_id=cpid,
|
||||
protocol_status=(prot.protocol_status if prot else "") or "",
|
||||
pay_mode=(pay.pay_mode if pay else "") or "",
|
||||
mother_name=(pc.mother_name if pc else "") or "",
|
||||
mother_id_card=(pc.mother_id_card if pc else "") or "",
|
||||
newborn_name=(pc.newborn_name if pc else "") or "",
|
||||
newborn_id_card=(pc.newborn_id_card if pc else "") or "",
|
||||
)
|
||||
item["attachments"] = urls_to_attachment_list(d.transfer_attachments, db, tenant_no)
|
||||
if post_process:
|
||||
post_process(item, d)
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
|
||||
def list_receipt_confirm_details(
|
||||
db: Session,
|
||||
*,
|
||||
@@ -729,15 +827,39 @@ def list_receipt_confirm_details(
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
out = [
|
||||
_detail_to_dict(
|
||||
d,
|
||||
protocol_no=pn or "",
|
||||
collection_protocol_id=pay.collection_protocol_id if pay else "",
|
||||
return _rows_to_payment_detail_dicts(db, rows, tenant_no), int(total)
|
||||
|
||||
|
||||
def list_receipt_confirmed_details(
|
||||
db: Session,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
tenant_no: Optional[str] = None,
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
"""财务已确认收款(yjf),按确认时间倒序。"""
|
||||
if not tenant_no:
|
||||
return [], 0
|
||||
q_base = _detail_query_with_protocol(db, tenant_no, "yjf")
|
||||
total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
rows = (
|
||||
q_base.order_by(BizProtocolPaymentDetail.update_time.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
|
||||
def _add_finance_files(item: Dict[str, Any], d: BizProtocolPaymentDetail) -> None:
|
||||
item["financeConfirmAttachmentFiles"] = urls_to_attachment_list(
|
||||
d.finance_confirm_attachments, db, tenant_no
|
||||
)
|
||||
for d, pay, pn in rows
|
||||
]
|
||||
return out, int(total)
|
||||
|
||||
return (
|
||||
_rows_to_payment_detail_dicts(db, rows, tenant_no, post_process=_add_finance_files),
|
||||
int(total),
|
||||
)
|
||||
|
||||
|
||||
def confirm_receipt(
|
||||
@@ -747,6 +869,7 @@ def confirm_receipt(
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
finance_confirm_attachments: Optional[str] = None,
|
||||
finance_remark: Optional[str] = None,
|
||||
) -> BizProtocolPaymentDetail:
|
||||
d = (
|
||||
db.query(BizProtocolPaymentDetail)
|
||||
@@ -768,6 +891,8 @@ def confirm_receipt(
|
||||
d.pay_status = "yjf"
|
||||
if finance_confirm_attachments is not None:
|
||||
d.finance_confirm_attachments = finance_confirm_attachments.strip() or None
|
||||
if finance_remark is not None:
|
||||
d.finance_remark = finance_remark.strip() or None
|
||||
d.update_by = user_id
|
||||
d.update_time = datetime.now()
|
||||
db.add(d)
|
||||
@@ -795,15 +920,49 @@ def list_refund_confirm_details(
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
out = [
|
||||
_detail_to_dict(
|
||||
d,
|
||||
protocol_no=pn or "",
|
||||
collection_protocol_id=pay.collection_protocol_id if pay else "",
|
||||
def _add_refund_apply_files(item: Dict[str, Any], d: BizProtocolPaymentDetail) -> None:
|
||||
item["refundAttachmentFiles"] = urls_to_attachment_list(
|
||||
d.refund_attachments, db, tenant_no
|
||||
)
|
||||
for d, pay, pn in rows
|
||||
]
|
||||
return out, int(total)
|
||||
|
||||
return (
|
||||
_rows_to_payment_detail_dicts(db, rows, tenant_no, post_process=_add_refund_apply_files),
|
||||
int(total),
|
||||
)
|
||||
|
||||
|
||||
def list_refund_confirmed_details(
|
||||
db: Session,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
tenant_no: Optional[str] = None,
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
"""财务已确认退费(ytf),按确认时间倒序。"""
|
||||
if not tenant_no:
|
||||
return [], 0
|
||||
q_base = _detail_query_with_protocol(db, tenant_no, "ytf")
|
||||
total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
rows = (
|
||||
q_base.order_by(BizProtocolPaymentDetail.update_time.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
def _add_refund_confirmed_files(item: Dict[str, Any], d: BizProtocolPaymentDetail) -> None:
|
||||
item["refundAttachmentFiles"] = urls_to_attachment_list(
|
||||
d.refund_attachments, db, tenant_no
|
||||
)
|
||||
item["refundFinanceConfirmAttachmentFiles"] = urls_to_attachment_list(
|
||||
d.refund_finance_confirm_attachments, db, tenant_no
|
||||
)
|
||||
|
||||
return (
|
||||
_rows_to_payment_detail_dicts(db, rows, tenant_no, post_process=_add_refund_confirmed_files),
|
||||
int(total),
|
||||
)
|
||||
|
||||
|
||||
def confirm_refund(
|
||||
@@ -812,7 +971,8 @@ def confirm_refund(
|
||||
detail_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
finance_confirm_attachments: Optional[str] = None,
|
||||
refund_finance_confirm_attachments: Optional[str] = None,
|
||||
refund_finance_remark: Optional[str] = None,
|
||||
) -> BizProtocolPaymentDetail:
|
||||
d = (
|
||||
db.query(BizProtocolPaymentDetail)
|
||||
@@ -832,8 +992,10 @@ def confirm_refund(
|
||||
raise ValueError("该明细已确认退费")
|
||||
raise ValueError("当前状态不可确认退费")
|
||||
d.pay_status = "ytf"
|
||||
if finance_confirm_attachments is not None:
|
||||
d.finance_confirm_attachments = finance_confirm_attachments.strip() or None
|
||||
if refund_finance_confirm_attachments is not None:
|
||||
d.refund_finance_confirm_attachments = refund_finance_confirm_attachments.strip() or None
|
||||
if refund_finance_remark is not None:
|
||||
d.refund_finance_remark = refund_finance_remark.strip() or None
|
||||
d.update_by = user_id
|
||||
d.update_time = datetime.now()
|
||||
db.add(d)
|
||||
@@ -842,6 +1004,224 @@ def confirm_refund(
|
||||
return d
|
||||
|
||||
|
||||
def _get_payment_detail_or_raise(
|
||||
db: Session,
|
||||
*,
|
||||
detail_id: str,
|
||||
tenant_no: str,
|
||||
) -> BizProtocolPaymentDetail:
|
||||
d = (
|
||||
db.query(BizProtocolPaymentDetail)
|
||||
.filter(
|
||||
and_(
|
||||
BizProtocolPaymentDetail.id == detail_id,
|
||||
BizProtocolPaymentDetail.tenant_no == tenant_no,
|
||||
BizProtocolPaymentDetail.del_flag == 0,
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not d:
|
||||
raise ValueError("缴费明细不存在")
|
||||
return d
|
||||
|
||||
|
||||
def _apply_submit_receipt_finance(
|
||||
d: BizProtocolPaymentDetail,
|
||||
*,
|
||||
user_id: Optional[str],
|
||||
) -> None:
|
||||
if d.pay_status != "dfj":
|
||||
raise ValueError("仅待缴费状态可提交财务确认")
|
||||
d.pay_status = "jfqrz"
|
||||
d.update_by = user_id
|
||||
d.update_time = datetime.now()
|
||||
|
||||
|
||||
def submit_receipt_finance(
|
||||
db: Session,
|
||||
*,
|
||||
detail_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
) -> BizProtocolPaymentDetail:
|
||||
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
|
||||
_apply_submit_receipt_finance(d, user_id=user_id)
|
||||
db.add(d)
|
||||
db.commit()
|
||||
db.refresh(d)
|
||||
return d
|
||||
|
||||
|
||||
def batch_submit_receipt_finance(
|
||||
db: Session,
|
||||
*,
|
||||
detail_ids: List[str],
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
) -> int:
|
||||
ids = [str(i).strip() for i in (detail_ids or []) if str(i).strip()]
|
||||
if not ids:
|
||||
raise ValueError("请选择缴费明细")
|
||||
for detail_id in ids:
|
||||
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
|
||||
_apply_submit_receipt_finance(d, user_id=user_id)
|
||||
db.add(d)
|
||||
db.commit()
|
||||
return len(ids)
|
||||
|
||||
|
||||
def _apply_submit_refund_finance(
|
||||
d: BizProtocolPaymentDetail,
|
||||
*,
|
||||
user_id: Optional[str],
|
||||
refund_amount: Any,
|
||||
refund_remark: Optional[str] = None,
|
||||
refund_attachments: Optional[str] = None,
|
||||
) -> None:
|
||||
if d.pay_status != "yjf":
|
||||
raise ValueError("仅已缴费状态可提交退费财务确认")
|
||||
if refund_amount is None or Decimal(str(refund_amount)) <= 0:
|
||||
raise ValueError("请填写有效的退费金额")
|
||||
d.pay_status = "tfqrz"
|
||||
d.refund_amount = Decimal(str(refund_amount))
|
||||
d.refund_remark = (refund_remark or "").strip() or None
|
||||
d.refund_attachments = (refund_attachments or "").strip() or None
|
||||
d.update_by = user_id
|
||||
d.update_time = datetime.now()
|
||||
|
||||
|
||||
def submit_refund_finance(
|
||||
db: Session,
|
||||
*,
|
||||
detail_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
refund_amount: Any,
|
||||
refund_remark: Optional[str] = None,
|
||||
refund_attachments: Optional[str] = None,
|
||||
) -> BizProtocolPaymentDetail:
|
||||
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
|
||||
_apply_submit_refund_finance(
|
||||
d,
|
||||
user_id=user_id,
|
||||
refund_amount=refund_amount,
|
||||
refund_remark=refund_remark,
|
||||
refund_attachments=refund_attachments,
|
||||
)
|
||||
db.add(d)
|
||||
db.commit()
|
||||
db.refresh(d)
|
||||
return d
|
||||
|
||||
|
||||
def batch_submit_refund_finance(
|
||||
db: Session,
|
||||
*,
|
||||
detail_ids: List[str],
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
refund_amount: Any,
|
||||
refund_remark: Optional[str] = None,
|
||||
refund_attachments: Optional[str] = None,
|
||||
) -> int:
|
||||
ids = [str(i).strip() for i in (detail_ids or []) if str(i).strip()]
|
||||
if not ids:
|
||||
raise ValueError("请选择缴费明细")
|
||||
for detail_id in ids:
|
||||
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
|
||||
_apply_submit_refund_finance(
|
||||
d,
|
||||
user_id=user_id,
|
||||
refund_amount=refund_amount,
|
||||
refund_remark=refund_remark,
|
||||
refund_attachments=refund_attachments,
|
||||
)
|
||||
db.add(d)
|
||||
db.commit()
|
||||
return len(ids)
|
||||
|
||||
|
||||
def _party_customer_map_by_protocol_ids(
|
||||
db: Session,
|
||||
protocol_ids: List[str],
|
||||
) -> Dict[str, BizProtocolPartyCustomer]:
|
||||
if not protocol_ids:
|
||||
return {}
|
||||
rows = (
|
||||
db.query(BizProtocolPartyCustomer)
|
||||
.filter(
|
||||
BizProtocolPartyCustomer.collection_protocol_id.in_(protocol_ids),
|
||||
BizProtocolPartyCustomer.del_flag == 0,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
return {r.collection_protocol_id: r for r in rows}
|
||||
|
||||
|
||||
def list_payment_details_page(
|
||||
db: Session,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
pay_status: Optional[str] = None,
|
||||
keyword: Optional[str] = None,
|
||||
transfer_type: Optional[str] = None,
|
||||
tenant_no: Optional[str] = None,
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
if not tenant_no:
|
||||
return [], 0
|
||||
q = (
|
||||
db.query(BizProtocolPaymentDetail, BizProtocolPayment, BizCollectionProtocol)
|
||||
.join(BizProtocolPayment, BizProtocolPaymentDetail.payment_id == BizProtocolPayment.id)
|
||||
.join(BizCollectionProtocol, BizProtocolPayment.collection_protocol_id == BizCollectionProtocol.id)
|
||||
.filter(
|
||||
BizProtocolPaymentDetail.del_flag == 0,
|
||||
BizProtocolPayment.del_flag == 0,
|
||||
BizCollectionProtocol.del_flag == 0,
|
||||
BizProtocolPaymentDetail.tenant_no == tenant_no,
|
||||
)
|
||||
)
|
||||
if pay_status:
|
||||
q = q.filter(BizProtocolPaymentDetail.pay_status == pay_status)
|
||||
if keyword and keyword.strip():
|
||||
like = f"%{keyword.strip()}%"
|
||||
q = q.filter(BizCollectionProtocol.protocol_no.like(like))
|
||||
if transfer_type and transfer_type.strip():
|
||||
q = q.filter(BizProtocolPaymentDetail.transfer_type == transfer_type.strip())
|
||||
total = q.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
rows = (
|
||||
q.order_by(BizProtocolPaymentDetail.installment_due_date.asc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
protocol_ids = [
|
||||
pay.collection_protocol_id for _, pay, _ in rows if pay and pay.collection_protocol_id
|
||||
]
|
||||
pc_map = _party_customer_map_by_protocol_ids(db, protocol_ids)
|
||||
out = []
|
||||
for d, pay, prot in rows:
|
||||
cpid = pay.collection_protocol_id if pay else ""
|
||||
pc = pc_map.get(cpid)
|
||||
out.append(
|
||||
_detail_to_dict(
|
||||
d,
|
||||
protocol_no=(prot.protocol_no if prot else "") or "",
|
||||
collection_protocol_id=cpid,
|
||||
protocol_status=(prot.protocol_status if prot else "") or "",
|
||||
pay_mode=(pay.pay_mode if pay else "") or "",
|
||||
mother_name=(pc.mother_name if pc else "") or "",
|
||||
mother_id_card=(pc.mother_id_card if pc else "") or "",
|
||||
newborn_name=(pc.newborn_name if pc else "") or "",
|
||||
newborn_id_card=(pc.newborn_id_card if pc else "") or "",
|
||||
)
|
||||
)
|
||||
return out, int(total)
|
||||
|
||||
|
||||
def _classify_installment_warning(due: str, today: str) -> str:
|
||||
"""overdue | warn | upcoming — due 为 YYYYMMDD"""
|
||||
if not due or len(due) != 8 or not due.isdigit():
|
||||
@@ -872,13 +1252,16 @@ def list_installment_warning(
|
||||
# 拉取当前租户下候选(分期号非空);日期过滤在内存中完成以保证与 classify 一致
|
||||
rows_all = q0.order_by(BizProtocolPaymentDetail.installment_due_date.asc()).all()
|
||||
flattened: List[Dict[str, Any]] = []
|
||||
for d, pay, pn in rows_all:
|
||||
for d, pay, prot in rows_all:
|
||||
due_raw = (d.installment_due_date or "").strip() if d.installment_due_date else ""
|
||||
wt = _classify_installment_warning(due_raw, today)
|
||||
cpid = pay.collection_protocol_id if pay else ""
|
||||
item = _detail_to_dict(
|
||||
d,
|
||||
protocol_no=pn or "",
|
||||
collection_protocol_id=pay.collection_protocol_id if pay else "",
|
||||
protocol_no=(prot.protocol_no if prot else "") or "",
|
||||
collection_protocol_id=cpid,
|
||||
protocol_status=(prot.protocol_status if prot else "") or "",
|
||||
pay_mode=(pay.pay_mode if pay else "") or "",
|
||||
)
|
||||
item["warningType"] = wt
|
||||
wt_filter = (warning_type or "all").strip().lower()
|
||||
|
||||
@@ -796,11 +796,17 @@ class BizProtocolPaymentDetail(Base):
|
||||
due_amount = Column(Numeric(18, 2), nullable=True, comment="应缴费金额")
|
||||
transfer_amount = Column(Numeric(18, 2), nullable=True, comment="转账金额")
|
||||
transfer_time = Column(DateTime, nullable=True, comment="转账时间")
|
||||
transfer_attachments = Column(String(4000), nullable=True, comment="转账附件")
|
||||
finance_confirm_attachments = Column(String(4000), nullable=True, comment="财务确认附件")
|
||||
transfer_attachments = Column(Text, nullable=True, comment="转账附件")
|
||||
finance_confirm_attachments = Column(Text, nullable=True, comment="缴费财务确认附件")
|
||||
finance_remark = Column(String(512), nullable=True, comment="缴费财务确认备注")
|
||||
refund_finance_confirm_attachments = Column(Text, nullable=True, comment="财务退费确认附件")
|
||||
refund_finance_remark = Column(String(512), nullable=True, comment="财务退费确认备注")
|
||||
pay_status = Column(String(32), nullable=False, default="dfj", comment="缴费状态")
|
||||
installment_no = Column(Integer, nullable=True, comment="分期期序号")
|
||||
installment_due_date = Column(CHAR(8), nullable=True, comment="分期应缴日期YYYYMMDD")
|
||||
refund_amount = Column(Numeric(18, 2), nullable=True, comment="退费金额")
|
||||
refund_remark = Column(String(512), nullable=True, comment="退费备注")
|
||||
refund_attachments = Column(Text, nullable=True, comment="退费附件逗号分隔")
|
||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||
remark = Column(String(512), nullable=True, comment="备注")
|
||||
|
||||
@@ -195,6 +195,60 @@ class TestListReceiptConfirmSortOrder(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestListReceiptConfirmedSortOrder(unittest.TestCase):
|
||||
"""list_receipt_confirmed_details should sort by update_time DESC."""
|
||||
|
||||
@patch("app.crud.collection_protocol.BizProtocolPaymentDetail")
|
||||
@patch("app.crud.collection_protocol.BizProtocolPayment")
|
||||
@patch("app.crud.collection_protocol.BizCollectionProtocol")
|
||||
def test_receipt_confirmed_sorts_by_update_time_desc(self, MockProtocol, MockPayment, MockDetail):
|
||||
from app.crud.collection_protocol import list_receipt_confirmed_details
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_query = MagicMock()
|
||||
mock_query.join.return_value = mock_query
|
||||
mock_query.filter.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.with_entities.return_value = mock_query
|
||||
mock_query.scalar.return_value = 0
|
||||
mock_query.offset.return_value = mock_query
|
||||
mock_query.limit.return_value = mock_query
|
||||
mock_query.all.return_value = []
|
||||
mock_db.query.return_value = mock_query
|
||||
|
||||
list_receipt_confirmed_details(mock_db, tenant_no="t1")
|
||||
|
||||
order_by_calls = mock_query.order_by.call_args_list
|
||||
self.assertGreaterEqual(len(order_by_calls), 1)
|
||||
|
||||
|
||||
class TestListRefundConfirmedSortOrder(unittest.TestCase):
|
||||
"""list_refund_confirmed_details should sort by update_time DESC."""
|
||||
|
||||
@patch("app.crud.collection_protocol.BizProtocolPaymentDetail")
|
||||
@patch("app.crud.collection_protocol.BizProtocolPayment")
|
||||
@patch("app.crud.collection_protocol.BizCollectionProtocol")
|
||||
def test_refund_confirmed_sorts_by_update_time_desc(self, MockProtocol, MockPayment, MockDetail):
|
||||
from app.crud.collection_protocol import list_refund_confirmed_details
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_query = MagicMock()
|
||||
mock_query.join.return_value = mock_query
|
||||
mock_query.filter.return_value = mock_query
|
||||
mock_query.order_by.return_value = mock_query
|
||||
mock_query.with_entities.return_value = mock_query
|
||||
mock_query.scalar.return_value = 0
|
||||
mock_query.offset.return_value = mock_query
|
||||
mock_query.limit.return_value = mock_query
|
||||
mock_query.all.return_value = []
|
||||
mock_db.query.return_value = mock_query
|
||||
|
||||
list_refund_confirmed_details(mock_db, tenant_no="t1")
|
||||
|
||||
order_by_calls = mock_query.order_by.call_args_list
|
||||
self.assertGreaterEqual(len(order_by_calls), 1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# M1: Sort order — list_refund_confirm_details should include order_num ASC
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
-- 采集协议单当事人:新生儿 Apgar 评分支持一位小数
|
||||
ALTER TABLE `biz_protocol_party_customer`
|
||||
MODIFY COLUMN `apgar_1min` decimal(4,1) DEFAULT NULL COMMENT '新生儿评分1分钟',
|
||||
MODIFY COLUMN `apgar_5min` decimal(4,1) DEFAULT NULL COMMENT '新生儿评分5分钟';
|
||||
@@ -1,3 +0,0 @@
|
||||
-- 样本状态允许为空,移除默认「已采集」,与前端清空行为一致
|
||||
ALTER TABLE `biz_protocol_sample`
|
||||
MODIFY COLUMN `sample_status` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'ycj已采集 jcz检测中 yrk已入库 yzf已作废';
|
||||
+8
-2
@@ -1474,11 +1474,17 @@ CREATE TABLE `biz_protocol_payment_detail` (
|
||||
`due_amount` decimal(18,2) DEFAULT NULL COMMENT '应缴费金额',
|
||||
`transfer_amount` decimal(18,2) DEFAULT NULL COMMENT '转账金额',
|
||||
`transfer_time` datetime DEFAULT NULL COMMENT '转账时间',
|
||||
`transfer_attachments` varchar(4000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '转账附件逗号分隔',
|
||||
`finance_confirm_attachments` varchar(4000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '财务确认附件逗号分隔',
|
||||
`transfer_attachments` text COLLATE utf8mb4_unicode_ci COMMENT '转账附件逗号分隔',
|
||||
`finance_confirm_attachments` text COLLATE utf8mb4_unicode_ci COMMENT '缴费财务确认附件逗号分隔',
|
||||
`finance_remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '缴费财务确认备注',
|
||||
`refund_finance_confirm_attachments` text COLLATE utf8mb4_unicode_ci COMMENT '财务退费确认附件逗号分隔',
|
||||
`refund_finance_remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '财务退费确认备注',
|
||||
`pay_status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'dfj' COMMENT 'dfj待缴费 jfqrz缴费确认中 yjf已缴费 tfqrz退费确认中 ytf已退费',
|
||||
`installment_no` int(11) DEFAULT NULL COMMENT '分期期序号,趸交为空',
|
||||
`installment_due_date` char(8) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '分期应缴费时间YYYYMMDD',
|
||||
`refund_amount` decimal(18,2) DEFAULT NULL COMMENT '退费金额',
|
||||
`refund_remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '退费备注',
|
||||
`refund_attachments` text COLLATE utf8mb4_unicode_ci COMMENT '退费附件逗号分隔URL',
|
||||
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
|
||||
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
|
||||
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
const buttons = document.querySelectorAll<HTMLElement>(selectors.join(','))
|
||||
buttons.forEach((button) => {
|
||||
autoIconClassList.forEach((className) => button.classList.remove(className))
|
||||
// 区域/状态 Tab 仅展示文案,不参与按钮自动图标
|
||||
if (button.classList.contains('region-tab-btn')) return
|
||||
const text = (button.textContent || '').replace(/\s+/g, '')
|
||||
const matched = iconClassByText.find(([rule]) => rule.test(text))
|
||||
if (matched) button.classList.add(matched[1])
|
||||
@@ -78,7 +80,7 @@
|
||||
const panelTitleClassList = panelTitleIconByText.map(([, cls]) => cls)
|
||||
const panelTitleBarClass = 'title-bar-heading'
|
||||
const panelTitleBarTextRules: RegExp[] = [
|
||||
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|租户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域|组织|组织管理|角色列表|菜单列表|字典类型|字典数据|站点列表|职位列表|快递公司列表|流程配置|流程列表|待办列表|已办列表|申请列表|客户列表|细胞样本类型列表|采集地区列表|采集协议号列表|样本编码列表|医院列表|协议单列表|审核记录列表|收款确认列表|退费确认列表|分期预警列表|请假列表|洁净室列表|库位树|预约列表)/
|
||||
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|租户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域|组织|组织管理|角色列表|菜单列表|字典类型|字典数据|站点列表|职位列表|快递公司列表|流程配置|流程列表|待办列表|已办列表|申请列表|客户列表|细胞样本类型列表|采集地区列表|采集协议号列表|样本编码列表|医院列表|协议单列表|审核记录列表|缴费确认列表|退费确认列表|分期预警列表|请假列表|洁净室列表|库位树|预约列表|缴费信息列表)/
|
||||
]
|
||||
|
||||
const refreshPanelTitleIcons = () => {
|
||||
|
||||
@@ -68,9 +68,16 @@ export function fetchReceiptConfirmList(params?: { page?: number; pageSize?: num
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchReceiptConfirmedList(params?: { page?: number; pageSize?: number }) {
|
||||
return request.get<PageList<Record<string, unknown>>>({
|
||||
url: '/web/admin/protocol-payment-details/receipt-confirmed',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchReceiptConfirmSubmit(
|
||||
detailId: string,
|
||||
data?: { financeConfirmAttachments?: string }
|
||||
data?: { financeConfirmAttachments?: string; financeRemark?: string }
|
||||
) {
|
||||
return request.post({
|
||||
url: `/web/admin/protocol-payment-details/${detailId}/receipt-confirm`,
|
||||
@@ -86,9 +93,16 @@ export function fetchRefundConfirmList(params?: { page?: number; pageSize?: numb
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchRefundConfirmedList(params?: { page?: number; pageSize?: number }) {
|
||||
return request.get<PageList<Record<string, unknown>>>({
|
||||
url: '/web/admin/protocol-payment-details/refund-confirmed',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchRefundConfirmSubmit(
|
||||
detailId: string,
|
||||
data?: { financeConfirmAttachments?: string }
|
||||
data?: { refundFinanceConfirmAttachments?: string; refundFinanceRemark?: string }
|
||||
) {
|
||||
return request.post({
|
||||
url: `/web/admin/protocol-payment-details/${detailId}/refund-confirm`,
|
||||
@@ -97,13 +111,58 @@ export function fetchRefundConfirmSubmit(
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchInstallmentWarningList(params?: {
|
||||
export function fetchSubmitReceiptFinance(detailId: string) {
|
||||
return request.post({
|
||||
url: `/web/admin/protocol-payment-details/${detailId}/submit-receipt-finance`,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSubmitReceiptFinanceBatch(detailIds: string[]) {
|
||||
return request.post({
|
||||
url: '/web/admin/protocol-payment-details/submit-receipt-finance-batch',
|
||||
data: { detailIds },
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSubmitRefundFinance(
|
||||
detailId: string,
|
||||
data: {
|
||||
refundAmount: number
|
||||
refundRemark?: string
|
||||
refundAttachments?: string
|
||||
}
|
||||
) {
|
||||
return request.post({
|
||||
url: `/web/admin/protocol-payment-details/${detailId}/submit-refund-finance`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSubmitRefundFinanceBatch(data: {
|
||||
detailIds: string[]
|
||||
refundAmount: number
|
||||
refundRemark?: string
|
||||
refundAttachments?: string
|
||||
}) {
|
||||
return request.post({
|
||||
url: '/web/admin/protocol-payment-details/submit-refund-finance-batch',
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchPaymentDetailList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
warningType?: string
|
||||
payStatus?: string
|
||||
keyword?: string
|
||||
transferType?: string
|
||||
}) {
|
||||
return request.get<PageList<Record<string, unknown>>>({
|
||||
url: '/web/admin/protocol-payment-details/installment-warning',
|
||||
url: '/web/admin/protocol-payment-details',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export type FinanceAttachmentItem = {
|
||||
id: string
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
fileType?: string
|
||||
_uploading?: boolean
|
||||
}
|
||||
|
||||
export function financeAttachmentsToUrlString(items: FinanceAttachmentItem[]) {
|
||||
return items
|
||||
.filter((f) => f.fileUrl && !f._uploading)
|
||||
.map((f) => f.fileUrl)
|
||||
.join(',')
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
<!-- 财务确认弹窗:附件上传 + 财务备注(对齐采集协议单附件 UI) -->
|
||||
<template>
|
||||
<div class="protocol-finance-confirm-fields">
|
||||
<el-form-item label="财务备注" class="form-col-span-2">
|
||||
<el-input
|
||||
:model-value="financeRemark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="选填"
|
||||
@update:model-value="emit('update:financeRemark', $event)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务确认附件" class="form-col-span-2">
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
|
||||
@change="handleAttachmentChange"
|
||||
>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="22" color="#10b981"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传附件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
v-for="(file, fIdx) in attachments"
|
||||
:key="file.id || fIdx"
|
||||
class="attachment-item"
|
||||
:class="{ 'is-uploading': file._uploading }"
|
||||
>
|
||||
<div
|
||||
class="attachment-preview"
|
||||
@click="isImageFile(file) && file.fileUrl ? handlePreview(file) : undefined"
|
||||
>
|
||||
<template v-if="isImageFile(file) && file.fileUrl">
|
||||
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
|
||||
</template>
|
||||
<template v-else-if="isImageFile(file) && !file.fileUrl">
|
||||
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
|
||||
</template>
|
||||
<template v-else-if="isVideoFile(file)">
|
||||
<div class="attachment-preview__icon attachment-preview__icon--video">
|
||||
<el-icon :size="28"><VideoPlay /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="attachment-preview__icon attachment-preview__icon--doc">
|
||||
<el-icon :size="28"><Document /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="attachment-footer">
|
||||
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
|
||||
<div class="attachment-actions">
|
||||
<el-icon
|
||||
v-if="file.fileUrl"
|
||||
class="action-btn"
|
||||
title="下载"
|
||||
@click.stop="downloadFile(file)"
|
||||
>
|
||||
<Download />
|
||||
</el-icon>
|
||||
<el-icon class="action-btn" title="删除" @click.stop="removeAttachment(fIdx)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="upload-hint">支持图片、视频、文档等,单文件不超过 20MB。</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Document,
|
||||
Delete,
|
||||
Download,
|
||||
Loading,
|
||||
VideoPlay
|
||||
} from '@element-plus/icons-vue'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import type { FinanceAttachmentItem } from './finance-attachments'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
financeRemark: string
|
||||
attachments: FinanceAttachmentItem[]
|
||||
uploadFolder?: string
|
||||
}>(),
|
||||
{ uploadFolder: 'protocol-finance-confirm' }
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:financeRemark', val: string): void
|
||||
(e: 'update:attachments', val: FinanceAttachmentItem[]): void
|
||||
}>()
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
function fullFileUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
|
||||
}
|
||||
|
||||
function isImageFile(file: FinanceAttachmentItem) {
|
||||
const t = file.fileType || ''
|
||||
const n = file.fileName || ''
|
||||
return t.startsWith('image/') || /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(n)
|
||||
}
|
||||
|
||||
function isVideoFile(file: FinanceAttachmentItem) {
|
||||
const t = file.fileType || ''
|
||||
const n = file.fileName || ''
|
||||
return t.startsWith('video/') || /\.(mp4|webm|mov|avi|mkv)$/i.test(n)
|
||||
}
|
||||
|
||||
function handlePreview(file: FinanceAttachmentItem) {
|
||||
if (!file.fileUrl) return
|
||||
window.open(fullFileUrl(file.fileUrl), '_blank')
|
||||
}
|
||||
|
||||
function downloadFile(file: FinanceAttachmentItem) {
|
||||
if (!file.fileUrl) return
|
||||
const a = document.createElement('a')
|
||||
a.href = fullFileUrl(file.fileUrl)
|
||||
a.download = file.fileName || 'download'
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
a.click()
|
||||
}
|
||||
|
||||
function removeAttachment(index: number) {
|
||||
const next = [...props.attachments]
|
||||
next.splice(index, 1)
|
||||
emit('update:attachments', next)
|
||||
}
|
||||
|
||||
async function handleAttachmentChange(uploadFile: { raw?: File }) {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
const maxSizeMb = 20
|
||||
if (file.size / 1024 / 1024 > maxSizeMb) {
|
||||
ElMessage.error(`文件大小不能超过 ${maxSizeMb}MB`)
|
||||
return
|
||||
}
|
||||
const tempId = `temp_${Date.now()}`
|
||||
const uploading: FinanceAttachmentItem = {
|
||||
id: tempId,
|
||||
fileName: file.name,
|
||||
fileUrl: '',
|
||||
fileType: file.type || '',
|
||||
_uploading: true
|
||||
}
|
||||
const listWithTemp = [...props.attachments, uploading]
|
||||
emit('update:attachments', listWithTemp)
|
||||
const idx = listWithTemp.length - 1
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', props.uploadFolder)
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
const updated = [...listWithTemp]
|
||||
updated[idx] = {
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
fileType: res.fileType || file.type || '',
|
||||
_uploading: false
|
||||
}
|
||||
emit('update:attachments', updated)
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
emit(
|
||||
'update:attachments',
|
||||
props.attachments.filter((a) => a.id !== tempId)
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.protocol-finance-confirm-fields {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attachment-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
width: 120px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 8px;
|
||||
background: #f0fdf4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
border-style: solid;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
font-size: 12px;
|
||||
color: #10b981;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.4;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
&.is-uploading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
|
||||
&--video {
|
||||
color: #10b981;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
font-size: 16px;
|
||||
|
||||
&:hover {
|
||||
color: #10b981;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,253 @@
|
||||
<!-- 缴费明细只读信息(核对用) -->
|
||||
<template>
|
||||
<div class="protocol-payment-detail-readonly">
|
||||
<h4 v-if="title" class="detail-block-title">{{ title }}</h4>
|
||||
<dl class="view-kv-grid view-kv-grid--4">
|
||||
<div class="view-kv">
|
||||
<dt>协议号</dt>
|
||||
<dd>{{ dash(detail?.protocolNo) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>协议状态</dt>
|
||||
<dd>{{ protocolStatusLabel(String(detail?.protocolStatus || '')) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>缴费方式</dt>
|
||||
<dd>{{ payModeLabel(String(detail?.payMode || '')) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>缴费状态</dt>
|
||||
<dd>{{ payStatusLabel(String(detail?.payStatus || '')) }}</dd>
|
||||
</div>
|
||||
<template v-if="showCustomer">
|
||||
<div class="view-kv">
|
||||
<dt>客户姓名</dt>
|
||||
<dd>{{ dash(detail?.motherName) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>身份证号</dt>
|
||||
<dd>{{ maskIdCard(String(detail?.motherIdCard || '')) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>新生儿姓名</dt>
|
||||
<dd>{{ dash(detail?.newbornName) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>新生儿身份证号</dt>
|
||||
<dd>{{ maskIdCard(String(detail?.newbornIdCard || '')) }}</dd>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="detail?.payMode === 'fq'" class="view-kv">
|
||||
<dt>分期期序号</dt>
|
||||
<dd>{{ dash(detail?.installmentNo) }}</dd>
|
||||
</div>
|
||||
<div v-if="detail?.payMode === 'fq'" class="view-kv">
|
||||
<dt>应缴费时间</dt>
|
||||
<dd>{{ formatPaymentDueDate(detail?.installmentDueDate) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>应缴费金额</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.dueAmount) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>转账方式</dt>
|
||||
<dd>{{ transferTypeLabel(String(detail?.transferType || '')) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>转账金额</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.transferAmount) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>转账时间</dt>
|
||||
<dd>{{ dash(detail?.transferTime) }}</dd>
|
||||
</div>
|
||||
<div v-if="detail?.remark" class="view-kv view-kv-span-2">
|
||||
<dt>备注</dt>
|
||||
<dd>{{ dash(detail?.remark) }}</dd>
|
||||
</div>
|
||||
<div class="view-field-block view-kv-span-2 payment-detail-readonly__attachments">
|
||||
<div class="view-field-block__label">转账凭证</div>
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="transferAttachmentFiles"
|
||||
@preview="(f) => emit('preview', f)"
|
||||
@download="(f) => emit('download', f)"
|
||||
/>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<template v-if="showRefundApply && hasRefundApplyInfo">
|
||||
<div class="form-section-divider form-section-divider--inner" />
|
||||
<h4 v-if="refundApplyTitle" class="detail-block-title">{{ refundApplyTitle }}</h4>
|
||||
<dl class="view-kv-grid view-kv-grid--4">
|
||||
<div class="view-kv">
|
||||
<dt>退费金额</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.refundAmount) }}</dd>
|
||||
</div>
|
||||
<div v-if="detail?.refundRemark" class="view-kv view-kv-span-2">
|
||||
<dt>退费备注</dt>
|
||||
<dd>{{ dash(detail?.refundRemark) }}</dd>
|
||||
</div>
|
||||
<div class="view-field-block view-kv-span-2 payment-detail-readonly__attachments">
|
||||
<div class="view-field-block__label">退费附件</div>
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="refundAttachmentFiles"
|
||||
@preview="(f) => emit('preview', f)"
|
||||
@download="(f) => emit('download', f)"
|
||||
/>
|
||||
</div>
|
||||
</dl>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
import {
|
||||
formatPaymentAmount,
|
||||
formatPaymentDueDate,
|
||||
payModeLabel,
|
||||
payStatusLabel,
|
||||
protocolStatusLabel,
|
||||
transferTypeLabel
|
||||
} from '@/utils/protocol-payment-labels'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
detail?: Record<string, unknown> | null
|
||||
title?: string
|
||||
showCustomer?: boolean
|
||||
showRefundApply?: boolean
|
||||
refundApplyTitle?: string
|
||||
}>(),
|
||||
{ showCustomer: true, showRefundApply: false, refundApplyTitle: '退费申请(请核对)' }
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'preview', file: Record<string, unknown>): void
|
||||
(e: 'download', file: Record<string, unknown>): void
|
||||
}>()
|
||||
|
||||
function dash(v: unknown) {
|
||||
if (v == null || v === '') return '—'
|
||||
return String(v)
|
||||
}
|
||||
|
||||
const transferAttachmentFiles = computed(() => {
|
||||
const row = props.detail
|
||||
if (!row) return []
|
||||
if (Array.isArray(row.attachments)) {
|
||||
return row.attachments as Array<Record<string, unknown>>
|
||||
}
|
||||
const urls = String(row.transferAttachments || '')
|
||||
if (!urls.trim()) return []
|
||||
return urls
|
||||
.split(',')
|
||||
.map((u, i) => {
|
||||
const url = u.trim()
|
||||
if (!url) return null
|
||||
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
|
||||
return { id: `url_${i}`, fileUrl: url, fileName: name }
|
||||
})
|
||||
.filter(Boolean) as Array<Record<string, unknown>>
|
||||
})
|
||||
|
||||
const refundAttachmentFiles = computed(() => {
|
||||
const row = props.detail
|
||||
if (!row) return []
|
||||
if (Array.isArray(row.refundAttachmentFiles)) {
|
||||
return row.refundAttachmentFiles as Array<Record<string, unknown>>
|
||||
}
|
||||
const urls = String(row.refundAttachments || '')
|
||||
if (!urls.trim()) return []
|
||||
return urls
|
||||
.split(',')
|
||||
.map((u, i) => {
|
||||
const url = u.trim()
|
||||
if (!url) return null
|
||||
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
|
||||
return { id: `url_${i}`, fileUrl: url, fileName: name }
|
||||
})
|
||||
.filter(Boolean) as Array<Record<string, unknown>>
|
||||
})
|
||||
|
||||
const hasRefundApplyInfo = computed(() => {
|
||||
const row = props.detail
|
||||
if (!row) return false
|
||||
const amount = row.refundAmount
|
||||
const hasAmount = amount != null && amount !== '' && Number(amount) > 0
|
||||
return (
|
||||
hasAmount ||
|
||||
String(row.refundRemark || '').trim() !== '' ||
|
||||
refundAttachmentFiles.value.length > 0
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.protocol-payment-detail-readonly {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detail-block-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.view-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.view-kv-grid--4 {
|
||||
@media (min-width: 720px) {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.view-kv {
|
||||
min-width: 0;
|
||||
|
||||
dt {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.view-kv-span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.view-field-block__label {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.payment-detail-readonly__attachments {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-section-divider--inner {
|
||||
height: 1px;
|
||||
margin: 16px 0;
|
||||
background: var(--mint-border, #e2e8f0);
|
||||
}
|
||||
</style>
|
||||
@@ -129,6 +129,13 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
component: '/cell-storage/protocol/index',
|
||||
meta: { title: '采集协议单', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000007',
|
||||
name: 'CellStoragePaymentInfo',
|
||||
path: 'payment-info',
|
||||
component: '/cell-storage/payment-info/index',
|
||||
meta: { title: '缴费信息', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000003',
|
||||
name: 'CellStorageProtocolAudit',
|
||||
@@ -141,7 +148,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
name: 'CellStoragePaymentConfirm',
|
||||
path: 'payment-confirm',
|
||||
component: '/cell-storage/payment-confirm/index',
|
||||
meta: { title: '收款确认(财务)', keepAlive: true }
|
||||
meta: { title: '缴费确认(财务)', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000005',
|
||||
@@ -149,13 +156,6 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
path: 'refund-confirm',
|
||||
component: '/cell-storage/refund-confirm/index',
|
||||
meta: { title: '退费确认(财务)', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000006',
|
||||
name: 'CellStorageInstallmentWarning',
|
||||
path: 'installment-warning',
|
||||
component: '/cell-storage/installment-warning/index',
|
||||
meta: { title: '分期缴费预警', keepAlive: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/** 身份证号脱敏:18 位保留前 6 后 4,15 位保留前 6 后 3 */
|
||||
export function maskIdCard(value: unknown): string {
|
||||
const raw = String(value ?? '').trim()
|
||||
if (!raw) return '-'
|
||||
const len = raw.length
|
||||
if (len === 18) {
|
||||
return `${raw.slice(0, 6)}${'*'.repeat(8)}${raw.slice(14)}`
|
||||
}
|
||||
if (len === 15) {
|
||||
return `${raw.slice(0, 6)}${'*'.repeat(6)}${raw.slice(12)}`
|
||||
}
|
||||
if (len <= 4) {
|
||||
return '*'.repeat(len)
|
||||
}
|
||||
const head = Math.min(3, Math.floor(len / 3))
|
||||
const tail = Math.min(4, len - head - 1)
|
||||
return `${raw.slice(0, head)}${'*'.repeat(len - head - tail)}${raw.slice(len - tail)}`
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export function protocolStatusLabel(s: string) {
|
||||
const map: Record<string, string> = {
|
||||
dsx: '待生效',
|
||||
sxz: '生效中',
|
||||
ygq: '已过期',
|
||||
yzf: '已作废',
|
||||
zky: '转科研'
|
||||
}
|
||||
return map[s] || s || '-'
|
||||
}
|
||||
|
||||
export function payModeLabel(s: string) {
|
||||
if (s === 'dj') return '趸交'
|
||||
if (s === 'fq') return '分期'
|
||||
return s || '-'
|
||||
}
|
||||
|
||||
export function payStatusLabel(s: string) {
|
||||
const map: Record<string, string> = {
|
||||
dfj: '待缴费',
|
||||
jfqrz: '缴费确认中',
|
||||
yjf: '已缴费',
|
||||
tfqrz: '退费确认中',
|
||||
ytf: '已退费'
|
||||
}
|
||||
return map[s] || '未知'
|
||||
}
|
||||
|
||||
export function transferTypeLabel(t: string) {
|
||||
const map: Record<string, string> = {
|
||||
wxzz: '微信转账',
|
||||
zzbzz: '支付宝转账',
|
||||
yhkzz: '银行卡转账',
|
||||
xj: '现金'
|
||||
}
|
||||
return map[t] || t || '-'
|
||||
}
|
||||
|
||||
export function formatPaymentDueDate(d: unknown) {
|
||||
const s = String(d || '')
|
||||
if (!s) return '-'
|
||||
if (s.length === 8) return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`
|
||||
return s
|
||||
}
|
||||
|
||||
export function formatPaymentAmount(v: unknown) {
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
<!-- 分期缴费预警 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>分期缴费预警</h1>
|
||||
<p class="page-desc">仅展示待缴费分期;预警窗口为未来 15 天内应缴;逾期为应缴日早于今日。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">分期预警列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="范围">
|
||||
<el-select v-model="warningType" style="width: 100%" @change="handleSearch">
|
||||
<el-option label="全部" value="all" />
|
||||
<el-option label="预警(15天内)" value="warn" />
|
||||
<el-option label="逾期" value="overdue" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id">
|
||||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||||
<el-table-column prop="protocolNo" label="协议号" min-width="130" align="center" header-align="center" />
|
||||
<el-table-column prop="installmentNo" label="期数" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="installmentDueDate" label="应缴日" width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="warningType" label="分级" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="warningTagType(String(row.warningType || ''))" effect="plain">{{
|
||||
warningTypeLabel(String(row.warningType || ''))
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferAmount" label="金额" width="120" align="center" header-align="center" />
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { fetchInstallmentWarningList } from '@/api/collection-protocol'
|
||||
|
||||
defineOptions({ name: 'CellStorageInstallmentWarning' })
|
||||
|
||||
function warningTypeLabel(w: string) {
|
||||
if (w === 'overdue') return '逾期'
|
||||
if (w === 'warn') return '预警'
|
||||
if (w === 'upcoming') return '未到期'
|
||||
return '其他'
|
||||
}
|
||||
|
||||
function warningTagType(w: string) {
|
||||
if (w === 'overdue') return 'danger'
|
||||
if (w === 'warn') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const warningType = ref('all')
|
||||
const tableData = ref<Record<string, unknown>[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchInstallmentWarningList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
warningType: warningType.value
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
@@ -1,31 +1,132 @@
|
||||
<!-- 收款确认(财务) -->
|
||||
<!-- 缴费确认(财务) -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>收款确认</h1>
|
||||
<p class="page-desc">仅处理「缴费确认中」明细,确认后变为已缴费。</p>
|
||||
<h1>缴费确认</h1>
|
||||
<p class="page-desc">
|
||||
待确认:处理「缴费确认中」明细;已确认:查看财务确认收款记录及当时填写的备注与附件。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="region-tabs region-tab-strip">
|
||||
<div class="tab-button-group region-tab-group">
|
||||
<el-button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="region-tab-btn"
|
||||
:type="activeTab === tab.key ? 'primary' : 'default'"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
<span class="region-tab-inner">
|
||||
<span class="region-tab-name">{{ tab.label }}</span>
|
||||
</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">收款确认列表</h2>
|
||||
<h2 class="title-bar-heading">缴费确认列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="list-body list-wrap payment-confirm-table-wrap">
|
||||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id">
|
||||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="protocolNo" label="协议号" min-width="130" align="center" header-align="center" />
|
||||
<el-table-column prop="installmentNo" label="期数" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="installmentDueDate" label="应缴日" width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="transferAmount" label="金额" width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="payStatus" label="状态" width="120" align="center" header-align="center">
|
||||
<template #default>缴费确认中</template>
|
||||
<el-table-column prop="motherName" label="客户姓名" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.motherName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="180" align="center" header-align="center" />
|
||||
<el-table-column prop="motherIdCard" label="身份证号" min-width="170" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ maskIdCard(row.motherIdCard) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="newbornName" label="新生儿姓名" min-width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.newbornName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornIdCard"
|
||||
label="新生儿身份证号"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ maskIdCard(row.newbornIdCard) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="protocolStatus" label="协议状态" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ protocolStatusLabel(String(row.protocolStatus || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payMode" label="缴费方式" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ payModeLabel(String(row.payMode || '')) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="installmentNo" label="期数" width="80" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.installmentNo ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="installmentDueDate" label="应缴费时间" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentDueDate(row.installmentDueDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dueAmount" label="应缴金额(元)" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentAmount(row.dueAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferAmount" label="实缴金额(元)" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentAmount(row.transferAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferType" label="转账方式" width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ transferTypeLabel(String(row.transferType || '')) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferTime" label="转账时间" min-width="170" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.transferTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payStatus" label="状态" width="110" align="center" header-align="center">
|
||||
<template #default>
|
||||
{{ activeTab === 'pending' ? '缴费确认中' : '已缴费' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'confirmed'"
|
||||
prop="updateTime"
|
||||
label="确认时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'confirmed'"
|
||||
prop="financeRemark"
|
||||
label="财务备注"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.financeRemark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'pending'"
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="操作" width="140" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="openConfirm(row)">确认收款</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'pending'"
|
||||
class="btn-op-item primary"
|
||||
@click="openConfirm(row)"
|
||||
>
|
||||
确认收款
|
||||
</el-button>
|
||||
<el-button v-else class="btn-op-item" @click="openViewConfirm(row)">查看确认信息</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -45,15 +146,26 @@
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="确认收款"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
width="900px"
|
||||
class="dialog-form-layout payment-confirm-dialog"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ProtocolPaymentDetailReadonly
|
||||
:detail="confirmRow"
|
||||
title="缴费明细(请核对)"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
<div class="form-section-divider" />
|
||||
<h4 class="detail-section-title">财务确认</h4>
|
||||
<el-form label-position="top" class="biz-dialog-form">
|
||||
<el-form-item label="财务确认附件(MinIO 地址,逗号分隔,可选)">
|
||||
<el-input v-model="confirmAttachments" type="textarea" :rows="3" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<ProtocolFinanceConfirmFields
|
||||
v-model:finance-remark="financeRemark"
|
||||
v-model:attachments="attachments"
|
||||
upload-folder="protocol-finance-receipt"
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||||
@@ -66,32 +178,150 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="viewDialogVisible"
|
||||
title="确认收款信息"
|
||||
width="900px"
|
||||
class="dialog-form-layout payment-confirm-dialog"
|
||||
align-center
|
||||
destroy-on-close
|
||||
>
|
||||
<ProtocolPaymentDetailReadonly
|
||||
:detail="viewRow"
|
||||
title="缴费明细"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
<div class="form-section-divider" />
|
||||
<h4 class="detail-section-title">财务确认信息</h4>
|
||||
<el-form label-position="top" class="biz-dialog-form view-confirm-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="确认时间" class="form-col-span-2">
|
||||
<span class="view-field-text">{{ viewRow?.updateTime || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务备注" class="form-col-span-2">
|
||||
<span class="view-field-text view-field-text--multiline">{{
|
||||
viewRow?.financeRemark || '—'
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务确认附件" class="form-col-span-2">
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="viewFinanceAttachmentFiles"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="viewDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close, Check } from '@element-plus/icons-vue'
|
||||
import { fetchReceiptConfirmList, fetchReceiptConfirmSubmit } from '@/api/collection-protocol'
|
||||
import {
|
||||
fetchReceiptConfirmList,
|
||||
fetchReceiptConfirmedList,
|
||||
fetchReceiptConfirmSubmit
|
||||
} from '@/api/collection-protocol'
|
||||
import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
|
||||
import ProtocolPaymentDetailReadonly from '@/components/business/protocol-payment-detail-readonly/index.vue'
|
||||
import ProtocolAttachmentReadonly from '../protocol/ProtocolAttachmentReadonly.vue'
|
||||
import {
|
||||
financeAttachmentsToUrlString,
|
||||
type FinanceAttachmentItem
|
||||
} from '@/components/business/protocol-finance-confirm-fields/finance-attachments'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
import {
|
||||
formatPaymentAmount,
|
||||
formatPaymentDueDate,
|
||||
payModeLabel,
|
||||
protocolStatusLabel,
|
||||
transferTypeLabel
|
||||
} from '@/utils/protocol-payment-labels'
|
||||
|
||||
defineOptions({ name: 'CellStoragePaymentConfirm' })
|
||||
|
||||
const tabs = [
|
||||
{ key: 'pending', label: '待确认' },
|
||||
{ key: 'confirmed', label: '已确认' }
|
||||
]
|
||||
|
||||
const activeTab = ref('pending')
|
||||
const tableData = ref<Record<string, unknown>[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const dialogVisible = ref(false)
|
||||
const viewDialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const confirmAttachments = ref('')
|
||||
const financeRemark = ref('')
|
||||
const attachments = ref<FinanceAttachmentItem[]>([])
|
||||
const currentDetailId = ref<string | null>(null)
|
||||
const confirmRow = ref<Record<string, unknown> | null>(null)
|
||||
const viewRow = ref<Record<string, unknown> | null>(null)
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
const viewFinanceAttachmentFiles = computed(() => {
|
||||
const row = viewRow.value
|
||||
if (!row) return []
|
||||
if (Array.isArray(row.financeConfirmAttachmentFiles)) {
|
||||
return row.financeConfirmAttachmentFiles as Array<Record<string, unknown>>
|
||||
}
|
||||
const urls = String(row.financeConfirmAttachments || '')
|
||||
if (!urls.trim()) return []
|
||||
return urls
|
||||
.split(',')
|
||||
.map((u, i) => {
|
||||
const url = u.trim()
|
||||
if (!url) return null
|
||||
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
|
||||
return { id: `url_${i}`, fileUrl: url, fileName: name }
|
||||
})
|
||||
.filter(Boolean) as Array<Record<string, unknown>>
|
||||
})
|
||||
|
||||
function fullFileUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
|
||||
}
|
||||
|
||||
function handleAttachmentPreview(file: Record<string, unknown>) {
|
||||
const url = String(file.fileUrl || '')
|
||||
if (!url) return
|
||||
window.open(fullFileUrl(url), '_blank')
|
||||
}
|
||||
|
||||
function handleAttachmentDownload(file: Record<string, unknown>) {
|
||||
const url = String(file.fileUrl || '')
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = fullFileUrl(url)
|
||||
a.download = String(file.fileName || 'download')
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
a.click()
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchReceiptConfirmList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const params = { page: pagination.page, pageSize: pagination.pageSize }
|
||||
const res =
|
||||
activeTab.value === 'pending'
|
||||
? await fetchReceiptConfirmList(params)
|
||||
: await fetchReceiptConfirmedList(params)
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
@@ -99,18 +329,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(key: string) {
|
||||
if (activeTab.value === key) return
|
||||
activeTab.value = key
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function openConfirm(row: Record<string, unknown>) {
|
||||
currentDetailId.value = String(row.id)
|
||||
confirmAttachments.value = ''
|
||||
confirmRow.value = row
|
||||
financeRemark.value = ''
|
||||
attachments.value = []
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openViewConfirm(row: Record<string, unknown>) {
|
||||
viewRow.value = row
|
||||
viewDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitConfirm() {
|
||||
if (!currentDetailId.value) return
|
||||
if (attachments.value.some((f) => f._uploading)) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await fetchReceiptConfirmSubmit(currentDetailId.value, {
|
||||
financeConfirmAttachments: confirmAttachments.value || undefined
|
||||
financeConfirmAttachments: financeAttachmentsToUrlString(attachments.value) || undefined,
|
||||
financeRemark: financeRemark.value.trim() || undefined
|
||||
})
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
@@ -123,3 +372,73 @@
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.region-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.region-tab-btn {
|
||||
position: relative;
|
||||
min-width: 108px;
|
||||
padding: 0 12px;
|
||||
|
||||
:deep(.el-icon) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.region-tab-btn.el-button > :deep(span) {
|
||||
position: static !important;
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.region-tab-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.payment-confirm-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.payment-confirm-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section-divider {
|
||||
height: 1px;
|
||||
margin: 20px 0;
|
||||
background: var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.view-confirm-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.view-field-text {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
|
||||
&--multiline {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,888 @@
|
||||
<!-- 缴费信息 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>缴费信息</h1>
|
||||
<p class="page-desc">查看采集协议单缴费明细,按状态分为待缴费、缴费确认中、已缴费、退费确认中、已退费。</p>
|
||||
</header>
|
||||
|
||||
<div class="region-tabs region-tab-strip">
|
||||
<div class="tab-button-group region-tab-group">
|
||||
<el-button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="region-tab-btn"
|
||||
:type="activeTab === tab.key ? 'primary' : 'default'"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
<span class="region-tab-inner">
|
||||
<span class="region-tab-name">{{ tab.label }}</span>
|
||||
</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">缴费信息列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" label-position="top" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="协议号"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账方式">
|
||||
<el-select v-model="searchForm.transferType" placeholder="全部" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="微信转账" value="wxzz" />
|
||||
<el-option label="支付宝转账" value="zzbzz" />
|
||||
<el-option label="银行卡转账" value="yhkzz" />
|
||||
<el-option label="现金" value="xj" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">支持按协议号模糊检索。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="activeTab === 'dfj' || activeTab === 'yjf'"
|
||||
class="list-toolbar"
|
||||
>
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="handleBatchSubmitFinance"
|
||||
>
|
||||
{{ activeTab === 'dfj' ? '批量缴费确认' : '批量退费确认' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'dfj' || activeTab === 'yjf'"
|
||||
type="selection"
|
||||
width="55"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="protocolNo"
|
||||
label="协议号"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="motherName"
|
||||
label="客户姓名"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.motherName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="motherIdCard"
|
||||
label="身份证号"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ maskIdCard(row.motherIdCard) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornName"
|
||||
label="新生儿姓名"
|
||||
min-width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.newbornName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornIdCard"
|
||||
label="新生儿身份证号"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ maskIdCard(row.newbornIdCard) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="protocolStatus"
|
||||
label="协议状态"
|
||||
width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ protocolStatusLabel(String(row.protocolStatus || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="payMode"
|
||||
label="缴费方式"
|
||||
width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ payModeLabel(String(row.payMode || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="installmentNo"
|
||||
label="期数"
|
||||
width="80"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="installmentDueDate"
|
||||
label="应缴费时间"
|
||||
width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(String(row.installmentDueDate || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="dueAmount"
|
||||
label="应缴金额(元)"
|
||||
width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="amount-text">{{ formatAmount(row.dueAmount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="transferAmount"
|
||||
label="实缴金额(元)"
|
||||
width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="amount-text">{{ formatAmount(row.transferAmount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="transferType"
|
||||
label="转账方式"
|
||||
width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ transferTypeLabel(String(row.transferType || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="transferTime"
|
||||
label="转账时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.transferTime || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="payStatus"
|
||||
label="状态"
|
||||
width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payStatusTagType(String(row.payStatus || ''))" size="small">
|
||||
{{ payStatusLabel(String(row.payStatus || '')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
:min-width="activeTab === 'dfj' || activeTab === 'yjf' ? 280 : 120"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'dfj'"
|
||||
class="btn-op-item primary"
|
||||
@click="handleSubmitReceiptFinance(row)"
|
||||
>
|
||||
缴费确认
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'yjf'"
|
||||
class="btn-op-item primary"
|
||||
@click="openRefundFinanceDialog(row)"
|
||||
>
|
||||
退费确认
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="refundDialogVisible"
|
||||
title="退费确认"
|
||||
width="640px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="refundFormRef"
|
||||
:model="refundForm"
|
||||
:rules="refundFormRules"
|
||||
label-position="top"
|
||||
class="biz-dialog-form"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="退费金额(元)" prop="refundAmount" class="form-col-span-2">
|
||||
<el-input-number
|
||||
v-model="refundForm.refundAmount"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
:step="100"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="退费备注" prop="refundRemark" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="refundForm.refundRemark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="选填"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="退费附件" class="form-col-span-2">
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
|
||||
@change="handleRefundAttachmentChange"
|
||||
>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="22" color="#10b981"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传附件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
v-for="(file, fIdx) in refundForm.attachments"
|
||||
:key="file.id || fIdx"
|
||||
class="attachment-item"
|
||||
:class="{ 'is-uploading': file._uploading }"
|
||||
>
|
||||
<div class="attachment-preview">
|
||||
<template v-if="file.fileUrl && isImageUrl(file.fileUrl)">
|
||||
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="attachment-preview__icon attachment-preview__icon--doc">
|
||||
<el-icon :size="28"><Document /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="attachment-footer">
|
||||
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
|
||||
<el-icon class="action-btn" title="删除" @click="removeRefundAttachment(fIdx)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="form-tip">支持图片、文档等,多个附件将一并提交。</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="refundDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="refundSubmitLoading"
|
||||
@click="submitRefundFinance"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
确认提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<ProtocolViewDialog
|
||||
v-model="viewVisible"
|
||||
:protocol-id="viewProtocolId"
|
||||
:list-row="viewRow"
|
||||
show-audit-history
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import {
|
||||
RefreshLeft,
|
||||
Search,
|
||||
View,
|
||||
Plus,
|
||||
Document,
|
||||
Delete,
|
||||
Close,
|
||||
Check
|
||||
} from '@element-plus/icons-vue'
|
||||
import ProtocolViewDialog from '../protocol/ProtocolViewDialog.vue'
|
||||
import {
|
||||
fetchPaymentDetailList,
|
||||
fetchSubmitReceiptFinance,
|
||||
fetchSubmitReceiptFinanceBatch,
|
||||
fetchSubmitRefundFinance,
|
||||
fetchSubmitRefundFinanceBatch,
|
||||
type CollectionProtocolRow
|
||||
} from '@/api/collection-protocol'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
|
||||
defineOptions({ name: 'CellStoragePaymentInfo' })
|
||||
|
||||
const tabs = [
|
||||
{ key: 'dfj', label: '待缴费' },
|
||||
{ key: 'jfqrz', label: '缴费确认中' },
|
||||
{ key: 'yjf', label: '已缴费' },
|
||||
{ key: 'tfqrz', label: '退费确认中' },
|
||||
{ key: 'ytf', label: '已退费' }
|
||||
]
|
||||
|
||||
const activeTab = ref('dfj')
|
||||
const searchForm = reactive({ keyword: '', transferType: '' })
|
||||
const tableData = ref<Record<string, unknown>[]>([])
|
||||
const selectedRows = ref<Record<string, unknown>[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const viewVisible = ref(false)
|
||||
const viewProtocolId = ref<string | null>(null)
|
||||
const viewRow = ref<CollectionProtocolRow | null>(null)
|
||||
const refundDialogVisible = ref(false)
|
||||
const refundSubmitLoading = ref(false)
|
||||
const refundTargetIds = ref<string[]>([])
|
||||
const refundFormRef = ref<FormInstance>()
|
||||
const refundForm = reactive({
|
||||
refundAmount: null as number | null,
|
||||
refundRemark: '',
|
||||
attachments: [] as Array<{
|
||||
id: string
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
_uploading?: boolean
|
||||
}>
|
||||
})
|
||||
const refundFormRules: FormRules = {
|
||||
refundAmount: [{ required: true, message: '请填写退费金额', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
function handleSelectionChange(rows: Record<string, unknown>[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function handleView(row: Record<string, unknown>) {
|
||||
const pid = String(row.collectionProtocolId || '')
|
||||
if (!pid) {
|
||||
ElMessage.warning('缺少关联协议单,无法查看')
|
||||
return
|
||||
}
|
||||
viewProtocolId.value = pid
|
||||
viewRow.value = {
|
||||
id: pid,
|
||||
protocolNo: row.protocolNo,
|
||||
protocolStatus: row.protocolStatus,
|
||||
motherName: row.motherName,
|
||||
motherIdCard: row.motherIdCard,
|
||||
newbornName: row.newbornName,
|
||||
newbornIdCard: row.newbornIdCard
|
||||
}
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
function protocolStatusLabel(s: string) {
|
||||
const map: Record<string, string> = {
|
||||
dsx: '待生效',
|
||||
sxz: '生效中',
|
||||
ygq: '已过期',
|
||||
yzf: '已作废',
|
||||
zky: '转科研'
|
||||
}
|
||||
return map[s] || s || '-'
|
||||
}
|
||||
|
||||
function payModeLabel(s: string) {
|
||||
if (s === 'dj') return '趸交'
|
||||
if (s === 'fq') return '分期'
|
||||
return s || '-'
|
||||
}
|
||||
|
||||
function payStatusLabel(s: string) {
|
||||
const map: Record<string, string> = {
|
||||
dfj: '待缴费',
|
||||
jfqrz: '缴费确认中',
|
||||
yjf: '已缴费',
|
||||
tfqrz: '退费确认中',
|
||||
ytf: '已退费'
|
||||
}
|
||||
return map[s] || '未知'
|
||||
}
|
||||
|
||||
function payStatusTagType(s: string) {
|
||||
const map: Record<string, string> = {
|
||||
dfj: 'info',
|
||||
jfqrz: 'warning',
|
||||
yjf: 'success',
|
||||
tfqrz: 'warning',
|
||||
ytf: 'danger'
|
||||
}
|
||||
return (map[s] || 'info') as 'info' | 'warning' | 'success' | 'danger'
|
||||
}
|
||||
|
||||
function transferTypeLabel(t: string) {
|
||||
const map: Record<string, string> = {
|
||||
wxzz: '微信转账',
|
||||
zzbzz: '支付宝转账',
|
||||
yhkzz: '银行卡转账',
|
||||
xj: '现金'
|
||||
}
|
||||
return map[t] || t || '-'
|
||||
}
|
||||
|
||||
function formatDate(d: string) {
|
||||
if (!d || d.length !== 8) return d || '-'
|
||||
return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`
|
||||
}
|
||||
|
||||
function formatAmount(v: unknown) {
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchPaymentDetailList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
payStatus: activeTab.value,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
transferType: searchForm.transferType || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(key: string) {
|
||||
if (activeTab.value === key) return
|
||||
activeTab.value = key
|
||||
selectedRows.value = []
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function handleSubmitReceiptFinance(row: Record<string, unknown>) {
|
||||
const id = String(row.id || '')
|
||||
if (!id) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确认对该缴费明细进行缴费确认?提交后状态变为「缴费确认中」。', '缴费确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await fetchSubmitReceiptFinance(id)
|
||||
loadData()
|
||||
} catch {
|
||||
/* cancel */
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchSubmitFinance() {
|
||||
if (!selectedRows.value.length) return
|
||||
const ids = selectedRows.value.map((r) => String(r.id || '')).filter(Boolean)
|
||||
if (!ids.length) return
|
||||
|
||||
if (activeTab.value === 'dfj') {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将选中的 ${ids.length} 条待缴费明细批量缴费确认?`,
|
||||
'批量缴费确认',
|
||||
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
await fetchSubmitReceiptFinanceBatch(ids)
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
} catch {
|
||||
/* cancel */
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (activeTab.value === 'yjf') {
|
||||
openRefundFinanceDialog(undefined, ids)
|
||||
}
|
||||
}
|
||||
|
||||
function resetRefundForm(defaultAmount?: number) {
|
||||
refundForm.refundAmount = defaultAmount ?? null
|
||||
refundForm.refundRemark = ''
|
||||
refundForm.attachments = []
|
||||
refundFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function openRefundFinanceDialog(
|
||||
row?: Record<string, unknown>,
|
||||
batchIds?: string[]
|
||||
) {
|
||||
const ids = batchIds?.length
|
||||
? batchIds
|
||||
: row?.id
|
||||
? [String(row.id)]
|
||||
: []
|
||||
if (!ids.length) return
|
||||
refundTargetIds.value = ids
|
||||
const defaultAmount =
|
||||
!batchIds?.length && row?.transferAmount != null
|
||||
? Number(row.transferAmount)
|
||||
: undefined
|
||||
resetRefundForm(
|
||||
defaultAmount != null && !Number.isNaN(defaultAmount) ? defaultAmount : undefined
|
||||
)
|
||||
refundDialogVisible.value = true
|
||||
}
|
||||
|
||||
function refundAttachmentsToUrls() {
|
||||
return refundForm.attachments
|
||||
.filter((f) => f.fileUrl && !f._uploading)
|
||||
.map((f) => f.fileUrl)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
function fullFileUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
|
||||
}
|
||||
|
||||
function isImageUrl(url: string) {
|
||||
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url)
|
||||
}
|
||||
|
||||
async function handleRefundAttachmentChange(uploadFile: { raw?: File }) {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
const maxSizeMb = 20
|
||||
if (file.size / 1024 / 1024 > maxSizeMb) {
|
||||
ElMessage.error(`文件大小不能超过 ${maxSizeMb}MB`)
|
||||
return
|
||||
}
|
||||
const tempId = `temp_${Date.now()}`
|
||||
refundForm.attachments.push({
|
||||
id: tempId,
|
||||
fileName: file.name,
|
||||
fileUrl: '',
|
||||
_uploading: true
|
||||
})
|
||||
const idx = refundForm.attachments.length - 1
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', 'protocol-payment-refund')
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
refundForm.attachments[idx] = {
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
_uploading: false
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
refundForm.attachments.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function removeRefundAttachment(index: number) {
|
||||
refundForm.attachments.splice(index, 1)
|
||||
}
|
||||
|
||||
async function submitRefundFinance() {
|
||||
if (!refundTargetIds.value.length) return
|
||||
const valid = await refundFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (refundForm.attachments.some((f) => f._uploading)) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
refundAmount: Number(refundForm.refundAmount),
|
||||
refundRemark: refundForm.refundRemark?.trim() || undefined,
|
||||
refundAttachments: refundAttachmentsToUrls() || undefined
|
||||
}
|
||||
refundSubmitLoading.value = true
|
||||
try {
|
||||
if (refundTargetIds.value.length === 1) {
|
||||
await fetchSubmitRefundFinance(refundTargetIds.value[0], payload)
|
||||
} else {
|
||||
await fetchSubmitRefundFinanceBatch({
|
||||
detailIds: refundTargetIds.value,
|
||||
...payload
|
||||
})
|
||||
}
|
||||
refundDialogVisible.value = false
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
} finally {
|
||||
refundSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.transferType = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.region-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.region-tab-btn {
|
||||
position: relative;
|
||||
min-width: 108px;
|
||||
padding: 0 12px;
|
||||
|
||||
:deep(.el-icon) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.region-tab-btn.el-button > :deep(span) {
|
||||
position: static !important;
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.region-tab-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.attachment-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
width: 120px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 8px;
|
||||
background: #f0fdf4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
font-size: 12px;
|
||||
color: #10b981;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 120px;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
&.is-uploading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
height: 96px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview__icon--doc {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.attachment-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -908,7 +908,7 @@
|
||||
<el-option label="已退费" value="ytf" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.payMode === 'fq'" label="应缴费时间">
|
||||
<el-form-item label="应缴费时间">
|
||||
<el-date-picker
|
||||
v-model="detail.installmentDueDate"
|
||||
type="date"
|
||||
@@ -1426,7 +1426,7 @@
|
||||
if (detail.payStatus) {
|
||||
parts.push(payStatusLabel(String(detail.payStatus)))
|
||||
}
|
||||
if (formData.payMode === 'fq' && detail.installmentDueDate) {
|
||||
if (detail.installmentDueDate) {
|
||||
parts.push(`应缴日 ${formatPaymentDateYmd(String(detail.installmentDueDate))}`)
|
||||
}
|
||||
const attCount = Array.isArray(detail.attachments) ? detail.attachments.length : 0
|
||||
@@ -1682,13 +1682,15 @@
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
const defaultTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
|
||||
const isFirst = formData.paymentDetails.length === 0
|
||||
// 趸交模式下应缴费时间默认等于趸交截止时间
|
||||
const defaultDueDate = formData.payMode === 'dj' ? formData.lumpSumDeadline : ''
|
||||
formData.paymentDetails.push({
|
||||
transferType: '',
|
||||
dueAmount: null,
|
||||
transferAmount: null,
|
||||
transferTime: defaultTime,
|
||||
payStatus: 'dfj',
|
||||
installmentDueDate: '',
|
||||
installmentDueDate: defaultDueDate,
|
||||
_expanded: isFirst,
|
||||
attachments: [] as Array<{
|
||||
id?: string
|
||||
@@ -1754,10 +1756,21 @@
|
||||
if (val === 'fq') {
|
||||
tryGenerateInstallmentDetails()
|
||||
} else {
|
||||
// 趸交模式:将趸交截止时间同步到所有明细的应缴费时间
|
||||
syncPaymentDueAmounts()
|
||||
syncLumpSumDueDate()
|
||||
}
|
||||
}
|
||||
|
||||
/** 趸交模式:将趸交截止时间同步到所有缴费明细的应缴费时间 */
|
||||
function syncLumpSumDueDate() {
|
||||
if (formData.payMode !== 'dj') return
|
||||
const deadline = formData.lumpSumDeadline || ''
|
||||
formData.paymentDetails.forEach((d) => {
|
||||
d.installmentDueDate = deadline
|
||||
})
|
||||
}
|
||||
|
||||
function handleOpenForAdd() {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增协议单'
|
||||
@@ -2621,6 +2634,16 @@
|
||||
() => calcBirthFromIdCard()
|
||||
)
|
||||
|
||||
/* ====== 趸交截止时间联动 ====== */
|
||||
watch(
|
||||
() => formData.lumpSumDeadline,
|
||||
() => {
|
||||
if (formData.payMode === 'dj') {
|
||||
syncLumpSumDueDate()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/* ====== 附件上传 ====== */
|
||||
const attachmentUploadRef = ref()
|
||||
|
||||
|
||||
@@ -313,6 +313,56 @@
|
||||
/>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<template
|
||||
v-for="flowCtx in [{ nodes: buildPaymentFlowNodes(detail) }]"
|
||||
:key="`${detail.id || dIdx}-flow`"
|
||||
>
|
||||
<div v-if="flowCtx.nodes.length" class="payment-flow">
|
||||
<div class="payment-flow-scroll">
|
||||
<div class="payment-flow-track">
|
||||
<div
|
||||
v-for="(node, nIdx) in flowCtx.nodes"
|
||||
:key="node.key"
|
||||
class="payment-flow-item"
|
||||
>
|
||||
<div class="payment-flow-rail">
|
||||
<span class="payment-flow-node" />
|
||||
<span
|
||||
v-if="nIdx < flowCtx.nodes.length - 1"
|
||||
class="payment-flow-line"
|
||||
/>
|
||||
</div>
|
||||
<div class="payment-flow-body">
|
||||
<div class="payment-flow-title">{{ node.title }}</div>
|
||||
<div
|
||||
v-if="node.amount != null && node.amount !== ''"
|
||||
class="payment-flow-kv"
|
||||
>
|
||||
<span class="payment-flow-kv__label">退费金额(元)</span>
|
||||
<span class="payment-flow-kv__value">{{ dash(node.amount) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="node.remark != null && String(node.remark).trim() !== ''"
|
||||
class="payment-flow-kv payment-flow-kv--remark"
|
||||
>
|
||||
<span class="payment-flow-kv__label">{{ node.remarkLabel }}</span>
|
||||
<span class="payment-flow-kv__value">{{ dash(node.remark) }}</span>
|
||||
</div>
|
||||
<div v-if="node.attachments.length" class="payment-flow-attachments">
|
||||
<span class="payment-flow-kv__label">{{ node.attachmentLabel }}</span>
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="node.attachments"
|
||||
@preview="openPreview"
|
||||
@download="downloadFile"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -494,6 +544,8 @@
|
||||
const collectorDisplay = computed(() => {
|
||||
const fromList = props.listRow?.collectorUserName
|
||||
if (fromList) return String(fromList)
|
||||
const fromBundle = protocol.value?.collectorUserName
|
||||
if (fromBundle) return String(fromBundle)
|
||||
const id = protocol.value?.collectorUserId
|
||||
const u = collectorUserOptions.value.find((x) => x.id === id)
|
||||
return u?.nickName || dash(id)
|
||||
@@ -502,6 +554,8 @@
|
||||
const salesDisplay = computed(() => {
|
||||
const fromList = props.listRow?.salesUserName
|
||||
if (fromList) return String(fromList)
|
||||
const fromBundle = collectionInfo.value?.salesUserName
|
||||
if (fromBundle) return String(fromBundle)
|
||||
const id = collectionInfo.value?.salesUserId
|
||||
const u = salesUserOptions.value.find((x) => x.id === id)
|
||||
return u?.nickName || dash(id)
|
||||
@@ -510,6 +564,8 @@
|
||||
const regionDisplay = computed(() => {
|
||||
const fromList = props.listRow?.regionName
|
||||
if (fromList) return String(fromList)
|
||||
const fromBundle = collectionInfo.value?.regionName
|
||||
if (fromBundle) return String(fromBundle)
|
||||
const id = collectionInfo.value?.regionId
|
||||
const r = regionOptions.value.find((x) => x.id === id)
|
||||
return r?.regionName || dash(id)
|
||||
@@ -518,6 +574,8 @@
|
||||
const hospitalDisplay = computed(() => {
|
||||
const fromList = props.listRow?.hospitalName
|
||||
if (fromList) return String(fromList)
|
||||
const fromBundle = collectionInfo.value?.hospitalName
|
||||
if (fromBundle) return String(fromBundle)
|
||||
const id = collectionInfo.value?.hospitalId
|
||||
const h = hospitalOptions.value.find((x) => x.id === id)
|
||||
return h?.hospitalName || dash(id)
|
||||
@@ -649,6 +707,131 @@
|
||||
return Array.isArray(detail.attachments) ? detail.attachments : []
|
||||
}
|
||||
|
||||
function financeConfirmAttachmentFiles(detail: Record<string, any>) {
|
||||
if (Array.isArray(detail.financeConfirmAttachmentFiles)) {
|
||||
return detail.financeConfirmAttachmentFiles
|
||||
}
|
||||
return urlsToAttachmentFiles(String(detail.financeConfirmAttachments || ''))
|
||||
}
|
||||
|
||||
function refundAttachmentFiles(detail: Record<string, any>) {
|
||||
if (Array.isArray(detail.refundAttachmentFiles)) {
|
||||
return detail.refundAttachmentFiles
|
||||
}
|
||||
return urlsToAttachmentFiles(String(detail.refundAttachments || ''))
|
||||
}
|
||||
|
||||
function refundFinanceConfirmAttachmentFiles(detail: Record<string, any>) {
|
||||
if (Array.isArray(detail.refundFinanceConfirmAttachmentFiles)) {
|
||||
return detail.refundFinanceConfirmAttachmentFiles
|
||||
}
|
||||
return urlsToAttachmentFiles(String(detail.refundFinanceConfirmAttachments || ''))
|
||||
}
|
||||
|
||||
function urlsToAttachmentFiles(urls: string) {
|
||||
if (!urls.trim()) return []
|
||||
return urls.split(',').map((u, i) => {
|
||||
const url = u.trim()
|
||||
if (!url) return null
|
||||
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
|
||||
return { id: `url_${i}`, fileUrl: url, fileName: name, fileSize: 0 }
|
||||
}).filter(Boolean) as Array<Record<string, any>>
|
||||
}
|
||||
|
||||
function hasFinanceRemark(detail: Record<string, any>) {
|
||||
return String(detail.financeRemark || '').trim() !== ''
|
||||
}
|
||||
|
||||
function hasFinanceConfirmAttachments(detail: Record<string, any>) {
|
||||
return financeConfirmAttachmentFiles(detail).length > 0
|
||||
}
|
||||
|
||||
function hasRefundApplyInfo(detail: Record<string, any>) {
|
||||
const amount = detail.refundAmount
|
||||
const hasAmount = amount != null && amount !== '' && Number(amount) > 0
|
||||
return (
|
||||
hasAmount ||
|
||||
String(detail.refundRemark || '').trim() !== '' ||
|
||||
refundAttachmentFiles(detail).length > 0
|
||||
)
|
||||
}
|
||||
|
||||
/** 缴费财务确认:已缴费或退费确认中,且已有财务确认字段 */
|
||||
function showPaymentFinanceConfirm(detail: Record<string, any>) {
|
||||
const status = String(detail.payStatus || '')
|
||||
if (status === 'ytf') return false
|
||||
if (status !== 'yjf' && status !== 'tfqrz') return false
|
||||
return hasFinanceRemark(detail) || hasFinanceConfirmAttachments(detail)
|
||||
}
|
||||
|
||||
/** 退费确认申请:发起退费时填写 */
|
||||
function showRefundApply(detail: Record<string, any>) {
|
||||
const status = String(detail.payStatus || '')
|
||||
if (status !== 'tfqrz' && status !== 'ytf') return false
|
||||
return hasRefundApplyInfo(detail)
|
||||
}
|
||||
|
||||
function hasRefundFinanceRemark(detail: Record<string, any>) {
|
||||
return String(detail.refundFinanceRemark || '').trim() !== ''
|
||||
}
|
||||
|
||||
function hasRefundFinanceConfirmAttachments(detail: Record<string, any>) {
|
||||
return refundFinanceConfirmAttachmentFiles(detail).length > 0
|
||||
}
|
||||
|
||||
/** 财务退费确认:已退费时展示的财务确认信息 */
|
||||
function showRefundFinanceConfirm(detail: Record<string, any>) {
|
||||
if (String(detail.payStatus || '') !== 'ytf') return false
|
||||
return hasRefundFinanceRemark(detail) || hasRefundFinanceConfirmAttachments(detail)
|
||||
}
|
||||
|
||||
interface PaymentFlowNode {
|
||||
key: string
|
||||
title: string
|
||||
remarkLabel: string
|
||||
attachmentLabel: string
|
||||
remark?: unknown
|
||||
amount?: unknown
|
||||
attachments: Array<Record<string, any>>
|
||||
}
|
||||
|
||||
/** 缴费明细 — 财务/退费流程横向节点(按业务顺序) */
|
||||
function buildPaymentFlowNodes(detail: Record<string, any>): PaymentFlowNode[] {
|
||||
const nodes: PaymentFlowNode[] = []
|
||||
if (showPaymentFinanceConfirm(detail)) {
|
||||
nodes.push({
|
||||
key: 'receipt-finance',
|
||||
title: '缴费财务确认',
|
||||
remarkLabel: '财务备注',
|
||||
attachmentLabel: '财务附件',
|
||||
remark: detail.financeRemark,
|
||||
attachments: financeConfirmAttachmentFiles(detail)
|
||||
})
|
||||
}
|
||||
if (showRefundApply(detail)) {
|
||||
nodes.push({
|
||||
key: 'refund-apply',
|
||||
title: '退费确认申请',
|
||||
remarkLabel: '退费备注',
|
||||
attachmentLabel: '退费附件',
|
||||
remark: detail.refundRemark,
|
||||
amount: detail.refundAmount,
|
||||
attachments: refundAttachmentFiles(detail)
|
||||
})
|
||||
}
|
||||
if (showRefundFinanceConfirm(detail)) {
|
||||
nodes.push({
|
||||
key: 'refund-finance',
|
||||
title: '财务退费确认',
|
||||
remarkLabel: '财务备注',
|
||||
attachmentLabel: '财务附件',
|
||||
remark: detail.refundFinanceRemark,
|
||||
attachments: refundFinanceConfirmAttachmentFiles(detail)
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
function fullFileUrl(suffix: string) {
|
||||
if (!suffix) return ''
|
||||
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/'))
|
||||
@@ -947,11 +1130,133 @@
|
||||
}
|
||||
|
||||
.payment-detail-card__attachments {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
margin-top: 4px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.payment-flow {
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.payment-flow-scroll {
|
||||
overflow-x: auto;
|
||||
padding: 0 0 6px 4px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-flow-track {
|
||||
display: inline-flex;
|
||||
min-width: 100%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.payment-flow-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 200px;
|
||||
flex: 1 0 200px;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.payment-flow-rail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.payment-flow-node {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
background: var(--primary, #10b981);
|
||||
box-shadow: 0 0 0 1px var(--primary, #10b981);
|
||||
}
|
||||
|
||||
.payment-flow-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
margin-left: 0;
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
.payment-flow-body {
|
||||
padding: 8px 12px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.payment-flow-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.payment-flow-kv {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.payment-flow-kv--remark .payment-flow-kv__value {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.payment-flow-kv__label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.payment-flow-kv__value {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.payment-flow-attachments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
|
||||
:deep(.attachment-list) {
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:deep(.attachment-item) {
|
||||
width: 88px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.attachment-preview) {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
:deep(.attachment-footer) {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
:deep(.attachment-name) {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.view-field-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -248,7 +248,12 @@
|
||||
/>
|
||||
|
||||
<!-- 查看弹窗 -->
|
||||
<ProtocolViewDialog v-model="viewVisible" :protocol-id="viewProtocolId" :list-row="viewRow" />
|
||||
<ProtocolViewDialog
|
||||
v-model="viewVisible"
|
||||
:protocol-id="viewProtocolId"
|
||||
:list-row="viewRow"
|
||||
show-audit-history
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,27 +4,142 @@
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>退费确认</h1>
|
||||
<p class="page-desc">仅处理「退费确认中」明细,确认后变为已退费。</p>
|
||||
<p class="page-desc">
|
||||
待确认:处理「退费确认中」明细;已确认:查看财务确认退费记录及当时填写的备注与附件。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="region-tabs region-tab-strip">
|
||||
<div class="tab-button-group region-tab-group">
|
||||
<el-button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="region-tab-btn"
|
||||
:type="activeTab === tab.key ? 'primary' : 'default'"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
<span class="region-tab-inner">
|
||||
<span class="region-tab-name">{{ tab.label }}</span>
|
||||
</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">退费确认列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="list-body list-wrap refund-confirm-table-wrap">
|
||||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id">
|
||||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="protocolNo" label="协议号" min-width="130" align="center" header-align="center" />
|
||||
<el-table-column prop="installmentNo" label="期数" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="transferAmount" label="金额" width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="payStatus" label="状态" width="120" align="center" header-align="center">
|
||||
<template #default>退费确认中</template>
|
||||
<el-table-column prop="motherName" label="客户姓名" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.motherName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="180" align="center" header-align="center" />
|
||||
<el-table-column prop="motherIdCard" label="身份证号" min-width="170" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ maskIdCard(row.motherIdCard) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="newbornName" label="新生儿姓名" min-width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.newbornName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornIdCard"
|
||||
label="新生儿身份证号"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ maskIdCard(row.newbornIdCard) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="protocolStatus" label="协议状态" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ protocolStatusLabel(String(row.protocolStatus || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payMode" label="缴费方式" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ payModeLabel(String(row.payMode || '')) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="installmentNo" label="期数" width="80" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.installmentNo ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="installmentDueDate" label="应缴费时间" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentDueDate(row.installmentDueDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dueAmount" label="应缴金额(元)" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentAmount(row.dueAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferAmount" label="实缴金额(元)" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentAmount(row.transferAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferType" label="转账方式" width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ transferTypeLabel(String(row.transferType || '')) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferTime" label="转账时间" min-width="170" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.transferTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="refundAmount" label="退费金额(元)" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentAmount(row.refundAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="refundRemark"
|
||||
label="退费备注"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.refundRemark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payStatus" label="状态" width="110" align="center" header-align="center">
|
||||
<template #default>
|
||||
{{ activeTab === 'pending' ? '退费确认中' : '已退费' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'confirmed'"
|
||||
prop="updateTime"
|
||||
label="确认时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'confirmed'"
|
||||
prop="refundFinanceRemark"
|
||||
label="财务备注"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.refundFinanceRemark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'pending'"
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="操作" width="140" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="openConfirm(row)">确认退费</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'pending'"
|
||||
class="btn-op-item primary"
|
||||
@click="openConfirm(row)"
|
||||
>
|
||||
确认退费
|
||||
</el-button>
|
||||
<el-button v-else class="btn-op-item" @click="openViewConfirm(row)">查看确认信息</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -44,15 +159,28 @@
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="确认退费"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
width="900px"
|
||||
class="dialog-form-layout refund-confirm-dialog"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ProtocolPaymentDetailReadonly
|
||||
:detail="confirmRow"
|
||||
title="缴费明细(请核对)"
|
||||
show-refund-apply
|
||||
refund-apply-title="退费申请(请核对)"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
<div class="form-section-divider" />
|
||||
<h4 class="detail-section-title">财务确认</h4>
|
||||
<el-form label-position="top" class="biz-dialog-form">
|
||||
<el-form-item label="财务确认附件(MinIO 地址,逗号分隔,可选)">
|
||||
<el-input v-model="confirmAttachments" type="textarea" :rows="3" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<ProtocolFinanceConfirmFields
|
||||
v-model:finance-remark="financeRemark"
|
||||
v-model:attachments="attachments"
|
||||
upload-folder="protocol-finance-refund"
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||||
@@ -65,32 +193,152 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="viewDialogVisible"
|
||||
title="确认退费信息"
|
||||
width="900px"
|
||||
class="dialog-form-layout refund-confirm-dialog"
|
||||
align-center
|
||||
destroy-on-close
|
||||
>
|
||||
<ProtocolPaymentDetailReadonly
|
||||
:detail="viewRow"
|
||||
title="缴费明细"
|
||||
show-refund-apply
|
||||
refund-apply-title="退费申请"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
<div class="form-section-divider" />
|
||||
<h4 class="detail-section-title">财务确认信息</h4>
|
||||
<el-form label-position="top" class="biz-dialog-form view-confirm-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="确认时间" class="form-col-span-2">
|
||||
<span class="view-field-text">{{ viewRow?.updateTime || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务备注" class="form-col-span-2">
|
||||
<span class="view-field-text view-field-text--multiline">{{
|
||||
viewRow?.refundFinanceRemark || '—'
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务确认附件" class="form-col-span-2">
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="viewFinanceAttachmentFiles"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="viewDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close, Check } from '@element-plus/icons-vue'
|
||||
import { fetchRefundConfirmList, fetchRefundConfirmSubmit } from '@/api/collection-protocol'
|
||||
import {
|
||||
fetchRefundConfirmList,
|
||||
fetchRefundConfirmedList,
|
||||
fetchRefundConfirmSubmit
|
||||
} from '@/api/collection-protocol'
|
||||
import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
|
||||
import ProtocolPaymentDetailReadonly from '@/components/business/protocol-payment-detail-readonly/index.vue'
|
||||
import ProtocolAttachmentReadonly from '../protocol/ProtocolAttachmentReadonly.vue'
|
||||
import {
|
||||
financeAttachmentsToUrlString,
|
||||
type FinanceAttachmentItem
|
||||
} from '@/components/business/protocol-finance-confirm-fields/finance-attachments'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
import {
|
||||
formatPaymentAmount,
|
||||
formatPaymentDueDate,
|
||||
payModeLabel,
|
||||
protocolStatusLabel,
|
||||
transferTypeLabel
|
||||
} from '@/utils/protocol-payment-labels'
|
||||
|
||||
defineOptions({ name: 'CellStorageRefundConfirm' })
|
||||
|
||||
const tabs = [
|
||||
{ key: 'pending', label: '待确认' },
|
||||
{ key: 'confirmed', label: '已确认' }
|
||||
]
|
||||
|
||||
const activeTab = ref('pending')
|
||||
const tableData = ref<Record<string, unknown>[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const dialogVisible = ref(false)
|
||||
const viewDialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const confirmAttachments = ref('')
|
||||
const financeRemark = ref('')
|
||||
const attachments = ref<FinanceAttachmentItem[]>([])
|
||||
const currentDetailId = ref<string | null>(null)
|
||||
const confirmRow = ref<Record<string, unknown> | null>(null)
|
||||
const viewRow = ref<Record<string, unknown> | null>(null)
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
const viewFinanceAttachmentFiles = computed(() => {
|
||||
const row = viewRow.value
|
||||
if (!row) return []
|
||||
if (Array.isArray(row.refundFinanceConfirmAttachmentFiles)) {
|
||||
return row.refundFinanceConfirmAttachmentFiles as Array<Record<string, unknown>>
|
||||
}
|
||||
const urls = String(row.refundFinanceConfirmAttachments || '')
|
||||
if (!urls.trim()) return []
|
||||
return urls
|
||||
.split(',')
|
||||
.map((u, i) => {
|
||||
const url = u.trim()
|
||||
if (!url) return null
|
||||
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
|
||||
return { id: `url_${i}`, fileUrl: url, fileName: name }
|
||||
})
|
||||
.filter(Boolean) as Array<Record<string, unknown>>
|
||||
})
|
||||
|
||||
function fullFileUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
|
||||
}
|
||||
|
||||
function handleAttachmentPreview(file: Record<string, unknown>) {
|
||||
const url = String(file.fileUrl || '')
|
||||
if (!url) return
|
||||
window.open(fullFileUrl(url), '_blank')
|
||||
}
|
||||
|
||||
function handleAttachmentDownload(file: Record<string, unknown>) {
|
||||
const url = String(file.fileUrl || '')
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = fullFileUrl(url)
|
||||
a.download = String(file.fileName || 'download')
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
a.click()
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchRefundConfirmList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const params = { page: pagination.page, pageSize: pagination.pageSize }
|
||||
const res =
|
||||
activeTab.value === 'pending'
|
||||
? await fetchRefundConfirmList(params)
|
||||
: await fetchRefundConfirmedList(params)
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
@@ -98,18 +346,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(key: string) {
|
||||
if (activeTab.value === key) return
|
||||
activeTab.value = key
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function openConfirm(row: Record<string, unknown>) {
|
||||
currentDetailId.value = String(row.id)
|
||||
confirmAttachments.value = ''
|
||||
confirmRow.value = row
|
||||
financeRemark.value = ''
|
||||
attachments.value = []
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openViewConfirm(row: Record<string, unknown>) {
|
||||
viewRow.value = row
|
||||
viewDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitConfirm() {
|
||||
if (!currentDetailId.value) return
|
||||
if (attachments.value.some((f) => f._uploading)) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await fetchRefundConfirmSubmit(currentDetailId.value, {
|
||||
financeConfirmAttachments: confirmAttachments.value || undefined
|
||||
refundFinanceConfirmAttachments: financeAttachmentsToUrlString(attachments.value) || undefined,
|
||||
refundFinanceRemark: financeRemark.value.trim() || undefined
|
||||
})
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
@@ -122,3 +389,73 @@
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.region-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.region-tab-btn {
|
||||
position: relative;
|
||||
min-width: 108px;
|
||||
padding: 0 12px;
|
||||
|
||||
:deep(.el-icon) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.region-tab-btn.el-button > :deep(span) {
|
||||
position: static !important;
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.region-tab-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.refund-confirm-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.refund-confirm-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section-divider {
|
||||
height: 1px;
|
||||
margin: 20px 0;
|
||||
background: var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.view-confirm-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.view-field-text {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
|
||||
&--multiline {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user