zfxxgl.vue 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725
  1. <script setup lang="ts">
  2. import { computed, onMounted, reactive, ref } from 'vue'
  3. import { ElMessage, ElMessageBox } from 'element-plus'
  4. import {
  5. Delete,
  6. Download,
  7. Edit,
  8. Eye,
  9. FileText,
  10. Home,
  11. Info,
  12. Package,
  13. Plus,
  14. Search,
  15. Settings,
  16. Users,
  17. Wrench,
  18. } from 'lucide-vue-next'
  19. import { clientGet, clientPost } from '@/utils/request.ts'
  20. import { renderAsync } from 'docx-preview'
  21. interface ASimplifiedHouseInfo {
  22. id: string
  23. assetType?: string | null
  24. building?: string | null
  25. floor?: string | null
  26. houseName?: string | null
  27. address?: string | null
  28. status?: string | null
  29. rentRange?: number | null
  30. createTime?: Date | null
  31. updateTime?: Date | null
  32. }
  33. interface ASimplifiedHouseInfoReponse extends BaseResponse {
  34. data: PageType<ASimplifiedHouseInfo[]>
  35. }
  36. interface AddASimplifiedHouseInfo {
  37. assetType?: string | null
  38. building?: string | null
  39. floor?: string | null
  40. houseName?: string | null
  41. address?: string | null
  42. status?: string | null
  43. rentRange?: number | null
  44. }
  45. interface UpdateASimplifiedHouseInfo {
  46. id: string
  47. assetType?: string | null
  48. building?: string | null
  49. floor?: string | null
  50. houseName?: string | null
  51. address?: string | null
  52. status?: string | null
  53. rentRange?: number | null
  54. }
  55. interface AHouseInfoDetail {
  56. id: string
  57. simplifiedHouseId?: string | null
  58. area?: string | null
  59. introduce?: string | null
  60. houseType?: string | null
  61. createTime?: Date | null
  62. updateTime?: Date | null
  63. }
  64. interface AHouseInfoDetailResponse extends BaseResponse {
  65. data: AHouseInfoDetail
  66. }
  67. interface AddAHouseInfoDetail {
  68. simplifiedHouseId: string
  69. area?: string | null
  70. introduce?: string | null
  71. houseType?: string | null
  72. }
  73. interface UpdateAHouseInfoDetail {
  74. id: string
  75. simplifiedHouseId: string
  76. area?: string | null
  77. introduce?: string | null
  78. houseType?: string | null
  79. }
  80. interface ADeviceInfo {
  81. id: string
  82. simplifiedHouseId?: string | null
  83. deviceName: string
  84. deviceType?: string | null
  85. deviceBrand?: string | null
  86. deviceNumber?: string | null
  87. devicePrice?: string | null
  88. createTime?: Date | null
  89. updateTime?: Date | null
  90. }
  91. interface ADeviceInfoListResponse extends BaseResponse {
  92. data: ADeviceInfo[]
  93. }
  94. interface AddADeviceInfo {
  95. simplifiedHouseId: string
  96. deviceName: string
  97. deviceType?: string | null
  98. deviceBrand?: string | null
  99. deviceNumber?: string | null
  100. devicePrice?: string | null
  101. }
  102. interface UpdateADeviceInfo {
  103. id: string
  104. simplifiedHouseId: string
  105. deviceName: string
  106. deviceType?: string | null
  107. deviceBrand?: string | null
  108. deviceNumber?: string | null
  109. devicePrice?: string | null
  110. }
  111. interface AMaintenanceRecords {
  112. id: string
  113. deviceId: string
  114. maintenanceDate: Date | null
  115. maintenanceContent: string
  116. maintenanceType: string
  117. maintenanceCost: number | null
  118. maintenancePerson: string
  119. maintenanceStatus: string
  120. createTime?: Date | null
  121. updateTime?: Date | null
  122. }
  123. interface AMaintenanceRecordsListReponse extends BaseResponse {
  124. data: AMaintenanceRecords[]
  125. }
  126. interface AddAMaintenanceRecords {
  127. deviceId: string
  128. maintenanceDate: Date | null
  129. maintenanceContent: string
  130. maintenanceType: string
  131. maintenanceCost: number | null
  132. maintenancePerson: string
  133. maintenanceStatus: string
  134. }
  135. interface UpdateAMaintenanceRecords {
  136. id: string
  137. deviceId: string
  138. maintenanceDate: Date | null
  139. maintenanceContent: string
  140. maintenanceType: string
  141. maintenanceCost: number | null
  142. maintenancePerson: string
  143. maintenanceStatus: string
  144. }
  145. interface ATenantInfo {
  146. id: string
  147. simplifiedHouseId: string
  148. tenantName: string
  149. tenantNumber: string
  150. tenantIdCard: string
  151. tenantInDate: string
  152. tenantTime: string
  153. tenantRent: number
  154. }
  155. interface ATenantInfoResponse extends BaseResponse {
  156. data: ATenantInfo
  157. }
  158. interface AContractInfo {
  159. id: string
  160. simplifiedHouseId: string
  161. contractNumber: string
  162. contractDate: string
  163. contractTime: string
  164. contractExpirationDate: string
  165. contractDeposit: number
  166. contractStatus: string
  167. originalContractUrl: string
  168. signContractUrl: string
  169. }
  170. interface AContractInfoResponse extends BaseResponse {
  171. data: AContractInfo
  172. }
  173. // 响应式数据
  174. const loading = ref(false)
  175. const tableData = ref<ASimplifiedHouseInfo[]>([])
  176. const total = ref(0)
  177. const currentPage = ref(1)
  178. const pageSize = ref(10)
  179. const MINIO_URL = import.meta.env.VITE_MINIO_BASE_URL
  180. // 搜索表单
  181. const searchForm = reactive({
  182. status: '',
  183. houseName: '',
  184. building: '',
  185. assetType: '',
  186. })
  187. // 对话框控制
  188. const dialogVisible = ref(false)
  189. const detailDialogVisible = ref(false)
  190. const deviceDialogVisible = ref(false)
  191. const maintenanceDialogVisible = ref(false)
  192. const maintenanceRecordDialogVisible = ref(false)
  193. const contractPreviewDialogVisible = ref(false)
  194. const isEdit = ref(false)
  195. const isDeviceEdit = ref(false)
  196. const isMaintenanceEdit = ref(false)
  197. const dialogTitle = ref('新增房屋信息')
  198. const activeTab = ref('detail')
  199. // 表单数据
  200. const houseForm = reactive<AddASimplifiedHouseInfo & { id?: string }>({
  201. assetType: '',
  202. building: '',
  203. floor: '',
  204. houseName: '',
  205. address: '',
  206. status: '空闲',
  207. rentRange: 0,
  208. })
  209. const detailForm = reactive<AddAHouseInfoDetail & { id?: string }>({
  210. simplifiedHouseId: '',
  211. area: '',
  212. introduce: '',
  213. houseType: '',
  214. })
  215. const deviceForm = reactive<AddADeviceInfo & { id?: string }>({
  216. simplifiedHouseId: '',
  217. deviceName: '',
  218. deviceType: '',
  219. deviceBrand: '',
  220. deviceNumber: '',
  221. devicePrice: '',
  222. })
  223. // 维护记录相关数据
  224. const currentDeviceId = ref('')
  225. const currentDeviceName = ref('')
  226. const maintenanceList = ref<AMaintenanceRecords[]>([])
  227. const maintenanceLoading = ref(false)
  228. const maintenanceForm = reactive<AddAMaintenanceRecords & { id?: string }>({
  229. deviceId: '',
  230. maintenanceDate: null,
  231. maintenanceContent: '',
  232. maintenanceType: '',
  233. maintenanceCost: null,
  234. maintenancePerson: '',
  235. maintenanceStatus: '待处理',
  236. })
  237. // 选中的行和设备数据
  238. const selectedRows = ref<ASimplifiedHouseInfo[]>([])
  239. const currentHouseId = ref('')
  240. const currentHouseName = ref('')
  241. const currentHouseStatus = ref('')
  242. const deviceList = ref<ADeviceInfo[]>([])
  243. const deviceLoading = ref(false)
  244. // 租户和合同信息
  245. const tenantInfo = ref<ATenantInfo | null>(null)
  246. const contractInfo = ref<AContractInfo | null>(null)
  247. const tenantLoading = ref(false)
  248. const contractLoading = ref(false)
  249. // 合同预览相关
  250. const previewLoading = ref(false)
  251. const previewTitle = ref('')
  252. const previewContent = ref<HTMLElement | null>(null)
  253. // 计算属性:是否显示租户和合同标签页
  254. const showTenantAndContractTabs = computed(() => {
  255. return currentHouseStatus.value === '已租'
  256. })
  257. // API方法
  258. const addRentingHouse = async (info: AddASimplifiedHouseInfo) => {
  259. const res = await clientPost<AddASimplifiedHouseInfo, BaseResponse>(
  260. '/asimplifiedHouseInfo/save',
  261. info,
  262. )
  263. if (res.code !== 200) {
  264. ElMessage.error(res.msg)
  265. return false
  266. }
  267. ElMessage.success(res.msg)
  268. return true
  269. }
  270. const updateRentingHouse = async (info: UpdateASimplifiedHouseInfo) => {
  271. const res = await clientPost<UpdateASimplifiedHouseInfo, BaseResponse>(
  272. '/asimplifiedHouseInfo/update',
  273. info,
  274. )
  275. if (res.code !== 200) {
  276. ElMessage.error(res.msg)
  277. return false
  278. }
  279. ElMessage.success(res.msg)
  280. return true
  281. }
  282. const deleteBatchRentingHouse = async (ids: string[]) => {
  283. const res = await clientPost<string[], BaseResponse>('/asimplifiedHouseInfo/deleteBatch', ids)
  284. if (res.code !== 200) {
  285. ElMessage.error(res.msg)
  286. return false
  287. }
  288. ElMessage.success(res.msg)
  289. return true
  290. }
  291. const getList = async () => {
  292. loading.value = true
  293. try {
  294. const params: any = {
  295. pageNum: currentPage.value,
  296. pageSize: pageSize.value,
  297. }
  298. if (searchForm.status) params.status = searchForm.status
  299. if (searchForm.houseName) params.houseName = searchForm.houseName
  300. if (searchForm.building) params.building = searchForm.building
  301. if (searchForm.assetType) params.assetType = searchForm.assetType
  302. const res = await clientGet<any, ASimplifiedHouseInfoReponse>(
  303. '/asimplifiedHouseInfo/findByPage',
  304. { params },
  305. )
  306. if (res.code !== 200) {
  307. ElMessage.error(res.msg)
  308. return
  309. }
  310. tableData.value = res.data.records
  311. total.value = res.data.total
  312. } finally {
  313. loading.value = false
  314. }
  315. }
  316. const addRentingHouseDetail = async (info: AddAHouseInfoDetail) => {
  317. const res = await clientPost<AddAHouseInfoDetail, BaseResponse>('/ahouseInfoDetail/save', info)
  318. if (res.code !== 200) {
  319. ElMessage.error(res.msg)
  320. return false
  321. }
  322. ElMessage.success(res.msg)
  323. return true
  324. }
  325. const updateRentingHouseDetail = async (info: UpdateAHouseInfoDetail) => {
  326. const res = await clientPost<UpdateAHouseInfoDetail, BaseResponse>(
  327. '/ahouseInfoDetail/update',
  328. info,
  329. )
  330. if (res.code !== 200) {
  331. ElMessage.error(res.msg)
  332. return false
  333. }
  334. ElMessage.success(res.msg)
  335. return true
  336. }
  337. const getDetail = async (simplifiedHouseId: string) => {
  338. const res = await clientGet<{ id: string }, AHouseInfoDetailResponse>(
  339. '/ahouseInfoDetail/getBySimplifiedHouseId',
  340. {
  341. params: { simplifiedHouseId },
  342. },
  343. )
  344. if (res.code !== 200) {
  345. ElMessage.error(res.msg)
  346. return null
  347. }
  348. return res.data
  349. }
  350. const addDevice = async (info: AddADeviceInfo) => {
  351. const res = await clientPost<AddADeviceInfo, BaseResponse>('/adeviceInfo/save', info)
  352. if (res.code !== 200) {
  353. ElMessage.error(res.msg)
  354. return false
  355. }
  356. ElMessage.success(res.msg)
  357. return true
  358. }
  359. const deleteDevice = async (ids: string[]) => {
  360. const res = await clientPost<string[], BaseResponse>('/adeviceInfo/deleteBatch', ids)
  361. if (res.code !== 200) {
  362. ElMessage.error(res.msg)
  363. return false
  364. }
  365. ElMessage.success(res.msg)
  366. return true
  367. }
  368. const getDeviceList = async (simplifiedHouseId: string) => {
  369. const res = await clientGet<{ id: string }, ADeviceInfoListResponse>(
  370. '/adeviceInfo/getBySimplifiedHouseId',
  371. {
  372. params: { simplifiedHouseId },
  373. },
  374. )
  375. if (res.code !== 200) {
  376. ElMessage.error(res.msg)
  377. return []
  378. }
  379. return res.data
  380. }
  381. const updateDevice = async (info: UpdateADeviceInfo) => {
  382. const res = await clientPost<UpdateADeviceInfo, BaseResponse>('/adeviceInfo/update', info)
  383. if (res.code !== 200) {
  384. ElMessage.error(res.msg)
  385. return false
  386. }
  387. ElMessage.success(res.msg)
  388. return true
  389. }
  390. const addDeviceMaintainRecord = async (info: AddAMaintenanceRecords) => {
  391. const res = await clientPost<AddAMaintenanceRecords, BaseResponse>(
  392. '/amaintenanceRecords/save',
  393. info,
  394. )
  395. if (res.code !== 200) {
  396. ElMessage.error(res.msg)
  397. return false
  398. }
  399. ElMessage.success(res.msg)
  400. return true
  401. }
  402. const updateDeviceMaintainRecord = async (info: UpdateAMaintenanceRecords) => {
  403. const res = await clientPost<UpdateAMaintenanceRecords, BaseResponse>(
  404. '/amaintenanceRecords/update',
  405. info,
  406. )
  407. if (res.code !== 200) {
  408. ElMessage.error(res.msg)
  409. return false
  410. }
  411. ElMessage.success(res.msg)
  412. return true
  413. }
  414. const deleteDeviceMaintainRecord = async (ids: string[]) => {
  415. const res = await clientPost<string[], BaseResponse>('/amaintenanceRecords/delete', ids)
  416. if (res.code !== 200) {
  417. ElMessage.error(res.msg)
  418. return false
  419. }
  420. ElMessage.success(res.msg)
  421. return true
  422. }
  423. const getDeviceMaintainRecord = async (deviceId: string) => {
  424. const res = await clientGet<{ deviceId: string }, AMaintenanceRecordsListReponse>(
  425. '/amaintenanceRecords/getByDeviceId',
  426. {
  427. params: { deviceId },
  428. },
  429. )
  430. if (res.code !== 200) {
  431. ElMessage.error(res.msg)
  432. return []
  433. }
  434. return res.data
  435. }
  436. // 根据房屋ID获取租户信息
  437. const getTenantInfo = async (simplifiedHouseId: string) => {
  438. tenantLoading.value = true
  439. try {
  440. const res = await clientGet<
  441. {
  442. simplifiedHouseId: string
  443. },
  444. ATenantInfoResponse
  445. >('/atenantInfo/getBySimplifiedHouseId', {
  446. params: {
  447. simplifiedHouseId: simplifiedHouseId,
  448. },
  449. })
  450. if (res.code !== 200) {
  451. ElMessage.error(res.msg)
  452. tenantInfo.value = null
  453. return
  454. }
  455. tenantInfo.value = res.data
  456. } finally {
  457. tenantLoading.value = false
  458. }
  459. }
  460. // 根据房屋ID获取合同信息
  461. const getContractInfo = async (simplifiedHouseId: string) => {
  462. contractLoading.value = true
  463. try {
  464. const res = await clientGet<
  465. {
  466. simplifiedHouseId: string
  467. },
  468. AContractInfoResponse
  469. >('/acontractInfo/getBySimplifiedHouseId', {
  470. params: {
  471. simplifiedHouseId: simplifiedHouseId,
  472. },
  473. })
  474. if (res.code !== 200) {
  475. ElMessage.error(res.msg)
  476. contractInfo.value = null
  477. return
  478. }
  479. contractInfo.value = res.data
  480. } finally {
  481. contractLoading.value = false
  482. }
  483. }
  484. // 预览docx文件
  485. const previewDocx = async (url: string, title: string) => {
  486. url = MINIO_URL + url
  487. if (!url) {
  488. ElMessage.warning('文档地址为空')
  489. return
  490. }
  491. previewLoading.value = true
  492. previewTitle.value = title
  493. try {
  494. // 获取文档内容
  495. const response = await fetch(url)
  496. if (!response.ok) {
  497. throw new Error('文档加载失败')
  498. }
  499. const arrayBuffer = await response.arrayBuffer()
  500. // 创建预览容器
  501. const container = document.createElement('div')
  502. container.style.padding = '20px'
  503. container.style.backgroundColor = '#fff'
  504. container.style.minHeight = '400px'
  505. // 使用docx-preview渲染文档
  506. await renderAsync(arrayBuffer, container)
  507. previewContent.value = container
  508. contractPreviewDialogVisible.value = true
  509. } catch (error) {
  510. console.error('预览文档失败:', error)
  511. ElMessage.error('预览文档失败,请检查文档地址是否正确')
  512. } finally {
  513. previewLoading.value = false
  514. }
  515. }
  516. // 下载文档
  517. const downloadDocument = (url: string, filename: string) => {
  518. url = MINIO_URL + url
  519. if (!url) {
  520. ElMessage.warning('文档地址为空')
  521. return
  522. }
  523. const link = document.createElement('a')
  524. link.href = url
  525. link.download = filename
  526. link.target = '_blank'
  527. document.body.appendChild(link)
  528. link.click()
  529. document.body.removeChild(link)
  530. }
  531. // 页面方法
  532. const handleSearch = () => {
  533. currentPage.value = 1
  534. getList()
  535. }
  536. const handleReset = () => {
  537. Object.assign(searchForm, {
  538. status: '',
  539. houseName: '',
  540. building: '',
  541. assetType: '',
  542. })
  543. handleSearch()
  544. }
  545. const handleAdd = () => {
  546. isEdit.value = false
  547. dialogTitle.value = '新增房屋信息'
  548. houseForm.id = undefined
  549. Object.assign(houseForm, {
  550. assetType: '',
  551. building: '',
  552. floor: '',
  553. houseName: '',
  554. address: '',
  555. status: '空闲',
  556. rentRange: 0,
  557. })
  558. dialogVisible.value = true
  559. }
  560. const handleEdit = (row: ASimplifiedHouseInfo) => {
  561. isEdit.value = true
  562. dialogTitle.value = '编辑房屋信息'
  563. Object.assign(houseForm, { ...row })
  564. dialogVisible.value = true
  565. }
  566. const handleDelete = async (row: ASimplifiedHouseInfo) => {
  567. try {
  568. await ElMessageBox.confirm('确定要删除这条记录吗?', '提示', {
  569. confirmButtonText: '确定',
  570. cancelButtonText: '取消',
  571. type: 'warning',
  572. })
  573. const success = await deleteBatchRentingHouse([row.id])
  574. if (success) {
  575. getList()
  576. }
  577. } catch {
  578. // 用户取消删除
  579. }
  580. }
  581. const handleBatchDelete = async () => {
  582. if (selectedRows.value.length === 0) {
  583. ElMessage.warning('请选择要删除的记录')
  584. return
  585. }
  586. try {
  587. await ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 条记录吗?`, '提示', {
  588. confirmButtonText: '确定',
  589. cancelButtonText: '取消',
  590. type: 'warning',
  591. })
  592. const ids = selectedRows.value.map((row) => row.id)
  593. const success = await deleteBatchRentingHouse(ids)
  594. if (success) {
  595. getList()
  596. }
  597. } catch {
  598. // 用户取消删除
  599. }
  600. }
  601. const handleViewDetail = async (row: ASimplifiedHouseInfo) => {
  602. currentHouseId.value = row.id
  603. currentHouseName.value = row.houseName || ''
  604. currentHouseStatus.value = row.status || ''
  605. activeTab.value = 'detail'
  606. // 获取房屋详细信息
  607. const detail = await getDetail(row.id)
  608. if (detail) {
  609. Object.assign(detailForm, { ...detail, simplifiedHouseId: row.id })
  610. } else {
  611. Object.assign(detailForm, {
  612. id: '',
  613. simplifiedHouseId: row.id,
  614. area: '',
  615. introduce: '',
  616. houseType: '',
  617. })
  618. }
  619. // 获取设备列表
  620. await loadDeviceList(row.id)
  621. // 如果是已租房屋,获取租户和合同信息
  622. if (row.status === '已租') {
  623. await getTenantInfo(row.id)
  624. await getContractInfo(row.id)
  625. }
  626. detailDialogVisible.value = true
  627. }
  628. const loadDeviceList = async (houseId: string) => {
  629. deviceLoading.value = true
  630. try {
  631. const devices = await getDeviceList(houseId)
  632. deviceList.value = devices || []
  633. } finally {
  634. deviceLoading.value = false
  635. }
  636. }
  637. const handleSubmit = async () => {
  638. let success = false
  639. if (isEdit.value) {
  640. success = await updateRentingHouse(houseForm as UpdateASimplifiedHouseInfo)
  641. } else {
  642. success = await addRentingHouse(houseForm)
  643. }
  644. if (success) {
  645. dialogVisible.value = false
  646. getList()
  647. }
  648. }
  649. const handleDetailSubmit = async () => {
  650. let success = false
  651. if (detailForm.id) {
  652. success = await updateRentingHouseDetail(detailForm as UpdateAHouseInfoDetail)
  653. } else {
  654. success = await addRentingHouseDetail(detailForm)
  655. }
  656. if (success) {
  657. // ElMessage.success('房屋详细信息保存成功')
  658. }
  659. }
  660. const handleAddDevice = () => {
  661. isDeviceEdit.value = false
  662. Object.assign(deviceForm, {
  663. id: '',
  664. simplifiedHouseId: currentHouseId.value,
  665. deviceName: '',
  666. deviceType: '',
  667. deviceBrand: '',
  668. deviceNumber: '',
  669. devicePrice: '',
  670. })
  671. deviceDialogVisible.value = true
  672. }
  673. const handleEditDevice = (device: ADeviceInfo) => {
  674. isDeviceEdit.value = true
  675. Object.assign(deviceForm, { ...device })
  676. deviceDialogVisible.value = true
  677. }
  678. const handleDeleteDevice = async (device: ADeviceInfo) => {
  679. try {
  680. await ElMessageBox.confirm('确定要删除这个设备吗?', '提示', {
  681. confirmButtonText: '确定',
  682. cancelButtonText: '取消',
  683. type: 'warning',
  684. })
  685. const success = await deleteDevice([device.id])
  686. if (success) {
  687. await loadDeviceList(currentHouseId.value)
  688. }
  689. } catch {
  690. // 用户取消删除
  691. }
  692. }
  693. const handleDeviceSubmit = async () => {
  694. let success = false
  695. if (isDeviceEdit.value) {
  696. success = await updateDevice(deviceForm as UpdateADeviceInfo)
  697. } else {
  698. success = await addDevice(deviceForm)
  699. }
  700. if (success) {
  701. deviceDialogVisible.value = false
  702. await loadDeviceList(currentHouseId.value)
  703. }
  704. }
  705. // 设备维护记录相关方法
  706. const handleViewMaintenance = async (device: ADeviceInfo) => {
  707. currentDeviceId.value = device.id
  708. currentDeviceName.value = device.deviceName
  709. await loadMaintenanceList(device.id)
  710. maintenanceDialogVisible.value = true
  711. }
  712. const loadMaintenanceList = async (deviceId: string) => {
  713. maintenanceLoading.value = true
  714. try {
  715. const records = await getDeviceMaintainRecord(deviceId)
  716. maintenanceList.value = records || []
  717. } finally {
  718. maintenanceLoading.value = false
  719. }
  720. }
  721. const handleAddMaintenance = () => {
  722. isMaintenanceEdit.value = false
  723. Object.assign(maintenanceForm, {
  724. id: '',
  725. deviceId: currentDeviceId.value,
  726. maintenanceDate: null,
  727. maintenanceContent: '',
  728. maintenanceType: '',
  729. maintenanceCost: null,
  730. maintenancePerson: '',
  731. maintenanceStatus: '待处理',
  732. })
  733. maintenanceRecordDialogVisible.value = true
  734. }
  735. const handleEditMaintenance = (record: AMaintenanceRecords) => {
  736. isMaintenanceEdit.value = true
  737. Object.assign(maintenanceForm, { ...record })
  738. maintenanceRecordDialogVisible.value = true
  739. }
  740. const handleDeleteMaintenance = async (record: AMaintenanceRecords) => {
  741. try {
  742. await ElMessageBox.confirm('确定要删除这条维护记录吗?', '提示', {
  743. confirmButtonText: '确定',
  744. cancelButtonText: '取消',
  745. type: 'warning',
  746. })
  747. const success = await deleteDeviceMaintainRecord([record.id])
  748. if (success) {
  749. await loadMaintenanceList(currentDeviceId.value)
  750. }
  751. } catch {
  752. // 用户取消删除
  753. }
  754. }
  755. const handleMaintenanceSubmit = async () => {
  756. let success = false
  757. if (isMaintenanceEdit.value) {
  758. success = await updateDeviceMaintainRecord(maintenanceForm as UpdateAMaintenanceRecords)
  759. } else {
  760. success = await addDeviceMaintainRecord(maintenanceForm)
  761. }
  762. if (success) {
  763. maintenanceRecordDialogVisible.value = false
  764. await loadMaintenanceList(currentDeviceId.value)
  765. }
  766. }
  767. const handleSelectionChange = (selection: ASimplifiedHouseInfo[]) => {
  768. selectedRows.value = selection
  769. }
  770. const handlePageChange = (page: number) => {
  771. currentPage.value = page
  772. getList()
  773. }
  774. const handleSizeChange = (size: number) => {
  775. pageSize.value = size
  776. currentPage.value = 1
  777. getList()
  778. }
  779. const getMaintenanceTypeColor = (type: string) => {
  780. const colorMap: Record<string, string> = {
  781. 定期保养: 'success',
  782. 故障维修: 'danger',
  783. 清洁保养: 'info',
  784. 零件更换: 'warning',
  785. 升级改造: 'primary',
  786. 其他: '',
  787. }
  788. return colorMap[type] || ''
  789. }
  790. const getMaintenanceStatusColor = (status: string) => {
  791. const colorMap: Record<string, string> = {
  792. 待处理: 'warning',
  793. 进行中: 'primary',
  794. 已完成: 'success',
  795. 已取消: 'info',
  796. }
  797. return colorMap[status] || ''
  798. }
  799. const getAssetTypeTag = (assetType: string | null | undefined): string => {
  800. if (!assetType) return 'default'
  801. if (assetType === '公租房') return 'primary'
  802. if (assetType === '厂房') return 'success'
  803. if (assetType === '创新创业基地') return 'danger'
  804. return 'warning'
  805. }
  806. const getContractStatusColor = (status: string) => {
  807. const colorMap: Record<string, string> = {
  808. 有效: 'success',
  809. 已终止: 'info',
  810. }
  811. return colorMap[status] || ''
  812. }
  813. onMounted(() => {
  814. getList()
  815. })
  816. </script>
  817. <template>
  818. <div class="p-6 bg-gray-50 min-h-screen">
  819. <div class="bg-white rounded-lg shadow-sm">
  820. <!-- 页面标题 -->
  821. <div class="flex items-center gap-3 p-6 border-b border-gray-200">
  822. <div class="flex items-center justify-center w-10 h-10 bg-blue-100 rounded-lg">
  823. <Home class="w-5 h-5 text-blue-600" />
  824. </div>
  825. <div>
  826. <h1 class="text-xl font-semibold text-gray-900">租房信息管理</h1>
  827. <p class="text-sm text-gray-500">管理房屋基本信息、详细信息和设备信息</p>
  828. </div>
  829. </div>
  830. <!-- 搜索区域 -->
  831. <div class="p-6 border-b border-gray-200">
  832. <el-form :model="searchForm" inline class="flex flex-wrap gap-4">
  833. <el-form-item label="资产类型">
  834. <el-select
  835. v-model="searchForm.assetType"
  836. placeholder="请选择资产类型"
  837. clearable
  838. style="width: 8rem"
  839. >
  840. <el-option label="公租房" value="公租房" />
  841. <el-option label="厂房" value="厂房" />
  842. <el-option label="创新创业基地" value="创新创业基地" />
  843. </el-select>
  844. </el-form-item>
  845. <el-form-item label="楼栋">
  846. <el-input
  847. v-model="searchForm.building"
  848. placeholder="请输入楼栋"
  849. clearable
  850. class="w-40"
  851. />
  852. </el-form-item>
  853. <el-form-item label="房间名称">
  854. <el-input
  855. v-model="searchForm.houseName"
  856. placeholder="请输入房间名称"
  857. clearable
  858. class="w-40"
  859. />
  860. </el-form-item>
  861. <el-form-item label="状态">
  862. <el-select
  863. v-model="searchForm.status"
  864. placeholder="请选择状态"
  865. clearable
  866. style="width: 8rem"
  867. >
  868. <el-option label="空闲" value="空闲" />
  869. <el-option label="已租" value="已租" />
  870. </el-select>
  871. </el-form-item>
  872. <el-form-item>
  873. <el-button type="primary" @click="handleSearch" :icon="Search">搜索</el-button>
  874. <el-button @click="handleReset">重置</el-button>
  875. </el-form-item>
  876. </el-form>
  877. </div>
  878. <!-- 操作按钮区域 -->
  879. <div class="flex items-center justify-between p-6">
  880. <div class="flex gap-3">
  881. <el-button type="primary" @click="handleAdd" :icon="Plus">新增房屋</el-button>
  882. <el-button
  883. type="danger"
  884. @click="handleBatchDelete"
  885. :disabled="selectedRows.length === 0"
  886. :icon="Delete"
  887. >
  888. 批量删除
  889. </el-button>
  890. </div>
  891. <div class="text-sm text-gray-500">共 {{ total }} 条记录</div>
  892. </div>
  893. <!-- 数据表格 -->
  894. <div class="px-6">
  895. <el-table
  896. :data="tableData"
  897. v-loading="loading"
  898. @selection-change="handleSelectionChange"
  899. class="w-full"
  900. stripe
  901. >
  902. <el-table-column type="selection" width="55" />
  903. <el-table-column prop="assetType" label="资产类型" width="100">
  904. <template #default="{ row }">
  905. <el-tag :type="getAssetTypeTag(row.assetType)">
  906. {{ row.assetType }}
  907. </el-tag>
  908. </template>
  909. </el-table-column>
  910. <el-table-column prop="houseName" label="房间名称" width="120" />
  911. <el-table-column prop="building" label="楼栋" width="100" />
  912. <el-table-column prop="floor" label="楼层" width="100" />
  913. <el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip />
  914. <el-table-column prop="status" label="状态" width="100">
  915. <template #default="{ row }">
  916. <el-tag :type="row.status === '空闲' ? 'success' : 'warning'">
  917. {{ row.status }}
  918. </el-tag>
  919. </template>
  920. </el-table-column>
  921. <el-table-column prop="rentRange" label="租金" width="150" />
  922. <el-table-column label="操作" width="250" fixed="right">
  923. <template #default="{ row }">
  924. <div class="flex gap-2">
  925. <el-button type="primary" size="small" @click="handleViewDetail(row)" :icon="Eye">
  926. 详情
  927. </el-button>
  928. <el-button type="warning" size="small" @click="handleEdit(row)" :icon="Edit">
  929. 编辑
  930. </el-button>
  931. <el-button type="danger" size="small" @click="handleDelete(row)" :icon="Delete">
  932. 删除
  933. </el-button>
  934. </div>
  935. </template>
  936. </el-table-column>
  937. </el-table>
  938. </div>
  939. <!-- 分页 -->
  940. <div class="flex justify-center p-6">
  941. <el-pagination
  942. v-model:current-page="currentPage"
  943. v-model:page-size="pageSize"
  944. :page-sizes="[10, 20, 50, 100]"
  945. :total="total"
  946. layout="total, sizes, prev, pager, next, jumper"
  947. @size-change="handleSizeChange"
  948. @current-change="handlePageChange"
  949. />
  950. </div>
  951. </div>
  952. <!-- 新增/编辑房屋对话框 -->
  953. <el-dialog
  954. v-model="dialogVisible"
  955. :title="dialogTitle"
  956. width="600px"
  957. :close-on-click-modal="false"
  958. >
  959. <el-form :model="houseForm" label-width="100px" class="grid grid-cols-2 gap-4">
  960. <el-form-item label="资产类型" required>
  961. <el-select v-model="houseForm.assetType" placeholder="请选择资产类型" class="w-full">
  962. <el-option label="公租房" value="公租房" />
  963. <el-option label="厂房" value="厂房" />
  964. <el-option label="创新创业基地" value="创新创业基地" />
  965. </el-select>
  966. </el-form-item>
  967. <el-form-item label="楼栋" required>
  968. <el-input v-model="houseForm.building" placeholder="请输入楼栋" />
  969. </el-form-item>
  970. <el-form-item label="楼层">
  971. <el-input v-model="houseForm.floor" placeholder="请输入楼层" />
  972. </el-form-item>
  973. <el-form-item label="房间名称" required>
  974. <el-input v-model="houseForm.houseName" placeholder="请输入房间名称" />
  975. </el-form-item>
  976. <el-form-item label="状态">
  977. <el-select
  978. v-model="houseForm.status"
  979. placeholder="请选择状态"
  980. class="w-full"
  981. default-first-option
  982. disabled
  983. >
  984. <el-option label="空闲" value="空闲" />
  985. <el-option label="已租" value="已租" />
  986. </el-select>
  987. </el-form-item>
  988. <el-form-item label="租金">
  989. <el-input-number
  990. :min="0"
  991. v-model="houseForm.rentRange"
  992. :precision="2"
  993. placeholder="请输入租金"
  994. />
  995. </el-form-item>
  996. <el-form-item label="地址" class="col-span-2">
  997. <el-input v-model="houseForm.address" placeholder="请输入详细地址" />
  998. </el-form-item>
  999. </el-form>
  1000. <template #footer>
  1001. <div class="flex justify-end gap-3">
  1002. <el-button @click="dialogVisible = false">取消</el-button>
  1003. <el-button type="primary" @click="handleSubmit">确定</el-button>
  1004. </div>
  1005. </template>
  1006. </el-dialog>
  1007. <!-- 房屋详细信息和设备管理对话框 -->
  1008. <el-dialog
  1009. v-model="detailDialogVisible"
  1010. :title="`${currentHouseName} - 详细信息管理`"
  1011. width="1200px"
  1012. :close-on-click-modal="false"
  1013. class="detail-dialog"
  1014. >
  1015. <el-tabs v-model="activeTab" class="detail-tabs">
  1016. <!-- 房屋详细信息标签页 -->
  1017. <el-tab-pane label="房屋详情" name="detail">
  1018. <template #label>
  1019. <div class="flex items-center gap-2">
  1020. <Info class="w-4 h-4" />
  1021. <span>房屋详情</span>
  1022. </div>
  1023. </template>
  1024. <div class="p-4">
  1025. <el-form :model="detailForm" label-width="100px" class="grid grid-cols-2 gap-4">
  1026. <el-form-item label="面积">
  1027. <el-input v-model="detailForm.area" placeholder="请输入面积" />
  1028. </el-form-item>
  1029. <el-form-item label="房屋类型">
  1030. <el-select
  1031. v-model="detailForm.houseType"
  1032. placeholder="请选择房屋类型"
  1033. class="w-full"
  1034. >
  1035. <el-option label="一室一厅" value="一室一厅" />
  1036. <el-option label="两室一厅" value="两室一厅" />
  1037. <el-option label="三室一厅" value="三室一厅" />
  1038. <el-option label="三室两厅" value="三室两厅" />
  1039. <el-option label="单间" value="单间" />
  1040. <el-option label="套间" value="套间" />
  1041. <el-option label="厂房" value="厂房" />
  1042. <el-option label="仓库" value="仓库" />
  1043. </el-select>
  1044. </el-form-item>
  1045. <el-form-item label="房屋介绍" class="col-span-2">
  1046. <el-input
  1047. v-model="detailForm.introduce"
  1048. type="textarea"
  1049. :rows="4"
  1050. placeholder="请输入房屋介绍"
  1051. />
  1052. </el-form-item>
  1053. </el-form>
  1054. <div class="flex justify-end mt-4">
  1055. <el-button type="primary" @click="handleDetailSubmit">保存房屋详情</el-button>
  1056. </div>
  1057. </div>
  1058. </el-tab-pane>
  1059. <!-- 设备管理标签页 -->
  1060. <el-tab-pane label="设备管理" name="device">
  1061. <template #label>
  1062. <div class="flex items-center gap-2">
  1063. <Settings class="w-4 h-4" />
  1064. <span>设备管理</span>
  1065. <el-badge :value="deviceList.length" class="ml-1" />
  1066. </div>
  1067. </template>
  1068. <div class="p-4">
  1069. <!-- 设备操作按钮 -->
  1070. <div class="flex justify-between items-center mb-4">
  1071. <el-button type="primary" @click="handleAddDevice" :icon="Plus" size="small">
  1072. 新增设备
  1073. </el-button>
  1074. <div class="text-sm text-gray-500">共 {{ deviceList.length }} 个设备</div>
  1075. </div>
  1076. <!-- 设备列表 -->
  1077. <div v-loading="deviceLoading">
  1078. <el-table :data="deviceList" stripe class="w-full">
  1079. <el-table-column prop="deviceName" label="设备名称" width="120" />
  1080. <el-table-column prop="deviceType" label="设备类型" width="100" />
  1081. <el-table-column prop="deviceBrand" label="品牌" width="100" />
  1082. <el-table-column prop="deviceNumber" label="型号" width="120" />
  1083. <el-table-column prop="devicePrice" label="价格" width="100">
  1084. <template #default="{ row }">
  1085. <span class="text-green-600 font-medium">{{ row.devicePrice }}</span>
  1086. </template>
  1087. </el-table-column>
  1088. <el-table-column label="操作" width="270" fixed="right">
  1089. <template #default="{ row }">
  1090. <div class="flex gap-1 flex-wrap">
  1091. <el-button
  1092. type="info"
  1093. size="small"
  1094. @click="handleViewMaintenance(row)"
  1095. :icon="Wrench"
  1096. >
  1097. 维护记录
  1098. </el-button>
  1099. <el-button
  1100. type="warning"
  1101. size="small"
  1102. @click="handleEditDevice(row)"
  1103. :icon="Edit"
  1104. >
  1105. 编辑
  1106. </el-button>
  1107. <el-button
  1108. type="danger"
  1109. size="small"
  1110. @click="handleDeleteDevice(row)"
  1111. :icon="Delete"
  1112. >
  1113. 删除
  1114. </el-button>
  1115. </div>
  1116. </template>
  1117. </el-table-column>
  1118. </el-table>
  1119. <!-- 空状态 -->
  1120. <div v-if="deviceList.length === 0" class="text-center py-12">
  1121. <Package class="w-16 h-16 text-gray-400 mx-auto mb-4" />
  1122. <p class="text-gray-500 text-lg">暂无设备信息</p>
  1123. <p class="text-gray-400 text-sm mt-2">点击上方按钮添加设备</p>
  1124. </div>
  1125. </div>
  1126. </div>
  1127. </el-tab-pane>
  1128. <!-- 租户信息标签页 (仅已租房屋显示) -->
  1129. <el-tab-pane v-if="showTenantAndContractTabs" label="租户信息" name="tenant">
  1130. <template #label>
  1131. <div class="flex items-center gap-2">
  1132. <Users class="w-4 h-4" />
  1133. <span>租户信息</span>
  1134. </div>
  1135. </template>
  1136. <div class="p-4" v-loading="tenantLoading">
  1137. <div v-if="tenantInfo" class="bg-gray-50 rounded-lg p-6">
  1138. <div class="grid grid-cols-2 gap-6">
  1139. <div class="space-y-4">
  1140. <div class="flex items-center gap-3">
  1141. <span class="text-gray-600 font-medium w-20">租户姓名:</span>
  1142. <span class="text-gray-900">{{ tenantInfo.tenantName }}</span>
  1143. </div>
  1144. <div class="flex items-center gap-3">
  1145. <span class="text-gray-600 font-medium w-20">联系电话:</span>
  1146. <span class="text-gray-900">{{ tenantInfo.tenantNumber }}</span>
  1147. </div>
  1148. <div class="flex items-center gap-3">
  1149. <span class="text-gray-600 font-medium w-20">身份证号:</span>
  1150. <span class="text-gray-900">{{ tenantInfo.tenantIdCard }}</span>
  1151. </div>
  1152. </div>
  1153. <div class="space-y-4">
  1154. <div class="flex items-center gap-3">
  1155. <span class="text-gray-600 font-medium w-20">入住时间:</span>
  1156. <span class="text-gray-900">{{ tenantInfo.tenantInDate }}</span>
  1157. </div>
  1158. <div class="flex items-center gap-3">
  1159. <span class="text-gray-600 font-medium w-20">租期:</span>
  1160. <span class="text-gray-900">{{ tenantInfo.tenantTime }}</span>
  1161. </div>
  1162. <div class="flex items-center gap-3">
  1163. <span class="text-gray-600 font-medium w-20">月租金:</span>
  1164. <span class="text-green-600 font-semibold">¥{{ tenantInfo.tenantRent }}</span>
  1165. </div>
  1166. </div>
  1167. </div>
  1168. </div>
  1169. <div v-else class="text-center py-12">
  1170. <Users class="w-16 h-16 text-gray-400 mx-auto mb-4" />
  1171. <p class="text-gray-500 text-lg">暂无租户信息</p>
  1172. </div>
  1173. </div>
  1174. </el-tab-pane>
  1175. <!-- 合同信息标签页 (仅已租房屋显示) -->
  1176. <el-tab-pane v-if="showTenantAndContractTabs" label="合同信息" name="contract">
  1177. <template #label>
  1178. <div class="flex items-center gap-2">
  1179. <FileText class="w-4 h-4" />
  1180. <span>合同信息</span>
  1181. </div>
  1182. </template>
  1183. <div class="p-4" v-loading="contractLoading">
  1184. <div v-if="contractInfo" class="space-y-6">
  1185. <!-- 合同基本信息 -->
  1186. <div class="bg-gray-50 rounded-lg p-6">
  1187. <h3 class="text-lg font-semibold text-gray-900 mb-4">合同基本信息</h3>
  1188. <div class="grid grid-cols-2 gap-6">
  1189. <div class="space-y-4">
  1190. <div class="flex items-center gap-3">
  1191. <span class="text-gray-600 font-medium w-24">合同编号:</span>
  1192. <span class="text-gray-900">{{ contractInfo.contractNumber }}</span>
  1193. </div>
  1194. <div class="flex items-center gap-3">
  1195. <span class="text-gray-600 font-medium w-24">签约日期:</span>
  1196. <span class="text-gray-900">{{ contractInfo.contractDate }}</span>
  1197. </div>
  1198. <div class="flex items-center gap-3">
  1199. <span class="text-gray-600 font-medium w-24">合同期限:</span>
  1200. <span class="text-gray-900">{{ contractInfo.contractTime }}</span>
  1201. </div>
  1202. </div>
  1203. <div class="space-y-4">
  1204. <div class="flex items-center gap-3">
  1205. <span class="text-gray-600 font-medium w-24">到期日期:</span>
  1206. <span class="text-gray-900">{{ contractInfo.contractExpirationDate }}</span>
  1207. </div>
  1208. <div class="flex items-center gap-3">
  1209. <span class="text-gray-600 font-medium w-24">押金:</span>
  1210. <span class="text-green-600 font-semibold"
  1211. >¥{{ contractInfo.contractDeposit }}</span
  1212. >
  1213. </div>
  1214. <div class="flex items-center gap-3">
  1215. <span class="text-gray-600 font-medium w-24">合同状态:</span>
  1216. <el-tag :type="getContractStatusColor(contractInfo.contractStatus)">
  1217. {{ contractInfo.contractStatus }}
  1218. </el-tag>
  1219. </div>
  1220. </div>
  1221. </div>
  1222. </div>
  1223. <!-- 合同文档 -->
  1224. <div class="bg-gray-50 rounded-lg p-6">
  1225. <h3 class="text-lg font-semibold text-gray-900 mb-4">合同文档</h3>
  1226. <div class="space-y-4">
  1227. <!-- 原合同 -->
  1228. <div class="flex items-center justify-between p-4 bg-white rounded-lg border">
  1229. <div class="flex items-center gap-3">
  1230. <FileText class="w-6 h-6 text-blue-600" />
  1231. <div>
  1232. <p class="font-medium text-gray-900">原合同文档</p>
  1233. <p class="text-sm text-gray-500">原始合同文件</p>
  1234. </div>
  1235. </div>
  1236. <div class="flex gap-2">
  1237. <el-button
  1238. type="primary"
  1239. size="small"
  1240. @click="previewDocx(contractInfo.originalContractUrl, '原合同文档')"
  1241. :loading="previewLoading"
  1242. :icon="Eye"
  1243. >
  1244. 预览
  1245. </el-button>
  1246. <el-button
  1247. type="success"
  1248. size="small"
  1249. @click="
  1250. downloadDocument(contractInfo.originalContractUrl, '原合同文档.docx')
  1251. "
  1252. :icon="Download"
  1253. >
  1254. 下载
  1255. </el-button>
  1256. </div>
  1257. </div>
  1258. <!-- 签订后合同 -->
  1259. <div
  1260. v-if="contractInfo.signContractUrl"
  1261. class="flex items-center justify-between p-4 bg-white rounded-lg border"
  1262. >
  1263. <div class="flex items-center gap-3">
  1264. <FileText class="w-6 h-6 text-green-600" />
  1265. <div>
  1266. <p class="font-medium text-gray-900">签订后合同</p>
  1267. <p class="text-sm text-gray-500">已签署的合同文件</p>
  1268. </div>
  1269. </div>
  1270. <div class="flex gap-2">
  1271. <el-button
  1272. type="primary"
  1273. size="small"
  1274. @click="previewDocx(contractInfo.signContractUrl, '签订后合同')"
  1275. :loading="previewLoading"
  1276. :icon="Eye"
  1277. >
  1278. 预览
  1279. </el-button>
  1280. <el-button
  1281. type="success"
  1282. size="small"
  1283. @click="downloadDocument(contractInfo.signContractUrl, '签订后合同.docx')"
  1284. :icon="Download"
  1285. >
  1286. 下载
  1287. </el-button>
  1288. </div>
  1289. </div>
  1290. <!-- 签订后合同为空的提示 -->
  1291. <div
  1292. v-else
  1293. class="flex items-center justify-center p-8 bg-white rounded-lg border border-dashed border-gray-300"
  1294. >
  1295. <div class="text-center">
  1296. <FileText class="w-12 h-12 text-gray-400 mx-auto mb-2" />
  1297. <p class="text-gray-500">签订后合同暂未上传</p>
  1298. </div>
  1299. </div>
  1300. </div>
  1301. </div>
  1302. </div>
  1303. <div v-else class="text-center py-12">
  1304. <FileText class="w-16 h-16 text-gray-400 mx-auto mb-4" />
  1305. <p class="text-gray-500 text-lg">暂无合同信息</p>
  1306. </div>
  1307. </div>
  1308. </el-tab-pane>
  1309. </el-tabs>
  1310. <template #footer>
  1311. <div class="flex justify-end">
  1312. <el-button @click="detailDialogVisible = false">关闭</el-button>
  1313. </div>
  1314. </template>
  1315. </el-dialog>
  1316. <!-- 合同文档预览对话框 -->
  1317. <el-dialog
  1318. v-model="contractPreviewDialogVisible"
  1319. :title="previewTitle"
  1320. width="60%"
  1321. :close-on-click-modal="false"
  1322. class="preview-dialog"
  1323. >
  1324. <div
  1325. v-if="previewContent"
  1326. v-html="previewContent.innerHTML"
  1327. class="preview-content max-h-[70vh] overflow-y-auto"
  1328. ></div>
  1329. <div v-else class="text-center py-12">
  1330. <p class="text-gray-500">文档加载中...</p>
  1331. </div>
  1332. <template #footer>
  1333. <div class="flex justify-end">
  1334. <el-button @click="contractPreviewDialogVisible = false">关闭</el-button>
  1335. </div>
  1336. </template>
  1337. </el-dialog>
  1338. <!-- 设备新增/编辑对话框 -->
  1339. <el-dialog
  1340. v-model="deviceDialogVisible"
  1341. :title="isDeviceEdit ? '编辑设备' : '新增设备'"
  1342. width="500px"
  1343. :close-on-click-modal="false"
  1344. >
  1345. <el-form :model="deviceForm" label-width="100px">
  1346. <el-form-item label="设备名称" required>
  1347. <el-input v-model="deviceForm.deviceName" placeholder="请输入设备名称" />
  1348. </el-form-item>
  1349. <el-form-item label="设备类型">
  1350. <el-select v-model="deviceForm.deviceType" placeholder="请选择设备类型" class="w-full">
  1351. <el-option label="家电" value="家电" />
  1352. <el-option label="家具" value="家具" />
  1353. <el-option label="厨具" value="厨具" />
  1354. <el-option label="卫浴" value="卫浴" />
  1355. <el-option label="办公设备" value="办公设备" />
  1356. <el-option label="其他" value="其他" />
  1357. </el-select>
  1358. </el-form-item>
  1359. <el-form-item label="设备品牌">
  1360. <el-input v-model="deviceForm.deviceBrand" placeholder="请输入设备品牌" />
  1361. </el-form-item>
  1362. <el-form-item label="设备型号">
  1363. <el-input v-model="deviceForm.deviceNumber" placeholder="请输入设备型号" />
  1364. </el-form-item>
  1365. <el-form-item label="设备价格">
  1366. <el-input v-model="deviceForm.devicePrice" placeholder="请输入设备价格" />
  1367. </el-form-item>
  1368. </el-form>
  1369. <template #footer>
  1370. <div class="flex justify-end gap-3">
  1371. <el-button @click="deviceDialogVisible = false">取消</el-button>
  1372. <el-button type="primary" @click="handleDeviceSubmit">确定</el-button>
  1373. </div>
  1374. </template>
  1375. </el-dialog>
  1376. <!-- 维护记录管理对话框 -->
  1377. <el-dialog
  1378. v-model="maintenanceDialogVisible"
  1379. :title="`${currentDeviceName} - 维护记录管理`"
  1380. width="900px"
  1381. :close-on-click-modal="false"
  1382. >
  1383. <div class="mb-4 flex justify-between items-center">
  1384. <el-button type="primary" @click="handleAddMaintenance" :icon="Plus" size="small">
  1385. 新增维护记录
  1386. </el-button>
  1387. <div class="text-sm text-gray-500">共 {{ maintenanceList.length }} 条维护记录</div>
  1388. </div>
  1389. <div v-loading="maintenanceLoading">
  1390. <el-table :data="maintenanceList" stripe class="w-full">
  1391. <el-table-column prop="maintenanceDate" label="维护时间" width="120">
  1392. <template #default="{ row }">
  1393. {{ row.maintenanceDate ? new Date(row.maintenanceDate).toLocaleDateString() : '-' }}
  1394. </template>
  1395. </el-table-column>
  1396. <el-table-column prop="maintenanceType" label="维护类型" width="100">
  1397. <template #default="{ row }">
  1398. <el-tag :type="getMaintenanceTypeColor(row.maintenanceType)">
  1399. {{ row.maintenanceType }}
  1400. </el-tag>
  1401. </template>
  1402. </el-table-column>
  1403. <el-table-column
  1404. prop="maintenanceContent"
  1405. label="维护内容"
  1406. min-width="150"
  1407. show-overflow-tooltip
  1408. />
  1409. <el-table-column prop="maintenancePerson" label="维护人员" width="100" />
  1410. <el-table-column prop="maintenanceCost" label="维护费用" width="100">
  1411. <template #default="{ row }">
  1412. <span class="text-red-600 font-medium">
  1413. {{ row.maintenanceCost ? `¥${row.maintenanceCost}` : '-' }}
  1414. </span>
  1415. </template>
  1416. </el-table-column>
  1417. <el-table-column prop="maintenanceStatus" label="状态" width="100">
  1418. <template #default="{ row }">
  1419. <el-tag :type="getMaintenanceStatusColor(row.maintenanceStatus)">
  1420. {{ row.maintenanceStatus }}
  1421. </el-tag>
  1422. </template>
  1423. </el-table-column>
  1424. <el-table-column label="操作" width="200" fixed="right">
  1425. <template #default="{ row }">
  1426. <div class="flex gap-2">
  1427. <el-button
  1428. type="warning"
  1429. size="small"
  1430. @click="handleEditMaintenance(row)"
  1431. :icon="Edit"
  1432. >
  1433. 编辑
  1434. </el-button>
  1435. <el-button
  1436. type="danger"
  1437. size="small"
  1438. @click="handleDeleteMaintenance(row)"
  1439. :icon="Delete"
  1440. >
  1441. 删除
  1442. </el-button>
  1443. </div>
  1444. </template>
  1445. </el-table-column>
  1446. </el-table>
  1447. <!-- 空状态 -->
  1448. <div v-if="maintenanceList.length === 0" class="text-center py-12">
  1449. <Wrench class="w-16 h-16 text-gray-400 mx-auto mb-4" />
  1450. <p class="text-gray-500 text-lg">暂无维护记录</p>
  1451. <p class="text-gray-400 text-sm mt-2">点击上方按钮添加维护记录</p>
  1452. </div>
  1453. </div>
  1454. <template #footer>
  1455. <div class="flex justify-end">
  1456. <el-button @click="maintenanceDialogVisible = false">关闭</el-button>
  1457. </div>
  1458. </template>
  1459. </el-dialog>
  1460. <!-- 维护记录新增/编辑对话框 -->
  1461. <el-dialog
  1462. v-model="maintenanceRecordDialogVisible"
  1463. :title="isMaintenanceEdit ? '编辑维护记录' : '新增维护记录'"
  1464. width="600px"
  1465. :close-on-click-modal="false"
  1466. >
  1467. <el-form :model="maintenanceForm" label-width="100px" class="grid grid-cols-2 gap-4">
  1468. <el-form-item label="维护时间" required>
  1469. <el-date-picker
  1470. v-model="maintenanceForm.maintenanceDate"
  1471. type="date"
  1472. placeholder="请选择维护时间"
  1473. class="w-full"
  1474. format="YYYY-MM-DD"
  1475. value-format="YYYY-MM-DD"
  1476. />
  1477. </el-form-item>
  1478. <el-form-item label="维护类型" required>
  1479. <el-select
  1480. v-model="maintenanceForm.maintenanceType"
  1481. placeholder="请选择维护类型"
  1482. class="w-full"
  1483. >
  1484. <el-option label="定期保养" value="定期保养" />
  1485. <el-option label="故障维修" value="故障维修" />
  1486. <el-option label="清洁保养" value="清洁保养" />
  1487. <el-option label="零件更换" value="零件更换" />
  1488. <el-option label="升级改造" value="升级改造" />
  1489. <el-option label="其他" value="其他" />
  1490. </el-select>
  1491. </el-form-item>
  1492. <el-form-item label="维护人员">
  1493. <el-input v-model="maintenanceForm.maintenancePerson" placeholder="请输入维护人员" />
  1494. </el-form-item>
  1495. <el-form-item label="维护费用">
  1496. <el-input-number
  1497. v-model="maintenanceForm.maintenanceCost"
  1498. :min="0"
  1499. :precision="2"
  1500. placeholder="请输入维护费用"
  1501. class="w-full"
  1502. />
  1503. </el-form-item>
  1504. <el-form-item label="维护状态" required>
  1505. <el-select
  1506. v-model="maintenanceForm.maintenanceStatus"
  1507. placeholder="请选择维护状态"
  1508. class="w-full"
  1509. >
  1510. <el-option label="待处理" value="待处理" />
  1511. <el-option label="进行中" value="进行中" />
  1512. <el-option label="已完成" value="已完成" />
  1513. <el-option label="已取消" value="已取消" />
  1514. </el-select>
  1515. </el-form-item>
  1516. <el-form-item label="维护内容" class="col-span-2">
  1517. <el-input
  1518. v-model="maintenanceForm.maintenanceContent"
  1519. type="textarea"
  1520. :rows="4"
  1521. placeholder="请输入详细的维护内容描述"
  1522. />
  1523. </el-form-item>
  1524. </el-form>
  1525. <template #footer>
  1526. <div class="flex justify-end gap-3">
  1527. <el-button @click="maintenanceRecordDialogVisible = false">取消</el-button>
  1528. <el-button type="primary" @click="handleMaintenanceSubmit">确定</el-button>
  1529. </div>
  1530. </template>
  1531. </el-dialog>
  1532. </div>
  1533. </template>
  1534. <style scoped>
  1535. .el-form-item {
  1536. margin-bottom: 18px;
  1537. }
  1538. .el-table {
  1539. border-radius: 8px;
  1540. overflow: hidden;
  1541. }
  1542. .detail-dialog .el-dialog__body {
  1543. padding: 0;
  1544. }
  1545. .detail-tabs .el-tabs__content {
  1546. padding: 0;
  1547. }
  1548. .detail-tabs .el-tab-pane {
  1549. background: #fafafa;
  1550. border-radius: 8px;
  1551. margin: 0 20px 20px;
  1552. }
  1553. .el-badge {
  1554. --el-badge-size: 16px;
  1555. --el-badge-padding: 4px;
  1556. }
  1557. .preview-dialog .el-dialog__body {
  1558. padding: 20px;
  1559. }
  1560. .preview-content {
  1561. background: white;
  1562. border-radius: 8px;
  1563. padding: 20px;
  1564. border: 1px solid #e5e7eb;
  1565. }
  1566. .preview-content p {
  1567. margin-bottom: 12px;
  1568. line-height: 1.6;
  1569. }
  1570. .preview-content h1,
  1571. .preview-content h2,
  1572. .preview-content h3 {
  1573. margin-bottom: 16px;
  1574. margin-top: 24px;
  1575. font-weight: 600;
  1576. }
  1577. .preview-content table {
  1578. width: 100%;
  1579. border-collapse: collapse;
  1580. margin: 16px 0;
  1581. }
  1582. .preview-content table th,
  1583. .preview-content table td {
  1584. border: 1px solid #d1d5db;
  1585. padding: 8px 12px;
  1586. text-align: left;
  1587. }
  1588. .preview-content table th {
  1589. background-color: #f9fafb;
  1590. font-weight: 600;
  1591. }
  1592. </style>