Commit 6d9d3e277658eacd7b61e339652c8f4e6ce513da
1 parent
0e3df9ef
完成物业首页功能
Showing
8 changed files
with
1092 additions
and
1 deletions
src/api/index/propertyIndexApi.js
0 → 100644
| 1 | +import request from '@/utils/request' | |
| 2 | + | |
| 3 | +// 查询物业资产指标 | |
| 4 | +export function getPropertyAssetsIndex(params) { | |
| 5 | + return new Promise((resolve, reject) => { | |
| 6 | + request({ | |
| 7 | + url: '/propertyIndex.queryPropertyAssetsIndex', | |
| 8 | + method: 'get', | |
| 9 | + params | |
| 10 | + }).then(response => { | |
| 11 | + const res = response.data | |
| 12 | + if (res.code === 0) { | |
| 13 | + resolve(res) | |
| 14 | + } else { | |
| 15 | + reject(new Error(res.msg || '查询物业资产指标失败')) | |
| 16 | + } | |
| 17 | + }).catch(error => { | |
| 18 | + reject(error) | |
| 19 | + }) | |
| 20 | + }) | |
| 21 | +} | |
| 22 | + | |
| 23 | +// 查询记事本列表 | |
| 24 | +export function getNotepadList(params) { | |
| 25 | + return new Promise((resolve, reject) => { | |
| 26 | + request({ | |
| 27 | + url: '/notepad.listNotepad', | |
| 28 | + method: 'get', | |
| 29 | + params | |
| 30 | + }).then(response => { | |
| 31 | + const res = response.data | |
| 32 | + if (res.code === 0) { | |
| 33 | + resolve(res) | |
| 34 | + } else { | |
| 35 | + reject(new Error(res.msg || '查询记事本列表失败')) | |
| 36 | + } | |
| 37 | + }).catch(error => { | |
| 38 | + reject(error) | |
| 39 | + }) | |
| 40 | + }) | |
| 41 | +} | |
| 42 | + | |
| 43 | +// 查询报修指标 | |
| 44 | +export function getRepairIndex(params) { | |
| 45 | + return new Promise((resolve, reject) => { | |
| 46 | + request({ | |
| 47 | + url: '/propertyIndex.queryRepairIndex', | |
| 48 | + method: 'get', | |
| 49 | + params | |
| 50 | + }).then(response => { | |
| 51 | + const res = response.data | |
| 52 | + if (res.code === 0) { | |
| 53 | + resolve(res) | |
| 54 | + } else { | |
| 55 | + reject(new Error(res.msg || '查询报修指标失败')) | |
| 56 | + } | |
| 57 | + }).catch(error => { | |
| 58 | + reject(error) | |
| 59 | + }) | |
| 60 | + }) | |
| 61 | +} | |
| 62 | + | |
| 63 | +// 查询投诉指标 | |
| 64 | +export function getComplaintIndex(params) { | |
| 65 | + return new Promise((resolve, reject) => { | |
| 66 | + request({ | |
| 67 | + url: '/propertyIndex.queryComplaintIndex', | |
| 68 | + method: 'get', | |
| 69 | + params | |
| 70 | + }).then(response => { | |
| 71 | + const res = response.data | |
| 72 | + if (res.code === 0) { | |
| 73 | + resolve(res) | |
| 74 | + } else { | |
| 75 | + reject(new Error(res.msg || '查询投诉指标失败')) | |
| 76 | + } | |
| 77 | + }).catch(error => { | |
| 78 | + reject(error) | |
| 79 | + }) | |
| 80 | + }) | |
| 81 | +} | |
| 82 | + | |
| 83 | +// 查询业主注册指标 | |
| 84 | +export function getOwnerRegisterIndex(params) { | |
| 85 | + return new Promise((resolve, reject) => { | |
| 86 | + request({ | |
| 87 | + url: '/propertyIndex.queryOwnerRegisterIndex', | |
| 88 | + method: 'get', | |
| 89 | + params | |
| 90 | + }).then(response => { | |
| 91 | + const res = response.data | |
| 92 | + if (res.code === 0) { | |
| 93 | + resolve(res) | |
| 94 | + } else { | |
| 95 | + reject(new Error(res.msg || '查询业主注册指标失败')) | |
| 96 | + } | |
| 97 | + }).catch(error => { | |
| 98 | + reject(error) | |
| 99 | + }) | |
| 100 | + }) | |
| 101 | +} | |
| 0 | 102 | \ No newline at end of file | ... | ... |
src/components/index/index-property.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <div class="property-index-container"> | |
| 3 | + <div class="vc-index-nav"> | |
| 4 | + <span><i class="el-icon-s-home margin-right-sm"></i>{{ $t('propertyIndex.home') }}</span> | |
| 5 | + <span class="margin-left-sm margin-right-sm">/</span> | |
| 6 | + <span>{{ $t('propertyIndex.dashboard') }}</span> | |
| 7 | + </div> | |
| 8 | + | |
| 9 | + <el-row class="vc-index-1" :gutter="20"> | |
| 10 | + <el-col :span="15"> | |
| 11 | + <index-community ref="indexCommunity" /> | |
| 12 | + </el-col> | |
| 13 | + <el-col :span="9"> | |
| 14 | + <index-notice ref="indexNotice" /> | |
| 15 | + </el-col> | |
| 16 | + </el-row> | |
| 17 | + | |
| 18 | + <el-row class="vc-index-1" :gutter="20"> | |
| 19 | + <el-col :span="15"> | |
| 20 | + <index-repair-complaint ref="indexRepairComplaint" /> | |
| 21 | + </el-col> | |
| 22 | + <el-col :span="9"> | |
| 23 | + <index-owner-room ref="indexOwnerRoom" /> | |
| 24 | + </el-col> | |
| 25 | + </el-row> | |
| 26 | + </div> | |
| 27 | +</template> | |
| 28 | + | |
| 29 | +<script> | |
| 30 | +import IndexCommunity from '@/components/index/indexCommunity' | |
| 31 | +import IndexNotice from '@/components/index/indexNotice' | |
| 32 | +import IndexRepairComplaint from '@/components/index/indexRepairComplaint' | |
| 33 | +import IndexOwnerRoom from '@/components/index/indexOwnerRoom' | |
| 34 | + | |
| 35 | +export default { | |
| 36 | + name: 'PropertyIndexList', | |
| 37 | + components: { | |
| 38 | + IndexCommunity, | |
| 39 | + IndexNotice, | |
| 40 | + IndexRepairComplaint, | |
| 41 | + IndexOwnerRoom | |
| 42 | + }, | |
| 43 | + mounted() { | |
| 44 | + this.initData() | |
| 45 | + }, | |
| 46 | + methods: { | |
| 47 | + initData() { | |
| 48 | + this.$refs.indexCommunity._loadPropertyIndexAssets() | |
| 49 | + this.$refs.indexNotice._loadPropertyIndexNotices() | |
| 50 | + this.$refs.indexRepairComplaint.initData() | |
| 51 | + this.$refs.indexOwnerRoom._loadIndexOwnerRegisterData() | |
| 52 | + } | |
| 53 | + } | |
| 54 | +} | |
| 55 | +</script> | |
| 56 | + | |
| 57 | +<style lang="scss" scoped> | |
| 58 | +.property-index-container { | |
| 59 | + | |
| 60 | + | |
| 61 | + .vc-index-nav { | |
| 62 | + padding: 10px; | |
| 63 | + font-size: 16px; | |
| 64 | + text-align: left; | |
| 65 | + } | |
| 66 | + | |
| 67 | + .margin-right-sm { | |
| 68 | + margin-right: 5px; | |
| 69 | + } | |
| 70 | + | |
| 71 | + .margin-left-sm { | |
| 72 | + margin-left: 5px; | |
| 73 | + } | |
| 74 | + | |
| 75 | + .margin-top-lg { | |
| 76 | + margin-top: 15px; | |
| 77 | + } | |
| 78 | + | |
| 79 | + .margin-bottom { | |
| 80 | + margin-bottom: 20px; | |
| 81 | + } | |
| 82 | + | |
| 83 | + .vc-index-1 { | |
| 84 | + margin-bottom: 20px; | |
| 85 | + | |
| 86 | + .index-1-left, | |
| 87 | + .index-1-right { | |
| 88 | + height: 100%; | |
| 89 | + } | |
| 90 | + } | |
| 91 | +} | |
| 92 | +</style> | |
| 0 | 93 | \ No newline at end of file | ... | ... |
src/components/index/indexCommunity.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <div class="index-1-left-1"> | |
| 3 | + <div class="index-title"> | |
| 4 | + <span>{{ $t('propertyIndex.communityInfo') }}</span> | |
| 5 | + </div> | |
| 6 | + <div class="flex justify-between border-bottom index-1-left-1-content"> | |
| 7 | + <div v-for="(item, index) in stats" :key="index" class="flex justify-center" > | |
| 8 | + <div class="index-1-left-img"> | |
| 9 | + <i :class="item.icon"></i> | |
| 10 | + </div> | |
| 11 | + <div> | |
| 12 | + <div class="index-number">{{ indexCommunityViewInfo[item.key] }}</div> | |
| 13 | + <div class="index-number-describe">{{ $t(`propertyIndex.${item.label}`) }}</div> | |
| 14 | + </div> | |
| 15 | + </div> | |
| 16 | + </div> | |
| 17 | + <div class="flex justify-between index-1-left-1-bottom"> | |
| 18 | + <div class="flex justify-between index-1-left-1-bottom-item"> | |
| 19 | + <div class="text-center"> | |
| 20 | + <div class="index-name">{{ $t('propertyIndex.residentReg') }}</div> | |
| 21 | + <div class="index-name-ico"> | |
| 22 | + <i class="el-icon-user"></i> | |
| 23 | + </div> | |
| 24 | + </div> | |
| 25 | + <div class="text-center"> | |
| 26 | + <div class="index-number">{{ indexCommunityViewInfo.ownerCount }} | |
| 27 | + <span class="index-number-unit">{{ $t('propertyIndex.unit') }}</span> | |
| 28 | + </div> | |
| 29 | + <div class="index-number-describe margin-top-sm" @click="_toOwner"> | |
| 30 | + {{ $t('propertyIndex.registerResident') }} > | |
| 31 | + </div> | |
| 32 | + </div> | |
| 33 | + </div> | |
| 34 | + <div class="flex justify-between index-1-left-1-bottom-item"> | |
| 35 | + <div class="text-center"> | |
| 36 | + <div class="index-name">{{ $t('propertyIndex.vehicle') }}</div> | |
| 37 | + <div class="index-name-ico"> | |
| 38 | + <i class="el-icon-truck"></i> | |
| 39 | + </div> | |
| 40 | + </div> | |
| 41 | + <div class="text-center"> | |
| 42 | + <div class="index-number">{{ indexCommunityViewInfo.carCount }} | |
| 43 | + <span class="index-number-unit">{{ $t('propertyIndex.unit') }}</span> | |
| 44 | + </div> | |
| 45 | + <div class="index-number-describe margin-top-sm" @click="_toCar"> | |
| 46 | + {{ $t('propertyIndex.viewVehicle') }} > | |
| 47 | + </div> | |
| 48 | + </div> | |
| 49 | + </div> | |
| 50 | + </div> | |
| 51 | + </div> | |
| 52 | +</template> | |
| 53 | + | |
| 54 | +<script> | |
| 55 | +import { getPropertyAssetsIndex } from '@/api/index/propertyIndexApi' | |
| 56 | + | |
| 57 | +export default { | |
| 58 | + name: 'IndexCommunity', | |
| 59 | + data() { | |
| 60 | + return { | |
| 61 | + indexCommunityViewInfo: { | |
| 62 | + floorCount: 0, | |
| 63 | + roomCount: 0, | |
| 64 | + shopCount: 0, | |
| 65 | + ownerCount: 0, | |
| 66 | + spaceCount: 0, | |
| 67 | + carCount: 0, | |
| 68 | + }, | |
| 69 | + stats: [ | |
| 70 | + { key: 'floorCount', label: 'building', icon: 'el-icon-office-building' }, | |
| 71 | + { key: 'roomCount', label: 'house', icon: 'el-icon-house' }, | |
| 72 | + { key: 'shopCount', label: 'shop', icon: 'el-icon-shopping-bag-2' }, | |
| 73 | + { key: 'spaceCount', label: 'parkingSpace', icon: 'el-icon-truck' } | |
| 74 | + ] | |
| 75 | + } | |
| 76 | + }, | |
| 77 | + created() { | |
| 78 | + }, | |
| 79 | + methods: { | |
| 80 | + _loadPropertyIndexAssets() { | |
| 81 | + const param = { | |
| 82 | + page: 1, | |
| 83 | + row: 10, | |
| 84 | + communityId: this.getCommunityId() | |
| 85 | + } | |
| 86 | + | |
| 87 | + getPropertyAssetsIndex(param) | |
| 88 | + .then(res => { | |
| 89 | + if (res.code === 0) { | |
| 90 | + Object.assign(this.indexCommunityViewInfo, res.data) | |
| 91 | + } | |
| 92 | + }) | |
| 93 | + .catch(err => { | |
| 94 | + console.error('请求失败处理', err) | |
| 95 | + }) | |
| 96 | + }, | |
| 97 | + _toOwner() { | |
| 98 | + this.$router.push('/property/listOwner?tab=ownerInfo') | |
| 99 | + }, | |
| 100 | + _toCar() { | |
| 101 | + this.$router.push('/property/listOwnerCar?tab=ownerCar') | |
| 102 | + } | |
| 103 | + } | |
| 104 | +} | |
| 105 | +</script> | |
| 106 | + | |
| 107 | +<style lang="scss" scoped> | |
| 108 | +.index-1-left-1 { | |
| 109 | + background: #fff; | |
| 110 | + border-radius: 4px; | |
| 111 | + padding: 15px; | |
| 112 | + | |
| 113 | + .index-title { | |
| 114 | + font-size: 16px; | |
| 115 | + font-weight: bold; | |
| 116 | + margin-bottom: 15px; | |
| 117 | + text-align: left; | |
| 118 | + color: #333; | |
| 119 | + } | |
| 120 | + | |
| 121 | + .index-1-left-1-content { | |
| 122 | + padding: 50px 0; | |
| 123 | + border-bottom: 1px solid #eee; | |
| 124 | + | |
| 125 | + > div { | |
| 126 | + width: 25%; | |
| 127 | + text-align: center; | |
| 128 | + } | |
| 129 | + | |
| 130 | + .index-1-left-img { | |
| 131 | + width: 40px; | |
| 132 | + height: 40px; | |
| 133 | + background: #f0f7ff; | |
| 134 | + border-radius: 50%; | |
| 135 | + display: flex; | |
| 136 | + align-items: center; | |
| 137 | + justify-content: center; | |
| 138 | + margin-right: 10px; | |
| 139 | + | |
| 140 | + i { | |
| 141 | + font-size: 20px; | |
| 142 | + color: #409EFF; | |
| 143 | + } | |
| 144 | + } | |
| 145 | + | |
| 146 | + .index-number { | |
| 147 | + font-size: 24px; | |
| 148 | + font-weight: bold; | |
| 149 | + color: #333; | |
| 150 | + } | |
| 151 | + | |
| 152 | + .index-number-describe { | |
| 153 | + font-size: 12px; | |
| 154 | + color: #999; | |
| 155 | + } | |
| 156 | + } | |
| 157 | + | |
| 158 | + .index-1-left-1-bottom { | |
| 159 | + padding: 28px 40px; | |
| 160 | + | |
| 161 | + .index-1-left-1-bottom-item { | |
| 162 | + width: 40%; | |
| 163 | + border-radius: 4px; | |
| 164 | + padding: 10px; | |
| 165 | + padding-left:30px; | |
| 166 | + padding-right:30px; | |
| 167 | + | |
| 168 | + .index-name { | |
| 169 | + font-size: 16px; | |
| 170 | + font-weight: bold; | |
| 171 | + color: #333; | |
| 172 | + } | |
| 173 | + | |
| 174 | + .index-name-ico { | |
| 175 | + width: 40px; | |
| 176 | + height: 40px; | |
| 177 | + background: #e6f7ff; | |
| 178 | + border-radius: 50%; | |
| 179 | + display: flex; | |
| 180 | + align-items: center; | |
| 181 | + justify-content: center; | |
| 182 | + margin: 10px auto; | |
| 183 | + | |
| 184 | + i { | |
| 185 | + font-size: 20px; | |
| 186 | + color: #1890ff; | |
| 187 | + } | |
| 188 | + } | |
| 189 | + | |
| 190 | + .index-number { | |
| 191 | + font-size: 24px; | |
| 192 | + font-weight: bold; | |
| 193 | + color: #333; | |
| 194 | + | |
| 195 | + .index-number-unit { | |
| 196 | + font-size: 14px; | |
| 197 | + color: #999; | |
| 198 | + } | |
| 199 | + } | |
| 200 | + | |
| 201 | + .index-number-describe { | |
| 202 | + font-size: 12px; | |
| 203 | + color: #1890ff; | |
| 204 | + cursor: pointer; | |
| 205 | + | |
| 206 | + &:hover { | |
| 207 | + text-decoration: underline; | |
| 208 | + } | |
| 209 | + } | |
| 210 | + } | |
| 211 | + } | |
| 212 | + | |
| 213 | + .margin-top-sm { | |
| 214 | + margin-top: 5px; | |
| 215 | + } | |
| 216 | +} | |
| 217 | +</style> | |
| 0 | 218 | \ No newline at end of file | ... | ... |
src/components/index/indexNotice.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <div class="index-1-right-1"> | |
| 3 | + <div class="index-title text-left"> | |
| 4 | + <span>{{ $t('propertyIndex.ownerFeedback') }}</span> | |
| 5 | + </div> | |
| 6 | + <ul id="pool" class="notice-list"> | |
| 7 | + <li v-if="!indexNoticeInfo.notices || indexNoticeInfo.notices.length < 1" | |
| 8 | + class="flex justify-between padding-top-sm no-data"> | |
| 9 | + <div class="vc-index-notice-title"> | |
| 10 | + {{ $t('propertyIndex.noFeedback') }} | |
| 11 | + </div> | |
| 12 | + </li> | |
| 13 | + <li v-else v-for="(item, index) in indexNoticeInfo.notices" :key="index" | |
| 14 | + class="flex justify-between padding-top-sm notice-item"> | |
| 15 | + <div class="vc-index-notice-title"> | |
| 16 | + {{ item.objName }}({{ item.roomName }}){{ $t('propertyIndex.feedback') }}{{ item.title }} | |
| 17 | + ({{ item.state === 'F' ? $t('propertyIndex.completed') : $t('propertyIndex.followingUp') }}<span v-if="item.thridId">-{{ $t('propertyIndex.transferRepair') }}</span>) | |
| 18 | + </div> | |
| 19 | + <div class="vc-index-notice-time"> | |
| 20 | + {{ item.createTime }} | |
| 21 | + </div> | |
| 22 | + </li> | |
| 23 | + </ul> | |
| 24 | + </div> | |
| 25 | +</template> | |
| 26 | + | |
| 27 | +<script> | |
| 28 | +import { getNotepadList } from '@/api/index/propertyIndexApi' | |
| 29 | + | |
| 30 | +export default { | |
| 31 | + name: 'IndexNotice', | |
| 32 | + data() { | |
| 33 | + return { | |
| 34 | + indexNoticeInfo: { | |
| 35 | + notices: [], | |
| 36 | + scrollInterval: null, | |
| 37 | + scrollTopValue: 0, | |
| 38 | + scrollDirection: true, | |
| 39 | + } | |
| 40 | + } | |
| 41 | + }, | |
| 42 | + created() { | |
| 43 | + }, | |
| 44 | + mounted() { | |
| 45 | + this.$nextTick(() => { | |
| 46 | + window.addEventListener('resize', this.checkPoolScroll) | |
| 47 | + }) | |
| 48 | + }, | |
| 49 | + beforeDestroy() { | |
| 50 | + if (this.indexNoticeInfo.scrollInterval) { | |
| 51 | + clearInterval(this.indexNoticeInfo.scrollInterval) | |
| 52 | + } | |
| 53 | + window.removeEventListener('resize', this.checkPoolScroll) | |
| 54 | + }, | |
| 55 | + methods: { | |
| 56 | + async _loadPropertyIndexNotices() { | |
| 57 | + const param = { | |
| 58 | + page: 1, | |
| 59 | + row: 10, | |
| 60 | + communityId: this.getCommunityId() | |
| 61 | + } | |
| 62 | + | |
| 63 | + try { | |
| 64 | + const res = await getNotepadList(param) | |
| 65 | + this.indexNoticeInfo.notices = res.data | |
| 66 | + this.$nextTick(() => { | |
| 67 | + this.checkPoolScroll() | |
| 68 | + }) | |
| 69 | + } catch (error) { | |
| 70 | + console.error('请求失败处理', error) | |
| 71 | + } | |
| 72 | + }, | |
| 73 | + checkPoolScroll() { | |
| 74 | + const element = document.getElementById('pool') | |
| 75 | + if (!element) return | |
| 76 | + | |
| 77 | + const clientHeight = element.clientHeight | |
| 78 | + const scrollHeight = element.scrollHeight | |
| 79 | + | |
| 80 | + if (scrollHeight > clientHeight) { | |
| 81 | + if (!this.indexNoticeInfo.scrollInterval) { | |
| 82 | + this.indexNoticeInfo.scrollInterval = setInterval(this.poolScroll, 5000) | |
| 83 | + } | |
| 84 | + } else { | |
| 85 | + clearInterval(this.indexNoticeInfo.scrollInterval) | |
| 86 | + this.indexNoticeInfo.scrollInterval = null | |
| 87 | + } | |
| 88 | + }, | |
| 89 | + poolScroll() { | |
| 90 | + const element = document.getElementById('pool') | |
| 91 | + if (!element) return | |
| 92 | + | |
| 93 | + const clientHeight = element.clientHeight | |
| 94 | + const scrollHeight = element.scrollHeight | |
| 95 | + const canScrollHeight = scrollHeight - clientHeight | |
| 96 | + | |
| 97 | + if (this.indexNoticeInfo.scrollTopValue <= 0) { | |
| 98 | + this.indexNoticeInfo.scrollDirection = true | |
| 99 | + } | |
| 100 | + | |
| 101 | + if (this.indexNoticeInfo.scrollDirection) { | |
| 102 | + this.indexNoticeInfo.scrollTopValue += canScrollHeight / 1200 | |
| 103 | + } else { | |
| 104 | + this.indexNoticeInfo.scrollTopValue -= canScrollHeight / 1200 | |
| 105 | + } | |
| 106 | + | |
| 107 | + if (canScrollHeight <= this.indexNoticeInfo.scrollTopValue) { | |
| 108 | + this.indexNoticeInfo.scrollDirection = !this.indexNoticeInfo.scrollDirection | |
| 109 | + } | |
| 110 | + | |
| 111 | + element.scrollTop = this.indexNoticeInfo.scrollTopValue | |
| 112 | + } | |
| 113 | + } | |
| 114 | +} | |
| 115 | +</script> | |
| 116 | + | |
| 117 | +<style lang="scss" scoped> | |
| 118 | +.index-1-right-1 { | |
| 119 | + background: #fff; | |
| 120 | + border-radius: 4px; | |
| 121 | + padding: 15px; | |
| 122 | + height: 100%; | |
| 123 | + | |
| 124 | + .index-title { | |
| 125 | + font-size: 16px; | |
| 126 | + font-weight: bold; | |
| 127 | + margin-bottom: 15px; | |
| 128 | + color: #333; | |
| 129 | + } | |
| 130 | + | |
| 131 | + .notice-list { | |
| 132 | + height: 300px; | |
| 133 | + overflow-y: auto; | |
| 134 | + padding: 0; | |
| 135 | + margin: 0; | |
| 136 | + list-style: none; | |
| 137 | + | |
| 138 | + .no-data { | |
| 139 | + color: #999; | |
| 140 | + text-align: center; | |
| 141 | + padding: 20px 0; | |
| 142 | + } | |
| 143 | + | |
| 144 | + .notice-item { | |
| 145 | + padding: 8px 0; | |
| 146 | + border-bottom: 1px solid #eee; | |
| 147 | + | |
| 148 | + &:last-child { | |
| 149 | + border-bottom: none; | |
| 150 | + } | |
| 151 | + | |
| 152 | + .vc-index-notice-title { | |
| 153 | + font-size: 14px; | |
| 154 | + color: #333; | |
| 155 | + width: 70%; | |
| 156 | + overflow: hidden; | |
| 157 | + text-overflow: ellipsis; | |
| 158 | + white-space: nowrap; | |
| 159 | + } | |
| 160 | + | |
| 161 | + .vc-index-notice-time { | |
| 162 | + font-size: 12px; | |
| 163 | + color: #999; | |
| 164 | + width: 30%; | |
| 165 | + text-align: right; | |
| 166 | + } | |
| 167 | + } | |
| 168 | + } | |
| 169 | + | |
| 170 | + .padding-top-sm { | |
| 171 | + padding-top: 5px; | |
| 172 | + } | |
| 173 | +} | |
| 174 | +</style> | |
| 0 | 175 | \ No newline at end of file | ... | ... |
src/components/index/indexOwnerRoom.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <div class="owner-room-container"> | |
| 3 | + <div class="bg-white index-1-right-item"> | |
| 4 | + <div class="index-title"><span>{{ $t('propertyIndex.residentRegStats') }}</span></div> | |
| 5 | + <div id="ownerRoomCount" style="height:250px"></div> | |
| 6 | + <div class="flex justify-between text-center stats-container"> | |
| 7 | + <div v-for="(item, index) in stats" :key="index"> | |
| 8 | + <div class="index-bottom-number">{{ item.value }}</div> | |
| 9 | + <div class="index-bottom-number-desc">{{ $t(`propertyIndex.${item.label}`) }}</div> | |
| 10 | + </div> | |
| 11 | + </div> | |
| 12 | + </div> | |
| 13 | + </div> | |
| 14 | +</template> | |
| 15 | + | |
| 16 | +<script> | |
| 17 | +import * as echarts from 'echarts' | |
| 18 | +import { getOwnerRegisterIndex } from '@/api/index/propertyIndexApi' | |
| 19 | + | |
| 20 | +export default { | |
| 21 | + name: 'IndexOwnerRoom', | |
| 22 | + data() { | |
| 23 | + return { | |
| 24 | + indexOwnerRoomInfo: { | |
| 25 | + unbindCount: 0, | |
| 26 | + bindCount: 0, | |
| 27 | + unbindRoomCount: 0, | |
| 28 | + bindRoomCount: 0, | |
| 29 | + }, | |
| 30 | + ownerRoomChart: null | |
| 31 | + } | |
| 32 | + }, | |
| 33 | + computed: { | |
| 34 | + stats() { | |
| 35 | + return [ | |
| 36 | + { label: 'unregistered', value: this.indexOwnerRoomInfo.unbindCount }, | |
| 37 | + { label: 'registered', value: this.indexOwnerRoomInfo.bindCount }, | |
| 38 | + { label: 'unboundRoom', value: this.indexOwnerRoomInfo.unbindRoomCount }, | |
| 39 | + { label: 'boundRoom', value: this.indexOwnerRoomInfo.bindRoomCount } | |
| 40 | + ] | |
| 41 | + } | |
| 42 | + }, | |
| 43 | + created() { | |
| 44 | + }, | |
| 45 | + mounted() { | |
| 46 | + window.addEventListener('resize', this.handleResize) | |
| 47 | + }, | |
| 48 | + beforeDestroy() { | |
| 49 | + window.removeEventListener('resize', this.handleResize) | |
| 50 | + if (this.ownerRoomChart) { | |
| 51 | + this.ownerRoomChart.dispose() | |
| 52 | + } | |
| 53 | + }, | |
| 54 | + methods: { | |
| 55 | + handleResize() { | |
| 56 | + if (this.ownerRoomChart) { | |
| 57 | + this.ownerRoomChart.resize() | |
| 58 | + } | |
| 59 | + }, | |
| 60 | + async _loadIndexOwnerRegisterData() { | |
| 61 | + const param = { | |
| 62 | + page: 1, | |
| 63 | + row: 10, | |
| 64 | + communityId: this.getCommunityId() | |
| 65 | + } | |
| 66 | + | |
| 67 | + try { | |
| 68 | + const res = await getOwnerRegisterIndex(param) | |
| 69 | + Object.assign(this.indexOwnerRoomInfo, res.data) | |
| 70 | + | |
| 71 | + this.$nextTick(() => { | |
| 72 | + const dom = document.getElementById('ownerRoomCount') | |
| 73 | + if (!dom) return | |
| 74 | + | |
| 75 | + if (this.ownerRoomChart) { | |
| 76 | + this.ownerRoomChart.dispose() | |
| 77 | + } | |
| 78 | + this.ownerRoomChart = echarts.init(dom) | |
| 79 | + this._initOwnerEcharts( | |
| 80 | + this.ownerRoomChart, | |
| 81 | + this.indexOwnerRoomInfo.bindCount, | |
| 82 | + this.indexOwnerRoomInfo.unbindCount, | |
| 83 | + this.$t('propertyIndex.residentInfo'), | |
| 84 | + this.$t('propertyIndex.registered'), | |
| 85 | + this.$t('propertyIndex.unregistered'), | |
| 86 | + '#4B7AF0', | |
| 87 | + '#E2EDF6' | |
| 88 | + ) | |
| 89 | + }) | |
| 90 | + } catch (error) { | |
| 91 | + console.error('请求失败处理', error) | |
| 92 | + } | |
| 93 | + }, | |
| 94 | + _initOwnerEcharts(chart, userCount, freeCount, _title, _userCountName, _freeCountName, userColor, freeColor) { | |
| 95 | + const option = { | |
| 96 | + tooltip: { | |
| 97 | + trigger: 'item', | |
| 98 | + formatter: '{a} <br/>{b}: {c} ({d}%)' | |
| 99 | + }, | |
| 100 | + legend: { | |
| 101 | + orient: 'vertical', | |
| 102 | + right: 10, | |
| 103 | + top: 'center', | |
| 104 | + textStyle: { | |
| 105 | + color: '#606266' | |
| 106 | + }, | |
| 107 | + data: [_userCountName, _freeCountName] | |
| 108 | + }, | |
| 109 | + color: [userColor, freeColor], | |
| 110 | + series: [{ | |
| 111 | + name: _title, | |
| 112 | + type: 'pie', | |
| 113 | + radius: ['40%', '65%'], | |
| 114 | + avoidLabelOverlap: false, | |
| 115 | + label: { | |
| 116 | + show: false, | |
| 117 | + position: 'center' | |
| 118 | + }, | |
| 119 | + emphasis: { | |
| 120 | + label: { | |
| 121 | + show: true, | |
| 122 | + fontSize: '16', | |
| 123 | + fontWeight: 'bold' | |
| 124 | + } | |
| 125 | + }, | |
| 126 | + labelLine: { | |
| 127 | + show: false | |
| 128 | + }, | |
| 129 | + data: [ | |
| 130 | + { value: userCount, name: _userCountName }, | |
| 131 | + { value: freeCount, name: _freeCountName } | |
| 132 | + ] | |
| 133 | + }] | |
| 134 | + } | |
| 135 | + | |
| 136 | + chart.setOption(option) | |
| 137 | + } | |
| 138 | + } | |
| 139 | +} | |
| 140 | +</script> | |
| 141 | + | |
| 142 | +<style lang="scss" scoped> | |
| 143 | +.owner-room-container { | |
| 144 | + .index-1-right-item { | |
| 145 | + background: #fff; | |
| 146 | + border-radius: 4px; | |
| 147 | + padding: 15px; | |
| 148 | + height: 100%; | |
| 149 | + | |
| 150 | + .index-title { | |
| 151 | + font-size: 16px; | |
| 152 | + font-weight: bold; | |
| 153 | + margin-bottom: 15px; | |
| 154 | + color: #333; | |
| 155 | + } | |
| 156 | + | |
| 157 | + .stats-container { | |
| 158 | + padding-top: 15px; | |
| 159 | + border-top: 1px solid #eee; | |
| 160 | + | |
| 161 | + > div { | |
| 162 | + flex: 1; | |
| 163 | + } | |
| 164 | + | |
| 165 | + .index-bottom-number { | |
| 166 | + font-size: 20px; | |
| 167 | + font-weight: bold; | |
| 168 | + color: #333; | |
| 169 | + } | |
| 170 | + | |
| 171 | + .index-bottom-number-desc { | |
| 172 | + font-size: 12px; | |
| 173 | + color: #999; | |
| 174 | + margin-top: 5px; | |
| 175 | + } | |
| 176 | + } | |
| 177 | + } | |
| 178 | +} | |
| 179 | +</style> | |
| 0 | 180 | \ No newline at end of file | ... | ... |
src/components/index/indexRepairComplaint.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <div class="repair-complaint-container"> | |
| 3 | + <el-row :gutter="20"> | |
| 4 | + <el-col :span="12"> | |
| 5 | + <div class="bg-white index-1-left-item"> | |
| 6 | + <div class="index-title"><span>{{ $t('propertyIndex.repairStats') }}</span></div> | |
| 7 | + <div id="repairCount" style="height:250px"></div> | |
| 8 | + <div class="flex justify-between text-center stats-container"> | |
| 9 | + <div v-for="(item, index) in repairStats" :key="index"> | |
| 10 | + <div class="index-bottom-number">{{ item.value }}</div> | |
| 11 | + <div class="index-bottom-number-desc">{{ $t(`propertyIndex.${item.label}`) }}</div> | |
| 12 | + </div> | |
| 13 | + </div> | |
| 14 | + </div> | |
| 15 | + </el-col> | |
| 16 | + <el-col :span="12"> | |
| 17 | + <div class="bg-white index-1-left-item"> | |
| 18 | + <div class="index-title"><span>{{ $t('propertyIndex.complaintStats') }}</span></div> | |
| 19 | + <div id="complaintCount" style="height:250px"></div> | |
| 20 | + <div class="flex justify-around text-center stats-container"> | |
| 21 | + <div v-for="(item, index) in complaintStats" :key="index"> | |
| 22 | + <div class="index-bottom-number">{{ item.value }}</div> | |
| 23 | + <div class="index-bottom-number-desc">{{ $t(`propertyIndex.${item.label}`) }}</div> | |
| 24 | + </div> | |
| 25 | + </div> | |
| 26 | + </div> | |
| 27 | + </el-col> | |
| 28 | + </el-row> | |
| 29 | + </div> | |
| 30 | +</template> | |
| 31 | + | |
| 32 | +<script> | |
| 33 | +import * as echarts from 'echarts' | |
| 34 | +import { getRepairIndex, getComplaintIndex } from '@/api/index/propertyIndexApi' | |
| 35 | + | |
| 36 | +export default { | |
| 37 | + name: 'IndexRepairComplaint', | |
| 38 | + data() { | |
| 39 | + return { | |
| 40 | + indexRepairComplaintInfo: { | |
| 41 | + allCount: 0, | |
| 42 | + waitCount: 0, | |
| 43 | + doingCount: 0, | |
| 44 | + finishCount: 0, | |
| 45 | + allComplaintCount: 0, | |
| 46 | + waitComplaintCount: 0, | |
| 47 | + finishComplaintCount: 0, | |
| 48 | + }, | |
| 49 | + repairChart: null, | |
| 50 | + complaintChart: null | |
| 51 | + } | |
| 52 | + }, | |
| 53 | + computed: { | |
| 54 | + repairStats() { | |
| 55 | + return [ | |
| 56 | + { label: 'allRepair', value: this.indexRepairComplaintInfo.allCount }, | |
| 57 | + { label: 'toBeDispatched', value: this.indexRepairComplaintInfo.waitCount }, | |
| 58 | + { label: 'processing', value: this.indexRepairComplaintInfo.doingCount }, | |
| 59 | + { label: 'processed', value: this.indexRepairComplaintInfo.finishCount } | |
| 60 | + ] | |
| 61 | + }, | |
| 62 | + complaintStats() { | |
| 63 | + return [ | |
| 64 | + { label: 'allComplaint', value: this.indexRepairComplaintInfo.allComplaintCount }, | |
| 65 | + { label: 'processing', value: this.indexRepairComplaintInfo.waitComplaintCount }, | |
| 66 | + { label: 'processed', value: this.indexRepairComplaintInfo.finishComplaintCount } | |
| 67 | + ] | |
| 68 | + } | |
| 69 | + }, | |
| 70 | + created() { | |
| 71 | + | |
| 72 | + }, | |
| 73 | + mounted() { | |
| 74 | + window.addEventListener('resize', this.handleResize) | |
| 75 | + }, | |
| 76 | + beforeDestroy() { | |
| 77 | + window.removeEventListener('resize', this.handleResize) | |
| 78 | + if (this.repairChart) { | |
| 79 | + this.repairChart.dispose() | |
| 80 | + } | |
| 81 | + if (this.complaintChart) { | |
| 82 | + this.complaintChart.dispose() | |
| 83 | + } | |
| 84 | + }, | |
| 85 | + methods: { | |
| 86 | + initData() { | |
| 87 | + this._loadIndexRepairData() | |
| 88 | + this._loadIndexComplaintData() | |
| 89 | + }, | |
| 90 | + handleResize() { | |
| 91 | + if (this.repairChart) { | |
| 92 | + this.repairChart.resize() | |
| 93 | + } | |
| 94 | + if (this.complaintChart) { | |
| 95 | + this.complaintChart.resize() | |
| 96 | + } | |
| 97 | + }, | |
| 98 | + async _loadIndexRepairData() { | |
| 99 | + const param = { | |
| 100 | + page: 1, | |
| 101 | + row: 10, | |
| 102 | + communityId: this.getCommunityId() | |
| 103 | + } | |
| 104 | + | |
| 105 | + try { | |
| 106 | + const res = await getRepairIndex(param) | |
| 107 | + Object.assign(this.indexRepairComplaintInfo, res.data) | |
| 108 | + | |
| 109 | + this.$nextTick(() => { | |
| 110 | + const dom = document.getElementById('repairCount') | |
| 111 | + if (!dom) return | |
| 112 | + | |
| 113 | + if (this.repairChart) { | |
| 114 | + this.repairChart.dispose() | |
| 115 | + } | |
| 116 | + this.repairChart = echarts.init(dom) | |
| 117 | + this._initEcharts( | |
| 118 | + this.repairChart, | |
| 119 | + this.indexRepairComplaintInfo.finishCount, | |
| 120 | + this.indexRepairComplaintInfo.allCount - this.indexRepairComplaintInfo.finishCount, | |
| 121 | + this.$t('propertyIndex.repairInfo'), | |
| 122 | + this.$t('propertyIndex.processed'), | |
| 123 | + this.$t('propertyIndex.unprocessed'), | |
| 124 | + '#4B7AF0', | |
| 125 | + '#E2EDF6' | |
| 126 | + ) | |
| 127 | + }) | |
| 128 | + } catch (error) { | |
| 129 | + console.error('请求失败处理', error) | |
| 130 | + } | |
| 131 | + }, | |
| 132 | + async _loadIndexComplaintData() { | |
| 133 | + const param = { | |
| 134 | + page: 1, | |
| 135 | + row: 10, | |
| 136 | + communityId: this.getCommunityId() | |
| 137 | + } | |
| 138 | + | |
| 139 | + try { | |
| 140 | + const res = await getComplaintIndex(param) | |
| 141 | + Object.assign(this.indexRepairComplaintInfo, res.data) | |
| 142 | + | |
| 143 | + this.$nextTick(() => { | |
| 144 | + const dom = document.getElementById('complaintCount') | |
| 145 | + if (!dom) return | |
| 146 | + | |
| 147 | + if (this.complaintChart) { | |
| 148 | + this.complaintChart.dispose() | |
| 149 | + } | |
| 150 | + this.complaintChart = echarts.init(dom) | |
| 151 | + this._initEcharts( | |
| 152 | + this.complaintChart, | |
| 153 | + this.indexRepairComplaintInfo.finishComplaintCount, | |
| 154 | + this.indexRepairComplaintInfo.allComplaintCount - this.indexRepairComplaintInfo.finishComplaintCount, | |
| 155 | + this.$t('propertyIndex.complaintStats'), | |
| 156 | + this.$t('propertyIndex.processed'), | |
| 157 | + this.$t('propertyIndex.unprocessed'), | |
| 158 | + '#01C36D', | |
| 159 | + '#E2EDF6' | |
| 160 | + ) | |
| 161 | + }) | |
| 162 | + } catch (error) { | |
| 163 | + console.error('请求失败处理', error) | |
| 164 | + } | |
| 165 | + }, | |
| 166 | + _initEcharts(chart, userCount, freeCount, _title, _userCountName, _freeCountName, userColor, freeColor) { | |
| 167 | + const option = { | |
| 168 | + tooltip: { | |
| 169 | + trigger: 'item', | |
| 170 | + formatter: '{a} <br/>{b}: {c} ({d}%)' | |
| 171 | + }, | |
| 172 | + legend: { | |
| 173 | + orient: 'vertical', | |
| 174 | + right: 10, | |
| 175 | + top: 'center', | |
| 176 | + textStyle: { | |
| 177 | + color: '#606266' | |
| 178 | + }, | |
| 179 | + data: [_userCountName, _freeCountName] | |
| 180 | + }, | |
| 181 | + color: [userColor, freeColor], | |
| 182 | + series: [{ | |
| 183 | + name: _title, | |
| 184 | + type: 'pie', | |
| 185 | + radius: ['40%', '65%'], | |
| 186 | + avoidLabelOverlap: false, | |
| 187 | + label: { | |
| 188 | + show: false, | |
| 189 | + position: 'center' | |
| 190 | + }, | |
| 191 | + emphasis: { | |
| 192 | + label: { | |
| 193 | + show: true, | |
| 194 | + fontSize: '16', | |
| 195 | + fontWeight: 'bold' | |
| 196 | + } | |
| 197 | + }, | |
| 198 | + labelLine: { | |
| 199 | + show: false | |
| 200 | + }, | |
| 201 | + data: [ | |
| 202 | + { value: userCount, name: _userCountName }, | |
| 203 | + { value: freeCount, name: _freeCountName } | |
| 204 | + ] | |
| 205 | + }] | |
| 206 | + } | |
| 207 | + | |
| 208 | + chart.setOption(option) | |
| 209 | + } | |
| 210 | + } | |
| 211 | +} | |
| 212 | +</script> | |
| 213 | + | |
| 214 | +<style lang="scss" scoped> | |
| 215 | +.repair-complaint-container { | |
| 216 | + | |
| 217 | +} | |
| 218 | + | |
| 219 | +.index-1-left-item { | |
| 220 | + background: #fff; | |
| 221 | + border-radius: 4px; | |
| 222 | + padding: 15px; | |
| 223 | + | |
| 224 | + .index-title { | |
| 225 | + text-align: left; | |
| 226 | + font-size: 16px; | |
| 227 | + font-weight: bold; | |
| 228 | + margin-bottom: 15px; | |
| 229 | + color: #333; | |
| 230 | + } | |
| 231 | + | |
| 232 | + .stats-container { | |
| 233 | + padding-top: 15px; | |
| 234 | + border-top: 1px solid #eee; | |
| 235 | + | |
| 236 | + >div { | |
| 237 | + flex: 1; | |
| 238 | + } | |
| 239 | + | |
| 240 | + .index-bottom-number { | |
| 241 | + font-size: 20px; | |
| 242 | + font-weight: bold; | |
| 243 | + color: #333; | |
| 244 | + } | |
| 245 | + | |
| 246 | + .index-bottom-number-desc { | |
| 247 | + font-size: 12px; | |
| 248 | + color: #999; | |
| 249 | + margin-top: 5px; | |
| 250 | + } | |
| 251 | + } | |
| 252 | +} | |
| 253 | +</style> | |
| 0 | 254 | \ No newline at end of file | ... | ... |
src/views/index/index.vue
| ... | ... | @@ -7,6 +7,9 @@ |
| 7 | 7 | <div v-if="storeInfo.storeTypeCd == '800900000001'"> |
| 8 | 8 | <index-admin></index-admin> |
| 9 | 9 | </div> |
| 10 | + <div v-if="storeInfo.storeTypeCd == '800900000003'"> | |
| 11 | + <index-property></index-property> | |
| 12 | + </div> | |
| 10 | 13 | </div> |
| 11 | 14 | </template> |
| 12 | 15 | |
| ... | ... | @@ -15,12 +18,14 @@ |
| 15 | 18 | import {deepCopy} from "@/utils/vc" |
| 16 | 19 | import indexDev from "@/components/index/index-dev.vue" |
| 17 | 20 | import indexAdmin from "@/components/index/index-admin.vue" |
| 21 | + import indexProperty from '@/components/index/index-property.vue' | |
| 18 | 22 | |
| 19 | 23 | export default { |
| 20 | 24 | name: 'index', |
| 21 | 25 | components: { |
| 22 | 26 | indexDev, |
| 23 | - indexAdmin | |
| 27 | + indexAdmin, | |
| 28 | + indexProperty | |
| 24 | 29 | }, |
| 25 | 30 | data() { |
| 26 | 31 | return { | ... | ... |
src/views/index/indexLang.js
| ... | ... | @@ -8,6 +8,41 @@ export const messages = { |
| 8 | 8 | repairCount: 'Repair Count', |
| 9 | 9 | communityFeeStats: 'Community Fee Statistics', |
| 10 | 10 | communityRepairStats: 'Community Repair Statistics' |
| 11 | + }, | |
| 12 | + propertyIndex: { | |
| 13 | + home: 'Home', | |
| 14 | + dashboard: 'Dashboard', | |
| 15 | + communityInfo: 'Community Information', | |
| 16 | + building: 'Building', | |
| 17 | + house: 'House', | |
| 18 | + shop: 'Shop', | |
| 19 | + parkingSpace: 'Parking Space', | |
| 20 | + residentReg: 'Resident Registration', | |
| 21 | + registerResident: 'Register Resident', | |
| 22 | + vehicle: 'Vehicle', | |
| 23 | + viewVehicle: 'View Vehicles', | |
| 24 | + unit: 'unit', | |
| 25 | + ownerFeedback: 'Owner Feedback', | |
| 26 | + noFeedback: 'No feedback content currently', | |
| 27 | + feedback: 'feedback', | |
| 28 | + completed: 'Completed', | |
| 29 | + followingUp: 'Following up', | |
| 30 | + transferRepair: 'Transferred to repair order', | |
| 31 | + repairStats: 'Repair Statistics', | |
| 32 | + allRepair: 'All Repairs', | |
| 33 | + toBeDispatched: 'To be dispatched', | |
| 34 | + processing: 'Processing', | |
| 35 | + processed: 'Processed', | |
| 36 | + complaintStats: 'Complaint Statistics', | |
| 37 | + allComplaint: 'All Complaints', | |
| 38 | + repairInfo: 'Repair Info', | |
| 39 | + unprocessed: 'Unprocessed', | |
| 40 | + residentRegStats: 'Resident Registration Statistics', | |
| 41 | + unregistered: 'Unregistered', | |
| 42 | + registered: 'Registered', | |
| 43 | + unboundRoom: 'Unbound Room', | |
| 44 | + boundRoom: 'Bound Room', | |
| 45 | + residentInfo: 'Resident Info' | |
| 11 | 46 | } |
| 12 | 47 | }, |
| 13 | 48 | zh: { |
| ... | ... | @@ -19,6 +54,41 @@ export const messages = { |
| 19 | 54 | repairCount: '报修单数', |
| 20 | 55 | communityFeeStats: '小区缴费统计', |
| 21 | 56 | communityRepairStats: '小区报修统计' |
| 57 | + }, | |
| 58 | + propertyIndex: { | |
| 59 | + home: '首页', | |
| 60 | + dashboard: '控制台', | |
| 61 | + communityInfo: '小区信息', | |
| 62 | + building: '楼宇', | |
| 63 | + house: '房屋', | |
| 64 | + shop: '商铺', | |
| 65 | + parkingSpace: '车位', | |
| 66 | + residentReg: '住户登记', | |
| 67 | + registerResident: '登记住户', | |
| 68 | + vehicle: '车辆', | |
| 69 | + viewVehicle: '查看车辆', | |
| 70 | + unit: '个', | |
| 71 | + ownerFeedback: '业主反馈', | |
| 72 | + noFeedback: '当前没有业主反馈内容', | |
| 73 | + feedback: '反馈', | |
| 74 | + completed: '完成', | |
| 75 | + followingUp: '跟进中', | |
| 76 | + transferRepair: '已转报修单', | |
| 77 | + repairStats: '报修统计', | |
| 78 | + allRepair: '全部报修', | |
| 79 | + toBeDispatched: '待派单', | |
| 80 | + processing: '处理中', | |
| 81 | + processed: '已处理', | |
| 82 | + complaintStats: '投诉统计', | |
| 83 | + allComplaint: '全部投诉', | |
| 84 | + repairInfo: '报修信息', | |
| 85 | + unprocessed: '未处理', | |
| 86 | + residentRegStats: '住户注册统计', | |
| 87 | + unregistered: '未注册', | |
| 88 | + registered: '已注册', | |
| 89 | + unboundRoom: '未绑定房屋', | |
| 90 | + boundRoom: '已绑定房屋', | |
| 91 | + residentInfo: '住户信息' | |
| 22 | 92 | } |
| 23 | 93 | } |
| 24 | 94 | } |
| 25 | 95 | \ No newline at end of file | ... | ... |