|
@@ -0,0 +1,432 @@
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { nextTick, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
+import {
|
|
|
|
|
+ ElButton,
|
|
|
|
|
+ ElCard,
|
|
|
|
|
+ ElDialog,
|
|
|
|
|
+ ElForm,
|
|
|
|
|
+ ElFormItem,
|
|
|
|
|
+ ElInput,
|
|
|
|
|
+ ElMessage,
|
|
|
|
|
+ ElPagination,
|
|
|
|
|
+ ElSpace,
|
|
|
|
|
+ ElTable,
|
|
|
|
|
+ ElTableColumn,
|
|
|
|
|
+ ElTag,
|
|
|
|
|
+} from 'element-plus'
|
|
|
|
|
+import { Download, Eye, FileText, RefreshCwIcon, Search } from 'lucide-vue-next'
|
|
|
|
|
+import { renderAsync } from 'docx-preview'
|
|
|
|
|
+import { clientGet } 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 BaseResponse {
|
|
|
|
|
+ code: number
|
|
|
|
|
+ msg: string
|
|
|
|
|
+ data?: object
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface PageType<T> {
|
|
|
|
|
+ countId?: object
|
|
|
|
|
+ current?: number
|
|
|
|
|
+ pages: number
|
|
|
|
|
+ records: T[]
|
|
|
|
|
+ size: number
|
|
|
|
|
+ total: number
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 响应式数据
|
|
|
|
|
+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 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) => {
|
|
|
|
|
+ url = MINIO_URL + url
|
|
|
|
|
+ if (!url) {
|
|
|
|
|
+ ElMessage.warning('文档地址为空,无法预览')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ previewTitle.value = title
|
|
|
|
|
+ previewVisible.value = true
|
|
|
|
|
+ previewLoading.value = true
|
|
|
|
|
+
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 模拟获取docx文件
|
|
|
|
|
+ const response = await fetch(url)
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('文档加载失败')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const arrayBuffer = await response.arrayBuffer()
|
|
|
|
|
+
|
|
|
|
|
+ if (previewContainer.value) {
|
|
|
|
|
+ // 清空容器
|
|
|
|
|
+ previewContainer.value.innerHTML = ''
|
|
|
|
|
+
|
|
|
|
|
+ // 使用docx-preview渲染文档
|
|
|
|
|
+ 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,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ ElMessage.error('文档预览失败: ' + (error as Error).message)
|
|
|
|
|
+ console.error('文档预览失败:', error)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ previewLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 下载文档
|
|
|
|
|
+const downloadDocument = (url: string, filename: string) => {
|
|
|
|
|
+ if (!url) {
|
|
|
|
|
+ ElMessage.warning('文档地址为空,无法下载')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const link = document.createElement('a')
|
|
|
|
|
+ link.href = url
|
|
|
|
|
+ link.download = filename
|
|
|
|
|
+ link.target = '_blank'
|
|
|
|
|
+ document.body.appendChild(link)
|
|
|
|
|
+ link.click()
|
|
|
|
|
+ document.body.removeChild(link)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 初始化
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ getList()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="contract-management p-6 bg-gray-50 min-h-screen">
|
|
|
|
|
+ <!-- 页面标题 -->
|
|
|
|
|
+ <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="search-form">
|
|
|
|
|
+ <ElFormItem label="合同编号">
|
|
|
|
|
+ <ElInput
|
|
|
|
|
+ v-model="searchForm.contractNumber"
|
|
|
|
|
+ placeholder="请输入合同编号"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ class="w-200px"
|
|
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ </ElFormItem>
|
|
|
|
|
+
|
|
|
|
|
+ <ElFormItem>
|
|
|
|
|
+ <ElSpace>
|
|
|
|
|
+ <ElButton type="primary" :icon="Search" @click="handleSearch"> 搜索 </ElButton>
|
|
|
|
|
+ <ElButton :icon="RefreshCwIcon" @click="handleReset"> 重置 </ElButton>
|
|
|
|
|
+ </ElSpace>
|
|
|
|
|
+ </ElFormItem>
|
|
|
|
|
+ </ElForm>
|
|
|
|
|
+ </ElCard>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 表格区域 -->
|
|
|
|
|
+ <ElCard shadow="never">
|
|
|
|
|
+ <ElTable
|
|
|
|
|
+ :data="tableData"
|
|
|
|
|
+ :loading="loading"
|
|
|
|
|
+ stripe
|
|
|
|
|
+ border
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ empty-text="暂无数据"
|
|
|
|
|
+ >
|
|
|
|
|
+ <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="240">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <ElSpace direction="vertical" size="small">
|
|
|
|
|
+ <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}_原合同.docx`)
|
|
|
|
|
+ "
|
|
|
|
|
+ >
|
|
|
|
|
+ 下载
|
|
|
|
|
+ </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}_签订合同.docx`)
|
|
|
|
|
+ "
|
|
|
|
|
+ >
|
|
|
|
|
+ 下载
|
|
|
|
|
+ </ElButton>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else class="text-sm text-gray-400">签订合同: 暂无</div>
|
|
|
|
|
+ </ElSpace>
|
|
|
|
|
+ </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="60%" top="5vh" destroy-on-close>
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-loading="previewLoading"
|
|
|
|
|
+ element-loading-text="正在加载文档..."
|
|
|
|
|
+ class="preview-container"
|
|
|
|
|
+ style="height: 70vh; overflow-y: auto"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div ref="previewContainer" class="docx-container"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </ElDialog>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.contract-management {
|
|
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.search-form {
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-table) {
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+: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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-container {
|
|
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.docx-container) {
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+: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;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|