feat(einvoice): add electronic invoice module (T13.4)

This commit is contained in:
2026-06-18 15:59:45 +08:00
parent 5ee15b348b
commit 46ae0f39ab
22 changed files with 692 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package com.healthlink.his.web.einvoice.appservice;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.healthlink.his.einvoice.domain.EinvoiceHeader;
import java.util.Map;
public interface IEinvoiceAppService {
EinvoiceHeader generate(EinvoiceHeader header);
IPage<EinvoiceHeader> page(String invoiceStatus, String patientName, Integer pageNum, Integer pageSize);
void voidInvoice(Long id, String reason);
Map<String, Object> getReconciliation(Integer pageNum, Integer pageSize);
}

View File

@@ -0,0 +1,82 @@
package com.healthlink.his.web.einvoice.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.healthlink.his.einvoice.domain.EinvoiceHeader;
import com.healthlink.his.einvoice.service.IEinvoiceHeaderService;
import com.healthlink.his.einvoice.service.IEinvoiceReconciliationService;
import com.healthlink.his.web.einvoice.appservice.IEinvoiceAppService;
import com.core.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Service
public class EinvoiceAppServiceImpl implements IEinvoiceAppService {
@Autowired
private IEinvoiceHeaderService headerService;
@Autowired
private IEinvoiceReconciliationService reconciliationService;
@Override
public EinvoiceHeader generate(EinvoiceHeader header) {
header.setInvoiceNo("EINV" + System.currentTimeMillis());
header.setInvoiceType("ELECTRONIC");
header.setInvoiceStatus("ISSUED");
header.setIssueTime(new Date());
header.setIssuerName(SecurityUtils.getUsername());
headerService.save(header);
return header;
}
@Override
public IPage<EinvoiceHeader> page(String invoiceStatus, String patientName, Integer pageNum, Integer pageSize) {
LambdaQueryWrapper<EinvoiceHeader> w = new LambdaQueryWrapper<>();
if (StringUtils.hasText(invoiceStatus)) {
w.eq(EinvoiceHeader::getInvoiceStatus, invoiceStatus);
}
if (StringUtils.hasText(patientName)) {
w.like(EinvoiceHeader::getPatientName, patientName);
}
w.orderByDesc(EinvoiceHeader::getCreateTime);
return headerService.page(new Page<>(pageNum, pageSize), w);
}
@Override
public void voidInvoice(Long id, String reason) {
EinvoiceHeader header = headerService.getById(id);
if (header == null) {
throw new RuntimeException("发票不存在");
}
header.setInvoiceStatus("VOID");
header.setVoidTime(new Date());
header.setVoidReason(reason);
headerService.updateById(header);
}
@Override
public Map<String, Object> getReconciliation(Integer pageNum, Integer pageSize) {
LambdaQueryWrapper<EinvoiceHeader> w = new LambdaQueryWrapper<>();
w.eq(EinvoiceHeader::getInvoiceStatus, "ISSUED");
w.orderByDesc(EinvoiceHeader::getIssueTime);
IPage<EinvoiceHeader> page = headerService.page(new Page<>(pageNum, pageSize), w);
Map<String, Object> result = new HashMap<>();
result.put("records", page.getRecords());
result.put("total", page.getTotal());
LambdaQueryWrapper<EinvoiceHeader> totalW = new LambdaQueryWrapper<>();
totalW.eq(EinvoiceHeader::getInvoiceStatus, "ISSUED");
long totalCount = headerService.count(totalW);
result.put("totalCount", totalCount);
return result;
}
}

View File

