unpay-preview.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. <template>
  2. <Dialog
  3. v-model="dialogVisible"
  4. title="缴费通知单预览"
  5. class="billPayPreview"
  6. width="70%"
  7. @closed="handleClosed"
  8. scroll
  9. >
  10. <div class="first" id="printTab1">
  11. <div class="header">
  12. <p class="title" style="font-size: 18px">缴费通知单</p>
  13. <!-- 右上角按钮:仅屏幕可见,打印时通过 .no-print + printExtraCss 隐藏 -->
  14. <el-button
  15. v-if="relatives.length"
  16. class="no-print relative-action"
  17. type="primary"
  18. plain
  19. size="small"
  20. @click="relativeDialogVisible = true"
  21. >
  22. 家属信息({{ relatives.length }})
  23. </el-button>
  24. <div class="info-wrap" style="margin-top: -4px">
  25. <span><b>机构:</b> {{ tenantName }}</span>
  26. <span><b>姓名:</b> {{ dataForm.elderName }}</span>
  27. <span><b>编号:</b> {{ dataForm.billOrderNumber }}</span>
  28. </div>
  29. <div class="info-wrap">
  30. <span><b>床位号:</b> {{ dataForm.bedName }}</span>
  31. <span><b>性别:</b> {{ getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, dataForm.elderSex) }}</span>
  32. <span><b>经手人:</b> {{ dataForm.createdBy }}</span>
  33. </div>
  34. </div>
  35. <el-divider style="margin-top: 0px" />
  36. <div class="content">
  37. <!-- <p class="info">以下是账单明细</p>-->
  38. <div class="flex">
  39. <b>{{
  40. dataForm.description
  41. ? tTitle + '(' + dataForm.description + ')'
  42. : tTitle + '(' + bTitle + ')'
  43. }}</b>
  44. </div>
  45. <table style="width: 100%" border="2">
  46. <tr align="center">
  47. <td>序号</td>
  48. <td>费用分类</td>
  49. <td>费用名称</td>
  50. <td>金额(元)</td>
  51. <td>缴费状态</td>
  52. <td>已缴金额(元)</td>
  53. <td>费用发生时间</td>
  54. <td>备注</td>
  55. </tr>
  56. <tr v-for="(item, index) in dataForm.items" :key="index" align="center">
  57. <td>{{ index + 1 }}</td>
  58. <td>{{ item.itemCategoryName }}</td>
  59. <td>{{ item.itemName }}</td>
  60. <td>{{ calcTotalPrice(item) }}</td>
  61. <td>{{ getDictLabel(DICT_TYPE.PAY_TYPE, item.payStatus) }}</td>
  62. <td>{{ item.payAmount ?? 0 }}元</td>
  63. <td>{{ formatStartDate(item) }}</td>
  64. <td>{{ item.description }}</td>
  65. </tr>
  66. <tr align="center">
  67. <td>合计</td>
  68. <td :colspan="8">
  69. <div>
  70. 金额:{{ billTotalPrice }}元;已缴费共计:&nbsp;{{
  71. rounding ? formatTrunc(payTotal) : formatNum(payTotal)
  72. }}&nbsp;元;剩余未缴费:&nbsp;{{
  73. rounding ? formatTrunc(unPayTotal) : formatNum(unPayTotal)
  74. }}&nbsp;元
  75. </div>
  76. <div
  77. v-show="tempResValue.payInfos && tempResValue.payInfos.length > 0"
  78. style="
  79. display: flex;
  80. align-items: center;
  81. flex-direction: row;
  82. justify-content: center;
  83. "
  84. >
  85. 其中(<div
  86. style="margin-right: 10px"
  87. v-for="(item, index) in tempResValue.payInfos || []"
  88. :key="index"
  89. >
  90. {{ getDictLabel(DICT_TYPE.BILL_PAY_TYPE, parseInt(item.payType)) }}支付:{{
  91. formatNum(item.items[0].payAmount)
  92. }}元 {{ formatTimestamp(item.items[0].createdTime) }};</div
  93. >)
  94. </div>
  95. </td>
  96. </tr>
  97. </table>
  98. </div>
  99. </div>
  100. <template #footer>
  101. <el-button @click="handleClosed">取消</el-button>
  102. <el-button type="primary" v-print="printTab">打印</el-button>
  103. </template>
  104. </Dialog>
  105. <!-- 家属信息弹窗(独立于缴费通知单预览,不在 #printTab1 范围内,自然不会被打印) -->
  106. <Dialog
  107. v-model="relativeDialogVisible"
  108. title="家属信息"
  109. width="60%"
  110. @closed="handleRelativeClosed"
  111. >
  112. <el-table :data="relatives" border size="default">
  113. <el-table-column type="index" label="序号" width="60" align="center" />
  114. <el-table-column label="姓名" prop="peopleName" min-width="100" />
  115. <el-table-column label="关系" prop="relation" min-width="80" />
  116. <el-table-column label="联系电话" prop="phone" min-width="130" />
  117. <el-table-column label="身份证号" prop="idCard" min-width="170" />
  118. <el-table-column label="账单发送人" min-width="100">
  119. <template #default="{ row }">
  120. <el-tag :type="row.isBillSender ? 'success' : 'info'" size="small">
  121. {{ row.isBillSender ? '是' : '否' }}
  122. </el-tag>
  123. </template>
  124. </el-table-column>
  125. </el-table>
  126. </Dialog>
  127. </template>
  128. <script setup lang="ts">
  129. import { getExpenseOrderDetail } from '@/api/elderly/fee/bill-pay'
  130. import { useUserStore } from '@/store/modules/user'
  131. import { formatNum, formatCeil, formatRound, formatTrunc } from '@/utils/formatter'
  132. import { DICT_TYPE, getDictLabel } from '@/utils/dict'
  133. import { useSettingStore } from '@/store/modules/setting'
  134. import { formatDateTime } from '@/components/UploadFile'
  135. import { formatTimestamp, formatToDateTime } from '@/utils/dateUtil'
  136. defineOptions({ name: 'BillPayPreview' })
  137. const settingStore = useSettingStore()
  138. const tenantName = useUserStore().getTenantName // 当前登录的编号
  139. const dialogVisible = ref(false)
  140. const dataForm = ref({
  141. insuranceAmount: 0,
  142. payAmount: 0,
  143. billingMonth: '',
  144. items: [],
  145. otherName: '',
  146. otherNum: '',
  147. monthNum: '',
  148. elderName: '',
  149. bedName: '',
  150. actualAmount: '',
  151. type: '',
  152. createdBy: '',
  153. invoiceNumber: '',
  154. description: ''
  155. })
  156. const total = ref(0)
  157. const totalDecimal = ref(0)
  158. const unPayTotal = ref(0) // 未缴总额
  159. const payTotal = ref(0) // 已缴总额
  160. const longTermAmount = ref(0) // 长护险
  161. // 取整金额
  162. const rounding = computed(() => {
  163. return settingStore.getRounding
  164. })
  165. // 四舍五入保留两位
  166. const fixed2Num = computed(() => {
  167. return settingStore.getFix2Num
  168. })
  169. // 长护险规则
  170. const longCareRuleValue = computed(() => {
  171. return settingStore.getLongCareRuleValue
  172. })
  173. /** 消费券/长护险都按各自的抹零配置单独取数 */
  174. const hasSourceRule = (item) =>
  175. item.expenseSource == 'consumer_voucher' || item.expenseSource == 'long_term_care_insurance'
  176. /** 长护险金额:按长护险抹零配置取数(2=取整,其它=保留两位小数) */
  177. const getLongCareAmountNumber = (item) => {
  178. const rawAmount = item?.totalAmount ?? 0
  179. if (longCareRuleValue.value == 2 || longCareRuleValue.value == '2') {
  180. return Number(formatRound(rawAmount))
  181. }
  182. return Number(formatNum(rawAmount))
  183. }
  184. /** 长护险金额:展示用字符串 */
  185. const getLongCareAmountText = (item) => {
  186. if (longCareRuleValue.value == 2 || longCareRuleValue.value == '2') {
  187. return formatRound(item?.totalAmount ?? 0)
  188. }
  189. return formatNum(item?.totalAmount ?? 0)
  190. }
  191. /** 按长护险规则处理后的补贴金额 */
  192. const formatLongCareAmount = (amount) => {
  193. if (longCareRuleValue.value == 2 || longCareRuleValue.value == '2') {
  194. return Number(formatRound(amount || 0))
  195. }
  196. return Number(formatNum(amount || 0))
  197. }
  198. const tempResValue = ref({})
  199. // 家属信息:后端响应 relatives 数组(每项含 peopleName/relation/phone/idCard/insurance/isBillSender 等)
  200. const relatives = ref<any[]>([])
  201. const relativeDialogVisible = ref(false)
  202. const getItemPreviewAmount = (item) => {
  203. // 消费券/长护险优先按各自抹零规则取数(与表格展示保持一致)
  204. if (item.expenseSource == 'consumer_voucher') {
  205. const ruleValue = settingStore.getCouponRuleValue
  206. if (ruleValue == 2 || ruleValue == '2') {
  207. return Number(formatRound(item.totalAmount))
  208. }
  209. return Number(formatNum(item.totalAmount))
  210. }
  211. if (item.expenseSource == 'long_term_care_insurance') {
  212. return getLongCareAmountNumber(item)
  213. }
  214. if (dataForm.value.type == '1' && item.isMonthlyExpense == 1) {
  215. if (settingStore.getAdmissionBill == 2) {
  216. return Number(formatCeil(item.totalAmount))
  217. } else if (settingStore.getAdmissionBill == 3) {
  218. return Number(item.roundTwoDecimalAmount)
  219. } else if (settingStore.getAdmissionBill == 4) {
  220. return Number(item.roundAmount)
  221. } else {
  222. return Number(formatNum(item.totalAmount))
  223. }
  224. } else if (rounding.value) {
  225. return Number(item.roundAmount)
  226. } else if (fixed2Num.value) {
  227. return Number(item.roundTwoDecimalAmount)
  228. } else {
  229. return Number(formatNum(item.totalAmount))
  230. }
  231. }
  232. /** 打开弹窗 */
  233. const open = async (id) => {
  234. dialogVisible.value = true
  235. unPayTotal.value = 0
  236. total.value = 0
  237. totalDecimal.value = 0
  238. longTermAmount.value = 0
  239. payTotal.value = 0
  240. const res = await getExpenseOrderDetail(id)
  241. tempResValue.value = res
  242. // 后端响应新增 relatives 字段:家属信息列表
  243. relatives.value = Array.isArray(res.relatives) ? res.relatives : []
  244. res.items = res.items.filter(
  245. (item) =>
  246. (item.expenseSource == 'long_term_care_insurance' ||
  247. item.payStatus == 0 ||
  248. item.payStatus == 1 ||
  249. item.payStatus == 2) &&
  250. item.isShow == 1
  251. )
  252. let flag = false
  253. let hasPartialPay = false
  254. dataForm.value = res
  255. res.items.map((item) => {
  256. if (item.expenseSource == 'long_term_care_insurance') {
  257. longTermAmount.value += formatLongCareAmount(item.actualPrice)
  258. item.payStatus = 0
  259. flag = true
  260. // 遍历长护险
  261. res.expenseSubsidyDO.map((d) => {
  262. if (Math.abs(d.id) == Math.abs(item.sourceExpenseItemId || 0)) {
  263. item.createdTime = d.month
  264. item.description = d.remarks
  265. if (!item.itemCategoryName) {
  266. item.itemCategoryName = '长护险'
  267. }
  268. }
  269. })
  270. }
  271. if (hasPartialPay) {
  272. total.value +=
  273. hasSourceRule(item) ? getItemPreviewAmount(item) : item.roundAmount
  274. totalDecimal.value +=
  275. hasSourceRule(item)
  276. ? getItemPreviewAmount(item)
  277. : item.roundTwoDecimalAmount
  278. return
  279. }
  280. if (item.payStatus == 0) {
  281. unPayTotal.value += getItemPreviewAmount(item)
  282. } else if (item.payStatus == 1) {
  283. payTotal.value += getItemPreviewAmount(item)
  284. } else if (item.payStatus == 2) {
  285. hasPartialPay = true
  286. try {
  287. payTotal.value = tempResValue.value.payableAmount || 0
  288. unPayTotal.value = tempResValue.value.actualAmount - tempResValue.value.payableAmount || 0
  289. } catch (e) {}
  290. }
  291. total.value +=
  292. hasSourceRule(item) ? getItemPreviewAmount(item) : item.roundAmount
  293. totalDecimal.value +=
  294. hasSourceRule(item) ? getItemPreviewAmount(item) : item.roundTwoDecimalAmount
  295. })
  296. // 有长护险
  297. if (!flag) {
  298. if (res.expenseSubsidyDO && res.expenseSubsidyDO.length) {
  299. let num = 0
  300. res.expenseSubsidyDO.map((item) => {
  301. // 按长护险抹零配置取数
  302. const amount = formatLongCareAmount(item.amount)
  303. num += amount
  304. dataForm.value.items.push({
  305. expenseSource: 'long_term_care_insurance',
  306. itemCategoryName: '长护险',
  307. itemName: '长护险抵扣',
  308. description: item.remarks ? item.remarks : item.month + '长护险抵扣',
  309. roundAmount: -amount,
  310. roundTwoDecimalAmount: -amount,
  311. totalAmount: -amount,
  312. createdTime: item.month,
  313. payTime: '',
  314. payStatus: item.status
  315. })
  316. })
  317. // 去除长护险费用
  318. unPayTotal.value -= num
  319. longTermAmount.value = num
  320. payTotal.value -= res.num
  321. }
  322. }
  323. }
  324. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  325. const handleClosed = () => {
  326. unPayTotal.value = 0
  327. total.value = 0
  328. totalDecimal.value = 0
  329. longTermAmount.value = 0
  330. payTotal.value = 0
  331. relatives.value = []
  332. relativeDialogVisible.value = false
  333. dialogVisible.value = false
  334. }
  335. /** 家属信息 Dialog 关闭 */
  336. const handleRelativeClosed = () => {
  337. // 不动 relatives.value,只关弹窗;点开可继续看同一组家属
  338. }
  339. const tTitle = computed(() => {
  340. const arr = dataForm.value.billingMonth ? dataForm.value.billingMonth.split('-') : ''
  341. if (arr.length) {
  342. return `预收${arr[0]}年${arr[1]}月费用`
  343. }
  344. return ''
  345. })
  346. const bTitle = computed(() => {
  347. const arr = dataForm.value.billingMonth ? dataForm.value.billingMonth.split('-') : ''
  348. if (arr.length) {
  349. // 获取当月最后一天
  350. const date = new Date(new Date(Number(arr[0]), Number(arr[1]), 0)).getDate()
  351. return `${arr[0]}年${arr[1]}月01日-${arr[0]}年${arr[1]}月${date}日`
  352. }
  353. return ''
  354. })
  355. // 入院账单费用计算
  356. const caclAdminssionTotalPrice = computed(() => {
  357. let num = 0
  358. dataForm.value.items.map((item) => {
  359. num += getItemPreviewAmount(item)
  360. })
  361. return num + longTermAmount.value
  362. })
  363. // 总额
  364. const calcTotalPrice = (item) => {
  365. // 消费券/长护险优先按各自抹零规则取数
  366. if (item.expenseSource == 'consumer_voucher') {
  367. const ruleValue = settingStore.getCouponRuleValue
  368. if (ruleValue == 2 || ruleValue == '2') {
  369. return formatRound(item.totalAmount)
  370. }
  371. return formatNum(item.totalAmount)
  372. } else if (item.expenseSource == 'long_term_care_insurance') {
  373. return getLongCareAmountText(item)
  374. } else if (dataForm.value.type == '1' && item.isMonthlyExpense == 1) {
  375. if (settingStore.getAdmissionBill == 2) {
  376. // 向上取整
  377. return formatCeil(item.totalAmount)
  378. } else if (settingStore.getAdmissionBill == 3) {
  379. // 四舍五入保留两位
  380. return formatNum(item.roundTwoDecimalAmount)
  381. } else if (settingStore.getAdmissionBill == 4) {
  382. // 四舍五入保留整数
  383. return formatNum(item.roundAmount)
  384. } else {
  385. // 应收
  386. return formatNum(item.totalAmount)
  387. }
  388. } else if (rounding.value) {
  389. return formatRound(item.roundAmount)
  390. } else if (fixed2Num.value) {
  391. return formatNum(item.roundTwoDecimalAmount)
  392. } else {
  393. return formatNum(item.totalAmount)
  394. }
  395. }
  396. // 账单总额
  397. const billTotalPrice = computed(() => {
  398. if (dataForm.value.type == '1') {
  399. return formatNum(caclAdminssionTotalPrice.value)
  400. } else {
  401. if (rounding.value) {
  402. return formatNum(total.value)
  403. } else if (fixed2Num.value) {
  404. return formatNum(totalDecimal.value)
  405. } else {
  406. return formatNum(totalDecimal.value)
  407. }
  408. }
  409. })
  410. const formatStartDate = (item) => {
  411. if (item.itemName == '长护险护理补贴费用' || item.expenseSource == 'long_term_care_insurance') {
  412. // 长护险
  413. return item.createdTime
  414. } else if (item.expenseSource == 'expense_item') {
  415. if (item.description && item.description.includes('的')) {
  416. return item.description.split('的')[0]
  417. } else if (item.startDate != null && item.endDate != null) {
  418. return `${item.startDate}至${item.endDate}`
  419. } else if (
  420. item.startDate == null &&
  421. dataForm.value.description &&
  422. dataForm.value.description.length > 21
  423. ) {
  424. return dataForm.value.description.toString().substring(0, 21)
  425. } else {
  426. return bTitle
  427. }
  428. } else {
  429. return item.startDate + '至' + item.endDate
  430. }
  431. }
  432. const printExtraCss = `
  433. html, body { height: auto !important; overflow: visible !important; }
  434. #printTab1 { height: auto !important; overflow: visible !important; }
  435. table { page-break-inside: auto; width: 100%; border-collapse: collapse; }
  436. tr { page-break-inside: avoid; page-break-after: auto; }
  437. thead { display: table-header-group; }
  438. tfoot { display: table-footer-group; }
  439. /* 家属信息按钮:屏幕可见,但打印时必须隐藏 */
  440. .no-print { display: none !important; }
  441. /* 兜底:屏幕视图的家属信息 Dialog 不在 #printTab1 内,本身就不会进 iframe */
  442. @media print { .no-print { display: none !important; } }
  443. `
  444. // 打印
  445. const printTab = {
  446. id: 'printTab1',
  447. extraCss: '',
  448. popTitle: '',
  449. extraHead: `<style>${printExtraCss}</style>`,
  450. preview: false,
  451. previewTitle: tenantName + '对账单预览',
  452. previewPrintBtnLabel: '预览结束,开始打印',
  453. zIndex: 20002,
  454. previewBeforeOpenCallback() {
  455. console.log('正在加载预览窗口!')
  456. },
  457. previewOpenCallback() {
  458. console.log('已经加载完预览窗口,预览打开了!')
  459. },
  460. beforeOpenCallback() {
  461. console.log('开始打印之前!')
  462. },
  463. openCallback() {
  464. console.log('执行打印了!')
  465. },
  466. closeCallback() {
  467. console.log('关闭了打印工具!')
  468. },
  469. clickMounted() {
  470. console.log('点击v-print绑定的按钮了!')
  471. },
  472. standard: ''
  473. }
  474. </script>
  475. <style lang="scss" scoped>
  476. .first {
  477. .header {
  478. position: relative; // 让右上角 .relative-action 按钮可绝对定位
  479. .title {
  480. font-size: 20px;
  481. font-weight: bold;
  482. text-align: center;
  483. }
  484. // 右上角"家属信息"按钮:屏幕可见 / 打印隐藏
  485. .relative-action {
  486. position: absolute;
  487. right: 0;
  488. top: 4px;
  489. }
  490. .info-wrap {
  491. position: relative;
  492. padding: 5px;
  493. span {
  494. display: inline-block;
  495. margin-bottom: 10px;
  496. width: 33%;
  497. }
  498. .right {
  499. text-align: right;
  500. }
  501. }
  502. }
  503. .content {
  504. .info {
  505. text-align: center;
  506. }
  507. .flex {
  508. display: flex;
  509. justify-content: space-between;
  510. margin-bottom: 10px;
  511. }
  512. }
  513. }
  514. </style>