zflb.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. <script setup lang="ts">
  2. import { computed, onMounted, ref, watch } from 'vue'
  3. import {
  4. ElButton,
  5. ElCard,
  6. ElCol,
  7. ElContainer,
  8. ElIcon,
  9. ElInput,
  10. ElInputNumber,
  11. ElMain,
  12. ElMessage,
  13. ElOption,
  14. ElPagination,
  15. ElRow,
  16. ElSelect,
  17. ElTabPane,
  18. ElTabs,
  19. ElTag,
  20. } from 'element-plus'
  21. import {
  22. ArrowLeft,
  23. Building,
  24. Building2,
  25. Eye,
  26. Factory,
  27. Lightbulb,
  28. MapPin,
  29. Maximize,
  30. RotateCcw,
  31. Search,
  32. Settings,
  33. } from 'lucide-vue-next'
  34. import { clientGet } from '@/utils/request.ts'
  35. import { useRoute, useRouter } from 'vue-router'
  36. // 接口类型定义
  37. export interface ASimplifiedHouseInfoVo {
  38. id: string // 主键
  39. assetType: string // 资产类型(公租房/厂房/创新创业基地)
  40. building: string // 楼栋
  41. floor: string // 楼层
  42. houseName: string // 房间名称 (如:A栋101)
  43. address: string // 地址
  44. status: string // 状态 (空闲/已租)
  45. rentRange: number | null // 租金范围
  46. createTime: string // 创建时间
  47. updateTime: string // 更新时间
  48. area: string // 面积
  49. isArrears: boolean // 是否欠费
  50. contractExpirationDate?: string | null // 合同到期日期
  51. }
  52. export interface BaseResponse {
  53. code: number
  54. message: string
  55. success: boolean
  56. }
  57. export interface ASimplifiedHouseInfoVoListResponse extends BaseResponse {
  58. data: {
  59. records: ASimplifiedHouseInfoVo[]
  60. total: number
  61. size: number
  62. current: number
  63. pages: number
  64. }
  65. }
  66. const router = useRouter()
  67. const route = useRoute()
  68. // 响应式数据
  69. const activeTab = ref(route.query.currentActive as string || 'housing')
  70. const currentPage = ref(1)
  71. const pageSize = ref(12)
  72. const sortBy = ref('default')
  73. const loading = ref(false)
  74. const total = ref(0)
  75. // 筛选条件 - 核心修改:默认选中空闲状态(value: '1')
  76. const filters = ref({
  77. priceRange: {
  78. min: undefined as number | undefined,
  79. max: undefined as number | undefined,
  80. },
  81. areaRange: {
  82. min: undefined as number | undefined,
  83. max: undefined as number | undefined,
  84. },
  85. district: '',
  86. building: undefined as number | undefined,
  87. floor: undefined as number | undefined,
  88. houseName: undefined as string | undefined,
  89. status: '1' as string | undefined // 默认值:空闲(数字字符串'1')
  90. })
  91. // 房屋列表数据
  92. const houseList = ref<ASimplifiedHouseInfoVo[]>([])
  93. // 资产类型映射
  94. const assetTypeMap = {
  95. housing: '公租房',
  96. factory: '厂房',
  97. innovation: '创新创业基地',
  98. }
  99. // 排序映射
  100. const sortMap = {
  101. 'price-desc': '1', // 租金从高到低
  102. 'price-asc': '2', // 租金从低到高
  103. 'area-desc': '3', // 面积从高到低
  104. 'area-asc': '4', // 面积从小到大
  105. default: '1', // 默认排序
  106. }
  107. // 解析租金范围
  108. const parseRentRange = () => {
  109. return {
  110. min: filters.value.priceRange.min,
  111. max: filters.value.priceRange.max,
  112. }
  113. }
  114. // 解析面积范围
  115. const parseAreaRange = () => {
  116. return {
  117. min: filters.value.areaRange.min,
  118. max: filters.value.areaRange.max,
  119. }
  120. }
  121. // 获取列表数据
  122. const getList = async () => {
  123. try {
  124. loading.value = true
  125. const rentRange = parseRentRange()
  126. const areaRange = parseAreaRange()
  127. // 构建请求参数
  128. const params = {
  129. assetType: assetTypeMap[activeTab.value as keyof typeof assetTypeMap],
  130. pageNum: currentPage.value,
  131. pageSize: pageSize.value,
  132. sortSearch: sortMap[sortBy.value as keyof typeof sortMap],
  133. ...(rentRange.min && { rentRangeMin: rentRange.min }),
  134. ...(rentRange.max && { rentRangeMax: rentRange.max }),
  135. ...(areaRange.min && { areaMin: areaRange.min }),
  136. ...(areaRange.max && { areaMax: areaRange.max }),
  137. ...(filters.value.building && { building: filters.value.building?.toString() }),
  138. ...(filters.value.floor && { floor: filters.value.floor?.toString() }),
  139. ...(filters.value.houseName && { houseName: filters.value.houseName }),
  140. // 始终传递状态参数(默认是'1',即空闲)
  141. ...(filters.value.status && { status: filters.value.status })
  142. }
  143. const response = await clientGet<typeof params, ASimplifiedHouseInfoVoListResponse>(
  144. '/asimplifiedHouseInfo/getList',
  145. { params: params }
  146. )
  147. if (response.code === 200) {
  148. houseList.value = response.data.records
  149. total.value = response.data.total
  150. } else {
  151. ElMessage.error(response.message || '获取数据失败')
  152. houseList.value = []
  153. total.value = 0
  154. }
  155. } catch (error) {
  156. console.error('获取列表数据失败:', error)
  157. ElMessage.error('网络请求失败,请稍后重试')
  158. houseList.value = []
  159. total.value = 0
  160. } finally {
  161. loading.value = false
  162. }
  163. }
  164. // 格式化显示数据
  165. const formatDisplayData = (item: ASimplifiedHouseInfoVo) => {
  166. // 状态反向映射:后端返回的数字值转前端显示文本
  167. const statusDisplayMap = {
  168. '1': '可租',
  169. '2': '已租',
  170. '3': '到期',
  171. '空闲': '可租', // 兼容原有文本值
  172. '已租': '已租',
  173. '到期': '到期'
  174. }
  175. return {
  176. id: item.id,
  177. buildingNumber: item.building || '-',
  178. houseNameNumber: item.houseName || '-',
  179. priceRange: item.rentRange ? `${item.rentRange}` : '面议',
  180. address: item.address || '未填写',
  181. area: parseInt(item.area) || 0,
  182. status: statusDisplayMap[item.status] || item.status,
  183. floor: item.floor || '-',
  184. facilities: '配套设施',
  185. building: item.building || '-',
  186. isArrears: item.isArrears,
  187. contractExpirationDate: item.contractExpirationDate || null,
  188. }
  189. }
  190. // 格式化后的列表数据
  191. const formattedList = computed(() => {
  192. return houseList.value.map(formatDisplayData)
  193. })
  194. // 优化状态标签类型判断逻辑
  195. const getStatusTagType = (contractExpirationDate: string | null, status: string) => {
  196. // 处理空值情况
  197. if (!contractExpirationDate) contractExpirationDate = null
  198. // 可租/空闲状态
  199. if (status === '可租' || status === '空闲') {
  200. return 'success'
  201. }
  202. // 已租且有到期日期(即将到期)
  203. if (status === '已租' && contractExpirationDate && contractExpirationDate !== '') {
  204. return 'danger'
  205. }
  206. // 已租无到期日期
  207. if (status === '已租') {
  208. return 'warning'
  209. }
  210. // 到期状态
  211. if (status === '到期') {
  212. return 'danger'
  213. }
  214. return 'success'
  215. }
  216. // 获取状态对应的按钮类型
  217. const getStatusButtonType = (contractExpirationDate: string | null, status: string) => {
  218. return getStatusTagType(contractExpirationDate, status)
  219. }
  220. // 标签页切换处理
  221. const handleTabClick = (tab: any) => {
  222. if (tab.paneName === activeTab.value) {
  223. return
  224. }
  225. activeTab.value = tab.paneName
  226. currentPage.value = 1
  227. // 切换标签页时保留默认的空闲状态筛选
  228. getList()
  229. }
  230. // 手动应用筛选
  231. const applyFilters = () => {
  232. currentPage.value = 1
  233. getList()
  234. }
  235. // 重置筛选条件 - 核心修改:重置后仍保留空闲状态
  236. const resetFilters = () => {
  237. filters.value = {
  238. priceRange: {
  239. min: undefined,
  240. max: undefined,
  241. },
  242. areaRange: {
  243. min: undefined,
  244. max: undefined,
  245. },
  246. district: '',
  247. building: undefined,
  248. floor: undefined,
  249. houseName: undefined,
  250. status: '1' // 重置后依然默认选中空闲状态
  251. }
  252. }
  253. // 分页变化处理
  254. const handlePageChange = (page: number) => {
  255. currentPage.value = page
  256. getList()
  257. }
  258. // 排序变化处理
  259. const handleSortChange = () => {
  260. currentPage.value = 1
  261. getList()
  262. }
  263. // 状态变化自动筛选
  264. const handleStatusChange = () => {
  265. currentPage.value = 1
  266. getList()
  267. }
  268. const goBack = () => {
  269. router.back()
  270. }
  271. // 查看详情
  272. const viewDetails = (item: any) => {
  273. router.push({
  274. path: '/zf/fwxq',
  275. query: {
  276. houseId: item.id,
  277. },
  278. })
  279. }
  280. // 监听排序变化
  281. watch(sortBy, handleSortChange)
  282. // 监听状态变化 - 选择后自动筛选
  283. watch(() => filters.value.status, (newVal) => {
  284. if (newVal !== undefined) {
  285. handleStatusChange()
  286. }
  287. }, { immediate: false })
  288. // 组件挂载时获取数据(默认加载空闲状态数据)
  289. onMounted(() => {
  290. getList()
  291. })
  292. </script>
  293. <template>
  294. <el-container class="min-h-screen bg-gradient-to-br from-blue-50">
  295. <el-main class="h-full p-0">
  296. <el-button
  297. type="primary"
  298. @click="goBack"
  299. class="relative top-1 z-50 bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white font-semibold py-3 px-6 rounded-full shadow-lg transition-all duration-300 ease-in-out transform hover:scale-105"
  300. >
  301. <el-icon class="mr-1"><ArrowLeft /></el-icon>
  302. 返回
  303. </el-button>
  304. <!-- 主要内容区域 -->
  305. <div class="pt-5">
  306. <div class="w-full">
  307. <!-- 标签页导航 -->
  308. <el-tabs v-model="activeTab" class="mb-2" @tab-click="handleTabClick">
  309. <el-tab-pane label="公租房" name="housing">
  310. <template #label>
  311. <div class="flex items-center">
  312. <el-icon class="mr-2"><Building /></el-icon>
  313. 公租房
  314. </div>
  315. </template>
  316. </el-tab-pane>
  317. <el-tab-pane label="标准化厂房" name="factory">
  318. <template #label>
  319. <div class="flex items-center">
  320. <el-icon class="mr-2"><Factory /></el-icon>
  321. 标准化厂房
  322. </div>
  323. </template>
  324. </el-tab-pane>
  325. <el-tab-pane label="创新创业基地" name="innovation">
  326. <template #label>
  327. <div class="flex items-center">
  328. <el-icon class="mr-2"><Lightbulb /></el-icon>
  329. 创新创业基地
  330. </div>
  331. </template>
  332. </el-tab-pane>
  333. </el-tabs>
  334. <!-- 筛选条件区域 -->
  335. <el-card class="mb-4 shadow-lg rounded-2xl border-0">
  336. <div>
  337. <el-row :gutter="24">
  338. <el-col :xs="24" :sm="12" :md="6">
  339. <div class="mb-4">
  340. <label class="block text-sm font-medium text-gray-700 mb-2">
  341. 租金范围 (元/月)
  342. </label>
  343. <div class="flex items-center space-x-2">
  344. <el-input-number
  345. v-model="filters.priceRange.min"
  346. placeholder="最低租金"
  347. :min="0"
  348. :max="filters.priceRange.max || 999999"
  349. :step="50"
  350. class="flex-1"
  351. controls-position="right"
  352. size="default"
  353. />
  354. <span class="text-gray-500">-</span>
  355. <el-input-number
  356. v-model="filters.priceRange.max"
  357. placeholder="最高租金"
  358. :min="filters.priceRange.min || 0"
  359. :max="999999"
  360. :step="50"
  361. class="flex-1"
  362. controls-position="right"
  363. size="default"
  364. />
  365. </div>
  366. </div>
  367. </el-col>
  368. <el-col :xs="24" :sm="12" :md="6">
  369. <div class="mb-4">
  370. <label class="block text-sm font-medium text-gray-700 mb-2">
  371. 面积范围 (㎡)
  372. </label>
  373. <div class="flex items-center space-x-2">
  374. <el-input-number
  375. v-model="filters.areaRange.min"
  376. placeholder="最小面积"
  377. :min="0"
  378. :max="filters.areaRange.max || 99999"
  379. :step="10"
  380. class="flex-1"
  381. controls-position="right"
  382. size="default"
  383. />
  384. <span class="text-gray-500">-</span>
  385. <el-input-number
  386. v-model="filters.areaRange.max"
  387. placeholder="最大面积"
  388. :min="filters.areaRange.min || 0"
  389. :max="99999"
  390. :step="10"
  391. class="flex-1"
  392. controls-position="right"
  393. size="default"
  394. />
  395. </div>
  396. </div>
  397. </el-col>
  398. <el-col :xs="24" :sm="12" :md="2">
  399. <div class="mb-4">
  400. <label class="block text-sm font-medium text-gray-700 mb-2">楼栋</label>
  401. <el-input-number
  402. v-model="filters.building"
  403. placeholder="楼栋号"
  404. :min="1"
  405. :max="999"
  406. class="w-full"
  407. controls-position="right"
  408. size="default"
  409. />
  410. </div>
  411. </el-col>
  412. <el-col :xs="24" :sm="12" :md="2">
  413. <div class="mb-4">
  414. <label class="block text-sm font-medium text-gray-700 mb-2">楼层</label>
  415. <el-input-number
  416. v-model="filters.floor"
  417. placeholder="楼层"
  418. :min="1"
  419. :max="99"
  420. class="w-full"
  421. controls-position="right"
  422. size="default"
  423. />
  424. </div>
  425. </el-col>
  426. <el-col :xs="24" :sm="12" :md="2">
  427. <div class="mb-4">
  428. <label class="block text-sm font-medium text-gray-700 mb-2">房间名称</label>
  429. <el-input
  430. v-model="filters.houseName"
  431. placeholder="房间名称"
  432. class="w-full"
  433. size="default"
  434. clearable
  435. />
  436. </div>
  437. </el-col>
  438. <el-col :xs="24" :sm="12" :md="2">
  439. <div class="mb-4">
  440. <label class="block text-sm font-medium text-gray-700 mb-2">房屋状态</label>
  441. <!-- 核心修改:默认选中空闲,且移除清空功能(避免删除默认值) -->
  442. <el-select
  443. v-model="filters.status"
  444. placeholder="请选择状态"
  445. style="width: 100%"
  446. size="default"
  447. @change="handleStatusChange"
  448. >
  449. <el-option label="空闲" value="1"/>
  450. <el-option label="已租" value="2"/>
  451. <el-option label="到期" value="3"/>
  452. </el-select>
  453. </div>
  454. </el-col>
  455. <el-col :xs="24" :sm="12" :md="6" style="display: flex">
  456. <div class="mb-4 flex items-end">
  457. <el-button
  458. type="primary"
  459. @click="applyFilters"
  460. class="mr-2"
  461. :loading="loading"
  462. size="default"
  463. >
  464. <el-icon class="mr-1"><Search /></el-icon>
  465. 搜索
  466. </el-button>
  467. <el-button @click="resetFilters" size="default">
  468. <el-icon class="mr-1"><RotateCcw /></el-icon>
  469. 重置
  470. </el-button>
  471. </div>
  472. </el-col>
  473. </el-row>
  474. </div>
  475. </el-card>
  476. <!-- 列表展示区域 -->
  477. <div class="mb-4 flex justify-between items-center">
  478. <div class="text-gray-600">
  479. 共找到
  480. <span class="font-semibold text-blue-600">{{ total }}</span> 个结果
  481. </div>
  482. <el-select v-model="sortBy" placeholder="排序方式" style="width: 20%" size="default">
  483. <el-option label="默认排序" value="default"></el-option>
  484. <el-option label="租金从低到高" value="price-asc"></el-option>
  485. <el-option label="租金从高到低" value="price-desc"></el-option>
  486. <el-option label="面积从小到大" value="area-asc"></el-option>
  487. <el-option label="面积从大到小" value="area-desc"></el-option>
  488. </el-select>
  489. </div>
  490. <!-- 加载状态 -->
  491. <div v-if="loading" class="text-center py-16">
  492. <el-icon :size="64" class="text-blue-500 mb-4 animate-spin"><Settings /></el-icon>
  493. <h3 class="text-xl font-semibold text-gray-600">加载中...</h3>
  494. </div>
  495. <!-- 列表内容 -->
  496. <el-row :gutter="24" v-else>
  497. <el-col
  498. :xs="24"
  499. :sm="12"
  500. :lg="8"
  501. :xl="6"
  502. v-for="item in formattedList"
  503. :key="item.id"
  504. class="mb-6"
  505. >
  506. <el-card
  507. class="h-full shadow-xl hover:shadow-2xl transition-all duration-300 rounded-2xl border-0 overflow-hidden group cursor-pointer"
  508. >
  509. <div>
  510. <!-- 状态标签 -->
  511. <div class="flex items-center justify-between pt-2 gap-2 flex-wrap">
  512. <div class="flex items-center gap-2 flex-wrap">
  513. <el-tag
  514. :type="getStatusTagType(item.contractExpirationDate,item.status)"
  515. size="large"
  516. class="custom-large-tag"
  517. >
  518. {{ item.status }}
  519. </el-tag>
  520. <el-tag
  521. v-if="item.contractExpirationDate && item.contractExpirationDate !== ''"
  522. type="danger"
  523. size="large"
  524. class="custom-large-tag"
  525. >
  526. 合同到期日: {{ item.contractExpirationDate }}
  527. </el-tag>
  528. <el-tag
  529. v-if="item.isArrears"
  530. type="danger"
  531. size="large"
  532. class="custom-large-tag"
  533. >缴费提醒</el-tag>
  534. </div>
  535. <div class="text-right">
  536. <div class="text-2xl font-bold text-blue-600">{{ item.priceRange }}</div>
  537. <div class="text-sm text-gray-500">元/月</div>
  538. </div>
  539. </div>
  540. <div class="flex justify-between items-start">
  541. <h1 class="text-xl font-bold text-gray-800">
  542. {{ item.buildingNumber }}栋{{ item.houseNameNumber }}
  543. </h1>
  544. </div>
  545. <div class="space-y-3 mb-6">
  546. <div class="flex items-center text-gray-600">
  547. <el-icon class="mr-2 text-blue-500"><MapPin /></el-icon>
  548. <span class="text-sm">{{ item.address }}</span>
  549. </div>
  550. <div class="flex items-center text-gray-600">
  551. <el-icon class="mr-2 text-green-500"><Maximize /></el-icon>
  552. <span class="text-sm">占地面积:{{ item.area }}㎡</span>
  553. </div>
  554. <div class="flex items-center text-gray-600" v-if="item.floor && item.floor !== '-'">
  555. <el-icon class="mr-2 text-purple-500"><Building2 /></el-icon>
  556. <span class="text-sm">楼层:{{ item.floor }}</span>
  557. </div>
  558. <div class="flex items-center text-gray-600" v-if="item.facilities">
  559. <el-icon class="mr-2 text-orange-500"><Settings /></el-icon>
  560. <span class="text-sm">配套:{{ item.facilities }}</span>
  561. </div>
  562. </div>
  563. <div class="flex gap-2">
  564. <el-button
  565. :type="getStatusButtonType(item.contractExpirationDate,item.status)"
  566. size="default"
  567. class="w-full"
  568. @click="viewDetails(item)"
  569. >
  570. <el-icon class="mr-1"><Eye /></el-icon>
  571. 查看详情
  572. </el-button>
  573. </div>
  574. </div>
  575. </el-card>
  576. </el-col>
  577. </el-row>
  578. <!-- 空状态 -->
  579. <div v-if="!loading && formattedList.length === 0" class="text-center py-16">
  580. <el-icon :size="64" class="text-gray-400 mb-4"><Search /></el-icon>
  581. <h3 class="text-xl font-semibold text-gray-600 mb-2">暂无符合条件的结果</h3>
  582. <p class="text-gray-500">请尝试调整筛选条件</p>
  583. </div>
  584. <!-- 分页 -->
  585. <div class="flex justify-center mt-12" v-if="!loading && total > 0">
  586. <el-pagination
  587. v-model:current-page="currentPage"
  588. :page-size="pageSize"
  589. :total="total"
  590. layout="prev, pager, next, jumper, total"
  591. class="justify-center"
  592. @current-change="handlePageChange"
  593. />
  594. </div>
  595. </div>
  596. </div>
  597. </el-main>
  598. </el-container>
  599. </template>
  600. <style scoped>
  601. .el-card {
  602. border: none;
  603. transition: all 0.3s ease;
  604. }
  605. .el-card:hover {
  606. transform: translateY(-2px);
  607. }
  608. .el-tabs__item {
  609. font-weight: 600;
  610. }
  611. .el-tabs__active-bar {
  612. background-color: #3b82f6;
  613. }
  614. .el-tabs__item.is-active {
  615. color: #3b82f6;
  616. }
  617. .el-button {
  618. font-weight: 600;
  619. }
  620. /* 渐变背景动画 */
  621. @keyframes gradient {
  622. 0% {
  623. background-position: 0% 50%;
  624. }
  625. 50% {
  626. background-position: 100% 50%;
  627. }
  628. 100% {
  629. background-position: 0% 50%;
  630. }
  631. }
  632. .bg-gradient-to-r {
  633. background-size: 200% 200%;
  634. animation: gradient 15s ease infinite;
  635. }
  636. /* 加载动画 */
  637. @keyframes spin {
  638. from {
  639. transform: rotate(0deg);
  640. }
  641. to {
  642. transform: rotate(360deg);
  643. }
  644. }
  645. .animate-spin {
  646. animation: spin 1s linear infinite;
  647. }
  648. /* 增大标签字体大小 */
  649. .custom-large-tag {
  650. font-size: 16px !important;
  651. padding: 10px 16px !important;
  652. }
  653. /* 响应式优化 */
  654. @media (max-width: 768px) {
  655. .custom-large-tag {
  656. font-size: 14px !important;
  657. padding: 8px 12px !important;
  658. }
  659. }
  660. </style>