|
@@ -6,12 +6,23 @@
|
|
|
<view class="avatar-section">
|
|
<view class="avatar-section">
|
|
|
<view class="avatar">
|
|
<view class="avatar">
|
|
|
<view class="avatar-frame">
|
|
<view class="avatar-frame">
|
|
|
- <image class="avatar-img" :src="avatarSrc" mode="aspectFill" />
|
|
|
|
|
|
|
+ <template v-if="isLoadingUser">
|
|
|
|
|
+ <view class="skeleton skeleton-circle avatar-skel" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <image class="avatar-img" :src="avatarSrc" mode="aspectFill" />
|
|
|
|
|
+ </template>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="user-details">
|
|
<view class="user-details">
|
|
|
- <text class="username">{{ user.nickname || '未登录' }}</text>
|
|
|
|
|
- <text class="user-role">家属</text>
|
|
|
|
|
|
|
+ <template v-if="isLoadingUser">
|
|
|
|
|
+ <view class="skeleton skeleton-text username-skel" />
|
|
|
|
|
+ <view class="skeleton skeleton-text small-skel" style="width:80rpx;margin-top:10rpx" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <text class="username">{{ user.nickname || '未登录' }}</text>
|
|
|
|
|
+ <text class="user-role">家属</text>
|
|
|
|
|
+ </template>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="message-button" @click="onMessageClick">
|
|
<view class="message-button" @click="onMessageClick">
|
|
|
<image src="/static/icons/remixicon/message-3-line.svg" class="message-icon" />
|
|
<image src="/static/icons/remixicon/message-3-line.svg" class="message-icon" />
|
|
@@ -52,18 +63,36 @@
|
|
|
<text class="card-title">健康资讯</text>
|
|
<text class="card-title">健康资讯</text>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="card-content">
|
|
<view class="card-content">
|
|
|
- <view class="news-item" v-for="(news, index) in newsList" :key="index" @click="onNewsClick(news)">
|
|
|
|
|
- <view v-if="news.coverImage" class="news-image-container">
|
|
|
|
|
- <image class="news-image" :src="getImageSrc(news.coverImage)" mode="aspectFill" />
|
|
|
|
|
- </view>
|
|
|
|
|
- <view v-else class="news-placeholder">
|
|
|
|
|
- <image class="placeholder-icon" src="/static/icons/remixicon/image-line.svg" />
|
|
|
|
|
|
|
+ <template v-if="isLoadingNews">
|
|
|
|
|
+ <view class="news-item" v-for="i in 3" :key="`skel-${i}`">
|
|
|
|
|
+ <view class="news-image-container">
|
|
|
|
|
+ <view class="skeleton skeleton-rect news-image-skel" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="news-text">
|
|
|
|
|
+ <view class="skeleton skeleton-text" style="width:70%" />
|
|
|
|
|
+ <view class="skeleton skeleton-text" style="width:90%;margin-top:10rpx;height:20rpx" />
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
- <view class="news-text">
|
|
|
|
|
- <text class="news-title">{{ news.title }}</text>
|
|
|
|
|
- <text class="news-desc">{{ news.summary }}</text>
|
|
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <view class="news-item" v-for="(news, index) in newsList" :key="index" @click="onNewsClick(news)">
|
|
|
|
|
+ <view v-if="getImageSrc(news.coverImage) !== defaultImage" class="news-image-container">
|
|
|
|
|
+ <image class="news-image" :src="getImageSrc(news.coverImage)" mode="aspectFill" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-else class="news-placeholder">
|
|
|
|
|
+ <view v-if="isProtectedUrl(resolveImageUrl(news.coverImage)) && !downloadedNewsImages[resolveImageUrl(news.coverImage)]" class="news-image-container">
|
|
|
|
|
+ <view class="skeleton skeleton-rect news-image-skel" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-else class="news-placeholder">
|
|
|
|
|
+ <image class="placeholder-icon" src="/static/icons/remixicon/image-line.svg" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="news-text">
|
|
|
|
|
+ <text class="news-title">{{ news.title }}</text>
|
|
|
|
|
+ <text class="news-desc">{{ news.summary }}</text>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
|
|
+ </template>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
@@ -73,7 +102,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { ref, computed, watch, onUnmounted } from 'vue'
|
|
|
|
|
|
|
+import { ref, computed, watch, onUnmounted, onMounted } from 'vue'
|
|
|
import { onShow, onHide } from '@dcloudio/uni-app'
|
|
import { onShow, onHide } from '@dcloudio/uni-app'
|
|
|
import CustomNav from '@/components/custom-nav.vue'
|
|
import CustomNav from '@/components/custom-nav.vue'
|
|
|
import { fetchUserInfo as fetchUserInfoApi } from '@/api/user'
|
|
import { fetchUserInfo as fetchUserInfoApi } from '@/api/user'
|
|
@@ -88,6 +117,10 @@ import { getUnreadMessageCount, getMessageList, markMessageAsRead, type MessageQ
|
|
|
|
|
|
|
|
const user = ref<{ avatar?: string; nickname?: string }>({})
|
|
const user = ref<{ avatar?: string; nickname?: string }>({})
|
|
|
|
|
|
|
|
|
|
+// 加载状态:用于显示骨架屏
|
|
|
|
|
+const isLoadingUser = ref(true)
|
|
|
|
|
+const isLoadingNews = ref(true)
|
|
|
|
|
+
|
|
|
// 未读消息数量
|
|
// 未读消息数量
|
|
|
const unreadMessageCount = ref(0)
|
|
const unreadMessageCount = ref(0)
|
|
|
|
|
|
|
@@ -161,6 +194,8 @@ const fetchUserInfo = async () => {
|
|
|
try {
|
|
try {
|
|
|
const token = uni.getStorageSync('token')
|
|
const token = uni.getStorageSync('token')
|
|
|
if (!token) return
|
|
if (!token) return
|
|
|
|
|
+ // 开始加载用户信息骨架
|
|
|
|
|
+ isLoadingUser.value = true
|
|
|
uni.showLoading({ title: '加载中...' })
|
|
uni.showLoading({ title: '加载中...' })
|
|
|
const response = await fetchUserInfoApi()
|
|
const response = await fetchUserInfoApi()
|
|
|
uni.hideLoading()
|
|
uni.hideLoading()
|
|
@@ -210,6 +245,8 @@ const fetchUserInfo = async () => {
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
uni.hideLoading()
|
|
uni.hideLoading()
|
|
|
console.error('Fetch user info error:', err)
|
|
console.error('Fetch user info error:', err)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ isLoadingUser.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -470,19 +507,36 @@ onUnmounted(() => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const fetchNews = async () => {
|
|
const fetchNews = async () => {
|
|
|
|
|
+ isLoadingNews.value = true
|
|
|
try {
|
|
try {
|
|
|
const result: any = await getNewsList({ page: 1, size: 5 })
|
|
const result: any = await getNewsList({ page: 1, size: 5 })
|
|
|
if (result?.data?.code === 200) {
|
|
if (result?.data?.code === 200) {
|
|
|
newsList.value = result.data.data.records || []
|
|
newsList.value = result.data.data.records || []
|
|
|
|
|
+ isLoadingNews.value = false
|
|
|
} else {
|
|
} else {
|
|
|
uni.showToast({ title: '获取资讯失败', icon: 'none' })
|
|
uni.showToast({ title: '获取资讯失败', icon: 'none' })
|
|
|
|
|
+ isLoadingNews.value = false
|
|
|
}
|
|
}
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
console.error('fetch news error', err)
|
|
console.error('fetch news error', err)
|
|
|
uni.showToast({ title: '获取资讯失败', icon: 'none' })
|
|
uni.showToast({ title: '获取资讯失败', icon: 'none' })
|
|
|
|
|
+ isLoadingNews.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ // 预读取本地 user_info,加快首屏显示
|
|
|
|
|
+ try {
|
|
|
|
|
+ const u = uni.getStorageSync('user_info')
|
|
|
|
|
+ if (u) {
|
|
|
|
|
+ user.value = u
|
|
|
|
|
+ isLoadingUser.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ // ignore
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
const onNewsClick = (news: News) => {
|
|
const onNewsClick = (news: News) => {
|
|
|
uni.navigateTo({
|
|
uni.navigateTo({
|
|
|
url: `/pages/public/news-detail?id=${news.id}`
|
|
url: `/pages/public/news-detail?id=${news.id}`
|
|
@@ -891,4 +945,53 @@ const onNewsClick = (news: News) => {
|
|
|
color: #666;
|
|
color: #666;
|
|
|
line-height: 1.4;
|
|
line-height: 1.4;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* Skeleton styles (与其他首页保持一致) */
|
|
|
|
|
+.skeleton {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #eee;
|
|
|
|
|
+}
|
|
|
|
|
+.skeleton::after {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: -150%;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ width: 150%;
|
|
|
|
|
+ background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.6), rgba(255,255,255,0));
|
|
|
|
|
+ animation: shimmer 1.2s infinite;
|
|
|
|
|
+}
|
|
|
|
|
+.skeleton-circle {
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+}
|
|
|
|
|
+.skeleton-text {
|
|
|
|
|
+ height: 30rpx;
|
|
|
|
|
+ border-radius: 6rpx;
|
|
|
|
|
+ width: 140rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.skeleton-rect {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.avatar-skel {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+.username-skel {
|
|
|
|
|
+ width: 220rpx;
|
|
|
|
|
+ height: 36rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.small-skel {
|
|
|
|
|
+ height: 26rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.news-image-skel {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes shimmer {
|
|
|
|
|
+ 0% { transform: translateX(0%); }
|
|
|
|
|
+ 100% { transform: translateX(100%); }
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|