Files
property-resident-side/property-uniapp-project/pages/index/index.vue
2026-08-11 11:44:30 +08:00

751 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="content-class">
<!-- 首次加载遮罩 -->
<view class="loading-mask" v-if="!pageReady">
<view class="loading-bg"></view>
</view>
<!-- 固定头部 -->
<view class="header-fixed" v-if="pageReady">
<view class="nar-top"></view>
<view class="status-bar"></view>
<view class="header-content">
<!-- 小区选择 -->
<view class="header-container" @click="navigateToPage">
<view class="location-area">
<view class="location-icon">
<image src="https://wuyeadmin.bugtc.com/images/home/adress.png"
style="height: 100%;width: 100%;"></image>
</view>
<view class="community-selector">
<text class="community-name">{{ currentCommunity }}</text>
<u-icon name="arrow-down-fill" size="16" :color="isDropdownOpen ? '#2979ff' : '#222222'"
:class="['dropdown-arrow', { 'rotate': isDropdownOpen }]"></u-icon>
</view>
</view>
</view>
</view>
</view>
<!-- 滚动内容区 -->
<pull-refresh-scroll
custom-class="main-scroll"
v-if="pageReady"
:refreshing="refreshing"
@refresh="onRefresh">
<!-- banner -->
<view class="bannarClass">
<up-swiper :key="bannerSwiperKey" :list="bannerList" indicator indicatorMode="dot" radius="10" circular>
<template #default="{ item, index }">
<image :src="item" mode="aspectFill" class="banner-img" @tap="bannerClick(index)"></image>
</template>
</up-swiper>
</view>
<!-- 公告 -->
<view class="noticeClass" @click="getNoticeMore">
<uni-notice-bar show-icon :text="noticeText" type="line" :key="noticeKey" show-get-more scrollable
color="#000" background-color="#fff" moreColor="#000">
<template #noticebarIcon>
<image src="https://wuyeadmin.bugtc.com/images/home/notice.png"
style="width: 38px;height: 38px;"></image>
</template>
</uni-notice-bar>
</view>
<!-- 快捷服务 -->
<view class="u-demo-block__content bg-card-open">
<up-row justify="space-between" gutter="10">
<up-col span="3" v-for="item in serviceItems" :key="item.title">
<view class="demo-layout bg-purple openClass" @click="serviceClick(item)">
<view class="iconClass">
<image :src="item.icon" class="iconClass-img"></image>
</view>
<view class="title-dome-calss">{{ item.title }}</view>
</view>
</up-col>
</up-row>
</view>
<!-- 广告卡片 -->
<view class="adCardClass">
<view class="u-demo-block__content">
<up-row justify="space-between" customStyle="margin-bottom: 10px">
<up-col span="3">
<view class="demo-layout bg-purple iconRightClass">
<image src="https://wuyeadmin.bugtc.com/images/home/ad1.png" mode=""
style="width: 100%;height: 100%;"></image>
</view>
</up-col>
<up-col span="8">
<view class="demo-layout bg-purple-light titleLeft">
<text style="color: #fff;font-size: 40rpx;font-weight: 700;">便民服务</text>
<view style="color: #fff;font-size: 28rpx;">邻里便民服务 即将上线</view>
</view>
</up-col>
</up-row>
</view>
</view>
<!-- 活动列表 -->
<uni-card class="activity-card">
<view class="eventListClass">
<view>
<text class="activity-name">活动列表</text>
</view>
<view class="nav-more">
<text class="look-more-title" @click="moreClick">查看更多</text>
</view>
</view>
<view class="activity-list">
<view class="eventListCard" v-for="(activity, index) in activityList" :key="activity.id"
@click="goToActivityDetail(activity)">
<view class="u-demo-block__content ">
<up-row justify="space-between" customStyle="margin-bottom: 10px">
<up-col span="4">
<view class="demo-layout bg-purple-light activity-img">
<view class="status-tag">
{{ activity.statusText }}
</view>
<image :src="activity.coverImage"
style="width: 100%;height: 100%;border-radius: 16rpx;" mode="aspectFill">
</image>
</view>
</up-col>
<up-col span="8">
<view class="demo-layout bg-purple activity-info">
<view class="activity-title">{{activity.title}}</view>
<view class="activity-time">
{{formatDate(activity.startTime)}}-{{formatDate(activity.endTime)}}</view>
<view class="activity-organizer">
<text>{{activity.createName}}</text>
</view>
<view class="activity-enrollment">
<text>
<text v-if="activity.quota !== 0">活动名额{{ activity.quota }},</text>
已报名{{activity.appliedCount}}
</text>
</view>
</view>
</up-col>
</up-row>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="activityList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
</uni-card>
<!-- 底部占位 -->
<view class="scroll-bottom-space"></view>
</pull-refresh-scroll>
</view>
</template>
<script setup>
import {
ref,
nextTick
} from 'vue'
import {
onLoad,
onUnload,
onShow
} from '@dcloudio/uni-app'
import moment from 'moment'
import {
getNoticeLatest,
getCurrentVillage,
getActivitylatest,
getBannarList,
getReloadUserInfo
} from "/pageSubPack/api/api.js"
import { requireLogin } from '@/common/checkLogin.js'
// 列表数据状态
const isDropdownOpen = ref(false)
const refreshing = ref(false)
const activityList = ref([])
const serviceItems = [
{ title: '一键开门', icon: 'https://wuyeadmin.bugtc.com/images/home/openDoor.png', url: '/pageSubPack/notice-list/oneClickOpen' },
{ title: '生活缴费', icon: 'https://wuyeadmin.bugtc.com/images/home/lifePay.png', url: '/pageSubPack/payment-list/paymentIndex' },
{ title: '在线报修', icon: 'https://wuyeadmin.bugtc.com/images/home/repair.png', url: '/pageSubPack/online-repair/onlineRepair' },
{ title: '访客邀请', icon: 'https://wuyeadmin.bugtc.com/images/home/visitorInvite.png', url: '/pageSubPack/visitor/visitor-list' }
]
const currentCommunity = ref('请绑定房屋')
const currentVillageId = ref('')
const bannerList = ref([])
const bannerRawData = ref([])
const bannerSwiperKey = ref(0)
const noticeText = ref('暂无公告')
const noticeKey = ref(0)
const noticeId = ref('')
const lastUserId = ref('')
const dataLoaded = ref(false)
const pageReady = ref(false)
// ======= 首页数据缓存(冷启动秒开) =======
const getHomeCacheKey = () => 'home_cache_' + (uni.getStorageSync('userId') || '')
function loadHomeCache() {
try {
const raw = uni.getStorageSync(getHomeCacheKey())
if (!raw) return false
const c = JSON.parse(raw)
if (c.community && c.community !== '请绑定房屋') currentCommunity.value = c.community
if (c.villageId) {
currentVillageId.value = c.villageId
uni.setStorageSync('currentVillageId', c.villageId)
}
if (c.bannerList && c.bannerList.length) bannerList.value = c.bannerList
if (c.bannerRaw && c.bannerRaw.length) bannerRawData.value = c.bannerRaw
if (c.notice && c.notice !== '暂无公告') noticeText.value = c.notice
if (c.noticeId) noticeId.value = c.noticeId
if (c.activityList && c.activityList.length) activityList.value = c.activityList
bannerSwiperKey.value++
return true
} catch (e) { return false }
}
function saveHomeCache() {
try {
uni.setStorageSync(getHomeCacheKey(), JSON.stringify({
community: currentCommunity.value,
villageId: currentVillageId.value,
bannerList: bannerList.value,
bannerRaw: bannerRawData.value,
notice: noticeText.value,
noticeId: noticeId.value,
activityList: activityList.value
}))
} catch (e) {}
}
// =======方法=======
const formatDate = (date) => {
if (!date) return ''
return moment(date).format('YYYY-MM-DD HH:mm')
}
const getNoticeMore = () => {
if (!requireLogin()) return
uni.setStorageSync('sourcePage', "index")
uni.navigateTo({ url: "/pageSubPack/notice-list/noticeList" })
}
const getbannarData = async () => {
try {
const res = await getBannarList({ villageId: uni.getStorageSync('currentVillageId') })
if (res.code === 200 && Array.isArray(res.data)) {
bannerRawData.value = res.data
const newList = res.data.map(item => item.url).filter(url => typeof url === 'string' && url)
if (JSON.stringify(newList) !== JSON.stringify(bannerList.value)) {
bannerList.value = newList
await nextTick()
bannerSwiperKey.value++
saveHomeCache()
}
} else if (res.code === 200) {
bannerRawData.value = []
bannerList.value = []
bannerSwiperKey.value++
}
} catch (err) {
// 静默失败
}
}
const bannerClick = (index) => {
if (!requireLogin()) return
const item = bannerRawData.value[index]
if (item && item.link) {
uni.navigateTo({ url: item.link })
}
}
const slelctHomeData = (data) => {
if (data.name) currentCommunity.value = data.name
if (data.villageId) {
currentVillageId.value = data.villageId
uni.setStorageSync('currentVillageId', data.villageId)
}
// 标记需要刷新,回到首页后 onShow 会触发 refreshAllData
dataLoaded.value = false
}
const getCurrentCommunityData = () => {
return getCurrentVillage().then(res => {
if (res.code === 200 && res.data && res.data.villageId) {
currentCommunity.value = res.data.villageName || '未命名小区'
saveHomeCache()
currentVillageId.value = res.data.villageId
uni.setStorageSync('currentVillageId', res.data.villageId)
} else {
currentCommunity.value = '请绑定房屋'
currentVillageId.value = ''
}
}).catch(err => {
// 有 token 才提示错误(无 token 用户正常浏览首页API 401 属于预期行为)
const token = uni.getStorageSync('token')
if (token) {
uni.showToast({ title: err.msg || '网络异常', icon: 'error' })
}
})
}
const getNoticeData = async () => {
try {
const res = await getNoticeLatest({ villageId: uni.getStorageSync('currentVillageId') })
if (res.code === 200) {
if (res.data) {
noticeText.value = res.data.noticeTitle || ''
noticeId.value = res.data.noticeId || ''
} else {
noticeText.value = '暂无公告'
noticeId.value = ''
}
}
} catch (err) {
noticeText.value = '暂无公告'
noticeId.value = ''
}
saveHomeCache()
await nextTick()
noticeKey.value++
}
const refreshAllData = async () => {
const token = uni.getStorageSync('token') || ''
dataLoaded.value = true
const needReload = uni.getStorageSync('needReloadUserInfo')
if (token && needReload) {
uni.removeStorageSync('needReloadUserInfo')
getReloadUserInfo().then(res => {
if (res.code === 200 && res.data) {
if (res.data.userId) uni.setStorageSync('userId', res.data.userId)
const reloadedVillageId = res.data.currentVillageId || 0
uni.setStorageSync('currentVillageId', reloadedVillageId)
}
}).catch(err => {
console.error('重新加载用户信息失败', err)
})
}
// 先获取小区信息,确保 villageId 已更新,再请求依赖 villageId 的接口
await getCurrentCommunityData()
getNoticeData()
getNewActivityList()
getbannarData()
pageReady.value = true
refreshing.value = false
}
const onRefresh = async () => {
refreshing.value = true
await nextTick()
await refreshAllData()
await nextTick()
refreshing.value = false
}
const resetPageData = () => {
currentCommunity.value = '请绑定房屋'
currentVillageId.value = ''
uni.removeStorageSync('currentVillageId')
activityList.value = []
bannerList.value = []
bannerRawData.value = []
noticeText.value = '暂无公告'
noticeId.value = ''
uni.removeStorageSync(getHomeCacheKey())
}
onShow(() => {
const currentUserId = uni.getStorageSync('userId') || ''
const userIdChanged = lastUserId.value !== currentUserId
refreshing.value = false
if (userIdChanged) {
resetPageData()
refreshAllData()
} else if (!dataLoaded.value) {
// 首次进入且之前因无 token 跳过了 API现在有 token 则补拉
refreshAllData()
}
lastUserId.value = currentUserId
})
onLoad(() => {
// 新用户(未看过引导页且未登录)跳转引导页
var hasShowGuide = uni.getStorageSync('hasShowGuide')
var token = uni.getStorageSync('token')
if (!hasShowGuide && !token) {
uni.reLaunch({ url: '/pages/navigation/navigation' })
return
}
// 优先展示缓存数据,实现冷启动秒开
loadHomeCache()
// 关闭原生 splash避免 WebView 初始化期间一直转圈
// #ifdef APP-PLUS
try { plus.navigator.closeSplashscreen() } catch (e) {}
// #endif
// 后台静默刷新最新数据
refreshAllData()
uni.$on('confirm', slelctHomeData)
lastUserId.value = uni.getStorageSync('userId') || ''
})
onUnload(() => {
uni.$off('confirm', slelctHomeData)
})
const navigateToPage = () => {
const cachedVillageId = uni.getStorageSync('currentVillageId')
if (!cachedVillageId || cachedVillageId === '0' || cachedVillageId === 0) {
uni.navigateTo({ url: '/pageSubPack/my/myHouseIndex' })
} else {
uni.navigateTo({ url: `/pageSubPack/notice-list/selectHome?villageId=${cachedVillageId}` })
}
}
const serviceClick = (item) => {
if (!requireLogin()) return
const cachedVillageId = uni.getStorageSync('currentVillageId')
if (!cachedVillageId || cachedVillageId === '0' || cachedVillageId === 0) {
uni.showToast({ title: '未绑定小区,没有权限', icon: 'none', duration: 2000 })
return
}
uni.setStorageSync('sourcePage', 'index')
uni.navigateTo({ url: item.url })
}
const getNewActivityList = async () => {
try {
const res = await getActivitylatest({ villageId: uni.getStorageSync('currentVillageId') })
if (res.code === 200) {
activityList.value = res.data || []; saveHomeCache()
} else {
activityList.value = []
}
} catch (err) {
activityList.value = []
}
}
const goToActivityDetail = (activity) => {
if (!requireLogin()) return
uni.navigateTo({
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied || ''}&statusText=${activity.statusText || ''}`
})
}
const moreClick = () => {
if (!requireLogin()) return
const cachedVillageId = uni.getStorageSync('currentVillageId')
if (!cachedVillageId || cachedVillageId === '0' || cachedVillageId === 0) {
uni.showToast({ title: '未绑定小区,没有权限', icon: 'none', duration: 2000 })
return
}
uni.setStorageSync('sourcePage', "index")
uni.navigateTo({ url: "/pageSubPack/activity-list/activityList" })
}
</script>
<style lang="scss" scoped>
.content-class {
background-color: #f9f9f9;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
/* 加载遮罩 */
.loading-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
}
.loading-bg {
width: 100%;
height: 100%;
background: repeating-linear-gradient(to right, #EDF4FA, #C0E4FB);
}
/* 固定头部 */
.header-fixed {
position: relative;
flex-shrink: 0;
.nar-top {
height: 400rpx;
width: 100%;
background: repeating-linear-gradient(to right, #EDF4FA, #C0E4FB);
position: absolute;
top: 0;
left: 0;
z-index: 0;
}
.status-bar {
height: var(--status-bar-height);
width: 100%;
flex-shrink: 0;
}
.header-content {
position: relative;
z-index: 1;
}
}
/* 滚动区域 */
.main-scroll {
flex: 1;
min-height: 0;
overflow: hidden;
position: relative;
z-index: 1;
}
.scroll-bottom-space {
height: 40rpx;
}
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
box-sizing: border-box;
position: relative;
z-index: 10;
}
// 左侧区域
.location-area {
display: flex;
align-items: center;
position: relative;
flex: 1;
min-width: 0;
.location-icon {
width: 45rpx;
height: 45rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.community-selector {
display: flex;
align-items: center;
max-width: 300rpx;
cursor: pointer;
.community-name {
font-size: 26rpx;
font-weight: 500;
color: #000;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx;
}
}
}
.bannarClass {
position: relative;
padding: 20rpx 40rpx;
height: 300rpx;
::v-deep .u-swiper {
height: 300rpx !important;
}
::v-deep .u-swiper__wrapper {
height: 300rpx !important;
}
::v-deep .u-swiper__wrapper__item__wrapper__image {
height: 300rpx !important;
}
::v-deep .banner-img {
height: 300rpx !important;
width: 100%;
border-radius: 10rpx;
}
}
.noticeClass {
padding: 0rpx 36rpx;
margin-bottom: 20rpx;
}
::v-deep .uni-noticebar__content-wrapper--scrollable {
margin-right: 20px;
}
::v-deep .uni-noticebar {
padding: 4px 12px;
}
.bg-card-open {
padding: 20rpx;
background-color: #fff;
border-radius: 8rpx;
margin: 0 18px 20rpx;
}
.openClass {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20rpx;
.iconClass {
width: 88rpx;
height: 88rpx;
border-radius: 10rpx;
}
.iconClass-img {
width: 100%;
height: 100%;
border-radius: 8px
}
.title-dome-calss {
margin-top: 10rpx;
font-size: 28rpx;
color: #000;
}
}
.adCardClass {
height: 200rpx;
margin: 0 30rpx 20rpx;
background-color: #3E8EFF;
border-radius: 28rpx;
.titleLeft {
text-align: left;
line-height: 2;
}
.iconRightClass {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
margin: 36rpx;
}
}
.uni-card {
margin: 0 18px 20rpx !important;
}
.eventListClass {
margin: 10rpx 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.activity-name {
font-size: 32rpx;
font-weight: 600;
color: #222;
}
.eventListCard {
position: relative;
margin: 30rpx 0rpx;
.activity-img {
width: 170rpx;
height: 230rpx;
position: relative;
}
.status-tag {
position: absolute;
top: 0;
left: 0;
padding: 0px 7px 10px 11px;
border-radius: 10px 0px 10px 0px;
font-size: 12px;
color: #fff;
font-weight: 500;
width: 80rpx;
height: 20rpx;
background: repeating-linear-gradient(to right, #FD7373, #FB3F3F);
z-index: 5;
}
.activity-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 12px;
line-height: 1.4;
white-space: normal;
word-break: break-all;
overflow-wrap: break-word;
}
.activity-time {
display: flex;
align-items: center;
font-size: 22rpx;
color: #999999;
margin-bottom: 8px;
}
.activity-organizer {
font-size: 28rpx;
display: flex;
align-items: center;
color: #999999;
margin-bottom: 8px;
}
.activity-enrollment {
display: flex;
align-items: center;
font-size: 28rpx;
margin-top: 20rpx;
color: #666666;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
.look-more-title {
color: #3E8EFF;
font-size: 28rpx;
}
}
</style>