zfhtgl.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. <script setup lang="ts">
  2. import { nextTick, onMounted, reactive, ref } from 'vue'
  3. import {
  4. ElButton,
  5. ElCard,
  6. ElDialog,
  7. ElForm,
  8. ElFormItem,
  9. ElInput,
  10. ElMessage,
  11. ElPagination,
  12. ElSpace,
  13. ElTable,
  14. ElTableColumn,
  15. ElTag,
  16. } from 'element-plus'
  17. import { Download, Eye, FileText, RefreshCwIcon, Search } from 'lucide-vue-next'
  18. import { renderAsync } from 'docx-preview'
  19. import { clientGet } from '@/utils/request.ts'
  20. // 接口定义
  21. export interface AContractInfo {
  22. id: string
  23. simplifiedHouseId: string
  24. contractNumber: string
  25. contractDate: string
  26. contractTime: string
  27. contractExpirationDate: string
  28. contractDeposit: number
  29. contractStatus: string
  30. originalContractUrl: string
  31. signContractUrl: string
  32. createTime: string
  33. updateTime: string
  34. }
  35. interface AContractInfoListResponse extends BaseResponse {
  36. data: PageType<AContractInfo>
  37. }
  38. interface BaseResponse {
  39. code: number
  40. msg: string
  41. data?: object
  42. }
  43. interface PageType<T> {
  44. countId?: object
  45. current?: number
  46. pages: number
  47. records: T[]
  48. size: number
  49. total: number
  50. }
  51. // 响应式数据
  52. const loading = ref(false)
  53. const tableData = ref<AContractInfo[]>([])
  54. const total = ref(0)
  55. const currentPage = ref(1)
  56. const pageSize = ref(10)
  57. const MINIO_URL = import.meta.env.VITE_MINIO_BASE_URL
  58. // 搜索表单
  59. const searchForm = reactive({
  60. contractNumber: '',
  61. })
  62. // 预览相关
  63. const previewVisible = ref(false)
  64. const previewLoading = ref(false)
  65. const previewTitle = ref('')
  66. const previewContainer = ref<HTMLElement>()
  67. // 获取合同状态标签类型
  68. const getStatusType = (status: string) => {
  69. const statusMap: Record<string, string> = {
  70. 有效: 'success',
  71. 已退租: 'warning',
  72. }
  73. return statusMap[status] || 'info'
  74. }
  75. // 获取列表数据
  76. const getList = async () => {
  77. loading.value = true
  78. try {
  79. const response = await clientGet<
  80. {
  81. pageNum: number
  82. pageSize: number
  83. contractNumber: string
  84. },
  85. AContractInfoListResponse
  86. >('/acontractInfo/findByPage', {
  87. params: {
  88. pageNum: currentPage.value,
  89. pageSize: pageSize.value,
  90. contractNumber: searchForm.contractNumber,
  91. },
  92. })
  93. if (response.code === 200 && response.data) {
  94. tableData.value = response.data.records
  95. total.value = response.data.total
  96. currentPage.value = response.data.current || 1
  97. } else {
  98. ElMessage.error(response.msg || '获取数据失败')
  99. }
  100. } catch (error) {
  101. ElMessage.error('网络请求失败')
  102. console.error('获取合同列表失败:', error)
  103. } finally {
  104. loading.value = false
  105. }
  106. }
  107. // 搜索
  108. const handleSearch = () => {
  109. currentPage.value = 1
  110. getList()
  111. }
  112. // 重置搜索
  113. const handleReset = () => {
  114. searchForm.contractNumber = ''
  115. currentPage.value = 1
  116. getList()
  117. }
  118. // 分页变化
  119. const handleCurrentChange = (page: number) => {
  120. currentPage.value = page
  121. getList()
  122. }
  123. const handleSizeChange = (size: number) => {
  124. pageSize.value = size
  125. currentPage.value = 1
  126. getList()
  127. }
  128. // 预览文档
  129. const previewDocument = async (url: string, title: string) => {
  130. url = MINIO_URL + url
  131. if (!url) {
  132. ElMessage.warning('文档地址为空,无法预览')
  133. return
  134. }
  135. previewTitle.value = title
  136. previewVisible.value = true
  137. previewLoading.value = true
  138. await nextTick()
  139. try {
  140. // 模拟获取docx文件
  141. const response = await fetch(url)
  142. if (!response.ok) {
  143. throw new Error('文档加载失败')
  144. }
  145. const arrayBuffer = await response.arrayBuffer()
  146. if (previewContainer.value) {
  147. // 清空容器
  148. previewContainer.value.innerHTML = ''
  149. // 使用docx-preview渲染文档
  150. await renderAsync(arrayBuffer, previewContainer.value, undefined, {
  151. className: 'docx-preview',
  152. inWrapper: true,
  153. ignoreWidth: false,
  154. ignoreHeight: false,
  155. ignoreFonts: false,
  156. breakPages: true,
  157. ignoreLastRenderedPageBreak: true,
  158. experimental: false,
  159. trimXmlDeclaration: true,
  160. useBase64URL: false,
  161. renderChanges: false,
  162. renderComments: false,
  163. renderEndnotes: true,
  164. renderFootnotes: true,
  165. renderFooters: true,
  166. renderHeaders: true,
  167. })
  168. }
  169. } catch (error) {
  170. ElMessage.error('文档预览失败: ' + (error as Error).message)
  171. console.error('文档预览失败:', error)
  172. } finally {
  173. previewLoading.value = false
  174. }
  175. }
  176. // 下载文档
  177. const downloadDocument = (url: string, filename: string) => {
  178. if (!url) {
  179. ElMessage.warning('文档地址为空,无法下载')
  180. return
  181. }
  182. const link = document.createElement('a')
  183. link.href = url
  184. link.download = filename
  185. link.target = '_blank'
  186. document.body.appendChild(link)
  187. link.click()
  188. document.body.removeChild(link)
  189. }
  190. // 初始化
  191. onMounted(() => {
  192. getList()
  193. })
  194. </script>
  195. <template>
  196. <div class="contract-management p-6 bg-gray-50 min-h-screen">
  197. <!-- 页面标题 -->
  198. <div class="mb-6">
  199. <h1 class="text-2xl font-bold text-gray-800 mb-2">租房合同管理</h1>
  200. <p class="text-gray-600">管理和查看所有租房合同信息</p>
  201. </div>
  202. <!-- 搜索区域 -->
  203. <ElCard class="mb-6" shadow="never">
  204. <ElForm :model="searchForm" inline class="search-form">
  205. <ElFormItem label="合同编号">
  206. <ElInput
  207. v-model="searchForm.contractNumber"
  208. placeholder="请输入合同编号"
  209. clearable
  210. class="w-200px"
  211. @keyup.enter="handleSearch"
  212. />
  213. </ElFormItem>
  214. <ElFormItem>
  215. <ElSpace>
  216. <ElButton type="primary" :icon="Search" @click="handleSearch"> 搜索 </ElButton>
  217. <ElButton :icon="RefreshCwIcon" @click="handleReset"> 重置 </ElButton>
  218. </ElSpace>
  219. </ElFormItem>
  220. </ElForm>
  221. </ElCard>
  222. <!-- 表格区域 -->
  223. <ElCard shadow="never">
  224. <ElTable
  225. :data="tableData"
  226. :loading="loading"
  227. stripe
  228. border
  229. style="width: 100%"
  230. empty-text="暂无数据"
  231. >
  232. <ElTableColumn prop="contractNumber" label="合同编号" width="160" />
  233. <ElTableColumn prop="contractDate" label="签约日期" width="140" />
  234. <ElTableColumn prop="contractTime" label="合同期限" width="120" />
  235. <ElTableColumn prop="contractExpirationDate" label="到期日期" width="140" />
  236. <ElTableColumn prop="contractDeposit" label="押金(元)" width="140">
  237. <template #default="{ row }">
  238. <span class="text-orange-600 font-medium"
  239. >¥{{ row.contractDeposit.toLocaleString() }}</span
  240. >
  241. </template>
  242. </ElTableColumn>
  243. <ElTableColumn prop="contractStatus" label="合同状态" width="120">
  244. <template #default="{ row }">
  245. <ElTag :type="getStatusType(row.contractStatus)" size="small">
  246. {{ row.contractStatus }}
  247. </ElTag>
  248. </template>
  249. </ElTableColumn>
  250. <ElTableColumn label="合同文档" min-width="240">
  251. <template #default="{ row }">
  252. <ElSpace direction="vertical" size="small">
  253. <div v-if="row.originalContractUrl" class="flex items-center gap-2">
  254. <FileText class="w-4 h-4 text-blue-500" />
  255. <span class="text-sm text-gray-600">原合同:</span>
  256. <ElButton
  257. type="primary"
  258. size="small"
  259. text
  260. :icon="Eye"
  261. @click="
  262. previewDocument(row.originalContractUrl, `${row.contractNumber} - 原合同`)
  263. "
  264. >
  265. 预览
  266. </ElButton>
  267. <ElButton
  268. type="success"
  269. size="small"
  270. text
  271. :icon="Download"
  272. @click="
  273. downloadDocument(row.originalContractUrl, `${row.contractNumber}_原合同.docx`)
  274. "
  275. >
  276. 下载
  277. </ElButton>
  278. </div>
  279. <div v-else class="text-sm text-gray-400">原合同: 暂无</div>
  280. <div v-if="row.signContractUrl" class="flex items-center gap-2">
  281. <FileText class="w-4 h-4 text-green-500" />
  282. <span class="text-sm text-gray-600">签订合同:</span>
  283. <ElButton
  284. type="primary"
  285. size="small"
  286. text
  287. :icon="Eye"
  288. @click="previewDocument(row.signContractUrl, `${row.contractNumber} - 签订合同`)"
  289. >
  290. 预览
  291. </ElButton>
  292. <ElButton
  293. type="success"
  294. size="small"
  295. text
  296. :icon="Download"
  297. @click="
  298. downloadDocument(row.signContractUrl, `${row.contractNumber}_签订合同.docx`)
  299. "
  300. >
  301. 下载
  302. </ElButton>
  303. </div>
  304. <div v-else class="text-sm text-gray-400">签订合同: 暂无</div>
  305. </ElSpace>
  306. </template>
  307. </ElTableColumn>
  308. <ElTableColumn prop="createTime" label="创建时间" width="180" />
  309. <ElTableColumn prop="updateTime" label="更新时间" width="180" />
  310. </ElTable>
  311. <!-- 分页 -->
  312. <div class="flex justify-center mt-6">
  313. <ElPagination
  314. v-model:current-page="currentPage"
  315. v-model:page-size="pageSize"
  316. :page-sizes="[10, 20, 50, 100]"
  317. :total="total"
  318. layout="total, sizes, prev, pager, next, jumper"
  319. @size-change="handleSizeChange"
  320. @current-change="handleCurrentChange"
  321. />
  322. </div>
  323. </ElCard>
  324. <!-- 文档预览对话框 -->
  325. <ElDialog v-model="previewVisible" :title="previewTitle" width="60%" top="5vh" destroy-on-close>
  326. <div
  327. v-loading="previewLoading"
  328. element-loading-text="正在加载文档..."
  329. class="preview-container"
  330. style="height: 70vh; overflow-y: auto"
  331. >
  332. <div ref="previewContainer" class="docx-container"></div>
  333. </div>
  334. </ElDialog>
  335. </div>
  336. </template>
  337. <style scoped>
  338. .contract-management {
  339. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  340. }
  341. .search-form {
  342. background: #fafafa;
  343. padding: 20px;
  344. border-radius: 8px;
  345. }
  346. :deep(.el-table) {
  347. border-radius: 8px;
  348. overflow: hidden;
  349. }
  350. :deep(.el-table th) {
  351. background-color: #f8f9fa;
  352. color: #495057;
  353. font-weight: 600;
  354. }
  355. :deep(.el-table td) {
  356. padding: 12px 0;
  357. }
  358. :deep(.el-pagination) {
  359. margin-top: 20px;
  360. }
  361. .preview-container {
  362. border: 1px solid #e0e0e0;
  363. border-radius: 4px;
  364. background: white;
  365. }
  366. :deep(.docx-container) {
  367. padding: 20px;
  368. line-height: 1.6;
  369. }
  370. :deep(.docx-container p) {
  371. margin: 8px 0;
  372. }
  373. :deep(.docx-container table) {
  374. border-collapse: collapse;
  375. width: 100%;
  376. margin: 16px 0;
  377. }
  378. :deep(.docx-container table td),
  379. :deep(.docx-container table th) {
  380. border: 1px solid #ddd;
  381. padding: 8px;
  382. text-align: left;
  383. }
  384. :deep(.docx-container table th) {
  385. background-color: #f2f2f2;
  386. font-weight: bold;
  387. }
  388. </style>