@@ -0,0 +1,54 @@
package com.healthlink.his.web.einvoice.controller;
import com.core.common.core.domain.AjaxResult;
import com.healthlink.his.einvoice.domain.EinvoiceHeader;
import com.healthlink.his.web.einvoice.appservice.IEinvoiceAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Tag(name = "电子票据管理")
@RestController
@RequestMapping("/invoice")
public class EinvoiceController {
@Autowired
private IEinvoiceAppService einvoiceAppService;
@Operation(summary = "生成电子票据")
@PreAuthorize("@ss.hasPermi('basicmanage:invoice:edit')")
@PostMapping("/generate")
public AjaxResult generate(@RequestBody EinvoiceHeader header) {
return AjaxResult.success(einvoiceAppService.generate(header));
}
@Operation(summary = "电子票据分页")
@PreAuthorize("@ss.hasPermi('basicmanage:invoice:list')")
@GetMapping("/page")
public AjaxResult page(@RequestParam(required = false) String invoiceStatus,
@RequestParam(required = false) String patientName,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
return AjaxResult.success(einvoiceAppService.page(invoiceStatus, patientName, pageNum, pageSize));
}
@Operation(summary = "作废电子票据")
@PreAuthorize("@ss.hasPermi('basicmanage:invoice:edit')")
@PostMapping("/void")
public AjaxResult voidInvoice(@RequestParam Long id, @RequestParam(required = false) String reason) {
einvoiceAppService.voidInvoice(id, reason);
return AjaxResult.success();
}
@Operation(summary = "票据对账")
@PreAuthorize("@ss.hasPermi('basicmanage:invoice:list')")
@GetMapping("/reconciliation")
public AjaxResult reconciliation(@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
return AjaxResult.success(einvoiceAppService.getReconciliation(pageNum, pageSize));
}
}

View File

@@ -0,0 +1,82 @@
CREATE TABLE IF NOT EXISTS invoice_header (
id BIGINT PRIMARY KEY,
invoice_no VARCHAR(64) NOT NULL,
invoice_type VARCHAR(20) NOT NULL DEFAULT 'ELECTRONIC',
encounter_id BIGINT,
patient_id BIGINT,
patient_name VARCHAR(64),
total_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
discount_amount NUMERIC(12,2) DEFAULT 0,
payable_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
paid_amount NUMERIC(12,2) DEFAULT 0,
invoice_status VARCHAR(20) NOT NULL DEFAULT 'ISSUED',
issue_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
void_time TIMESTAMP,
void_reason VARCHAR(256),
issuer_id BIGINT,
issuer_name VARCHAR(64),
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id INT DEFAULT 1,
delete_flag VARCHAR(1) DEFAULT '0'
);
CREATE TABLE IF NOT EXISTS invoice_detail (
id BIGINT PRIMARY KEY,
header_id BIGINT NOT NULL,
item_code VARCHAR(64),
item_name VARCHAR(128) NOT NULL,
item_type VARCHAR(20),
quantity NUMERIC(10,2) DEFAULT 1,
unit_price NUMERIC(12,2) DEFAULT 0,
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id INT DEFAULT 1,
delete_flag VARCHAR(1) DEFAULT '0'
);
CREATE TABLE IF NOT EXISTS invoice_segment (
id BIGINT PRIMARY KEY,
segment_name VARCHAR(128) NOT NULL,
start_no VARCHAR(64) NOT NULL,
end_no VARCHAR(64) NOT NULL,
current_no VARCHAR(64),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
invoice_type VARCHAR(20) DEFAULT 'ELECTRONIC',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id INT DEFAULT 1,
delete_flag VARCHAR(1) DEFAULT '0'
);
CREATE TABLE IF NOT EXISTS invoice_reconciliation (
id BIGINT PRIMARY KEY,
recon_date DATE NOT NULL,
total_invoices INT DEFAULT 0,
total_amount NUMERIC(14,2) DEFAULT 0,
reconciled_count INT DEFAULT 0,
reconciled_amount NUMERIC(14,2) DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
reconciler_id BIGINT,
reconciler_name VARCHAR(64),
reconcile_time TIMESTAMP,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id INT DEFAULT 1,
delete_flag VARCHAR(1) DEFAULT '0'
);
CREATE INDEX idx_inv_header_no ON invoice_header(invoice_no);
CREATE INDEX idx_inv_header_status ON invoice_header(invoice_status);
CREATE INDEX idx_inv_header_patient ON invoice_header(patient_id);
CREATE INDEX idx_inv_detail_header ON invoice_detail(header_id);
CREATE INDEX idx_inv_recon_date ON invoice_reconciliation(recon_date);

