tjbb.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. <template>
  2. <div class="dashboard-container">
  3. <!-- 筛选区域(新增类型选择器) -->
  4. <div class="filter-panel">
  5. <div class="filter-item">
  6. <label class="filter-label">选择年份:</label>
  7. <el-date-picker
  8. size="default"
  9. v-model="searchForm.selectedYear"
  10. type="year"
  11. placeholder="选择年份"
  12. @change="handleYearChange"
  13. format="YYYY"
  14. value-format="YYYY"
  15. />
  16. </div>
  17. <div class="filter-item">
  18. <label class="filter-label">选择月份:</label>
  19. <el-select
  20. v-model="searchForm.selectedMonth"
  21. placeholder="请先选择年份"
  22. size="default"
  23. class="w-48!"
  24. :disabled="!searchForm.selectedYear"
  25. >
  26. <el-option
  27. v-for="month in availableMonths"
  28. :key="month"
  29. :label="`${month}月`"
  30. :value="month"
  31. />
  32. </el-select>
  33. </div>
  34. <!-- 新增:类型选择器(和收据报表保持一致) -->
  35. <div class="filter-item">
  36. <label class="filter-label">选择类型:</label>
  37. <el-select
  38. v-model="searchForm.selectedType"
  39. placeholder="请选择数据类型"
  40. size="default"
  41. class="w-48!"
  42. :disabled="!searchForm.selectedYear"
  43. >
  44. <el-option
  45. v-for="type in availableTypes"
  46. :key="type.value"
  47. :label="type.label"
  48. :value="type.value"
  49. />
  50. </el-select>
  51. </div>
  52. <div>
  53. <el-button type="primary" @click="getSelection()" :icon="Search" size="default">
  54. 搜索
  55. </el-button>
  56. </div>
  57. <div>
  58. <el-button
  59. color="#626aef"
  60. @click="getMonthDetail()"
  61. :icon="Tickets"
  62. size="default"
  63. :disabled="!isMonthDetailEnabled"
  64. :title="!isMonthDetailEnabled ? '请先选择年份和月份' : ''"
  65. >
  66. 月详情
  67. </el-button>
  68. </div>
  69. <div>
  70. <el-button type="success" @click="printTable" size="default">
  71. 打印表格
  72. </el-button>
  73. </div>
  74. </div>
  75. <!-- 数据表格 -->
  76. <div class="table-container" id="printTarget">
  77. <el-table
  78. :data="filteredTableData"
  79. border
  80. stripe
  81. style="width: 100%"
  82. :loading="tableLoading"
  83. empty-text="暂无相关收据数据"
  84. :summary-method="getSummaries"
  85. show-summary
  86. :span-method="handleSpanMethod"
  87. >
  88. <!-- 1. 序号列 -->
  89. <el-table-column label="序号" align="center" width="50">
  90. <template #default="scope">
  91. {{ scope.$index + 1 }}
  92. </template>
  93. </el-table-column>
  94. <!-- 2. 房间信息(仅详情显示) -->
  95. <el-table-column
  96. v-if="showMonthDetailColumns"
  97. prop="tenant"
  98. label="房间信息"
  99. align="center"
  100. />
  101. <!-- 3. 退款时间 -->
  102. <el-table-column
  103. prop="refundTime"
  104. label="退款时间"
  105. align="center"
  106. />
  107. <!-- 4. 租金 -->
  108. <el-table-column
  109. prop="rent"
  110. label="租金(元)"
  111. align="center"
  112. >
  113. <template #default="scope">
  114. {{ scope.row.rent?.toLocaleString('zh-CN', {minimumFractionDigits: 2}) || '0.00' }}
  115. </template>
  116. </el-table-column>
  117. <!-- 5. 押金 -->
  118. <el-table-column
  119. prop="deposit"
  120. label="押金(元)"
  121. align="center"
  122. >
  123. <template #default="scope">
  124. {{ scope.row.deposit?.toLocaleString('zh-CN', {minimumFractionDigits: 2}) || '0.00' }}
  125. </template>
  126. </el-table-column>
  127. <!-- 6. 物业费 -->
  128. <el-table-column
  129. prop="propertyFee"
  130. label="物业费(元)"
  131. align="center"
  132. >
  133. <template #default="scope">
  134. {{ scope.row.propertyFee?.toLocaleString('zh-CN', {minimumFractionDigits: 2}) || '0.00' }}
  135. </template>
  136. </el-table-column>
  137. <!-- 7. 经手人(仅详情显示) -->
  138. <el-table-column
  139. v-if="showMonthDetailColumns"
  140. prop="operator"
  141. label="经手人"
  142. align="center"
  143. />
  144. </el-table>
  145. </div>
  146. </div>
  147. </template>
  148. <script setup lang="ts">
  149. import { ref, computed, onMounted, reactive } from 'vue'
  150. import { clientGet } from "@/utils/request.js";
  151. import { ElMessage } from "element-plus";
  152. import { Search, Tickets, Printer } from "lucide-vue-next";
  153. // 类型定义优化
  154. interface RefundData {
  155. deposit: number // 押金
  156. propertyFee: number // 物业费
  157. refundTime: string // 退款时间
  158. rent: number // 租金
  159. tenant?: string // 房间信息
  160. operator?: string // 经手人
  161. // 其他备用字段
  162. waterFee?: number // 水费
  163. electricityBill?: number// 电费
  164. refundCode?: string // 收据号
  165. payType?: string // 付款方式
  166. rentPeriod?: string // 收取时段
  167. }
  168. // 新增:类型选择器选项类型(和收据报表一致)
  169. interface TypeOption {
  170. label: string // 显示文本
  171. value: string | number // 传递值
  172. }
  173. interface ApiResponse<T> {
  174. code: number
  175. msg: string
  176. data: T[]
  177. }
  178. // 常量定义
  179. const availableMonths = ref<number[]>([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
  180. // 新增:类型选择器可选选项(和收据报表完全一致)
  181. const availableTypes = ref<TypeOption[]>([
  182. { label: '公租房', value: '公租房' },
  183. { label: '厂房', value: '厂房' },
  184. { label: '创新创业基地', value: '创新创业基地' }
  185. ])
  186. const API_BASE_URL = '/arefund/refund/stat'
  187. // 响应式数据
  188. const tableLoading = ref(false)
  189. const showMonthDetailColumns = ref(false)
  190. const rawTableData = ref<RefundData[]>([])
  191. // 搜索表单(新增selectedType,默认选中公租房)
  192. const searchForm = reactive({
  193. selectedYear: '', // YYYY格式
  194. selectedMonth: '',
  195. selectedType: '公租房' // 新增:类型筛选值,和收据报表默认值一致
  196. })
  197. // 计算属性
  198. const isMonthDetailEnabled = computed(() => {
  199. return !!searchForm.selectedYear && !!searchForm.selectedMonth
  200. })
  201. const filteredTableData = computed(() => {
  202. return [...rawTableData.value]
  203. })
  204. // 接口请求方法(修改:传递类型参数assetType到后端)
  205. const fetchRefundData = async (apiPath: string, isDetail = false) => {
  206. tableLoading.value = true
  207. try {
  208. const params = {
  209. year: searchForm.selectedYear,
  210. month: searchForm.selectedMonth,
  211. assetType: searchForm.selectedType // 新增:传递类型参数,和收据报表字段名一致
  212. }
  213. const res = await clientGet<ApiResponse<RefundData>>(apiPath, { params })
  214. if (!res) {
  215. ElMessage.error('接口返回数据为空')
  216. rawTableData.value = []
  217. showMonthDetailColumns.value = false
  218. return
  219. }
  220. if (res.code !== 200) {
  221. ElMessage.error(res.msg || '数据获取失败')
  222. rawTableData.value = []
  223. showMonthDetailColumns.value = false
  224. return
  225. }
  226. rawTableData.value = res.data || []
  227. if (rawTableData.value.length === 0) {
  228. ElMessage.warning('暂无收据数据')
  229. showMonthDetailColumns.value = false
  230. } else {
  231. showMonthDetailColumns.value = isDetail
  232. }
  233. } catch (error) {
  234. ElMessage.error('网络异常,数据获取失败')
  235. console.error('获取退款数据失败:', error)
  236. rawTableData.value = []
  237. showMonthDetailColumns.value = false
  238. } finally {
  239. tableLoading.value = false
  240. }
  241. }
  242. // 事件处理方法(修改:切换年份时重置类型为默认公租房)
  243. const handleYearChange = (val: string) => {
  244. if (!val) {
  245. searchForm.selectedMonth = ''
  246. searchForm.selectedType = '公租房' // 新增:重置类型选择
  247. rawTableData.value = []
  248. ElMessage.warning('请选择年份进行筛选')
  249. showMonthDetailColumns.value = false
  250. return
  251. }
  252. searchForm.selectedMonth = ''
  253. searchForm.selectedType = '公租房' // 新增:重置类型选择
  254. showMonthDetailColumns.value = false
  255. fetchRefundData(`${API_BASE_URL}/year`)
  256. }
  257. const getSelection = () => {
  258. if (!searchForm.selectedYear) {
  259. ElMessage.warning('请先选择年份')
  260. return
  261. }
  262. showMonthDetailColumns.value = false
  263. const apiPath = !searchForm.selectedMonth
  264. ? `${API_BASE_URL}/year`
  265. : `${API_BASE_URL}/month`
  266. fetchRefundData(apiPath)
  267. }
  268. const getMonthDetail = () => {
  269. if (!isMonthDetailEnabled.value) {
  270. ElMessage.warning('请先选择年份和月份')
  271. return
  272. }
  273. fetchRefundData(`${API_BASE_URL}/day`, true)
  274. }
  275. // 合计行计算(适配当前列结构)
  276. const getSummaries = (param: { columns: any[], data: RefundData[] }) => {
  277. const { columns, data } = param
  278. const sums = Array(columns.length).fill('')
  279. sums[0] = '合计' // 序号列显示合计
  280. // 定义合计字段和对应索引
  281. const summaryConfig = showMonthDetailColumns.value
  282. ? {
  283. // 详情模式:3=租金、4=押金、5=物业费
  284. indexes: [3, 4, 5],
  285. fields: ['rent', 'deposit', 'propertyFee']
  286. }
  287. : {
  288. // 汇总模式:2=租金、3=押金、4=物业费
  289. indexes: [2, 3, 4],
  290. fields: ['rent', 'deposit', 'propertyFee']
  291. }
  292. // 计算合计值
  293. summaryConfig.indexes.forEach((index, fieldIndex) => {
  294. if (index >= columns.length) return
  295. const field = summaryConfig.fields[fieldIndex]
  296. const sum = data.reduce((total, item) => {
  297. return total + (Number(item[field]) || 0)
  298. }, 0)
  299. sums[index] = sum.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
  300. })
  301. return sums
  302. }
  303. // 合并单元格方法(优化合计行显示)
  304. const handleSpanMethod = ({ row, column, columnIndex }: {
  305. row: any,
  306. column: any,
  307. columnIndex: number
  308. }) => {
  309. // 仅处理合计行
  310. if (row && row.type === 'footer') {
  311. // 需要显示的列:序号列 + 数值列(租金、押金、物业费)
  312. const valueColumns = ['rent', 'deposit', 'propertyFee']
  313. const isValueColumn = valueColumns.includes(column.prop as string)
  314. if (columnIndex === 0 || isValueColumn) {
  315. return { rowspan: 1, colspan: 1 }
  316. } else {
  317. return { rowspan: 0, colspan: 0 } // 隐藏非数值列
  318. }
  319. }
  320. return { rowspan: 1, colspan: 1 }
  321. }
  322. // 打印功能(修改:新增显示选中的类型,和收据报表保持一致)
  323. const printTable = () => {
  324. if (filteredTableData.value.length === 0) {
  325. ElMessage.warning('暂无收据数据,无法打印')
  326. return
  327. }
  328. try {
  329. const printWindow = window.open('', '_blank')
  330. if (!printWindow) {
  331. ElMessage.error('浏览器弹窗被拦截,请允许弹窗后重试')
  332. return
  333. }
  334. // 新增:获取当前选中类型的显示文本
  335. const getSelectedTypeLabel = () => {
  336. const currentType = availableTypes.value.find(item => item.value === searchForm.selectedType)
  337. return currentType?.label || '公租房'
  338. }
  339. // 计算打印用的合计数据
  340. const calculatePrintSummary = () => {
  341. return filteredTableData.value.reduce((summary, item) => {
  342. return {
  343. rent: summary.rent + (Number(item.rent) || 0),
  344. deposit: summary.deposit + (Number(item.deposit) || 0),
  345. propertyFee: summary.propertyFee + (Number(item.propertyFee) || 0)
  346. }
  347. }, { rent: 0, deposit: 0, propertyFee: 0 })
  348. }
  349. const printSummary = calculatePrintSummary()
  350. // 构建打印表格HTML
  351. const buildPrintTable = () => {
  352. // 定义表头和列宽
  353. let headers = showMonthDetailColumns.value
  354. ? [
  355. { label: '序号', width: '50px' },
  356. { label: '房间信息', width: '120px' },
  357. { label: '退款时间', width: '150px' },
  358. { label: '租金(元)', width: '120px' },
  359. { label: '押金(元)', width: '120px' },
  360. { label: '物业费(元)', width: '120px' },
  361. { label: '经手人', width: '60px' }
  362. ]
  363. : [
  364. { label: '序号', width: '50px' },
  365. { label: '退款时间', width: '150px' },
  366. { label: '租金(元)', width: '120px' },
  367. { label: '押金(元)', width: '120px' },
  368. { label: '物业费(元)', width: '120px' }
  369. ]
  370. // 构建表头行
  371. const headerHtml = headers.map(h =>
  372. `<th style="width: ${h.width}; text-align: center; padding: 8px; border: 1px solid #000; background: #f0f0f0;">${h.label}</th>`
  373. ).join('')
  374. // 构建数据行
  375. const rowsHtml = filteredTableData.value.map((item, index) => {
  376. const cells = showMonthDetailColumns.value
  377. ? [
  378. `<td style="width: 50px; text-align: center; padding: 8px; border: 1px solid #000;">${index + 1}</td>`,
  379. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000;">${item.tenant || '-'}</td>`,
  380. `<td style="width: 150px; text-align: center; padding: 8px; border: 1px solid #000;">${item.refundTime || '-'}</td>`,
  381. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000;">${(item.rent || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`,
  382. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000;">${(item.deposit || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`,
  383. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000;">${(item.propertyFee || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`,
  384. `<td style="width: 60px; text-align: center; padding: 8px; border: 1px solid #000;">${item.operator || '-'}</td>`
  385. ]
  386. : [
  387. `<td style="width: 50px; text-align: center; padding: 8px; border: 1px solid #000;">${index + 1}</td>`,
  388. `<td style="width: 150px; text-align: center; padding: 8px; border: 1px solid #000;">${item.refundTime || '-'}</td>`,
  389. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000;">${(item.rent || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`,
  390. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000;">${(item.deposit || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`,
  391. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000;">${(item.propertyFee || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`
  392. ]
  393. return `<tr>${cells.join('')}</tr>`
  394. }).join('')
  395. // 构建合计行
  396. const summaryCells = showMonthDetailColumns.value
  397. ? [
  398. `<td style="width: 50px; text-align: center; padding: 8px; border: 1px solid #000; font-weight: bold; background: #f0f0f0;">合计</td>`,
  399. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000; background: #f0f0f0;"></td>`,
  400. `<td style="width: 150px; text-align: center; padding: 8px; border: 1px solid #000; background: #f0f0f0;"></td>`,
  401. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000; font-weight: bold; background: #f0f0f0;">${printSummary.rent.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`,
  402. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000; font-weight: bold; background: #f0f0f0;">${printSummary.deposit.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`,
  403. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000; font-weight: bold; background: #f0f0f0;">${printSummary.propertyFee.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`,
  404. `<td style="width: 60px; text-align: center; padding: 8px; border: 1px solid #000; background: #f0f0f0;"></td>`
  405. ]
  406. : [
  407. `<td style="width: 50px; text-align: center; padding: 8px; border: 1px solid #000; font-weight: bold; background: #f0f0f0;">合计</td>`,
  408. `<td style="width: 150px; text-align: center; padding: 8px; border: 1px solid #000; background: #f0f0f0;"></td>`,
  409. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000; font-weight: bold; background: #f0f0f0;">${printSummary.rent.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`,
  410. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000; font-weight: bold; background: #f0f0f0;">${printSummary.deposit.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`,
  411. `<td style="width: 120px; text-align: center; padding: 8px; border: 1px solid #000; font-weight: bold; background: #f0f0f0;">${printSummary.propertyFee.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</td>`
  412. ]
  413. const summaryHtml = `<tr>${summaryCells.join('')}</tr>`
  414. // 完整表格
  415. return `
  416. <table style="width: 100%; border-collapse: collapse; font-family: 'Microsoft YaHei', sans-serif;">
  417. <thead><tr>${headerHtml}</tr></thead>
  418. <tbody>
  419. ${rowsHtml}
  420. ${summaryHtml}
  421. </tbody>
  422. </table>
  423. `
  424. }
  425. // 构建打印页面内容(修改:标题中加入选中的类型)
  426. const printContent = `
  427. <!DOCTYPE html>
  428. <html>
  429. <head>
  430. <meta charset="UTF-8">
  431. <title>${showMonthDetailColumns.value ? '退款详情' : '退款汇总'}-${searchForm.selectedYear}年${searchForm.selectedMonth ? searchForm.selectedMonth + '月' : ''}-${getSelectedTypeLabel()}</title>
  432. <style>
  433. body {
  434. padding: 20px;
  435. font-family: 'Microsoft YaHei', sans-serif;
  436. font-size: 12px;
  437. }
  438. h1 {
  439. text-align: center;
  440. font-size: 16px;
  441. margin-bottom: 20px;
  442. }
  443. @media print {
  444. @page {
  445. margin: 15mm;
  446. }
  447. }
  448. </style>
  449. </head>
  450. <body>
  451. <h1>${showMonthDetailColumns.value ? '退款详情数据' : '退款汇总数据'} - ${searchForm.selectedYear}年${searchForm.selectedMonth ? searchForm.selectedMonth + '月' : ''} - ${getSelectedTypeLabel()}</h1>
  452. ${buildPrintTable()}
  453. </body>
  454. </html>
  455. `
  456. printWindow.document.write(printContent)
  457. printWindow.document.close()
  458. // 执行打印
  459. setTimeout(() => {
  460. printWindow.focus()
  461. printWindow.print()
  462. setTimeout(() => printWindow.close(), 500)
  463. }, 200)
  464. } catch (error) {
  465. ElMessage.error('打印失败:' + (error as Error).message)
  466. console.error('打印错误:', error)
  467. }
  468. }
  469. // 初始化
  470. onMounted(() => {
  471. // 默认选中当前年份
  472. searchForm.selectedYear = new Date().getFullYear().toString()
  473. fetchRefundData(`${API_BASE_URL}/year`)
  474. })
  475. </script>
  476. <style scoped>
  477. .dashboard-container {
  478. margin: 20px auto;
  479. padding: 0 20px;
  480. font-family: 'Microsoft YaHei', sans-serif;
  481. }
  482. .filter-panel {
  483. display: flex;
  484. align-items: center;
  485. gap: 20px;
  486. padding: 16px 20px;
  487. background: #f8f9fa;
  488. border-radius: 8px;
  489. margin-bottom: 20px;
  490. flex-wrap: wrap;
  491. }
  492. .filter-item {
  493. display: flex;
  494. align-items: center;
  495. gap: 8px;
  496. }
  497. .filter-label {
  498. font-size: 14px;
  499. color: #333;
  500. font-weight: 500;
  501. white-space: nowrap;
  502. }
  503. .table-container {
  504. background: #fff;
  505. padding: 20px;
  506. border-radius: 8px;
  507. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  508. }
  509. /* 响应式适配 */
  510. @media (max-width: 768px) {
  511. .filter-panel {
  512. flex-direction: column;
  513. align-items: flex-start;
  514. }
  515. }
  516. /* 表格样式优化 */
  517. :deep(.el-table__cell) {
  518. padding: 12px 0;
  519. }
  520. :deep(.el-select.is-disabled) {
  521. opacity: 0.7;
  522. }
  523. :deep(.el-button.is-disabled) {
  524. cursor: not-allowed !important;
  525. }
  526. /* 合计行样式增强 */
  527. :deep(.el-table__footer-wrapper) {
  528. font-weight: bold !important;
  529. }
  530. :deep(.el-table__footer-wrapper .el-table__cell) {
  531. background-color: #f0f0f0 !important;
  532. color: #333 !important;
  533. font-size: 14px !important;
  534. }
  535. :deep(.el-table__footer) {
  536. border-top: 2px solid #e6e6e6 !important;
  537. }
  538. </style>