zfhtgl.vue 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521
  1. <script setup lang="ts">
  2. import { nextTick, onMounted, reactive, ref } from 'vue'
  3. import type { UploadProps, UploadUserFile } from 'element-plus'
  4. import {
  5. ElButton,
  6. ElCard,
  7. ElDialog,
  8. ElForm,
  9. ElFormItem,
  10. ElInput,
  11. ElInputNumber,
  12. ElMessage,
  13. ElMessageBox,
  14. ElOption,
  15. ElPagination,
  16. ElSelect,
  17. ElSpace,
  18. ElTable,
  19. ElTableColumn,
  20. ElTag,
  21. ElUpload,
  22. } from 'element-plus'
  23. import {
  24. AlertCircle,
  25. Download,
  26. Eye,
  27. FileText,
  28. Plus,
  29. Receipt,
  30. RefreshCw,
  31. RotateCcw,
  32. Search,
  33. Upload,
  34. } from 'lucide-vue-next'
  35. import { renderAsync } from 'docx-preview'
  36. import VuePdfEmbed from 'vue-pdf-embed'
  37. import { clientGet, clientPost } from '@/utils/request.ts'
  38. // 接口定义
  39. export interface AContractInfo {
  40. id: string
  41. simplifiedHouseId: string
  42. contractNumber: string
  43. contractDate: string
  44. contractTime: string
  45. contractExpirationDate: string
  46. contractDeposit: number
  47. contractStatus: string
  48. originalContractUrl: string
  49. signContractUrl: string
  50. createTime: string
  51. updateTime: string
  52. }
  53. interface AContractInfoListResponse extends BaseResponse {
  54. data: PageType<AContractInfo>
  55. }
  56. interface CanBeReceiptDataReponse extends BaseResponse {
  57. data: {
  58. zj: number //租金
  59. yj: number //押金
  60. jkdw: string //交款单位
  61. }
  62. }
  63. interface ReceiptDto {
  64. paymentUnit: string // 交款单位
  65. paymentMethod: string // 付款方式
  66. rmb: number // 人民币金额
  67. yj: number // 押金
  68. zj: number // 租金
  69. wyf: number // 物业费
  70. sf: number // 水费
  71. paymentReason: string // 付款事由
  72. }
  73. interface CanBeReturnReceiptDataReponse extends BaseResponse {
  74. data: {
  75. zh: string //租户
  76. yj: number //押金
  77. }
  78. }
  79. interface ReturnReceiptDto {
  80. dw: string // 单位
  81. rmb: number // 人民币金额
  82. yj: number // 押金
  83. zj: number // 租金
  84. wyf: number // 物业费
  85. zh: string // 租户
  86. }
  87. // 文件类型枚举
  88. enum FileType {
  89. DOCX = 'docx',
  90. PDF = 'pdf',
  91. JPG = 'jpg',
  92. PNG = 'png',
  93. }
  94. // 响应式数据
  95. const loading = ref(false)
  96. const tableData = ref<AContractInfo[]>([])
  97. const total = ref(0)
  98. const currentPage = ref(1)
  99. const pageSize = ref(10)
  100. const MINIO_URL = import.meta.env.VITE_MINIO_BASE_URL
  101. // 搜索表单
  102. const searchForm = reactive({
  103. contractNumber: '',
  104. })
  105. // 预览相关
  106. const previewVisible = ref(false)
  107. const previewLoading = ref(false)
  108. const previewTitle = ref('')
  109. const previewContainer = ref<HTMLElement>()
  110. const previewFileType = ref<FileType>(FileType.DOCX)
  111. const previewFileUrl = ref('')
  112. const previewRelativeUrl = ref('') // 新增:存储相对路径用于下载
  113. // PDF预览相关
  114. const pdfSource = ref('')
  115. const pdfCurrentPage = ref(1)
  116. const pdfTotalPages = ref(0)
  117. const pdfScale = ref(1.0)
  118. // 上传相关
  119. const uploadVisible = ref(false)
  120. const uploadLoading = ref(false)
  121. const currentContractId = ref('')
  122. const currentContractNumber = ref('')
  123. const fileList = ref<UploadUserFile[]>([])
  124. // 收据相关
  125. const receiptVisible = ref(false)
  126. const receiptLoading = ref(false)
  127. const receiptGenerating = ref(false)
  128. const currentReceiptContractId = ref('')
  129. const currentReceiptContractNumber = ref('')
  130. // 收据表单数据
  131. const receiptForm = reactive<ReceiptDto>({
  132. paymentUnit: '',
  133. paymentMethod: '转账',
  134. rmb: 0,
  135. yj: 0,
  136. zj: 0,
  137. wyf: 0,
  138. sf: 0,
  139. paymentReason: '房屋租赁费用',
  140. })
  141. // 退据相关
  142. const returnReceiptVisible = ref(false)
  143. const returnReceiptLoading = ref(false)
  144. const returnReceiptGenerating = ref(false)
  145. const currentReturnReceiptContractId = ref('')
  146. const currentReturnReceiptContractNumber = ref('')
  147. // 退据表单数据
  148. const returnReceiptForm = reactive<ReturnReceiptDto>({
  149. dw: '',
  150. rmb: 0,
  151. yj: 0,
  152. zj: 0,
  153. wyf: 0,
  154. zh: '',
  155. })
  156. // 付款方式选项
  157. const paymentMethods = [
  158. { label: '转账', value: '转账' },
  159. { label: '扫码', value: '扫码' },
  160. { label: '其他', value: '其他' },
  161. ]
  162. // 付款事由选项
  163. const paymentReasons = [
  164. { label: '房屋租赁费用', value: '房屋租赁费用' },
  165. { label: '押金', value: '押金' },
  166. { label: '物业费', value: '物业费' },
  167. { label: '水电费', value: '水电费' },
  168. { label: '其他费用', value: '其他费用' },
  169. ]
  170. // 获取文件类型
  171. const getFileType = (url: string): FileType => {
  172. const extension = url.split('.').pop()?.toLowerCase()
  173. switch (extension) {
  174. case 'pdf':
  175. return FileType.PDF
  176. case 'jpg':
  177. case 'jpeg':
  178. return FileType.JPG
  179. case 'png':
  180. return FileType.PNG
  181. case 'docx':
  182. default:
  183. return FileType.DOCX
  184. }
  185. }
  186. // 获取合同状态标签类型
  187. const getStatusType = (status: string) => {
  188. const statusMap: Record<string, string> = {
  189. 有效: 'success',
  190. 已退租: 'warning',
  191. }
  192. return statusMap[status] || 'info'
  193. }
  194. // 获取列表数据
  195. const getList = async () => {
  196. loading.value = true
  197. try {
  198. const response = await clientGet<
  199. {
  200. pageNum: number
  201. pageSize: number
  202. contractNumber: string
  203. },
  204. AContractInfoListResponse
  205. >('/acontractInfo/findByPage', {
  206. params: {
  207. pageNum: currentPage.value,
  208. pageSize: pageSize.value,
  209. contractNumber: searchForm.contractNumber,
  210. },
  211. })
  212. if (response.code === 200 && response.data) {
  213. tableData.value = response.data.records
  214. total.value = response.data.total
  215. currentPage.value = response.data.current || 1
  216. } else {
  217. ElMessage.error(response.msg || '获取数据失败')
  218. }
  219. } catch (error) {
  220. ElMessage.error('网络请求失败')
  221. console.error('获取合同列表失败:', error)
  222. } finally {
  223. loading.value = false
  224. }
  225. }
  226. // 搜索
  227. const handleSearch = () => {
  228. currentPage.value = 1
  229. getList()
  230. }
  231. // 重置搜索
  232. const handleReset = () => {
  233. searchForm.contractNumber = ''
  234. currentPage.value = 1
  235. getList()
  236. }
  237. // 分页变化
  238. const handleCurrentChange = (page: number) => {
  239. currentPage.value = page
  240. getList()
  241. }
  242. const handleSizeChange = (size: number) => {
  243. pageSize.value = size
  244. currentPage.value = 1
  245. getList()
  246. }
  247. // 预览文档
  248. const previewDocument = async (url: string, title: string) => {
  249. const fullUrl = MINIO_URL + url
  250. if (!url) {
  251. ElMessage.warning('文档地址为空,无法预览')
  252. return
  253. }
  254. previewTitle.value = title
  255. previewFileUrl.value = fullUrl
  256. previewRelativeUrl.value = url // 存储相对路径用于下载
  257. previewFileType.value = getFileType(url)
  258. previewVisible.value = true
  259. previewLoading.value = true
  260. // 重置PDF相关状态
  261. if (previewFileType.value === FileType.PDF) {
  262. pdfCurrentPage.value = 1
  263. pdfTotalPages.value = 0
  264. pdfScale.value = 1.0
  265. pdfSource.value = fullUrl
  266. }
  267. await nextTick()
  268. try {
  269. if (previewFileType.value === FileType.DOCX) {
  270. await previewDocx(fullUrl)
  271. } else if (previewFileType.value === FileType.JPG || previewFileType.value === FileType.PNG) {
  272. await previewImage(fullUrl)
  273. }
  274. // PDF预览由vue-pdf-embed组件自动处理
  275. } catch (error) {
  276. ElMessage.error('文档预览失败: ' + (error as Error).message)
  277. console.error('文档预览失败:', error)
  278. } finally {
  279. previewLoading.value = false
  280. }
  281. }
  282. // 预览DOCX文档
  283. const previewDocx = async (url: string) => {
  284. const response = await fetch(url)
  285. if (!response.ok) {
  286. throw new Error('文档加载失败')
  287. }
  288. const arrayBuffer = await response.arrayBuffer()
  289. if (previewContainer.value) {
  290. previewContainer.value.innerHTML = ''
  291. await renderAsync(arrayBuffer, previewContainer.value, undefined, {
  292. className: 'docx-preview',
  293. inWrapper: true,
  294. ignoreWidth: false,
  295. ignoreHeight: false,
  296. ignoreFonts: false,
  297. breakPages: true,
  298. ignoreLastRenderedPageBreak: true,
  299. experimental: false,
  300. trimXmlDeclaration: true,
  301. useBase64URL: false,
  302. renderChanges: false,
  303. renderComments: false,
  304. renderEndnotes: true,
  305. renderFootnotes: true,
  306. renderFooters: true,
  307. renderHeaders: true,
  308. })
  309. }
  310. }
  311. // 预览图片
  312. const previewImage = async (url: string) => {
  313. if (previewContainer.value) {
  314. previewContainer.value.innerHTML = `
  315. <div class="flex justify-center items-center h-full">
  316. <img
  317. src="${url}"
  318. alt="预览图片"
  319. class="max-w-full max-h-full object-contain"
  320. style="max-height: 600px;"
  321. />
  322. </div>
  323. `
  324. }
  325. }
  326. // PDF相关方法
  327. const onPdfLoaded = (pdf: any) => {
  328. pdfTotalPages.value = pdf.numPages
  329. previewLoading.value = false
  330. }
  331. const onPdfError = (error: any) => {
  332. console.error('PDF加载失败:', error)
  333. ElMessage.error('PDF文档加载失败')
  334. previewLoading.value = false
  335. }
  336. const pdfPrevPage = () => {
  337. if (pdfCurrentPage.value > 1) {
  338. pdfCurrentPage.value--
  339. }
  340. }
  341. const pdfNextPage = () => {
  342. if (pdfCurrentPage.value < pdfTotalPages.value) {
  343. pdfCurrentPage.value++
  344. }
  345. }
  346. const pdfZoomIn = () => {
  347. if (pdfScale.value < 3) {
  348. pdfScale.value += 0.2
  349. }
  350. }
  351. const pdfZoomOut = () => {
  352. if (pdfScale.value > 0.4) {
  353. pdfScale.value -= 0.2
  354. }
  355. }
  356. const pdfResetZoom = () => {
  357. pdfScale.value = 1.0
  358. }
  359. // 下载文档
  360. const downloadDocument = (url: string, filename: string) => {
  361. const fullUrl = MINIO_URL + url
  362. if (!url) {
  363. ElMessage.warning('文档地址为空,无法下载')
  364. return
  365. }
  366. const link = document.createElement('a')
  367. link.href = fullUrl
  368. link.download = filename
  369. link.target = '_blank'
  370. document.body.appendChild(link)
  371. link.click()
  372. document.body.removeChild(link)
  373. }
  374. // 从预览对话框下载文档
  375. const downloadFromPreview = () => {
  376. if (!previewRelativeUrl.value) {
  377. ElMessage.warning('文档地址为空,无法下载')
  378. return
  379. }
  380. // 从标题中提取文件名,如果没有则使用默认名称
  381. const filename = previewTitle.value || '文档'
  382. downloadDocument(previewRelativeUrl.value, filename)
  383. }
  384. // 打开上传对话框
  385. const openUploadDialog = (contractId: string, contractNumber: string) => {
  386. currentContractId.value = contractId
  387. currentContractNumber.value = contractNumber
  388. fileList.value = []
  389. uploadVisible.value = true
  390. }
  391. // 文件上传前的检查
  392. const beforeUpload: UploadProps['beforeUpload'] = (file) => {
  393. const allowedTypes = [
  394. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // docx
  395. 'application/pdf', // pdf
  396. 'image/jpeg', // jpg
  397. 'image/png', // png
  398. ]
  399. const allowedExtensions = ['.docx', '.pdf', '.jpg', '.jpeg', '.png']
  400. const isValidType =
  401. allowedTypes.includes(file.type) ||
  402. allowedExtensions.some((ext) => file.name.toLowerCase().endsWith(ext))
  403. if (!isValidType) {
  404. ElMessage.error('只能上传 .docx、.pdf、.jpg、.png 格式的文件!')
  405. return false
  406. }
  407. const isLt10M = file.size / 1024 / 1024 < 10
  408. if (!isLt10M) {
  409. ElMessage.error('文件大小不能超过 10MB!')
  410. return false
  411. }
  412. return true
  413. }
  414. // 文件选择变化
  415. const handleFileChange: UploadProps['onChange'] = (file, fileList) => {
  416. console.log(file, fileList)
  417. //移除第一个元素
  418. if (fileList.length > 1) {
  419. fileList.splice(0, 1)
  420. ElMessage.warning('每次只能上传一个文件,已自动替换为最新选择的文件')
  421. // 确保只保留最新的文件
  422. // fileList.value = [fileList[fileList.length - 1]]
  423. } else {
  424. // fileList = [...fileList]
  425. }
  426. }
  427. // 移除文件
  428. const handleRemove: UploadProps['onRemove'] = () => {
  429. fileList.value = []
  430. }
  431. // 上传签约合同
  432. const uploadSignContract = async () => {
  433. if (fileList.value.length === 0) {
  434. ElMessage.warning('请选择要上传的文件')
  435. return
  436. }
  437. const file = fileList.value[0].raw
  438. if (!file) {
  439. ElMessage.warning('文件无效,请重新选择')
  440. return
  441. }
  442. try {
  443. await ElMessageBox.confirm(
  444. `确定要上传合同 ${currentContractNumber.value} 的签约文档吗?`,
  445. '确认上传',
  446. {
  447. confirmButtonText: '确定',
  448. cancelButtonText: '取消',
  449. type: 'warning',
  450. },
  451. )
  452. uploadLoading.value = true
  453. const formData = new FormData()
  454. formData.append('contractId', currentContractId.value)
  455. formData.append('file', file)
  456. const response = await clientPost<FormData, BaseResponse>(
  457. '/acontractInfo/uploadSignContract',
  458. formData,
  459. {
  460. headers: {
  461. 'Content-Type': 'multipart/form-data',
  462. },
  463. },
  464. )
  465. if (response.code === 200) {
  466. ElMessage.success('签约合同上传成功!')
  467. uploadVisible.value = false
  468. fileList.value = []
  469. await getList()
  470. } else {
  471. ElMessage.error(response.msg || '上传失败')
  472. }
  473. } catch (error: any) {
  474. if (error !== 'cancel') {
  475. ElMessage.error('上传失败:' + (error.message || '网络错误'))
  476. console.error('上传签约合同失败:', error)
  477. }
  478. } finally {
  479. uploadLoading.value = false
  480. }
  481. }
  482. // 关闭上传对话框
  483. const closeUploadDialog = () => {
  484. uploadVisible.value = false
  485. fileList.value = []
  486. currentContractId.value = ''
  487. currentContractNumber.value = ''
  488. }
  489. // 获取能获取的收据数据
  490. const getCanBeReceiptData = async (contractId: string) => {
  491. const res = await clientGet<
  492. {
  493. contractId: string
  494. },
  495. CanBeReceiptDataReponse
  496. >('/acontractInfo/canBeDetReceiptData', {
  497. params: {
  498. contractId,
  499. },
  500. })
  501. console.log(res) // res的类型就是CanBeReceiptDataReponse
  502. return res
  503. }
  504. // 获取收据
  505. const gainReceipt = async (contractId: string, dto: ReceiptDto) => {
  506. const res = await clientPost<ReceiptDto, BaseResponse & { data: string }>(
  507. '/acontractInfo/getReceipt',
  508. dto,
  509. {
  510. params: {
  511. contractId: contractId,
  512. },
  513. },
  514. )
  515. console.log(res) // res.data就是收据的url地址 前面还是要添加MINIO_URL
  516. return res
  517. }
  518. // 打开收据对话框
  519. const openReceiptDialog = async (contractId: string, contractNumber: string) => {
  520. currentReceiptContractId.value = contractId
  521. currentReceiptContractNumber.value = contractNumber
  522. receiptVisible.value = true
  523. receiptLoading.value = true
  524. // 重置表单
  525. Object.assign(receiptForm, {
  526. paymentUnit: '',
  527. paymentMethod: '转账',
  528. rmb: 0,
  529. yj: 0,
  530. zj: 0,
  531. wyf: 0,
  532. sf: 0,
  533. paymentReason: '房屋租赁费用',
  534. })
  535. try {
  536. // 获取可用的收据数据
  537. const response = await getCanBeReceiptData(contractId)
  538. if (response.code === 200 && response.data) {
  539. // 填充表单数据
  540. receiptForm.paymentUnit = response.data.jkdw || ''
  541. receiptForm.yj = response.data.yj || 0
  542. receiptForm.zj = response.data.zj || 0
  543. // 计算总金额
  544. calculateTotalAmount()
  545. } else {
  546. ElMessage.error(response.msg || '获取收据数据失败')
  547. }
  548. } catch (error) {
  549. ElMessage.error('获取收据数据失败')
  550. console.error('获取收据数据失败:', error)
  551. } finally {
  552. receiptLoading.value = false
  553. }
  554. }
  555. // 计算总金额
  556. const calculateTotalAmount = () => {
  557. receiptForm.rmb = receiptForm.yj + receiptForm.zj + receiptForm.wyf + receiptForm.sf
  558. }
  559. // 监听金额变化,自动计算总额
  560. const handleAmountChange = () => {
  561. calculateTotalAmount()
  562. }
  563. // 生成收据
  564. const generateReceipt = async () => {
  565. // 表单验证
  566. if (!receiptForm.paymentUnit.trim()) {
  567. ElMessage.warning('请填写交款单位')
  568. return
  569. }
  570. if (receiptForm.rmb <= 0) {
  571. ElMessage.warning('总金额必须大于0')
  572. return
  573. }
  574. try {
  575. await ElMessageBox.confirm(
  576. `确定要为合同 ${currentReceiptContractNumber.value} 生成收据吗?`,
  577. '确认生成收据',
  578. {
  579. confirmButtonText: '确定',
  580. cancelButtonText: '取消',
  581. type: 'warning',
  582. },
  583. )
  584. receiptGenerating.value = true
  585. const response = await gainReceipt(currentReceiptContractId.value, receiptForm)
  586. if (response.code === 999) {
  587. // 收据已存在,让用户选择是否预览
  588. ElMessage.success('收据已存在!')
  589. receiptVisible.value = false
  590. try {
  591. await ElMessageBox.confirm(
  592. `合同 ${currentReceiptContractNumber.value} 的收据已存在,是否要预览该收据?`,
  593. '收据已存在',
  594. {
  595. confirmButtonText: '预览收据',
  596. cancelButtonText: '不预览',
  597. type: 'info',
  598. },
  599. )
  600. // 用户选择预览
  601. await previewDocument(response.data, `${currentReceiptContractNumber.value} - 收据`)
  602. } catch (error) {
  603. // 用户选择不预览,不做任何操作
  604. console.log('用户选择不预览收据', error)
  605. }
  606. } else if (response.code === 200 && response.data) {
  607. ElMessage.success('收据生成成功!')
  608. // 自动预览生成的收据
  609. const receiptUrl = response.data
  610. await previewDocument(receiptUrl, `${currentReceiptContractNumber.value} - 收据`)
  611. // 关闭收据对话框
  612. receiptVisible.value = false
  613. } else {
  614. ElMessage.error(response.msg || '收据生成失败')
  615. }
  616. } catch (error: any) {
  617. if (error !== 'cancel') {
  618. ElMessage.error('收据生成失败:' + (error.message || '网络错误'))
  619. console.error('收据生成失败:', error)
  620. }
  621. } finally {
  622. receiptGenerating.value = false
  623. }
  624. }
  625. // 关闭收据对话框
  626. const closeReceiptDialog = () => {
  627. receiptVisible.value = false
  628. currentReceiptContractId.value = ''
  629. currentReceiptContractNumber.value = ''
  630. // 重置表单
  631. Object.assign(receiptForm, {
  632. paymentUnit: '',
  633. paymentMethod: '转账',
  634. rmb: 0,
  635. yj: 0,
  636. zj: 0,
  637. wyf: 0,
  638. sf: 0,
  639. paymentReason: '房屋租赁费用',
  640. })
  641. }
  642. // 获取能获取的退据数据
  643. const getCanBeReturnReceiptData = async (contractId: string) => {
  644. const res = await clientGet<
  645. {
  646. contractId: string
  647. },
  648. CanBeReturnReceiptDataReponse
  649. >('/acontractInfo/canBeDetReturnReceiptData', {
  650. params: {
  651. contractId,
  652. },
  653. })
  654. console.log(res) // res的类型就是CanBeReturnReceiptDataReponse
  655. return res
  656. }
  657. // 获取退据
  658. const gainReturnReceipt = async (contractId: string, dto: ReturnReceiptDto) => {
  659. const res = await clientPost<ReturnReceiptDto, BaseResponse & { data: string }>(
  660. '/acontractInfo/getReturnReceipt',
  661. dto,
  662. {
  663. params: {
  664. contractId: contractId,
  665. },
  666. },
  667. )
  668. console.log(res) // res.data就是退据的url地址 前面还是要添加MINIO_URL
  669. return res
  670. }
  671. // 打开退据对话框
  672. const openReturnReceiptDialog = async (contractId: string, contractNumber: string) => {
  673. currentReturnReceiptContractId.value = contractId
  674. currentReturnReceiptContractNumber.value = contractNumber
  675. returnReceiptVisible.value = true
  676. returnReceiptLoading.value = true
  677. // 重置表单
  678. Object.assign(returnReceiptForm, {
  679. dw: '',
  680. rmb: 0,
  681. yj: 0,
  682. zj: 0,
  683. wyf: 0,
  684. zh: '',
  685. })
  686. try {
  687. // 获取可用的退据数据
  688. const response = await getCanBeReturnReceiptData(contractId)
  689. if (response.code === 200 && response.data) {
  690. // 填充表单数据
  691. returnReceiptForm.zh = response.data.zh || ''
  692. returnReceiptForm.yj = response.data.yj || 0
  693. // 计算总金额
  694. calculateReturnTotalAmount()
  695. } else {
  696. ElMessage.error(response.msg || '获取退据数据失败')
  697. }
  698. } catch (error) {
  699. ElMessage.error('获取退据数据失败')
  700. console.error('获取退据数据失败:', error)
  701. } finally {
  702. returnReceiptLoading.value = false
  703. }
  704. }
  705. // 计算退据总金额
  706. const calculateReturnTotalAmount = () => {
  707. returnReceiptForm.rmb = returnReceiptForm.yj + returnReceiptForm.zj + returnReceiptForm.wyf
  708. }
  709. // 监听退据金额变化,自动计算总额
  710. const handleReturnAmountChange = () => {
  711. calculateReturnTotalAmount()
  712. }
  713. // 生成退据
  714. const generateReturnReceipt = async () => {
  715. // 表单验证
  716. if (!returnReceiptForm.dw.trim()) {
  717. ElMessage.warning('请填写单位名称')
  718. return
  719. }
  720. if (!returnReceiptForm.zh.trim()) {
  721. ElMessage.warning('请填写租户名称')
  722. return
  723. }
  724. if (returnReceiptForm.rmb <= 0) {
  725. ElMessage.warning('总金额必须大于0')
  726. return
  727. }
  728. try {
  729. await ElMessageBox.confirm(
  730. `确定要为合同 ${currentReturnReceiptContractNumber.value} 生成退据吗?`,
  731. '确认生成退据',
  732. {
  733. confirmButtonText: '确定',
  734. cancelButtonText: '取消',
  735. type: 'warning',
  736. },
  737. )
  738. returnReceiptGenerating.value = true
  739. const response = await gainReturnReceipt(
  740. currentReturnReceiptContractId.value,
  741. returnReceiptForm,
  742. )
  743. if (response.code === 999) {
  744. // 退据已存在,让用户选择是否预览
  745. ElMessage.success('退据已存在!')
  746. returnReceiptVisible.value = false
  747. try {
  748. await ElMessageBox.confirm(
  749. `合同 ${currentReturnReceiptContractNumber.value} 的退据已存在,是否要预览该退据?`,
  750. '退据已存在',
  751. {
  752. confirmButtonText: '预览退据',
  753. cancelButtonText: '不预览',
  754. type: 'info',
  755. },
  756. )
  757. // 用户选择预览
  758. await previewDocument(response.data, `${currentReturnReceiptContractNumber.value} - 退据`)
  759. } catch (error) {
  760. // 用户选择不预览,不做任何操作
  761. console.log('用户选择不预览退据', error)
  762. }
  763. } else if (response.code === 200 && response.data) {
  764. ElMessage.success('退据生成成功!')
  765. // 自动预览生成的退据
  766. const returnReceiptUrl = response.data
  767. await previewDocument(returnReceiptUrl, `${currentReturnReceiptContractNumber.value} - 退据`)
  768. // 关闭退据对话框
  769. returnReceiptVisible.value = false
  770. } else {
  771. ElMessage.error(response.msg || '退据生成失败')
  772. }
  773. } catch (error: any) {
  774. if (error !== 'cancel') {
  775. ElMessage.error('退据生成失败:' + (error.message || '网络错误'))
  776. console.error('退据生成失败:', error)
  777. }
  778. } finally {
  779. returnReceiptGenerating.value = false
  780. }
  781. }
  782. // 关闭退据对话框
  783. const closeReturnReceiptDialog = () => {
  784. returnReceiptVisible.value = false
  785. currentReturnReceiptContractId.value = ''
  786. currentReturnReceiptContractNumber.value = ''
  787. // 重置表单
  788. Object.assign(returnReceiptForm, {
  789. dw: '',
  790. rmb: 0,
  791. yj: 0,
  792. zj: 0,
  793. wyf: 0,
  794. zh: '',
  795. })
  796. }
  797. // 初始化
  798. onMounted(() => {
  799. getList()
  800. })
  801. </script>
  802. <template>
  803. <div class="p-6 bg-gray-50 min-h-screen font-sans">
  804. <!-- 页面标题 -->
  805. <div class="mb-6">
  806. <h1 class="text-2xl font-bold text-gray-800 mb-2">租房合同管理</h1>
  807. <p class="text-gray-600">管理和查看所有租房合同信息</p>
  808. </div>
  809. <!-- 搜索区域 -->
  810. <ElCard class="mb-6" shadow="never">
  811. <ElForm :model="searchForm" inline class="bg-gray-50 p-5 rounded-lg">
  812. <ElFormItem label="合同编号">
  813. <ElInput
  814. v-model="searchForm.contractNumber"
  815. placeholder="请输入合同编号"
  816. clearable
  817. class="w-50"
  818. @keyup.enter="handleSearch"
  819. />
  820. </ElFormItem>
  821. <ElFormItem>
  822. <ElSpace>
  823. <ElButton type="primary" :icon="Search" @click="handleSearch">搜索</ElButton>
  824. <ElButton :icon="RefreshCw" @click="handleReset">重置</ElButton>
  825. </ElSpace>
  826. </ElFormItem>
  827. </ElForm>
  828. </ElCard>
  829. <!-- 表格区域 -->
  830. <ElCard shadow="never" class="rounded-lg overflow-hidden">
  831. <ElTable
  832. :data="tableData"
  833. :loading="loading"
  834. stripe
  835. border
  836. style="width: 100%"
  837. empty-text="暂无数据"
  838. class="rounded-lg overflow-hidden"
  839. >
  840. <ElTableColumn prop="contractNumber" label="合同编号" width="160" />
  841. <ElTableColumn prop="contractDate" label="签约日期" width="140" />
  842. <ElTableColumn prop="contractTime" label="合同期限" width="120" />
  843. <ElTableColumn prop="contractExpirationDate" label="到期日期" width="140" />
  844. <ElTableColumn prop="contractDeposit" label="押金(元)" width="140">
  845. <template #default="{ row }">
  846. <span class="text-orange-600 font-medium">
  847. ¥{{ row.contractDeposit.toLocaleString() }}
  848. </span>
  849. </template>
  850. </ElTableColumn>
  851. <ElTableColumn prop="contractStatus" label="合同状态" width="120">
  852. <template #default="{ row }">
  853. <ElTag :type="getStatusType(row.contractStatus)" size="small">
  854. {{ row.contractStatus }}
  855. </ElTag>
  856. </template>
  857. </ElTableColumn>
  858. <ElTableColumn label="合同文档" min-width="380">
  859. <template #default="{ row }">
  860. <div class="flex flex-col gap-4">
  861. <div v-if="row.originalContractUrl" class="flex items-center gap-2">
  862. <FileText class="w-4 h-4 text-blue-500" />
  863. <span class="text-sm text-gray-600">原合同:</span>
  864. <ElButton
  865. type="primary"
  866. size="small"
  867. text
  868. :icon="Eye"
  869. @click="
  870. previewDocument(row.originalContractUrl, `${row.contractNumber} - 原合同`)
  871. "
  872. >
  873. 预览
  874. </ElButton>
  875. <ElButton
  876. type="success"
  877. size="small"
  878. text
  879. :icon="Download"
  880. @click="downloadDocument(row.originalContractUrl, `${row.contractNumber}_原合同`)"
  881. >
  882. 下载
  883. </ElButton>
  884. </div>
  885. <div v-else class="text-sm text-gray-400">原合同: 暂无</div>
  886. <div v-if="row.signContractUrl" class="flex items-center gap-2">
  887. <FileText class="w-4 h-4 text-green-500" />
  888. <span class="text-sm text-gray-600">签订合同:</span>
  889. <ElButton
  890. type="primary"
  891. size="small"
  892. text
  893. :icon="Eye"
  894. @click="previewDocument(row.signContractUrl, `${row.contractNumber} - 签订合同`)"
  895. >
  896. 预览
  897. </ElButton>
  898. <ElButton
  899. type="success"
  900. size="small"
  901. text
  902. :icon="Download"
  903. @click="downloadDocument(row.signContractUrl, `${row.contractNumber}_签订合同`)"
  904. >
  905. 下载
  906. </ElButton>
  907. <ElButton
  908. type="warning"
  909. size="small"
  910. text
  911. :icon="Upload"
  912. @click="openUploadDialog(row.id, row.contractNumber)"
  913. >
  914. 重新上传
  915. </ElButton>
  916. </div>
  917. <div v-else class="flex items-center gap-2">
  918. <span class="text-sm text-gray-400">签订合同: 暂无</span>
  919. <ElButton
  920. type="warning"
  921. size="small"
  922. text
  923. :icon="Upload"
  924. @click="openUploadDialog(row.id, row.contractNumber)"
  925. >
  926. 上传
  927. </ElButton>
  928. </div>
  929. </div>
  930. </template>
  931. </ElTableColumn>
  932. <ElTableColumn label="操作" width="160" fixed="right">
  933. <template #default="{ row }">
  934. <div class="flex flex-col gap-1">
  935. <div>
  936. <ElButton
  937. type="primary"
  938. size="small"
  939. text
  940. :icon="Receipt"
  941. @click="openReceiptDialog(row.id, row.contractNumber)"
  942. >
  943. 获取收据
  944. </ElButton>
  945. </div>
  946. <div>
  947. <ElButton
  948. type="warning"
  949. size="small"
  950. text
  951. :icon="RotateCcw"
  952. @click="openReturnReceiptDialog(row.id, row.contractNumber)"
  953. >
  954. 获取退据
  955. </ElButton>
  956. </div>
  957. </div>
  958. </template>
  959. </ElTableColumn>
  960. <ElTableColumn prop="createTime" label="创建时间" width="180" />
  961. <ElTableColumn prop="updateTime" label="更新时间" width="180" />
  962. </ElTable>
  963. <!-- 分页 -->
  964. <div class="flex justify-center mt-6">
  965. <ElPagination
  966. v-model:current-page="currentPage"
  967. v-model:page-size="pageSize"
  968. :page-sizes="[10, 20, 50, 100]"
  969. :total="total"
  970. layout="total, sizes, prev, pager, next, jumper"
  971. @size-change="handleSizeChange"
  972. @current-change="handleCurrentChange"
  973. />
  974. </div>
  975. </ElCard>
  976. <!-- 文档预览对话框 -->
  977. <ElDialog
  978. v-model="previewVisible"
  979. :title="previewTitle"
  980. width="70%"
  981. top="5vh"
  982. destroy-on-close
  983. class="preview-dialog"
  984. >
  985. <!-- 预览对话框标题栏添加下载按钮 -->
  986. <template #header="{ titleId, titleClass }">
  987. <div class="flex items-center justify-between w-full">
  988. <span :id="titleId" :class="titleClass">{{ previewTitle }}</span>
  989. <ElButton
  990. type="primary"
  991. :icon="Download"
  992. @click="downloadFromPreview"
  993. :disabled="!previewRelativeUrl"
  994. >
  995. 下载文档
  996. </ElButton>
  997. </div>
  998. </template>
  999. <div
  1000. v-loading="previewLoading"
  1001. element-loading-text="正在加载文档..."
  1002. class="border border-gray-200 rounded bg-white overflow-hidden"
  1003. style="height: 75vh"
  1004. >
  1005. <!-- PDF预览 -->
  1006. <div v-if="previewFileType === FileType.PDF" class="h-full flex flex-col">
  1007. <!-- PDF工具栏 -->
  1008. <div class="flex items-center justify-between p-3 bg-gray-50 border-b border-gray-200">
  1009. <div class="flex items-center gap-2">
  1010. <ElButton size="small" @click="pdfPrevPage" :disabled="pdfCurrentPage <= 1">
  1011. 上一页
  1012. </ElButton>
  1013. <span class="text-sm text-gray-600">
  1014. {{ pdfCurrentPage }} / {{ pdfTotalPages }}
  1015. </span>
  1016. <ElButton
  1017. size="small"
  1018. @click="pdfNextPage"
  1019. :disabled="pdfCurrentPage >= pdfTotalPages"
  1020. >
  1021. 下一页
  1022. </ElButton>
  1023. </div>
  1024. <div class="flex items-center gap-2">
  1025. <ElButton size="small" @click="pdfZoomOut" :disabled="pdfScale <= 0.4">
  1026. 缩小
  1027. </ElButton>
  1028. <span class="text-sm text-gray-600 min-w-12 text-center">
  1029. {{ Math.round(pdfScale * 100) }}%
  1030. </span>
  1031. <ElButton size="small" @click="pdfZoomIn" :disabled="pdfScale >= 3"> 放大 </ElButton>
  1032. <ElButton size="small" @click="pdfResetZoom"> 重置 </ElButton>
  1033. </div>
  1034. </div>
  1035. <!-- PDF内容 -->
  1036. <div class="flex-1 overflow-auto p-4 bg-gray-100 flex justify-center">
  1037. <VuePdfEmbed
  1038. :source="pdfSource"
  1039. :page="pdfCurrentPage"
  1040. :scale="pdfScale"
  1041. class="shadow-lg w-50%"
  1042. @loaded="onPdfLoaded"
  1043. @loading-failed="onPdfError"
  1044. />
  1045. </div>
  1046. </div>
  1047. <!-- 其他格式预览 -->
  1048. <div v-else class="h-full relative">
  1049. <div ref="previewContainer" class="p-5 leading-relaxed overflow-auto h-full"></div>
  1050. </div>
  1051. </div>
  1052. </ElDialog>
  1053. <!-- 上传签约合同对话框 -->
  1054. <ElDialog
  1055. v-model="uploadVisible"
  1056. :title="`上传签约合同 - ${currentContractNumber}`"
  1057. width="500px"
  1058. :before-close="closeUploadDialog"
  1059. destroy-on-close
  1060. >
  1061. <div class="py-5">
  1062. <!-- 文件限制提示 -->
  1063. <div class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
  1064. <div class="flex items-start gap-2">
  1065. <AlertCircle class="w-4 h-4 text-blue-500 mt-0.5 flex-shrink-0" />
  1066. <div class="text-sm">
  1067. <p class="text-blue-800 font-medium mb-1">上传说明:</p>
  1068. <ul class="text-blue-700 space-y-1">
  1069. <li>• 每次只能上传一个文件</li>
  1070. <li>• 支持格式:.docx、.pdf、.jpg、.png</li>
  1071. <li>• 文件大小不超过 10MB</li>
  1072. <li>• 上传新文件会替换已选择的文件</li>
  1073. </ul>
  1074. </div>
  1075. </div>
  1076. </div>
  1077. <ElUpload
  1078. v-model:file-list="fileList"
  1079. class="upload-demo"
  1080. drag
  1081. :auto-upload="false"
  1082. :before-upload="beforeUpload"
  1083. :on-change="handleFileChange"
  1084. :on-remove="handleRemove"
  1085. accept=".docx,.pdf,.jpg,.jpeg,.png,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/pdf,image/jpeg,image/png"
  1086. >
  1087. <div class="py-10 px-5 text-center">
  1088. <Plus class="w-12 h-12 text-gray-400 mx-auto mb-4" />
  1089. <div class="text-gray-600 text-base mb-2">
  1090. 将文件拖到此处,或<em class="text-blue-500 not-italic">点击上传</em>
  1091. </div>
  1092. <div class="text-gray-400 text-sm">支持.docx、.pdf、.jpg、.png格式,且不超过10MB</div>
  1093. <div class="text-orange-500 text-xs mt-2 font-medium">
  1094. ⚠️ 限制上传一个文件,选择新文件将替换当前文件
  1095. </div>
  1096. </div>
  1097. </ElUpload>
  1098. </div>
  1099. <template #footer>
  1100. <div class="text-right">
  1101. <ElButton @click="closeUploadDialog">取消</ElButton>
  1102. <ElButton
  1103. type="primary"
  1104. :loading="uploadLoading"
  1105. :disabled="fileList.length === 0"
  1106. @click="uploadSignContract"
  1107. >
  1108. {{ uploadLoading ? '上传中...' : '确定上传' }}
  1109. </ElButton>
  1110. </div>
  1111. </template>
  1112. </ElDialog>
  1113. <!-- 收据生成对话框 -->
  1114. <ElDialog
  1115. v-model="receiptVisible"
  1116. :title="`生成收据 - ${currentReceiptContractNumber}`"
  1117. width="600px"
  1118. :before-close="closeReceiptDialog"
  1119. destroy-on-close
  1120. >
  1121. <div v-loading="receiptLoading" element-loading-text="正在获取收据数据...">
  1122. <ElForm :model="receiptForm" label-width="100px" class="py-4">
  1123. <div class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
  1124. <div class="flex items-start gap-2">
  1125. <AlertCircle class="w-4 h-4 text-blue-500 mt-0.5 flex-shrink-0" />
  1126. <div class="text-sm">
  1127. <p class="text-blue-800 font-medium mb-1">收据说明:</p>
  1128. <ul class="text-blue-700 space-y-1">
  1129. <li>• 系统已自动填充合同相关的押金和租金信息</li>
  1130. <li>• 请根据实际情况填写物业费、水费等其他费用</li>
  1131. <li>• 总金额会根据各项费用自动计算</li>
  1132. <li>• 生成后的收据可以预览和下载</li>
  1133. </ul>
  1134. </div>
  1135. </div>
  1136. </div>
  1137. <ElFormItem label="交款单位" required>
  1138. <ElInput v-model="receiptForm.paymentUnit" placeholder="请输入交款单位名称" clearable />
  1139. </ElFormItem>
  1140. <ElFormItem label="付款方式">
  1141. <ElSelect
  1142. v-model="receiptForm.paymentMethod"
  1143. placeholder="请选择付款方式"
  1144. style="width: 100%"
  1145. >
  1146. <ElOption
  1147. v-for="item in paymentMethods"
  1148. :key="item.value"
  1149. :label="item.label"
  1150. :value="item.value"
  1151. />
  1152. </ElSelect>
  1153. </ElFormItem>
  1154. <ElFormItem label="付款事由">
  1155. <ElSelect
  1156. v-model="receiptForm.paymentReason"
  1157. placeholder="请选择付款事由"
  1158. style="width: 100%"
  1159. >
  1160. <ElOption
  1161. v-for="item in paymentReasons"
  1162. :key="item.value"
  1163. :label="item.label"
  1164. :value="item.value"
  1165. />
  1166. </ElSelect>
  1167. </ElFormItem>
  1168. <div class="grid grid-cols-2 gap-4">
  1169. <ElFormItem label="押金(元)">
  1170. <ElInputNumber
  1171. v-model="receiptForm.yj"
  1172. :min="0"
  1173. :precision="2"
  1174. style="width: 100%"
  1175. @change="handleAmountChange"
  1176. />
  1177. </ElFormItem>
  1178. <ElFormItem label="租金(元)">
  1179. <ElInputNumber
  1180. v-model="receiptForm.zj"
  1181. :min="0"
  1182. :precision="2"
  1183. style="width: 100%"
  1184. @change="handleAmountChange"
  1185. />
  1186. </ElFormItem>
  1187. <ElFormItem label="物业费(元)">
  1188. <ElInputNumber
  1189. v-model="receiptForm.wyf"
  1190. :min="0"
  1191. :precision="2"
  1192. style="width: 100%"
  1193. @change="handleAmountChange"
  1194. />
  1195. </ElFormItem>
  1196. <ElFormItem label="水费(元)">
  1197. <ElInputNumber
  1198. v-model="receiptForm.sf"
  1199. :min="0"
  1200. :precision="2"
  1201. style="width: 100%"
  1202. @change="handleAmountChange"
  1203. />
  1204. </ElFormItem>
  1205. </div>
  1206. <ElFormItem label="总金额(元)">
  1207. <ElInputNumber
  1208. v-model="receiptForm.rmb"
  1209. :min="0"
  1210. :precision="2"
  1211. style="width: 100%"
  1212. readonly
  1213. class="total-amount"
  1214. />
  1215. <div class="text-sm text-gray-500 mt-1">总金额 = 押金 + 租金 + 物业费 + 水费</div>
  1216. </ElFormItem>
  1217. </ElForm>
  1218. </div>
  1219. <template #footer>
  1220. <div class="text-right">
  1221. <ElButton @click="closeReceiptDialog">取消</ElButton>
  1222. <ElButton
  1223. type="primary"
  1224. :loading="receiptGenerating"
  1225. :disabled="receiptLoading || !receiptForm.paymentUnit.trim() || receiptForm.rmb <= 0"
  1226. @click="generateReceipt"
  1227. >
  1228. {{ receiptGenerating ? '生成中...' : '生成收据' }}
  1229. </ElButton>
  1230. </div>
  1231. </template>
  1232. </ElDialog>
  1233. <!-- 退据生成对话框 -->
  1234. <ElDialog
  1235. v-model="returnReceiptVisible"
  1236. :title="`生成退据 - ${currentReturnReceiptContractNumber}`"
  1237. width="600px"
  1238. :before-close="closeReturnReceiptDialog"
  1239. destroy-on-close
  1240. >
  1241. <div v-loading="returnReceiptLoading" element-loading-text="正在获取退据数据...">
  1242. <ElForm :model="returnReceiptForm" label-width="100px" class="py-4">
  1243. <div class="mb-4 p-3 bg-orange-50 border border-orange-200 rounded-lg">
  1244. <div class="flex items-start gap-2">
  1245. <AlertCircle class="w-4 h-4 text-orange-500 mt-0.5 flex-shrink-0" />
  1246. <div class="text-sm">
  1247. <p class="text-orange-800 font-medium mb-1">退据说明:</p>
  1248. <ul class="text-orange-700 space-y-1">
  1249. <li>• 系统已自动填充合同相关的租户和押金信息</li>
  1250. <li>• 请根据实际情况填写退还的租金、物业费等费用</li>
  1251. <li>• 总金额会根据各项费用自动计算</li>
  1252. <li>• 生成后的退据可以预览和下载</li>
  1253. </ul>
  1254. </div>
  1255. </div>
  1256. </div>
  1257. <ElFormItem label="单位名称" required>
  1258. <ElInput v-model="returnReceiptForm.dw" placeholder="请输入单位名称" clearable />
  1259. </ElFormItem>
  1260. <ElFormItem label="租户名称" required>
  1261. <ElInput v-model="returnReceiptForm.zh" placeholder="请输入租户名称" clearable />
  1262. </ElFormItem>
  1263. <div class="grid grid-cols-2 gap-4">
  1264. <ElFormItem label="押金(元)">
  1265. <ElInputNumber
  1266. v-model="returnReceiptForm.yj"
  1267. :min="0"
  1268. :precision="2"
  1269. style="width: 100%"
  1270. @change="handleReturnAmountChange"
  1271. />
  1272. </ElFormItem>
  1273. <ElFormItem label="租金(元)">
  1274. <ElInputNumber
  1275. v-model="returnReceiptForm.zj"
  1276. :min="0"
  1277. :precision="2"
  1278. style="width: 100%"
  1279. @change="handleReturnAmountChange"
  1280. />
  1281. </ElFormItem>
  1282. <ElFormItem label="物业费(元)" class="col-span-2">
  1283. <ElInputNumber
  1284. v-model="returnReceiptForm.wyf"
  1285. :min="0"
  1286. :precision="2"
  1287. style="width: 100%"
  1288. @change="handleReturnAmountChange"
  1289. />
  1290. </ElFormItem>
  1291. </div>
  1292. <ElFormItem label="总金额(元)">
  1293. <ElInputNumber
  1294. v-model="returnReceiptForm.rmb"
  1295. :min="0"
  1296. :precision="2"
  1297. style="width: 100%"
  1298. readonly
  1299. class="total-amount"
  1300. />
  1301. <div class="text-sm text-gray-500 mt-1">总金额 = 押金 + 租金 + 物业费</div>
  1302. </ElFormItem>
  1303. </ElForm>
  1304. </div>
  1305. <template #footer>
  1306. <div class="text-right">
  1307. <ElButton @click="closeReturnReceiptDialog">取消</ElButton>
  1308. <ElButton
  1309. type="primary"
  1310. :loading="returnReceiptGenerating"
  1311. :disabled="
  1312. returnReceiptLoading ||
  1313. !returnReceiptForm.dw.trim() ||
  1314. !returnReceiptForm.zh.trim() ||
  1315. returnReceiptForm.rmb <= 0
  1316. "
  1317. @click="generateReturnReceipt"
  1318. >
  1319. {{ returnReceiptGenerating ? '生成中...' : '生成退据' }}
  1320. </ElButton>
  1321. </div>
  1322. </template>
  1323. </ElDialog>
  1324. </div>
  1325. </template>
  1326. <style scoped>
  1327. /* Element Plus 样式覆盖 */
  1328. :deep(.el-table th) {
  1329. background-color: #f8f9fa;
  1330. color: #495057;
  1331. font-weight: 600;
  1332. }
  1333. :deep(.el-table td) {
  1334. padding: 12px 0;
  1335. }
  1336. :deep(.el-pagination) {
  1337. margin-top: 20px;
  1338. }
  1339. /* DOCX 预览样式 */
  1340. :deep(.docx-container p) {
  1341. margin: 8px 0;
  1342. }
  1343. :deep(.docx-container table) {
  1344. border-collapse: collapse;
  1345. width: 100%;
  1346. margin: 16px 0;
  1347. }
  1348. :deep(.docx-container table td),
  1349. :deep(.docx-container table th) {
  1350. border: 1px solid #ddd;
  1351. padding: 8px;
  1352. text-align: left;
  1353. }
  1354. :deep(.docx-container table th) {
  1355. background-color: #f2f2f2;
  1356. font-weight: bold;
  1357. }
  1358. /* 上传组件样式 */
  1359. :deep(.el-upload-dragger) {
  1360. border: 2px dashed #d9d9d9;
  1361. border-radius: 6px;
  1362. width: 100%;
  1363. height: auto;
  1364. text-align: center;
  1365. background: #fafafa;
  1366. transition: border-color 0.3s;
  1367. }
  1368. :deep(.el-upload-dragger:hover) {
  1369. border-color: #409eff;
  1370. }
  1371. :deep(.el-upload-dragger.is-dragover) {
  1372. border-color: #409eff;
  1373. background-color: rgba(64, 158, 255, 0.06);
  1374. }
  1375. :deep(.el-upload__text em) {
  1376. color: #409eff;
  1377. font-style: normal;
  1378. }
  1379. /* PDF预览样式 */
  1380. :deep(.vue-pdf-embed) {
  1381. border: 1px solid #e0e0e0;
  1382. border-radius: 4px;
  1383. }
  1384. /* 收据表单样式 */
  1385. :deep(.total-amount .el-input__inner) {
  1386. background-color: #f5f7fa;
  1387. font-weight: 600;
  1388. color: #409eff;
  1389. }
  1390. /* 预览对话框样式优化 */
  1391. :deep(.preview-dialog .el-dialog__header) {
  1392. padding: 16px 20px;
  1393. border-bottom: 1px solid #ebeef5;
  1394. }
  1395. :deep(.preview-dialog .el-dialog__body) {
  1396. padding: 0;
  1397. }
  1398. </style>