View File

@@ -0,0 +1,29 @@
package com.healthlink.his.einvoice.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import tools.jackson.databind.annotation.JsonSerialize;
import tools.jackson.databind.ser.std.ToStringSerializer;
import java.math.BigDecimal;
@Data
@TableName("invoice_detail")
@EqualsAndHashCode(callSuper = true)
public class EinvoiceDetail extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
@JsonSerialize(using = ToStringSerializer.class)
private Long headerId;
private String itemCode;
private String itemName;
private String itemType;
private BigDecimal quantity;
private BigDecimal unitPrice;
private BigDecimal amount;
}

View File

@@ -0,0 +1,43 @@
package com.healthlink.his.einvoice.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import tools.jackson.databind.annotation.JsonSerialize;
import tools.jackson.databind.ser.std.ToStringSerializer;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("invoice_header")
@EqualsAndHashCode(callSuper = true)
public class EinvoiceHeader extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String invoiceNo;
private String invoiceType;
@JsonSerialize(using = ToStringSerializer.class)
private Long encounterId;
@JsonSerialize(using = ToStringSerializer.class)
private Long patientId;
private String patientName;
private BigDecimal totalAmount;
private BigDecimal discountAmount;
private BigDecimal payableAmount;
private BigDecimal paidAmount;
private String invoiceStatus;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date issueTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date voidTime;
private String voidReason;
@JsonSerialize(using = ToStringSerializer.class)
private Long issuerId;
private String issuerName;
}

View File

@@ -0,0 +1,35 @@
package com.healthlink.his.einvoice.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import tools.jackson.databind.annotation.JsonSerialize;
import tools.jackson.databind.ser.std.ToStringSerializer;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("invoice_reconciliation")
@EqualsAndHashCode(callSuper = true)
public class EinvoiceReconciliation extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date reconDate;
private Integer totalInvoices;
private BigDecimal totalAmount;
private Integer reconciledCount;
private BigDecimal reconciledAmount;
private String status;
@JsonSerialize(using = ToStringSerializer.class)
private Long reconcilerId;
private String reconcilerName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reconcileTime;
}

View File

