|
|
|
@ -21,6 +21,17 @@ |
|
|
|
<text style="margin-top: 6rpx;">发布</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<scroll-view |
|
|
|
class="page-scroll" |
|
|
|
scroll-y |
|
|
|
refresher-enabled |
|
|
|
:refresher-triggered="refreshing" |
|
|
|
:style="{ paddingTop: headerHeight + 'px' }" |
|
|
|
:lower-threshold="120" |
|
|
|
@refresherrefresh="onPullRefresh" |
|
|
|
@scrolltolower="loadMore" |
|
|
|
> |
|
|
|
<view class="category-row"> |
|
|
|
<view |
|
|
|
class="category-card" |
|
|
|
@ -30,15 +41,13 @@ |
|
|
|
@tap="selectCategory(item.tone)" |
|
|
|
> |
|
|
|
<view class="hot-category-icon"> |
|
|
|
<image class="hot-category-img" :src="item.icon" mode="aspectFit"></image> |
|
|
|
<image class="hot-category-img" :src="getReleaseChannelIcon(item)" mode="aspectFit"></image> |
|
|
|
</view> |
|
|
|
<view class="category-badge" v-if="item.tone === 'mine' && myCircleUnread > 0">{{formatBadge(myCircleUnread)}}</view> |
|
|
|
<view class="hot-category-name">{{item.name}}</view> |
|
|
|
<view class="hot-category-desc">{{item.desc}}</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<scroll-view class="page-scroll" scroll-y :style="{ paddingTop: headerHeight + 'px' }" :lower-threshold="120" @scrolltolower="loadMore"> |
|
|
|
<view class="sub-category-scroll" v-if="activeCategory !== 'mine'"> |
|
|
|
<view |
|
|
|
class="sub-category" |
|
|
|
@ -98,30 +107,40 @@ |
|
|
|
<image class="avatar" :src="displayAvatar(item)" mode="aspectFill"></image> |
|
|
|
<view class="author-main"> |
|
|
|
<view class="author">{{displayName(item)}}</view> |
|
|
|
<view class="time">{{item.createTime || ''}} · {{item.categoryName || '本校'}}</view> |
|
|
|
<view class="time">{{item.createTime || ''}} · <text class="time-category">{{item.categoryName || '本校'}}</text></view> |
|
|
|
</view> |
|
|
|
<view class="partner-action" v-if="canUsePartnerChat(item)" @tap.stop="goChat(item)">搭一下</view> |
|
|
|
</view> |
|
|
|
<view class="title">{{item.title}}</view> |
|
|
|
<view class="content">{{item.contentText || stripHtml(item.content)}}</view> |
|
|
|
<view v-if="getImages(item).length" class="image-grid" :class="'image-grid--' + Math.min(getImages(item).length, 3)"> |
|
|
|
<view v-if="getImages(item).length === 1" class="image-grid image-grid--1"> |
|
|
|
<image |
|
|
|
v-for="(pic, index) in getImages(item).slice(0, 3)" |
|
|
|
:key="pic" |
|
|
|
:src="pic" |
|
|
|
:mode="getImages(item).length === 1 ? 'aspectFit' : 'aspectFill'" |
|
|
|
:style="getPostImageStyle(pic, getImages(item).length)" |
|
|
|
@load="onPostImageLoad(pic, $event)" |
|
|
|
@tap.stop="previewImages(item, index)" |
|
|
|
:src="getImages(item)[0]" |
|
|
|
mode="aspectFit" |
|
|
|
:style="getPostImageStyle(getImages(item)[0], 1)" |
|
|
|
@load="onPostImageLoad(getImages(item)[0], $event)" |
|
|
|
@tap.stop="previewImages(item, 0)" |
|
|
|
></image> |
|
|
|
</view> |
|
|
|
<scroll-view v-else-if="getImages(item).length > 1" scroll-x class="image-scroll" @tap.stop> |
|
|
|
<view class="image-scroll-inner"> |
|
|
|
<image |
|
|
|
class="image-scroll-item" |
|
|
|
v-for="(pic, index) in getImages(item)" |
|
|
|
:key="pic" |
|
|
|
:src="pic" |
|
|
|
mode="aspectFill" |
|
|
|
@tap.stop="previewImages(item, index)" |
|
|
|
></image> |
|
|
|
</view> |
|
|
|
</scroll-view> |
|
|
|
<view class="tag-row" v-if="getTags(item).length"> |
|
|
|
<text v-for="tag in getTags(item)" :key="tag">{{tag}}</text> |
|
|
|
</view> |
|
|
|
<view class="partner-info" v-if="item.postType === 'partner'"> |
|
|
|
<text>{{item.location || '地点待定'}}</text> |
|
|
|
<text>{{item.activityTime || '时间可商量'}}</text> |
|
|
|
<text>{{item.joinedCount || 0}}/{{item.targetCount || '不限'}} 人联系</text> |
|
|
|
<text>人数 {{item.targetCount ? item.targetCount + '人' : '不限'}}</text> |
|
|
|
</view> |
|
|
|
<view class="contact-row" v-if="item.contactWechat || item.contactPhone"> |
|
|
|
<text v-if="item.contactWechat">微信 {{item.contactWechat}}</text> |
|
|
|
@ -223,12 +242,17 @@ |
|
|
|
{ tone: 'sport', name: '运动搭子', desc: '约球夜跑', icon: 'https://jewel-shop.oss-cn-beijing.aliyuncs.com/01444d0812c544fe9cde7cbbb919b473.png' }, |
|
|
|
{ tone: 'mine', name: '我的圈子', desc: '消息动态', icon: 'https://jewel-shop.oss-cn-beijing.aliyuncs.com/430e4ae880574845b7d028bd96abde02.jpg' } |
|
|
|
] |
|
|
|
const RELEASE_ICON_CACHE_KEY = 'release_channel_icon_cache_v1' |
|
|
|
const RELEASE_ICON_CACHE_TTL = 30 * 24 * 60 * 60 * 1000 |
|
|
|
const RELEASE_ICON_CACHE_LIMIT = 12 |
|
|
|
const releaseIconDownloading = {} |
|
|
|
|
|
|
|
export default { |
|
|
|
data() { |
|
|
|
return { |
|
|
|
statusBarHeight: 24, |
|
|
|
channels: CHANNELS, |
|
|
|
releaseChannelIcons: {}, |
|
|
|
activeCategory: 'wall', |
|
|
|
activeCategoryId: '', |
|
|
|
keyword: '', |
|
|
|
@ -239,6 +263,7 @@ |
|
|
|
pageSize: 10, |
|
|
|
totalPages: 1, |
|
|
|
loading: false, |
|
|
|
refreshing: false, |
|
|
|
inited: false, |
|
|
|
matching: false, |
|
|
|
showMatchPanel: false, |
|
|
|
@ -260,7 +285,7 @@ |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
headerHeight() { |
|
|
|
return this.statusBarHeight + (uni.upx2px ? uni.upx2px(424) : 212) |
|
|
|
return this.statusBarHeight + (uni.upx2px ? uni.upx2px(204) : 102) |
|
|
|
}, |
|
|
|
hasMore() { |
|
|
|
return this.pageNum < this.totalPages |
|
|
|
@ -289,6 +314,9 @@ |
|
|
|
return '支持关键词和热门标签模糊匹配' |
|
|
|
} |
|
|
|
}, |
|
|
|
created() { |
|
|
|
this.applyCachedReleaseChannelIcons() |
|
|
|
}, |
|
|
|
mounted() { |
|
|
|
if (uni.getMenuButtonBoundingClientRect) { |
|
|
|
this.statusBarHeight = uni.getMenuButtonBoundingClientRect().top || this.statusBarHeight |
|
|
|
@ -302,6 +330,136 @@ |
|
|
|
if (this.reactionBurstTimer) clearTimeout(this.reactionBurstTimer) |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
getReleaseChannelIcon(item) { |
|
|
|
if (!item) return '' |
|
|
|
return this.releaseChannelIcons[item.tone] || item.icon || '' |
|
|
|
}, |
|
|
|
getReleaseIconCache() { |
|
|
|
const cache = uni.getStorageSync(RELEASE_ICON_CACHE_KEY) |
|
|
|
return cache && typeof cache === 'object' ? cache : {} |
|
|
|
}, |
|
|
|
saveReleaseIconCache(cache) { |
|
|
|
uni.setStorageSync(RELEASE_ICON_CACHE_KEY, cache) |
|
|
|
}, |
|
|
|
isSavedFileExists(filePath) { |
|
|
|
if (!filePath || filePath.indexOf('__tmp__') !== -1) return false |
|
|
|
try { |
|
|
|
const fs = (uni.getFileSystemManager && uni.getFileSystemManager()) |
|
|
|
|| (typeof wx !== 'undefined' && wx.getFileSystemManager ? wx.getFileSystemManager() : null) |
|
|
|
if (fs && fs.accessSync) { |
|
|
|
fs.accessSync(filePath) |
|
|
|
return true |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
return false |
|
|
|
} |
|
|
|
return true |
|
|
|
}, |
|
|
|
isReleaseIconCacheValid(cacheItem) { |
|
|
|
return !!(cacheItem |
|
|
|
&& Date.now() - Number(cacheItem.savedAt || 0) < RELEASE_ICON_CACHE_TTL |
|
|
|
&& this.isSavedFileExists(cacheItem.path)) |
|
|
|
}, |
|
|
|
applyCachedReleaseChannelIcons() { |
|
|
|
const cache = this.getReleaseIconCache() |
|
|
|
CHANNELS.forEach((item) => { |
|
|
|
const imageUrl = item.icon |
|
|
|
const cacheItem = cache[imageUrl] |
|
|
|
if (this.isReleaseIconCacheValid(cacheItem)) { |
|
|
|
this.$set(this.releaseChannelIcons, item.tone, cacheItem.path) |
|
|
|
return |
|
|
|
} |
|
|
|
if (cacheItem && cacheItem.path) { |
|
|
|
delete cache[imageUrl] |
|
|
|
} |
|
|
|
this.$set(this.releaseChannelIcons, item.tone, imageUrl) |
|
|
|
this.cacheReleaseChannelIcon(item.tone, imageUrl) |
|
|
|
}) |
|
|
|
this.pruneReleaseIconCache(cache, CHANNELS.map(item => item.icon)) |
|
|
|
}, |
|
|
|
cacheReleaseChannelIcon(tone, imageUrl) { |
|
|
|
if (releaseIconDownloading[imageUrl]) return |
|
|
|
releaseIconDownloading[imageUrl] = true |
|
|
|
uni.downloadFile({ |
|
|
|
url: imageUrl, |
|
|
|
success: (downloadRes) => { |
|
|
|
if (downloadRes.statusCode !== 200 || !downloadRes.tempFilePath) { |
|
|
|
this.$set(this.releaseChannelIcons, tone, imageUrl) |
|
|
|
delete releaseIconDownloading[imageUrl] |
|
|
|
return |
|
|
|
} |
|
|
|
uni.saveFile({ |
|
|
|
tempFilePath: downloadRes.tempFilePath, |
|
|
|
success: (saveRes) => { |
|
|
|
const cache = this.getReleaseIconCache() |
|
|
|
const oldCacheItem = cache[imageUrl] |
|
|
|
if (oldCacheItem && oldCacheItem.path && oldCacheItem.path !== saveRes.savedFilePath) { |
|
|
|
uni.removeSavedFile({ |
|
|
|
filePath: oldCacheItem.path, |
|
|
|
fail: () => {} |
|
|
|
}) |
|
|
|
} |
|
|
|
cache[imageUrl] = { |
|
|
|
path: saveRes.savedFilePath, |
|
|
|
savedAt: Date.now() |
|
|
|
} |
|
|
|
this.saveReleaseIconCache(cache) |
|
|
|
this.$set(this.releaseChannelIcons, tone, saveRes.savedFilePath) |
|
|
|
}, |
|
|
|
fail: () => { |
|
|
|
this.$set(this.releaseChannelIcons, tone, imageUrl) |
|
|
|
}, |
|
|
|
complete: () => { |
|
|
|
delete releaseIconDownloading[imageUrl] |
|
|
|
} |
|
|
|
}) |
|
|
|
}, |
|
|
|
fail: () => { |
|
|
|
this.$set(this.releaseChannelIcons, tone, imageUrl) |
|
|
|
delete releaseIconDownloading[imageUrl] |
|
|
|
}, |
|
|
|
complete: (downloadRes) => { |
|
|
|
if (!downloadRes || downloadRes.statusCode !== 200) { |
|
|
|
this.$set(this.releaseChannelIcons, tone, imageUrl) |
|
|
|
delete releaseIconDownloading[imageUrl] |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
}, |
|
|
|
pruneReleaseIconCache(cache, activeUrls) { |
|
|
|
const activeUrlMap = activeUrls.reduce((result, url) => { |
|
|
|
result[url] = true |
|
|
|
return result |
|
|
|
}, {}) |
|
|
|
const now = Date.now() |
|
|
|
Object.keys(cache).forEach((url) => { |
|
|
|
const item = cache[url] |
|
|
|
const isExpired = !this.isReleaseIconCacheValid(item) |
|
|
|
if (isExpired && !activeUrlMap[url]) { |
|
|
|
item && item.path && uni.removeSavedFile({ |
|
|
|
filePath: item.path, |
|
|
|
fail: () => {} |
|
|
|
}) |
|
|
|
delete cache[url] |
|
|
|
} |
|
|
|
}) |
|
|
|
const urls = Object.keys(cache).sort((a, b) => Number(cache[a].savedAt || now) - Number(cache[b].savedAt || now)) |
|
|
|
while (urls.length > RELEASE_ICON_CACHE_LIMIT) { |
|
|
|
const url = urls.shift() |
|
|
|
if (activeUrlMap[url]) { |
|
|
|
urls.push(url) |
|
|
|
if (urls.every(itemUrl => activeUrlMap[itemUrl])) break |
|
|
|
continue |
|
|
|
} |
|
|
|
const item = cache[url] |
|
|
|
item && item.path && uni.removeSavedFile({ |
|
|
|
filePath: item.path, |
|
|
|
fail: () => {} |
|
|
|
}) |
|
|
|
delete cache[url] |
|
|
|
} |
|
|
|
this.saveReleaseIconCache(cache) |
|
|
|
}, |
|
|
|
init() { |
|
|
|
if (this.inited && this.loading) return |
|
|
|
this.inited = true |
|
|
|
@ -354,6 +512,10 @@ |
|
|
|
this.myCircleUnread = Number(summary.myCircleUnread || summary.campusUnread) || 0 |
|
|
|
}, |
|
|
|
selectCategory(tone) { |
|
|
|
if (tone === 'mine') { |
|
|
|
uni.navigateTo({ url: '/package1/campus/myCampusPosts' }) |
|
|
|
return |
|
|
|
} |
|
|
|
if (this.activeCategory === tone) return |
|
|
|
this.activeCategory = tone |
|
|
|
this.activeCategoryId = '' |
|
|
|
@ -427,6 +589,16 @@ |
|
|
|
this.posts = [] |
|
|
|
this.loadPosts() |
|
|
|
}, |
|
|
|
onPullRefresh() { |
|
|
|
if (this.refreshing) return |
|
|
|
if (this.loading) return |
|
|
|
this.refreshing = true |
|
|
|
this.loadUnread() |
|
|
|
this.pageNum = 1 |
|
|
|
this.totalPages = 1 |
|
|
|
this.posts = [] |
|
|
|
this.loadPosts() |
|
|
|
}, |
|
|
|
loadMore() { |
|
|
|
if (this.loading || !this.hasMore) return |
|
|
|
this.pageNum += 1 |
|
|
|
@ -459,6 +631,7 @@ |
|
|
|
this.posts = this.pageNum === 1 ? records : this.posts.concat(records) |
|
|
|
}).finally(() => { |
|
|
|
this.loading = false |
|
|
|
this.refreshing = false |
|
|
|
}) |
|
|
|
}, |
|
|
|
getImages(item) { |
|
|
|
@ -515,7 +688,7 @@ |
|
|
|
return item && item.userAvatar ? item.userAvatar : this.defaultAvatar |
|
|
|
}, |
|
|
|
canUsePartnerChat(item) { |
|
|
|
return !!(item && item.postType === 'partner' && item.partnerType !== 'game') |
|
|
|
return false |
|
|
|
}, |
|
|
|
getReactionCount(item, emoji) { |
|
|
|
if (!item || !item.reactionCounts) return 0 |
|
|
|
@ -617,7 +790,7 @@ |
|
|
|
.search-input { flex: 1; height: 62rpx; margin-left: 9rpx; color: #26302c; font-size: 22rpx; line-height: 62rpx; } |
|
|
|
.placeholder { color: #a0a6a4; } |
|
|
|
.search-button { width: 94rpx; height: 56rpx; margin-right: 5rpx; border-radius: 30rpx; background: linear-gradient(135deg, #2bd398, #0db779); box-shadow: 0 7rpx 15rpx rgba(20, 182, 123, .24); color: #fff; font-size: 21rpx; font-weight: 800; line-height: 56rpx; text-align: center; } |
|
|
|
.category-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14rpx; margin-top: 18rpx; padding: 10rpx 24rpx 20rpx; } |
|
|
|
.category-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14rpx; padding: 10rpx 24rpx 20rpx; } |
|
|
|
.category-card { position: relative; height: 170rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 24rpx; background: linear-gradient(145deg, #ffffff, #edf5ff); text-align: center; box-shadow: 0 10rpx 24rpx rgba(35, 74, 61, 0.075); border: 2rpx solid transparent; box-sizing: border-box; } |
|
|
|
.category-card.active { border-color: rgba(34, 191, 134, 0.55); box-shadow: 0 14rpx 30rpx rgba(34, 191, 134, 0.14); transform: translateY(-3rpx); } |
|
|
|
.category-badge { position: absolute; right: 8rpx; top: 8rpx; min-width: 32rpx; height: 32rpx; line-height: 32rpx; padding: 0 8rpx; border-radius: 999rpx; background: #ff5b5b; color: #fff; font-size: 19rpx; box-sizing: border-box; } |
|
|
|
@ -677,6 +850,7 @@ |
|
|
|
.author-main { flex: 1; margin-left: 14rpx; min-width: 0; } |
|
|
|
.author { font-size: 26rpx; font-weight: 700; color: #263d38; } |
|
|
|
.time { margin-top: 4rpx; font-size: 21rpx; color: #93a09c; } |
|
|
|
.time-category { color: #56cfa4; font-weight: 800; } |
|
|
|
.partner-action { padding: 10rpx 18rpx; border-radius: 999rpx; background: #34c7a2; color: #fff; font-size: 23rpx; } |
|
|
|
.title { margin-top: 20rpx; font-size: 31rpx; font-weight: 800; color: #162d28; } |
|
|
|
.content { margin-top: 12rpx; font-size: 26rpx; color: #51645f; line-height: 1.6; } |
|
|
|
@ -686,6 +860,9 @@ |
|
|
|
.image-grid--3 { grid-template-columns: repeat(3, 1fr); } |
|
|
|
.image-grid image { width: 100%; height: 190rpx; max-width: 100%; border-radius: 20rpx; background: #eef5f3; } |
|
|
|
.image-grid--1 image { max-width: 560rpx; max-height: 360rpx; background: transparent; } |
|
|
|
.image-scroll { width: 100%; margin-top: 18rpx; white-space: nowrap; } |
|
|
|
.image-scroll-inner { display: inline-flex; gap: 8rpx; padding-right: 4rpx; } |
|
|
|
.image-scroll-item { flex: 0 0 208rpx; width: 208rpx; height: 190rpx; border-radius: 20rpx; background: #eef5f3; } |
|
|
|
.tag-row { margin-top: 16rpx; display: flex; flex-wrap: wrap; gap: 10rpx; } |
|
|
|
.tag-row text { padding: 8rpx 16rpx; border-radius: 999rpx; background: #eefaf6; color: #2f9a81; font-size: 22rpx; } |
|
|
|
.partner-info { margin-top: 14rpx; display: flex; flex-wrap: wrap; gap: 12rpx; color: #71807c; font-size: 22rpx; } |
|
|
|
|