deploy: auto commit repo-root changes 2026-08-03 21:44:47

This commit is contained in:
hajimi
2026-08-03 21:44:48 +08:00
parent 9df5f5f620
commit 29b4913234
3 changed files with 305 additions and 25 deletions
@@ -155,6 +155,12 @@
<u-icon name="thumb-up" size="34" :color="post.is_liked ? '#1468f5' : '#737373'" />
<text :class="{ 'is-active': post.is_liked }">{{ formatCount(post.like_count) }}</text>
</view>
<view class="detail-interaction__stat detail-interaction__stat--favorite"
@tap="handleFavorite">
<u-icon :name="isFavorite ? 'star-fill' : 'star'" size="34"
:color="isFavorite ? '#e6a23c' : '#737373'" />
<text :class="{ 'is-active': isFavorite }">{{ isFavorite ? '已藏' : '收藏' }}</text>
</view>
<view class="detail-interaction__stat">
<u-icon name="chat" size="34" color="#737373" />
<text>{{ formatCount(post.comment_count) }}</text>
@@ -243,7 +249,7 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { onLoad, onShow } from '@dcloudio/uni-app'
import SharePopup from '@/components/share-popup/share-popup.vue'
import {
addComment,
@@ -263,6 +269,7 @@ import { useUserStore } from '@/stores/user'
import { AgreementEnum } from '@/enums/agreementEnums'
import { formatCount } from '@/utils/number'
import { getToken } from '@/utils/auth'
import { isPostFavorite, togglePostFavorite } from '@/utils/post-favorite'
const LIUHE_TAGS = ['旧澳六合', '新澳六合']
const appStore = useAppStore()
@@ -290,6 +297,7 @@ const inputContent = ref('')
const replyTarget = ref<any>(null)
const purchasing = ref(false)
const likePending = ref(false)
const isFavorite = ref(false)
const commentLikePending = ref<Set<number>>(new Set())
const userPoints = ref<number | null>(null)
@@ -541,6 +549,7 @@ const fetchDetail = async (id: number) => {
try {
const result = await getCommunityPostDetail({ id })
post.value = result
isFavorite.value = isPostFavorite(result?.id || id)
translatedText.value = result?.translated_content || ''
autoTranslateStarted.value = false
previewExpanded.value = !result?.is_locked
@@ -607,6 +616,10 @@ onLoad((options) => {
void fetchComments()
})
onShow(() => {
if (post.value?.id) isFavorite.value = isPostFavorite(post.value.id)
})
const submitComment = async () => {
if (!checkLogin()) return
const content = inputContent.value.trim()
@@ -642,6 +655,13 @@ const handleLike = async () => {
}
}
const handleFavorite = () => {
if (!post.value?.id) return
const result = togglePostFavorite(post.value)
isFavorite.value = result.favorite
uni.showToast({ title: result.favorite ? '已收藏帖子' : '已取消收藏', icon: 'none' })
}
const handleCommentLike = async (comment: any) => {
if (!checkLogin()) return
const commentId = Number(comment?.id || 0)
+201 -24
View File
@@ -1,10 +1,7 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<navigation-bar :front-color="$theme.navColor" :background-color="$theme.navBgColor" />
<!-- #endif -->
</page-meta>
@@ -26,14 +23,17 @@
<scroll-view scroll-x class="collection-tabs" :show-scrollbar="false">
<view class="collection-tabs__inner">
<view v-for="tab in collectionTabs" :key="tab.key" class="collection-tab"
:class="{ 'collection-tab--active': activeType === tab.key }"
@tap="switchType(tab.key)">
:class="{ 'collection-tab--active': activeType === tab.key }" @tap="switchType(tab.key)">
<u-icon :name="tab.icon" size="28" :color="activeType === tab.key ? '#1468f5' : '#7b8492'" />
<text>{{ tab.label }}</text>
<text v-if="tab.key === 'article' && articleCount" class="collection-tab__count">{{ articleCount }}</text>
<text v-if="tab.key === 'article' && articleCount" class="collection-tab__count">{{ articleCount
}}</text>
<text v-if="tab.key === 'post' && postCount" class="collection-tab__count">{{ postCount }}</text>
<text v-if="tab.key === 'match' && matchCount" class="collection-tab__count">{{ matchCount }}</text>
<text v-if="tab.key === 'lottery' && lotteryCount" class="collection-tab__count">{{ lotteryCount }}</text>
<text v-if="tab.key === 'crypto' && cryptoCount" class="collection-tab__count">{{ cryptoCount }}</text>
<text v-if="tab.key === 'lottery' && lotteryCount" class="collection-tab__count">{{ lotteryCount
}}</text>
<text v-if="tab.key === 'crypto' && cryptoCount" class="collection-tab__count">{{ cryptoCount
}}</text>
</view>
</view>
</scroll-view>
@@ -77,17 +77,53 @@
</view>
</template>
<template v-else-if="activeType === 'post'">
<view v-if="postFavorites.length" class="collection-list post-list">
<view v-for="item in postFavorites" :key="item.id" class="post-item" @tap="openPost(item)">
<view class="post-item__header">
<image v-if="item.user_avatar" class="post-item__avatar" :src="item.user_avatar"
mode="aspectFill" />
<view v-else class="post-item__avatar post-item__avatar--fallback">
{{ item.user_name.slice(0, 1) || '?' }}
</view>
<view class="post-item__author">
<text>{{ item.user_name || '匿名用户' }}</text>
<text>{{ formatPostFavoriteTime(item.create_time) }}</text>
</view>
<view class="post-item__remove" @tap.stop="removePost(item)">
<u-icon name="star-fill" size="27" color="#e6a23c" />
</view>
</view>
<view class="post-item__content">
<text class="post-item__text">{{ item.content || '图片帖子' }}</text>
<image v-if="item.images[0]" class="post-item__image" :src="item.images[0]"
mode="aspectFill" />
</view>
<view v-if="item.tags.length" class="post-item__tags">
<text v-for="tag in item.tags.slice(0, 2)" :key="tag">#{{ tag }}</text>
</view>
</view>
</view>
<view v-else class="collection-state collection-state--empty">
<u-icon name="chat" size="72" color="#cbd5e1" />
<text class="collection-state__title">暂无帖子收藏</text>
<text class="collection-state__desc">在帖子详情互动区点击星标即可加入收藏</text>
</view>
</template>
<template v-else-if="activeType === 'match'">
<view v-if="matchFavorites.length" class="collection-list match-list">
<view v-for="item in matchFavorites" :key="item.id" class="match-item" @tap="openMatch(item)">
<view class="match-item__top">
<text class="match-item__league">{{ item.league_name || '赛事' }}{{ item.round_name ? ` · ${item.round_name}` : '' }}</text>
<text class="match-item__league">{{ item.league_name || '赛事' }}{{ item.round_name ? ` ·
${item.round_name}` : '' }}</text>
<text class="match-item__status">{{ getMatchFavoriteStatus(item) }}</text>
</view>
<view class="match-item__content">
<view class="match-item__team">
<image v-if="item.home_icon" :src="item.home_icon" mode="aspectFit" />
<view v-else class="match-item__team-logo">{{ item.home_team.slice(0, 1) || '?' }}</view>
<view v-else class="match-item__team-logo">{{ item.home_team.slice(0, 1) || '?' }}
</view>
<text>{{ item.home_team || '主队' }}</text>
</view>
<view class="match-item__center">
@@ -100,7 +136,8 @@
</view>
<view class="match-item__team">
<image v-if="item.away_icon" :src="item.away_icon" mode="aspectFit" />
<view v-else class="match-item__team-logo">{{ item.away_team.slice(0, 1) || '?' }}</view>
<view v-else class="match-item__team-logo">{{ item.away_team.slice(0, 1) || '?' }}
</view>
<text>{{ item.away_team || '客队' }}</text>
</view>
<view class="match-item__remove" @tap.stop="removeMatch(item)">
@@ -150,7 +187,8 @@
<view v-if="coins.length" class="collection-list coin-list">
<view v-for="coin in coins" :key="coin.symbol" class="coin-item" @tap="openCoin(coin)">
<image v-if="coin.image" class="coin-item__icon" :src="coin.image" mode="aspectFit" />
<view v-else class="coin-item__icon coin-item__icon--fallback">{{ coin.symbol.slice(0, 1) }}</view>
<view v-else class="coin-item__icon coin-item__icon--fallback">{{ coin.symbol.slice(0, 1) }}
</view>
<view class="coin-item__name">
<text>{{ coin.symbol }}</text>
<text>{{ coin.name_zh || coin.name }}</text>
@@ -196,6 +234,7 @@ import {
} from '@/api/crypto'
import { getLotteryFavoriteCodes, setLotteryFavoriteCodes } from '@/utils/lottery-favorite'
import { getMatchFavorites, setMatchFavorites, type MatchFavoriteItem } from '@/utils/match-favorite'
import { getPostFavorites, setPostFavorites, type PostFavoriteItem } from '@/utils/post-favorite'
import { getLocalMatchDate } from '@/utils/match-time'
type CollectionType = 'article' | 'post' | 'match' | 'lottery' | 'crypto'
@@ -205,11 +244,12 @@ const collectionTabs: Array<{ key: CollectionType; label: string; icon: string }
{ key: 'post', label: '帖子', icon: 'chat' },
{ key: 'match', label: '比赛', icon: 'calendar' },
{ key: 'lottery', label: '彩票', icon: 'gift' },
{ key: 'crypto', label: '加密货币', icon: 'star' }
{ key: 'crypto', label: '行情', icon: 'star' }
]
const activeType = ref<CollectionType>('article')
const articles = ref<any[]>([])
const postFavorites = ref<PostFavoriteItem[]>(getPostFavorites())
const matchFavorites = ref<MatchFavoriteItem[]>(getMatchFavorites())
const lotteryFavorites = ref<any[]>(getLotteryFavoriteCodes().map((code) => ({ code })))
const coins = ref<CryptoMarketCoin[]>([])
@@ -217,10 +257,11 @@ const loading = ref(false)
const refreshing = ref(false)
const articleCount = computed(() => articles.value.length)
const postCount = computed(() => postFavorites.value.length)
const matchCount = computed(() => matchFavorites.value.length)
const lotteryCount = computed(() => lotteryFavorites.value.length)
const cryptoCount = computed(() => coins.value.length)
const collectionCount = computed(() => articleCount.value + matchCount.value + lotteryCount.value + cryptoCount.value)
const collectionCount = computed(() => articleCount.value + postCount.value + matchCount.value + lotteryCount.value + cryptoCount.value)
const activeTabLabel = computed(() => collectionTabs.find((tab) => tab.key === activeType.value)?.label || '')
const loadArticles = async () => {
@@ -243,6 +284,10 @@ const loadMatches = () => {
matchFavorites.value = getMatchFavorites()
}
const loadPosts = () => {
postFavorites.value = getPostFavorites()
}
const loadLottery = async () => {
const favoriteCodes = getLotteryFavoriteCodes()
if (!favoriteCodes.length) {
@@ -282,6 +327,8 @@ const loadCollection = async () => {
try {
if (activeType.value === 'article') {
await loadArticles()
} else if (activeType.value === 'post') {
loadPosts()
} else if (activeType.value === 'match') {
loadMatches()
} else if (activeType.value === 'lottery') {
@@ -326,6 +373,26 @@ const removeArticle = async (item: any) => {
}
}
const openPost = (item: PostFavoriteItem) => {
uni.navigateTo({ url: `/packages_community/pages/post_detail?id=${item.id}` })
}
const removePost = (item: PostFavoriteItem) => {
postFavorites.value = setPostFavorites(postFavorites.value.filter((favorite) => favorite.id !== item.id))
uni.showToast({ title: '已取消收藏', icon: 'none' })
}
const formatPostFavoriteTime = (value: number | string) => {
const timestamp = Number(value)
if (Number.isFinite(timestamp) && timestamp > 0) {
const date = new Date(timestamp * 1000)
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${month}-${day}`
}
return String(value || '').replace(/^\d{4}-/, '').slice(0, 10)
}
const openMatch = (item: MatchFavoriteItem) => {
uni.navigateTo({ url: `/pages/match_detail/match_detail?id=${item.id}` })
}
@@ -395,7 +462,7 @@ const formatChange = (value: number) => {
}
onShow(() => {
if (activeType.value === 'article' || activeType.value === 'match' || activeType.value === 'lottery' || activeType.value === 'crypto') {
if (activeType.value === 'article' || activeType.value === 'post' || activeType.value === 'match' || activeType.value === 'lottery' || activeType.value === 'crypto') {
loadCollection()
}
})
@@ -518,7 +585,7 @@ onShow(() => {
min-width: 100%;
box-sizing: border-box;
padding: 0 24rpx;
gap: 18rpx;
gap: 8rpx;
}
}
@@ -528,24 +595,24 @@ onShow(() => {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8rpx;
gap: 2rpx;
position: relative;
padding: 0 10rpx;
padding: 0 2rpx;
box-sizing: border-box;
color: #252b36;
font-size: 27rpx;
font-size: 24rpx;
white-space: nowrap;
&--active {
color: #1468f5;
font-weight: 700;
font-weight: 600;
&::after {
content: '';
position: absolute;
left: 50%;
bottom: 0;
width: 48rpx;
width: 140rpx;
height: 6rpx;
border-radius: 999rpx;
background: #1468f5;
@@ -675,6 +742,111 @@ onShow(() => {
}
}
.post-item {
margin-bottom: 16rpx;
padding: 20rpx;
background: #fff;
border: 1rpx solid #e7eaf0;
border-radius: 22rpx;
box-shadow: 0 8rpx 30rpx rgba(29, 45, 78, 0.04);
animation: collection-rise 0.32s ease both;
&__header {
display: flex;
align-items: center;
gap: 10rpx;
}
&__avatar {
width: 42rpx;
height: 42rpx;
flex-shrink: 0;
border-radius: 50%;
background: #edf3ff;
&--fallback {
display: flex;
align-items: center;
justify-content: center;
color: #1468f5;
font-size: 21rpx;
font-weight: 700;
}
}
&__author {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 2rpx;
text:first-child {
overflow: hidden;
color: #252b36;
font-size: 23rpx;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
text:last-child {
color: #98a2b3;
font-size: 19rpx;
}
}
&__remove {
width: 42rpx;
display: flex;
flex-shrink: 0;
justify-content: flex-end;
}
&__content {
display: flex;
align-items: stretch;
gap: 16rpx;
margin-top: 16rpx;
}
&__text {
min-width: 0;
flex: 1;
display: -webkit-box;
overflow: hidden;
color: #252b36;
font-size: 25rpx;
line-height: 1.5;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
&__image {
width: 126rpx;
height: 100rpx;
flex-shrink: 0;
border-radius: 12rpx;
background: #f3f5f8;
}
&__tags {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-top: 13rpx;
text {
padding: 3rpx 9rpx;
border-radius: 6rpx;
background: #edf4ff;
color: #1468f5;
font-size: 18rpx;
line-height: 1.3;
}
}
}
.match-item {
margin-bottom: 16rpx;
padding: 20rpx;
@@ -936,8 +1108,13 @@ onShow(() => {
}
}
.is-up { color: #16a36a; }
.is-down { color: #ef5350; }
.is-up {
color: #16a36a;
}
.is-down {
color: #ef5350;
}
.collection-state {
min-height: 480rpx;
+83
View File
@@ -0,0 +1,83 @@
const POST_FAVORITE_STORAGE_KEY = 'community_post_favorites'
const MAX_CONTENT_LENGTH = 600
export interface PostFavoriteItem {
id: number
content: string
images: string[]
tags: string[]
user_id: number
user_name: string
user_avatar: string
create_time: number | string
is_paid: boolean
}
const normalizeText = (value: unknown, maxLength = MAX_CONTENT_LENGTH): string => {
return String(value || '').replace(/\s+/g, ' ').trim().slice(0, maxLength)
}
const normalizeFavorite = (value: any): PostFavoriteItem | null => {
const id = Number(value?.id)
if (!Number.isFinite(id) || id <= 0) return null
return {
id,
content: normalizeText(value?.content),
images: Array.isArray(value?.images)
? value.images.map((item: unknown) => String(item || '').trim()).filter(Boolean).slice(0, 9)
: [],
tags: Array.isArray(value?.tags)
? value.tags.map((item: unknown) => String(item || '').trim()).filter(Boolean).slice(0, 8)
: [],
user_id: Number(value?.user_id || value?.user?.id || 0),
user_name: normalizeText(value?.user_name || value?.user?.nickname || '匿名用户', 48),
user_avatar: String(value?.user_avatar || value?.user?.avatar || ''),
create_time: value?.create_time ?? '',
is_paid: Number(value?.is_paid || 0) === 1
}
}
export const getPostFavorites = (): PostFavoriteItem[] => {
const stored = uni.getStorageSync(POST_FAVORITE_STORAGE_KEY)
if (!Array.isArray(stored)) return []
const seen = new Set<number>()
return stored.reduce<PostFavoriteItem[]>((favorites, item) => {
const favorite = normalizeFavorite(item)
if (!favorite || seen.has(favorite.id)) return favorites
seen.add(favorite.id)
favorites.push(favorite)
return favorites
}, [])
}
export const setPostFavorites = (favorites: PostFavoriteItem[]): PostFavoriteItem[] => {
const seen = new Set<number>()
const normalized = favorites.reduce<PostFavoriteItem[]>((items, item) => {
const favorite = normalizeFavorite(item)
if (!favorite || seen.has(favorite.id)) return items
seen.add(favorite.id)
items.push(favorite)
return items
}, [])
uni.setStorageSync(POST_FAVORITE_STORAGE_KEY, normalized)
return normalized
}
export const isPostFavorite = (postId: number | string): boolean => {
const id = Number(postId)
return getPostFavorites().some((favorite) => favorite.id === id)
}
export const togglePostFavorite = (post: any): { favorite: boolean; favorites: PostFavoriteItem[] } => {
const item = normalizeFavorite(post)
const favorites = getPostFavorites()
if (!item) return { favorite: false, favorites }
const favorite = favorites.some((existing) => existing.id === item.id)
const nextFavorites = favorite
? favorites.filter((existing) => existing.id !== item.id)
: [item, ...favorites]
return { favorite: !favorite, favorites: setPostFavorites(nextFavorites) }
}