@@ -0,0 +1,25 @@
package com.healthlink.his.einvoice.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import tools.jackson.databind.annotation.JsonSerialize;
import tools.jackson.databind.ser.std.ToStringSerializer;
@Data
@TableName("invoice_segment")
@EqualsAndHashCode(callSuper = true)
public class EinvoiceSegment extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String segmentName;
private String startNo;
private String endNo;
private String currentNo;
private String status;
private String invoiceType;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.einvoice.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.einvoice.domain.EinvoiceDetail;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EinvoiceDetailMapper extends BaseMapper<EinvoiceDetail> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.einvoice.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.einvoice.domain.EinvoiceHeader;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EinvoiceHeaderMapper extends BaseMapper<EinvoiceHeader> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.einvoice.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.einvoice.domain.EinvoiceReconciliation;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EinvoiceReconciliationMapper extends BaseMapper<EinvoiceReconciliation> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.einvoice.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.einvoice.domain.EinvoiceSegment;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EinvoiceSegmentMapper extends BaseMapper<EinvoiceSegment> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.einvoice.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.einvoice.domain.EinvoiceDetail;
public interface IEinvoiceDetailService extends IService<EinvoiceDetail> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.einvoice.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.einvoice.domain.EinvoiceHeader;
public interface IEinvoiceHeaderService extends IService<EinvoiceHeader> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.einvoice.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.einvoice.domain.EinvoiceReconciliation;
public interface IEinvoiceReconciliationService extends IService<EinvoiceReconciliation> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.einvoice.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.einvoice.domain.EinvoiceSegment;
public interface IEinvoiceSegmentService extends IService<EinvoiceSegment> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.einvoice.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.einvoice.domain.EinvoiceDetail;
import com.healthlink.his.einvoice.mapper.EinvoiceDetailMapper;
import com.healthlink.his.einvoice.service.IEinvoiceDetailService;
import org.springframework.stereotype.Service;
@Service
public class EinvoiceDetailServiceImpl extends ServiceImpl<EinvoiceDetailMapper, EinvoiceDetail> implements IEinvoiceDetailService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.einvoice.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.einvoice.domain.EinvoiceHeader;
import com.healthlink.his.einvoice.mapper.EinvoiceHeaderMapper;
import com.healthlink.his.einvoice.service.IEinvoiceHeaderService;
import org.springframework.stereotype.Service;
@Service
public class EinvoiceHeaderServiceImpl extends ServiceImpl<EinvoiceHeaderMapper, EinvoiceHeader> implements IEinvoiceHeaderService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.einvoice.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.einvoice.domain.EinvoiceReconciliation;
import com.healthlink.his.einvoice.mapper.EinvoiceReconciliationMapper;
import com.healthlink.his.einvoice.service.IEinvoiceReconciliationService;
import org.springframework.stereotype.Service;
@Service
public class EinvoiceReconciliationServiceImpl extends ServiceImpl<EinvoiceReconciliationMapper, EinvoiceReconciliation> implements IEinvoiceReconciliationService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.einvoice.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.einvoice.domain.EinvoiceSegment;
import com.healthlink.his.einvoice.mapper.EinvoiceSegmentMapper;
import com.healthlink.his.einvoice.service.IEinvoiceSegmentService;
import org.springframework.stereotype.Service;
@Service
public class EinvoiceSegmentServiceImpl extends ServiceImpl<EinvoiceSegmentMapper, EinvoiceSegment> implements IEinvoiceSegmentService {
}

View File

