| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521 |
- <script setup lang="ts">
- import { nextTick, onMounted, reactive, ref } from 'vue'
- import type { UploadProps, UploadUserFile } from 'element-plus'
- import {
- ElButton,
- ElCard,
- ElDialog,
- ElForm,
- ElFormItem,
- ElInput,
- ElInputNumber,
- ElMessage,
- ElMessageBox,
- ElOption,
- ElPagination,
- ElSelect,
- ElSpace,
- ElTable,
- ElTableColumn,
- ElTag,
- ElUpload,
- } from 'element-plus'
- import {
- AlertCircle,
- Download,
- Eye,
- FileText,
- Plus,
- Receipt,
- RefreshCw,
- RotateCcw,
- Search,
- Upload,
- } from 'lucide-vue-next'
- import { renderAsync } from 'docx-preview'
- import VuePdfEmbed from 'vue-pdf-embed'
- import { clientGet, clientPost } from '@/utils/request.ts'
- // 接口定义
- export interface AContractInfo {
- id: string
- simplifiedHouseId: string
- contractNumber: string
- contractDate: string
- contractTime: string
- contractExpirationDate: string
- contractDeposit: number
- contractStatus: string
- originalContractUrl: string
- signContractUrl: string
- createTime: string
- updateTime: string
- }
- interface AContractInfoListResponse extends BaseResponse {
- data: PageType<AContractInfo>
- }
- interface CanBeReceiptDataReponse extends BaseResponse {
- data: {
- zj: number //租金
- yj: number //押金
- jkdw: string //交款单位
- }
- }
- interface ReceiptDto {
- paymentUnit: string // 交款单位
- paymentMethod: string // 付款方式
- rmb: number // 人民币金额
- yj: number // 押金
- zj: number // 租金
- wyf: number // 物业费
- sf: number // 水费
- paymentReason: string // 付款事由
- }
- interface CanBeReturnReceiptDataReponse extends BaseResponse {
- data: {
- zh: string //租户
- yj: number //押金
- }
- }
- interface ReturnReceiptDto {
- dw: string // 单位
- rmb: number // 人民币金额
- yj: number // 押金
- zj: number // 租金
- wyf: number // 物业费
- zh: string // 租户
- }
- // 文件类型枚举
- enum FileType {
- DOCX = 'docx',
- PDF = 'pdf',
- JPG = 'jpg',
- PNG = 'png',
- }
- // 响应式数据
- const loading = ref(false)
- const tableData = ref<AContractInfo[]>([])
- const total = ref(0)
- const currentPage = ref(1)
- const pageSize = ref(10)
- const MINIO_URL = import.meta.env.VITE_MINIO_BASE_URL
- // 搜索表单
- const searchForm = reactive({
- contractNumber: '',
- })
- // 预览相关
- const previewVisible = ref(false)
- const previewLoading = ref(false)
- const previewTitle = ref('')
- const previewContainer = ref<HTMLElement>()
- const previewFileType = ref<FileType>(FileType.DOCX)
- const previewFileUrl = ref('')
- const previewRelativeUrl = ref('') // 新增:存储相对路径用于下载
- // PDF预览相关
- const pdfSource = ref('')
- const pdfCurrentPage = ref(1)
- const pdfTotalPages = ref(0)
- const pdfScale = ref(1.0)
- // 上传相关
- const uploadVisible = ref(false)
- const uploadLoading = ref(false)
- const currentContractId = ref('')
- const currentContractNumber = ref('')
- const fileList = ref<UploadUserFile[]>([])
- // 收据相关
- const receiptVisible = ref(false)
- const receiptLoading = ref(false)
- const receiptGenerating = ref(false)
- const currentReceiptContractId = ref('')
- const currentReceiptContractNumber = ref('')
- // 收据表单数据
- const receiptForm = reactive<ReceiptDto>({
- paymentUnit: '',
- paymentMethod: '转账',
- rmb: 0,
- yj: 0,
- zj: 0,
- wyf: 0,
- sf: 0,
- paymentReason: '房屋租赁费用',
- })
- // 退据相关
- const returnReceiptVisible = ref(false)
- const returnReceiptLoading = ref(false)
- const returnReceiptGenerating = ref(false)
- const currentReturnReceiptContractId = ref('')
- const currentReturnReceiptContractNumber = ref('')
- // 退据表单数据
- const returnReceiptForm = reactive<ReturnReceiptDto>({
- dw: '',
- rmb: 0,
- yj: 0,
- zj: 0,
- wyf: 0,
- zh: '',
- })
- // 付款方式选项
- const paymentMethods = [
- { label: '转账', value: '转账' },
- { label: '扫码', value: '扫码' },
- { label: '其他', value: '其他' },
- ]
- // 付款事由选项
- const paymentReasons = [
- { label: '房屋租赁费用', value: '房屋租赁费用' },
- { label: '押金', value: '押金' },
- { label: '物业费', value: '物业费' },
- { label: '水电费', value: '水电费' },
- { label: '其他费用', value: '其他费用' },
- ]
- // 获取文件类型
- const getFileType = (url: string): FileType => {
- const extension = url.split('.').pop()?.toLowerCase()
- switch (extension) {
- case 'pdf':
- return FileType.PDF
- case 'jpg':
- case 'jpeg':
- return FileType.JPG
- case 'png':
- return FileType.PNG
- case 'docx':
- default:
- return FileType.DOCX
- }
- }
- // 获取合同状态标签类型
- const getStatusType = (status: string) => {
- const statusMap: Record<string, string> = {
- 有效: 'success',
- 已退租: 'warning',
- }
- return statusMap[status] || 'info'
- }
- // 获取列表数据
- const getList = async () => {
- loading.value = true
- try {
- const response = await clientGet<
- {
- pageNum: number
- pageSize: number
- contractNumber: string
- },
- AContractInfoListResponse
- >('/acontractInfo/findByPage', {
- params: {
- pageNum: currentPage.value,
- pageSize: pageSize.value,
- contractNumber: searchForm.contractNumber,
- },
- })
- if (response.code === 200 && response.data) {
- tableData.value = response.data.records
- total.value = response.data.total
- currentPage.value = response.data.current || 1
- } else {
- ElMessage.error(response.msg || '获取数据失败')
- }
- } catch (error) {
- ElMessage.error('网络请求失败')
- console.error('获取合同列表失败:', error)
- } finally {
- loading.value = false
- }
- }
- // 搜索
- const handleSearch = () => {
- currentPage.value = 1
- getList()
- }
- // 重置搜索
- const handleReset = () => {
- searchForm.contractNumber = ''
- currentPage.value = 1
- getList()
- }
- // 分页变化
- const handleCurrentChange = (page: number) => {
- currentPage.value = page
- getList()
- }
- const handleSizeChange = (size: number) => {
- pageSize.value = size
- currentPage.value = 1
- getList()
- }
- // 预览文档
- const previewDocument = async (url: string, title: string) => {
- const fullUrl = MINIO_URL + url
- if (!url) {
- ElMessage.warning('文档地址为空,无法预览')
- return
- }
- previewTitle.value = title
- previewFileUrl.value = fullUrl
- previewRelativeUrl.value = url // 存储相对路径用于下载
- previewFileType.value = getFileType(url)
- previewVisible.value = true
- previewLoading.value = true
- // 重置PDF相关状态
- if (previewFileType.value === FileType.PDF) {
- pdfCurrentPage.value = 1
- pdfTotalPages.value = 0
- pdfScale.value = 1.0
- pdfSource.value = fullUrl
- }
- await nextTick()
- try {
- if (previewFileType.value === FileType.DOCX) {
- await previewDocx(fullUrl)
- } else if (previewFileType.value === FileType.JPG || previewFileType.value === FileType.PNG) {
- await previewImage(fullUrl)
- }
- // PDF预览由vue-pdf-embed组件自动处理
- } catch (error) {
- ElMessage.error('文档预览失败: ' + (error as Error).message)
- console.error('文档预览失败:', error)
- } finally {
- previewLoading.value = false
- }
- }
- // 预览DOCX文档
- const previewDocx = async (url: string) => {
- const response = await fetch(url)
- if (!response.ok) {
- throw new Error('文档加载失败')
- }
- const arrayBuffer = await response.arrayBuffer()
- if (previewContainer.value) {
- previewContainer.value.innerHTML = ''
- await renderAsync(arrayBuffer, previewContainer.value, undefined, {
- className: 'docx-preview',
- inWrapper: true,
- ignoreWidth: false,
- ignoreHeight: false,
- ignoreFonts: false,
- breakPages: true,
- ignoreLastRenderedPageBreak: true,
- experimental: false,
- trimXmlDeclaration: true,
- useBase64URL: false,
- renderChanges: false,
- renderComments: false,
- renderEndnotes: true,
- renderFootnotes: true,
- renderFooters: true,
- renderHeaders: true,
- })
- }
- }
- // 预览图片
- const previewImage = async (url: string) => {
- if (previewContainer.value) {
- previewContainer.value.innerHTML = `
- <div class="flex justify-center items-center h-full">
- <img
- src="${url}"
- alt="预览图片"
- class="max-w-full max-h-full object-contain"
- style="max-height: 600px;"
- />
- </div>
- `
- }
- }
- // PDF相关方法
- const onPdfLoaded = (pdf: any) => {
- pdfTotalPages.value = pdf.numPages
- previewLoading.value = false
- }
- const onPdfError = (error: any) => {
- console.error('PDF加载失败:', error)
- ElMessage.error('PDF文档加载失败')
- previewLoading.value = false
- }
- const pdfPrevPage = () => {
- if (pdfCurrentPage.value > 1) {
- pdfCurrentPage.value--
- }
- }
- const pdfNextPage = () => {
- if (pdfCurrentPage.value < pdfTotalPages.value) {
- pdfCurrentPage.value++
- }
- }
- const pdfZoomIn = () => {
- if (pdfScale.value < 3) {
- pdfScale.value += 0.2
- }
- }
- const pdfZoomOut = () => {
- if (pdfScale.value > 0.4) {
- pdfScale.value -= 0.2
- }
- }
- const pdfResetZoom = () => {
- pdfScale.value = 1.0
- }
- // 下载文档
- const downloadDocument = (url: string, filename: string) => {
- const fullUrl = MINIO_URL + url
- if (!url) {
- ElMessage.warning('文档地址为空,无法下载')
- return
- }
- const link = document.createElement('a')
- link.href = fullUrl
- link.download = filename
- link.target = '_blank'
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- }
- // 从预览对话框下载文档
- const downloadFromPreview = () => {
- if (!previewRelativeUrl.value) {
- ElMessage.warning('文档地址为空,无法下载')
- return
- }
- // 从标题中提取文件名,如果没有则使用默认名称
- const filename = previewTitle.value || '文档'
- downloadDocument(previewRelativeUrl.value, filename)
- }
- // 打开上传对话框
- const openUploadDialog = (contractId: string, contractNumber: string) => {
- currentContractId.value = contractId
- currentContractNumber.value = contractNumber
- fileList.value = []
- uploadVisible.value = true
- }
- // 文件上传前的检查
- const beforeUpload: UploadProps['beforeUpload'] = (file) => {
- const allowedTypes = [
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // docx
- 'application/pdf', // pdf
- 'image/jpeg', // jpg
- 'image/png', // png
- ]
- const allowedExtensions = ['.docx', '.pdf', '.jpg', '.jpeg', '.png']
- const isValidType =
- allowedTypes.includes(file.type) ||
- allowedExtensions.some((ext) => file.name.toLowerCase().endsWith(ext))
- if (!isValidType) {
- ElMessage.error('只能上传 .docx、.pdf、.jpg、.png 格式的文件!')
- return false
- }
- const isLt10M = file.size / 1024 / 1024 < 10
- if (!isLt10M) {
- ElMessage.error('文件大小不能超过 10MB!')
- return false
- }
- return true
- }
- // 文件选择变化
- const handleFileChange: UploadProps['onChange'] = (file, fileList) => {
- console.log(file, fileList)
- //移除第一个元素
- if (fileList.length > 1) {
- fileList.splice(0, 1)
- ElMessage.warning('每次只能上传一个文件,已自动替换为最新选择的文件')
- // 确保只保留最新的文件
- // fileList.value = [fileList[fileList.length - 1]]
- } else {
- // fileList = [...fileList]
- }
- }
- // 移除文件
- const handleRemove: UploadProps['onRemove'] = () => {
- fileList.value = []
- }
- // 上传签约合同
- const uploadSignContract = async () => {
- if (fileList.value.length === 0) {
- ElMessage.warning('请选择要上传的文件')
- return
- }
- const file = fileList.value[0].raw
- if (!file) {
- ElMessage.warning('文件无效,请重新选择')
- return
- }
- try {
- await ElMessageBox.confirm(
- `确定要上传合同 ${currentContractNumber.value} 的签约文档吗?`,
- '确认上传',
- {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- },
- )
- uploadLoading.value = true
- const formData = new FormData()
- formData.append('contractId', currentContractId.value)
- formData.append('file', file)
- const response = await clientPost<FormData, BaseResponse>(
- '/acontractInfo/uploadSignContract',
- formData,
- {
- headers: {
- 'Content-Type': 'multipart/form-data',
- },
- },
- )
- if (response.code === 200) {
- ElMessage.success('签约合同上传成功!')
- uploadVisible.value = false
- fileList.value = []
- await getList()
- } else {
- ElMessage.error(response.msg || '上传失败')
- }
- } catch (error: any) {
- if (error !== 'cancel') {
- ElMessage.error('上传失败:' + (error.message || '网络错误'))
- console.error('上传签约合同失败:', error)
- }
- } finally {
- uploadLoading.value = false
- }
- }
- // 关闭上传对话框
- const closeUploadDialog = () => {
- uploadVisible.value = false
- fileList.value = []
- currentContractId.value = ''
- currentContractNumber.value = ''
- }
- // 获取能获取的收据数据
- const getCanBeReceiptData = async (contractId: string) => {
- const res = await clientGet<
- {
- contractId: string
- },
- CanBeReceiptDataReponse
- >('/acontractInfo/canBeDetReceiptData', {
- params: {
- contractId,
- },
- })
- console.log(res) // res的类型就是CanBeReceiptDataReponse
- return res
- }
- // 获取收据
- const gainReceipt = async (contractId: string, dto: ReceiptDto) => {
- const res = await clientPost<ReceiptDto, BaseResponse & { data: string }>(
- '/acontractInfo/getReceipt',
- dto,
- {
- params: {
- contractId: contractId,
- },
- },
- )
- console.log(res) // res.data就是收据的url地址 前面还是要添加MINIO_URL
- return res
- }
- // 打开收据对话框
- const openReceiptDialog = async (contractId: string, contractNumber: string) => {
- currentReceiptContractId.value = contractId
- currentReceiptContractNumber.value = contractNumber
- receiptVisible.value = true
- receiptLoading.value = true
- // 重置表单
- Object.assign(receiptForm, {
- paymentUnit: '',
- paymentMethod: '转账',
- rmb: 0,
- yj: 0,
- zj: 0,
- wyf: 0,
- sf: 0,
- paymentReason: '房屋租赁费用',
- })
- try {
- // 获取可用的收据数据
- const response = await getCanBeReceiptData(contractId)
- if (response.code === 200 && response.data) {
- // 填充表单数据
- receiptForm.paymentUnit = response.data.jkdw || ''
- receiptForm.yj = response.data.yj || 0
- receiptForm.zj = response.data.zj || 0
- // 计算总金额
- calculateTotalAmount()
- } else {
- ElMessage.error(response.msg || '获取收据数据失败')
- }
- } catch (error) {
- ElMessage.error('获取收据数据失败')
- console.error('获取收据数据失败:', error)
- } finally {
- receiptLoading.value = false
- }
- }
- // 计算总金额
- const calculateTotalAmount = () => {
- receiptForm.rmb = receiptForm.yj + receiptForm.zj + receiptForm.wyf + receiptForm.sf
- }
- // 监听金额变化,自动计算总额
- const handleAmountChange = () => {
- calculateTotalAmount()
- }
- // 生成收据
- const generateReceipt = async () => {
- // 表单验证
- if (!receiptForm.paymentUnit.trim()) {
- ElMessage.warning('请填写交款单位')
- return
- }
- if (receiptForm.rmb <= 0) {
- ElMessage.warning('总金额必须大于0')
- return
- }
- try {
- await ElMessageBox.confirm(
- `确定要为合同 ${currentReceiptContractNumber.value} 生成收据吗?`,
- '确认生成收据',
- {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- },
- )
- receiptGenerating.value = true
- const response = await gainReceipt(currentReceiptContractId.value, receiptForm)
- if (response.code === 999) {
- // 收据已存在,让用户选择是否预览
- ElMessage.success('收据已存在!')
- receiptVisible.value = false
- try {
- await ElMessageBox.confirm(
- `合同 ${currentReceiptContractNumber.value} 的收据已存在,是否要预览该收据?`,
- '收据已存在',
- {
- confirmButtonText: '预览收据',
- cancelButtonText: '不预览',
- type: 'info',
- },
- )
- // 用户选择预览
- await previewDocument(response.data, `${currentReceiptContractNumber.value} - 收据`)
- } catch (error) {
- // 用户选择不预览,不做任何操作
- console.log('用户选择不预览收据', error)
- }
- } else if (response.code === 200 && response.data) {
- ElMessage.success('收据生成成功!')
- // 自动预览生成的收据
- const receiptUrl = response.data
- await previewDocument(receiptUrl, `${currentReceiptContractNumber.value} - 收据`)
- // 关闭收据对话框
- receiptVisible.value = false
- } else {
- ElMessage.error(response.msg || '收据生成失败')
- }
- } catch (error: any) {
- if (error !== 'cancel') {
- ElMessage.error('收据生成失败:' + (error.message || '网络错误'))
- console.error('收据生成失败:', error)
- }
- } finally {
- receiptGenerating.value = false
- }
- }
- // 关闭收据对话框
- const closeReceiptDialog = () => {
- receiptVisible.value = false
- currentReceiptContractId.value = ''
- currentReceiptContractNumber.value = ''
- // 重置表单
- Object.assign(receiptForm, {
- paymentUnit: '',
- paymentMethod: '转账',
- rmb: 0,
- yj: 0,
- zj: 0,
- wyf: 0,
- sf: 0,
- paymentReason: '房屋租赁费用',
- })
- }
- // 获取能获取的退据数据
- const getCanBeReturnReceiptData = async (contractId: string) => {
- const res = await clientGet<
- {
- contractId: string
- },
- CanBeReturnReceiptDataReponse
- >('/acontractInfo/canBeDetReturnReceiptData', {
- params: {
- contractId,
- },
- })
- console.log(res) // res的类型就是CanBeReturnReceiptDataReponse
- return res
- }
- // 获取退据
- const gainReturnReceipt = async (contractId: string, dto: ReturnReceiptDto) => {
- const res = await clientPost<ReturnReceiptDto, BaseResponse & { data: string }>(
- '/acontractInfo/getReturnReceipt',
- dto,
- {
- params: {
- contractId: contractId,
- },
- },
- )
- console.log(res) // res.data就是退据的url地址 前面还是要添加MINIO_URL
- return res
- }
- // 打开退据对话框
- const openReturnReceiptDialog = async (contractId: string, contractNumber: string) => {
- currentReturnReceiptContractId.value = contractId
- currentReturnReceiptContractNumber.value = contractNumber
- returnReceiptVisible.value = true
- returnReceiptLoading.value = true
- // 重置表单
- Object.assign(returnReceiptForm, {
- dw: '',
- rmb: 0,
- yj: 0,
- zj: 0,
- wyf: 0,
- zh: '',
- })
- try {
- // 获取可用的退据数据
- const response = await getCanBeReturnReceiptData(contractId)
- if (response.code === 200 && response.data) {
- // 填充表单数据
- returnReceiptForm.zh = response.data.zh || ''
- returnReceiptForm.yj = response.data.yj || 0
- // 计算总金额
- calculateReturnTotalAmount()
- } else {
- ElMessage.error(response.msg || '获取退据数据失败')
- }
- } catch (error) {
- ElMessage.error('获取退据数据失败')
- console.error('获取退据数据失败:', error)
- } finally {
- returnReceiptLoading.value = false
- }
- }
- // 计算退据总金额
- const calculateReturnTotalAmount = () => {
- returnReceiptForm.rmb = returnReceiptForm.yj + returnReceiptForm.zj + returnReceiptForm.wyf
- }
- // 监听退据金额变化,自动计算总额
- const handleReturnAmountChange = () => {
- calculateReturnTotalAmount()
- }
- // 生成退据
- const generateReturnReceipt = async () => {
- // 表单验证
- if (!returnReceiptForm.dw.trim()) {
- ElMessage.warning('请填写单位名称')
- return
- }
- if (!returnReceiptForm.zh.trim()) {
- ElMessage.warning('请填写租户名称')
- return
- }
- if (returnReceiptForm.rmb <= 0) {
- ElMessage.warning('总金额必须大于0')
- return
- }
- try {
- await ElMessageBox.confirm(
- `确定要为合同 ${currentReturnReceiptContractNumber.value} 生成退据吗?`,
- '确认生成退据',
- {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- },
- )
- returnReceiptGenerating.value = true
- const response = await gainReturnReceipt(
- currentReturnReceiptContractId.value,
- returnReceiptForm,
- )
- if (response.code === 999) {
- // 退据已存在,让用户选择是否预览
- ElMessage.success('退据已存在!')
- returnReceiptVisible.value = false
- try {
- await ElMessageBox.confirm(
- `合同 ${currentReturnReceiptContractNumber.value} 的退据已存在,是否要预览该退据?`,
- '退据已存在',
- {
- confirmButtonText: '预览退据',
- cancelButtonText: '不预览',
- type: 'info',
- },
- )
- // 用户选择预览
- await previewDocument(response.data, `${currentReturnReceiptContractNumber.value} - 退据`)
- } catch (error) {
- // 用户选择不预览,不做任何操作
- console.log('用户选择不预览退据', error)
- }
- } else if (response.code === 200 && response.data) {
- ElMessage.success('退据生成成功!')
- // 自动预览生成的退据
- const returnReceiptUrl = response.data
- await previewDocument(returnReceiptUrl, `${currentReturnReceiptContractNumber.value} - 退据`)
- // 关闭退据对话框
- returnReceiptVisible.value = false
- } else {
- ElMessage.error(response.msg || '退据生成失败')
- }
- } catch (error: any) {
- if (error !== 'cancel') {
- ElMessage.error('退据生成失败:' + (error.message || '网络错误'))
- console.error('退据生成失败:', error)
- }
- } finally {
- returnReceiptGenerating.value = false
- }
- }
- // 关闭退据对话框
- const closeReturnReceiptDialog = () => {
- returnReceiptVisible.value = false
- currentReturnReceiptContractId.value = ''
- currentReturnReceiptContractNumber.value = ''
- // 重置表单
- Object.assign(returnReceiptForm, {
- dw: '',
- rmb: 0,
- yj: 0,
- zj: 0,
- wyf: 0,
- zh: '',
- })
- }
- // 初始化
- onMounted(() => {
- getList()
- })
- </script>
- <template>
- <div class="p-6 bg-gray-50 min-h-screen font-sans">
- <!-- 页面标题 -->
- <div class="mb-6">
- <h1 class="text-2xl font-bold text-gray-800 mb-2">租房合同管理</h1>
- <p class="text-gray-600">管理和查看所有租房合同信息</p>
- </div>
- <!-- 搜索区域 -->
- <ElCard class="mb-6" shadow="never">
- <ElForm :model="searchForm" inline class="bg-gray-50 p-5 rounded-lg">
- <ElFormItem label="合同编号">
- <ElInput
- v-model="searchForm.contractNumber"
- placeholder="请输入合同编号"
- clearable
- class="w-50"
- @keyup.enter="handleSearch"
- />
- </ElFormItem>
- <ElFormItem>
- <ElSpace>
- <ElButton type="primary" :icon="Search" @click="handleSearch">搜索</ElButton>
- <ElButton :icon="RefreshCw" @click="handleReset">重置</ElButton>
- </ElSpace>
- </ElFormItem>
- </ElForm>
- </ElCard>
- <!-- 表格区域 -->
- <ElCard shadow="never" class="rounded-lg overflow-hidden">
- <ElTable
- :data="tableData"
- :loading="loading"
- stripe
- border
- style="width: 100%"
- empty-text="暂无数据"
- class="rounded-lg overflow-hidden"
- >
- <ElTableColumn prop="contractNumber" label="合同编号" width="160" />
- <ElTableColumn prop="contractDate" label="签约日期" width="140" />
- <ElTableColumn prop="contractTime" label="合同期限" width="120" />
- <ElTableColumn prop="contractExpirationDate" label="到期日期" width="140" />
- <ElTableColumn prop="contractDeposit" label="押金(元)" width="140">
- <template #default="{ row }">
- <span class="text-orange-600 font-medium">
- ¥{{ row.contractDeposit.toLocaleString() }}
- </span>
- </template>
- </ElTableColumn>
- <ElTableColumn prop="contractStatus" label="合同状态" width="120">
- <template #default="{ row }">
- <ElTag :type="getStatusType(row.contractStatus)" size="small">
- {{ row.contractStatus }}
- </ElTag>
- </template>
- </ElTableColumn>
- <ElTableColumn label="合同文档" min-width="380">
- <template #default="{ row }">
- <div class="flex flex-col gap-4">
- <div v-if="row.originalContractUrl" class="flex items-center gap-2">
- <FileText class="w-4 h-4 text-blue-500" />
- <span class="text-sm text-gray-600">原合同:</span>
- <ElButton
- type="primary"
- size="small"
- text
- :icon="Eye"
- @click="
- previewDocument(row.originalContractUrl, `${row.contractNumber} - 原合同`)
- "
- >
- 预览
- </ElButton>
- <ElButton
- type="success"
- size="small"
- text
- :icon="Download"
- @click="downloadDocument(row.originalContractUrl, `${row.contractNumber}_原合同`)"
- >
- 下载
- </ElButton>
- </div>
- <div v-else class="text-sm text-gray-400">原合同: 暂无</div>
- <div v-if="row.signContractUrl" class="flex items-center gap-2">
- <FileText class="w-4 h-4 text-green-500" />
- <span class="text-sm text-gray-600">签订合同:</span>
- <ElButton
- type="primary"
- size="small"
- text
- :icon="Eye"
- @click="previewDocument(row.signContractUrl, `${row.contractNumber} - 签订合同`)"
- >
- 预览
- </ElButton>
- <ElButton
- type="success"
- size="small"
- text
- :icon="Download"
- @click="downloadDocument(row.signContractUrl, `${row.contractNumber}_签订合同`)"
- >
- 下载
- </ElButton>
- <ElButton
- type="warning"
- size="small"
- text
- :icon="Upload"
- @click="openUploadDialog(row.id, row.contractNumber)"
- >
- 重新上传
- </ElButton>
- </div>
- <div v-else class="flex items-center gap-2">
- <span class="text-sm text-gray-400">签订合同: 暂无</span>
- <ElButton
- type="warning"
- size="small"
- text
- :icon="Upload"
- @click="openUploadDialog(row.id, row.contractNumber)"
- >
- 上传
- </ElButton>
- </div>
- </div>
- </template>
- </ElTableColumn>
- <ElTableColumn label="操作" width="160" fixed="right">
- <template #default="{ row }">
- <div class="flex flex-col gap-1">
- <div>
- <ElButton
- type="primary"
- size="small"
- text
- :icon="Receipt"
- @click="openReceiptDialog(row.id, row.contractNumber)"
- >
- 获取收据
- </ElButton>
- </div>
- <div>
- <ElButton
- type="warning"
- size="small"
- text
- :icon="RotateCcw"
- @click="openReturnReceiptDialog(row.id, row.contractNumber)"
- >
- 获取退据
- </ElButton>
- </div>
- </div>
- </template>
- </ElTableColumn>
- <ElTableColumn prop="createTime" label="创建时间" width="180" />
- <ElTableColumn prop="updateTime" label="更新时间" width="180" />
- </ElTable>
- <!-- 分页 -->
- <div class="flex justify-center mt-6">
- <ElPagination
- v-model:current-page="currentPage"
- v-model:page-size="pageSize"
- :page-sizes="[10, 20, 50, 100]"
- :total="total"
- layout="total, sizes, prev, pager, next, jumper"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- />
- </div>
- </ElCard>
- <!-- 文档预览对话框 -->
- <ElDialog
- v-model="previewVisible"
- :title="previewTitle"
- width="70%"
- top="5vh"
- destroy-on-close
- class="preview-dialog"
- >
- <!-- 预览对话框标题栏添加下载按钮 -->
- <template #header="{ titleId, titleClass }">
- <div class="flex items-center justify-between w-full">
- <span :id="titleId" :class="titleClass">{{ previewTitle }}</span>
- <ElButton
- type="primary"
- :icon="Download"
- @click="downloadFromPreview"
- :disabled="!previewRelativeUrl"
- >
- 下载文档
- </ElButton>
- </div>
- </template>
- <div
- v-loading="previewLoading"
- element-loading-text="正在加载文档..."
- class="border border-gray-200 rounded bg-white overflow-hidden"
- style="height: 75vh"
- >
- <!-- PDF预览 -->
- <div v-if="previewFileType === FileType.PDF" class="h-full flex flex-col">
- <!-- PDF工具栏 -->
- <div class="flex items-center justify-between p-3 bg-gray-50 border-b border-gray-200">
- <div class="flex items-center gap-2">
- <ElButton size="small" @click="pdfPrevPage" :disabled="pdfCurrentPage <= 1">
- 上一页
- </ElButton>
- <span class="text-sm text-gray-600">
- {{ pdfCurrentPage }} / {{ pdfTotalPages }}
- </span>
- <ElButton
- size="small"
- @click="pdfNextPage"
- :disabled="pdfCurrentPage >= pdfTotalPages"
- >
- 下一页
- </ElButton>
- </div>
- <div class="flex items-center gap-2">
- <ElButton size="small" @click="pdfZoomOut" :disabled="pdfScale <= 0.4">
- 缩小
- </ElButton>
- <span class="text-sm text-gray-600 min-w-12 text-center">
- {{ Math.round(pdfScale * 100) }}%
- </span>
- <ElButton size="small" @click="pdfZoomIn" :disabled="pdfScale >= 3"> 放大 </ElButton>
- <ElButton size="small" @click="pdfResetZoom"> 重置 </ElButton>
- </div>
- </div>
- <!-- PDF内容 -->
- <div class="flex-1 overflow-auto p-4 bg-gray-100 flex justify-center">
- <VuePdfEmbed
- :source="pdfSource"
- :page="pdfCurrentPage"
- :scale="pdfScale"
- class="shadow-lg w-50%"
- @loaded="onPdfLoaded"
- @loading-failed="onPdfError"
- />
- </div>
- </div>
- <!-- 其他格式预览 -->
- <div v-else class="h-full relative">
- <div ref="previewContainer" class="p-5 leading-relaxed overflow-auto h-full"></div>
- </div>
- </div>
- </ElDialog>
- <!-- 上传签约合同对话框 -->
- <ElDialog
- v-model="uploadVisible"
- :title="`上传签约合同 - ${currentContractNumber}`"
- width="500px"
- :before-close="closeUploadDialog"
- destroy-on-close
- >
- <div class="py-5">
- <!-- 文件限制提示 -->
- <div class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
- <div class="flex items-start gap-2">
- <AlertCircle class="w-4 h-4 text-blue-500 mt-0.5 flex-shrink-0" />
- <div class="text-sm">
- <p class="text-blue-800 font-medium mb-1">上传说明:</p>
- <ul class="text-blue-700 space-y-1">
- <li>• 每次只能上传一个文件</li>
- <li>• 支持格式:.docx、.pdf、.jpg、.png</li>
- <li>• 文件大小不超过 10MB</li>
- <li>• 上传新文件会替换已选择的文件</li>
- </ul>
- </div>
- </div>
- </div>
- <ElUpload
- v-model:file-list="fileList"
- class="upload-demo"
- drag
- :auto-upload="false"
- :before-upload="beforeUpload"
- :on-change="handleFileChange"
- :on-remove="handleRemove"
- accept=".docx,.pdf,.jpg,.jpeg,.png,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/pdf,image/jpeg,image/png"
- >
- <div class="py-10 px-5 text-center">
- <Plus class="w-12 h-12 text-gray-400 mx-auto mb-4" />
- <div class="text-gray-600 text-base mb-2">
- 将文件拖到此处,或<em class="text-blue-500 not-italic">点击上传</em>
- </div>
- <div class="text-gray-400 text-sm">支持.docx、.pdf、.jpg、.png格式,且不超过10MB</div>
- <div class="text-orange-500 text-xs mt-2 font-medium">
- ⚠️ 限制上传一个文件,选择新文件将替换当前文件
- </div>
- </div>
- </ElUpload>
- </div>
- <template #footer>
- <div class="text-right">
- <ElButton @click="closeUploadDialog">取消</ElButton>
- <ElButton
- type="primary"
- :loading="uploadLoading"
- :disabled="fileList.length === 0"
- @click="uploadSignContract"
- >
- {{ uploadLoading ? '上传中...' : '确定上传' }}
- </ElButton>
- </div>
- </template>
- </ElDialog>
- <!-- 收据生成对话框 -->
- <ElDialog
- v-model="receiptVisible"
- :title="`生成收据 - ${currentReceiptContractNumber}`"
- width="600px"
- :before-close="closeReceiptDialog"
- destroy-on-close
- >
- <div v-loading="receiptLoading" element-loading-text="正在获取收据数据...">
- <ElForm :model="receiptForm" label-width="100px" class="py-4">
- <div class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
- <div class="flex items-start gap-2">
- <AlertCircle class="w-4 h-4 text-blue-500 mt-0.5 flex-shrink-0" />
- <div class="text-sm">
- <p class="text-blue-800 font-medium mb-1">收据说明:</p>
- <ul class="text-blue-700 space-y-1">
- <li>• 系统已自动填充合同相关的押金和租金信息</li>
- <li>• 请根据实际情况填写物业费、水费等其他费用</li>
- <li>• 总金额会根据各项费用自动计算</li>
- <li>• 生成后的收据可以预览和下载</li>
- </ul>
- </div>
- </div>
- </div>
- <ElFormItem label="交款单位" required>
- <ElInput v-model="receiptForm.paymentUnit" placeholder="请输入交款单位名称" clearable />
- </ElFormItem>
- <ElFormItem label="付款方式">
- <ElSelect
- v-model="receiptForm.paymentMethod"
- placeholder="请选择付款方式"
- style="width: 100%"
- >
- <ElOption
- v-for="item in paymentMethods"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </ElSelect>
- </ElFormItem>
- <ElFormItem label="付款事由">
- <ElSelect
- v-model="receiptForm.paymentReason"
- placeholder="请选择付款事由"
- style="width: 100%"
- >
- <ElOption
- v-for="item in paymentReasons"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </ElSelect>
- </ElFormItem>
- <div class="grid grid-cols-2 gap-4">
- <ElFormItem label="押金(元)">
- <ElInputNumber
- v-model="receiptForm.yj"
- :min="0"
- :precision="2"
- style="width: 100%"
- @change="handleAmountChange"
- />
- </ElFormItem>
- <ElFormItem label="租金(元)">
- <ElInputNumber
- v-model="receiptForm.zj"
- :min="0"
- :precision="2"
- style="width: 100%"
- @change="handleAmountChange"
- />
- </ElFormItem>
- <ElFormItem label="物业费(元)">
- <ElInputNumber
- v-model="receiptForm.wyf"
- :min="0"
- :precision="2"
- style="width: 100%"
- @change="handleAmountChange"
- />
- </ElFormItem>
- <ElFormItem label="水费(元)">
- <ElInputNumber
- v-model="receiptForm.sf"
- :min="0"
- :precision="2"
- style="width: 100%"
- @change="handleAmountChange"
- />
- </ElFormItem>
- </div>
- <ElFormItem label="总金额(元)">
- <ElInputNumber
- v-model="receiptForm.rmb"
- :min="0"
- :precision="2"
- style="width: 100%"
- readonly
- class="total-amount"
- />
- <div class="text-sm text-gray-500 mt-1">总金额 = 押金 + 租金 + 物业费 + 水费</div>
- </ElFormItem>
- </ElForm>
- </div>
- <template #footer>
- <div class="text-right">
- <ElButton @click="closeReceiptDialog">取消</ElButton>
- <ElButton
- type="primary"
- :loading="receiptGenerating"
- :disabled="receiptLoading || !receiptForm.paymentUnit.trim() || receiptForm.rmb <= 0"
- @click="generateReceipt"
- >
- {{ receiptGenerating ? '生成中...' : '生成收据' }}
- </ElButton>
- </div>
- </template>
- </ElDialog>
- <!-- 退据生成对话框 -->
- <ElDialog
- v-model="returnReceiptVisible"
- :title="`生成退据 - ${currentReturnReceiptContractNumber}`"
- width="600px"
- :before-close="closeReturnReceiptDialog"
- destroy-on-close
- >
- <div v-loading="returnReceiptLoading" element-loading-text="正在获取退据数据...">
- <ElForm :model="returnReceiptForm" label-width="100px" class="py-4">
- <div class="mb-4 p-3 bg-orange-50 border border-orange-200 rounded-lg">
- <div class="flex items-start gap-2">
- <AlertCircle class="w-4 h-4 text-orange-500 mt-0.5 flex-shrink-0" />
- <div class="text-sm">
- <p class="text-orange-800 font-medium mb-1">退据说明:</p>
- <ul class="text-orange-700 space-y-1">
- <li>• 系统已自动填充合同相关的租户和押金信息</li>
- <li>• 请根据实际情况填写退还的租金、物业费等费用</li>
- <li>• 总金额会根据各项费用自动计算</li>
- <li>• 生成后的退据可以预览和下载</li>
- </ul>
- </div>
- </div>
- </div>
- <ElFormItem label="单位名称" required>
- <ElInput v-model="returnReceiptForm.dw" placeholder="请输入单位名称" clearable />
- </ElFormItem>
- <ElFormItem label="租户名称" required>
- <ElInput v-model="returnReceiptForm.zh" placeholder="请输入租户名称" clearable />
- </ElFormItem>
- <div class="grid grid-cols-2 gap-4">
- <ElFormItem label="押金(元)">
- <ElInputNumber
- v-model="returnReceiptForm.yj"
- :min="0"
- :precision="2"
- style="width: 100%"
- @change="handleReturnAmountChange"
- />
- </ElFormItem>
- <ElFormItem label="租金(元)">
- <ElInputNumber
- v-model="returnReceiptForm.zj"
- :min="0"
- :precision="2"
- style="width: 100%"
- @change="handleReturnAmountChange"
- />
- </ElFormItem>
- <ElFormItem label="物业费(元)" class="col-span-2">
- <ElInputNumber
- v-model="returnReceiptForm.wyf"
- :min="0"
- :precision="2"
- style="width: 100%"
- @change="handleReturnAmountChange"
- />
- </ElFormItem>
- </div>
- <ElFormItem label="总金额(元)">
- <ElInputNumber
- v-model="returnReceiptForm.rmb"
- :min="0"
- :precision="2"
- style="width: 100%"
- readonly
- class="total-amount"
- />
- <div class="text-sm text-gray-500 mt-1">总金额 = 押金 + 租金 + 物业费</div>
- </ElFormItem>
- </ElForm>
- </div>
- <template #footer>
- <div class="text-right">
- <ElButton @click="closeReturnReceiptDialog">取消</ElButton>
- <ElButton
- type="primary"
- :loading="returnReceiptGenerating"
- :disabled="
- returnReceiptLoading ||
- !returnReceiptForm.dw.trim() ||
- !returnReceiptForm.zh.trim() ||
- returnReceiptForm.rmb <= 0
- "
- @click="generateReturnReceipt"
- >
- {{ returnReceiptGenerating ? '生成中...' : '生成退据' }}
- </ElButton>
- </div>
- </template>
- </ElDialog>
- </div>
- </template>
- <style scoped>
- /* Element Plus 样式覆盖 */
- :deep(.el-table th) {
- background-color: #f8f9fa;
- color: #495057;
- font-weight: 600;
- }
- :deep(.el-table td) {
- padding: 12px 0;
- }
- :deep(.el-pagination) {
- margin-top: 20px;
- }
- /* DOCX 预览样式 */
- :deep(.docx-container p) {
- margin: 8px 0;
- }
- :deep(.docx-container table) {
- border-collapse: collapse;
- width: 100%;
- margin: 16px 0;
- }
- :deep(.docx-container table td),
- :deep(.docx-container table th) {
- border: 1px solid #ddd;
- padding: 8px;
- text-align: left;
- }
- :deep(.docx-container table th) {
- background-color: #f2f2f2;
- font-weight: bold;
- }
- /* 上传组件样式 */
- :deep(.el-upload-dragger) {
- border: 2px dashed #d9d9d9;
- border-radius: 6px;
- width: 100%;
- height: auto;
- text-align: center;
- background: #fafafa;
- transition: border-color 0.3s;
- }
- :deep(.el-upload-dragger:hover) {
- border-color: #409eff;
- }
- :deep(.el-upload-dragger.is-dragover) {
- border-color: #409eff;
- background-color: rgba(64, 158, 255, 0.06);
- }
- :deep(.el-upload__text em) {
- color: #409eff;
- font-style: normal;
- }
- /* PDF预览样式 */
- :deep(.vue-pdf-embed) {
- border: 1px solid #e0e0e0;
- border-radius: 4px;
- }
- /* 收据表单样式 */
- :deep(.total-amount .el-input__inner) {
- background-color: #f5f7fa;
- font-weight: 600;
- color: #409eff;
- }
- /* 预览对话框样式优化 */
- :deep(.preview-dialog .el-dialog__header) {
- padding: 16px 20px;
- border-bottom: 1px solid #ebeef5;
- }
- :deep(.preview-dialog .el-dialog__body) {
- padding: 0;
- }
- </style>
|