@@ -0,0 +1,204 @@
<template>
<div style="padding: 16px">
<div style="margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center">
<span style="font-size: 18px; font-weight: bold">电子票据管理</span>
<el-button type="primary" @click="handleGenerate">开具票据</el-button>
</div>
<el-card shadow="never" style="margin-bottom: 16px">
<div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center">
<el-input v-model="query.patientName" placeholder="患者姓名" clearable style="width: 160px" />
<el-select v-model="query.invoiceStatus" placeholder="状态" clearable style="width: 120px">
<el-option label="已开具" value="ISSUED" />
<el-option label="已作废" value="VOID" />
</el-select>
<el-button type="primary" @click="loadPage">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button @click="showReconciliation">对账</el-button>
</div>
</el-card>
<el-card shadow="never">
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="invoiceNo" label="票据号" width="200" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="totalAmount" label="总金额" width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.totalAmount) }}</template>
</el-table-column>
<el-table-column prop="discountAmount" label="优惠金额" width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.discountAmount) }}</template>
</el-table-column>
<el-table-column prop="payableAmount" label="应付金额" width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.payableAmount) }}</template>
</el-table-column>
<el-table-column prop="invoiceStatus" label="状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.invoiceStatus === 'ISSUED'" type="success">已开具</el-tag>
<el-tag v-else-if="row.invoiceStatus === 'VOID'" type="danger">已作废</el-tag>
<el-tag v-else>{{ row.invoiceStatus }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="issueTime" label="开具时间" width="170" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button v-if="row.invoiceStatus === 'ISSUED'" type="danger" size="small" @click="handleVoid(row)">作废</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="query.pageNum"
v-model:page-size="query.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next"
style="margin-top: 16px; justify-content: flex-end"
@size-change="loadPage"
@current-change="loadPage"
/>
</el-card>
<el-dialog v-model="generateVisible" title="开具电子票据" width="500px">
<el-form :model="generateForm" label-width="90px">
<el-form-item label="就诊ID">
<el-input v-model="generateForm.encounterId" placeholder="就诊ID" />
</el-form-item>
<el-form-item label="患者ID">
<el-input v-model="generateForm.patientId" placeholder="患者ID" />
</el-form-item>
<el-form-item label="患者姓名">
<el-input v-model="generateForm.patientName" placeholder="患者姓名" />
</el-form-item>
<el-form-item label="总金额">
<el-input v-model="generateForm.totalAmount" placeholder="总金额" />
</el-form-item>
<el-form-item label="优惠金额">
<el-input v-model="generateForm.discountAmount" placeholder="优惠金额" />
</el-form-item>
<el-form-item label="应付金额">
<el-input v-model="generateForm.payableAmount" placeholder="应付金额" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="generateVisible = false">取消</el-button>
<el-button type="primary" @click="submitGenerate">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="voidVisible" title="作废票据" width="400px">
<el-form label-width="80px">
<el-form-item label="作废原因">
<el-input v-model="voidReason" type="textarea" :rows="3" placeholder="请输入作废原因" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="voidVisible = false">取消</el-button>
<el-button type="primary" @click="confirmVoid">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="reconVisible" title="票据对账" width="700px">
<el-table :data="reconData" border stripe v-loading="reconLoading">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="invoiceNo" label="票据号" width="200" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="payableAmount" label="应付金额" width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.payableAmount) }}</template>
</el-table-column>
<el-table-column prop="invoiceStatus" label="状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.invoiceStatus === 'ISSUED'" type="success">已开具</el-tag>
<el-tag v-else>{{ row.invoiceStatus }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="issueTime" label="开具时间" width="170" />
</el-table>
<div style="margin-top: 12px; color: #666">
{{ reconTotal }} 条已开具票据
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { generateInvoice, getInvoicePage, voidInvoice, getReconciliation } from './api'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const query = ref({ invoiceStatus: '', patientName: '', pageNum: 1, pageSize: 10 })
const generateVisible = ref(false)
const generateForm = ref({ encounterId: '', patientId: '', patientName: '', totalAmount: 0, discountAmount: 0, payableAmount: 0 })
const voidVisible = ref(false)
const voidReason = ref('')
const voidRow = ref(null)
const reconVisible = ref(false)
const reconLoading = ref(false)
const reconData = ref([])
const reconTotal = ref(0)
function formatMoney(val) {
if (!val) return '0.00'
return Number(val).toFixed(2)
}
async function loadPage() {
loading.value = true
try {
const res = await getInvoicePage(query.value)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} finally {
loading.value = false
}
}
function resetQuery() {
query.value = { invoiceStatus: '', patientName: '', pageNum: 1, pageSize: 10 }
loadPage()
}
function handleGenerate() {
generateForm.value = { encounterId: '', patientId: '', patientName: '', totalAmount: 0, discountAmount: 0, payableAmount: 0 }
generateVisible.value = true
}
async function submitGenerate() {
await generateInvoice(generateForm.value)
ElMessage.success('开具成功')
generateVisible.value = false
loadPage()
}
function handleVoid(row) {
voidRow.value = row
voidReason.value = ''
voidVisible.value = true
}
async function confirmVoid() {
await voidInvoice({ id: voidRow.value.id, reason: voidReason.value })
ElMessage.success('作废成功')
voidVisible.value = false
loadPage()
}
async function showReconciliation() {
reconVisible.value = true
reconLoading.value = true
try {
const res = await getReconciliation({ pageNum: 1, pageSize: 50 })
reconData.value = res.data?.records || []
reconTotal.value = res.data?.totalCount || 0
} finally {
reconLoading.value = false
}
}
onMounted(() => loadPage())
</script>

View File

@@ -0,0 +1,17 @@
import request from '@/utils/request'
export function generateInvoice(data) {
return request({ url: '/invoice/generate', method: 'post', data })
}
export function getInvoicePage(params) {
return request({ url: '/invoice/page', method: 'get', params })
}
export function voidInvoice(params) {
return request({ url: '/invoice/void', method: 'post', params })
}
export function getReconciliation(params) {
return request({ url: '/invoice/reconciliation', method: 'get', params })
}