Files
rating/pages/rating/detail.vue
2026-06-15 07:59:47 +08:00

1075 lines
29 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="page-container">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @tap="goBack">
<text class="back-icon"></text>
<text class="nav-title">夯拉评分</text>
</view>
<view class="nav-right">
<text class="search-icon"></text>
<image class="nav-avatar" src="/static/images/default-avatar.png" mode="aspectFill" />
</view>
</view>
</view>
<view :style="{ height: statusBarHeight + 44 + 'px' }"></view>
<view class="content-wrap">
<!-- 头部信息卡片 -->
<view class="hero-card card">
<image class="hero-avatar" :src="detailData.avatar" mode="aspectFill" />
<view class="hero-info">
<view class="hero-name-row">
<text class="hero-name">{{ detailData.name }}</text>
<text class="hero-score">{{ detailData.score }}</text>
</view>
<view class="hero-meta">
<text class="meta-rank">🥇 No.1</text>
<view class="meta-heat">
<image class="meta-icon" src="/static/images/icon/fire.png" mode="aspectFit"></image>
<text>{{ detailData.hotScore }}w 热度</text>
</view>
<text class="meta-count" style="margin-left: 20rpx; color: #999;">{{ detailData.ratingCount }} 人评分</text>
</view>
</view>
</view>
<!-- AI 观察员 -->
<view class="ai-card">
<view class="ai-header">
<image class="ai-icon ai-icon-img" src="/static/images/icon/robot.png" mode="aspectFit"></image>
<text class="ai-title">AI观察员</text>
</view>
<text class="ai-content">{{ detailData.aiSummary }}</text>
</view>
<!-- 你的态度 -->
<AttitudePanel
ref="attitudePanelRef"
:initial-action="currentAction"
:sending="isSendingComment"
:reply-target="replyTarget"
@action-change="handleActionChange"
@send-comment="handleSendComment"
@cancel-reply="clearReplyTarget"
/>
<!-- 大家都在 PK -->
<!-- <view v-if="pkPreview" class="pk-panel card" @tap="goToPk()">
<view class="pk-header">
<text class="section-title">大家都在 PK</text>
<view class="go-compare">去对比<text class="arrow-right"></text></view>
</view>
<view class="pk-content">
<view class="pk-person">
<image class="pk-avatar blue-border" :src="pkPreview.left.avatar" mode="aspectFill" />
<text class="pk-name">{{ pkPreview.left.name }}</text>
<text class="pk-score blue-text">{{ pkPreview.left.scoreText }}</text>
</view>
<text class="vs-text">VS</text>
<view class="pk-person">
<image class="pk-avatar yellow-border" :src="pkPreview.right.avatar" mode="aspectFill" />
<text class="pk-name">{{ pkPreview.right.name }}</text>
<text class="pk-score yellow-text">{{ pkPreview.right.scoreText }}</text>
</view>
</view>
<view class="pk-bar-wrap">
<view class="pk-bar blue-bar" :style="{ width: `${pkPreview.leftPercent}%` }"></view>
<view class="pk-bar yellow-bar" :style="{ width: `${pkPreview.rightPercent}%` }"></view>
</view>
<view class="pk-data-text">
<text class="blue-text">{{ pkPreview.leftPercent }}% 看好</text>
<text class="yellow-text">{{ pkPreview.rightPercent }}% 看好</text>
</view>
</view> -->
<!-- 评论区 -->
<view class="comments-section card">
<view class="tabs-header">
<view class="tabs">
<text class="tab" :class="{ active: currentTab === 'hot' }" @tap="switchTab('hot')">最热</text>
<text class="tab" :class="{ active: currentTab === 'new' }" @tap="switchTab('new')">最新</text>
</view>
<!-- <text class="filter-icon"></text> -->
</view>
<view class="comment-list">
<view v-if="commentLoading && !comments.length" class="comment-state">
<text class="comment-state-title">评论加载中...</text>
<text class="comment-state-desc">正在拉取大家的锐评</text>
</view>
<view v-else-if="commentError && !comments.length" class="comment-state">
<text class="comment-state-title">评论加载失败</text>
<text class="comment-state-desc">{{ commentError }}</text>
<button class="comment-state-btn" @tap="retryLoadComments">重新加载</button>
</view>
<view v-else-if="!comments.length" class="comment-state">
<view class="comment-empty-visual">
<view class="comment-empty-orbit orbit-1"></view>
<view class="comment-empty-orbit orbit-2"></view>
<view class="comment-empty-core">
<text class="comment-empty-emoji">💬</text>
</view>
</view>
<text class="comment-state-title">还没有评论</text>
<text class="comment-state-desc">来发第一条锐评带动一下讨论气氛</text>
<text class="comment-state-tip">你的观点可能就是这条榜单最有意思的开始</text>
</view>
<CommentItem
v-for="(comment, index) in comments"
:key="comment.id"
:comment="comment"
@need-login="openLoginPopup"
@like-change="handleCommentLikeChange"
@reply="handleReplyComment"
/>
</view>
<!-- 加载更多 -->
<view v-if="comments.length" class="load-more">
<text>{{ commentLoadText }}</text>
</view>
</view>
</view>
<!-- 评分成功特效弹窗 -->
<SuccessPopup ref="successPopupRef" />
<LoginPopup ref="loginPopupRef" @logind="handleLoginSuccess" />
</view>
</template>
<script setup>
import { computed, ref } from 'vue';
import { getStatusBarHeight } from "@/utils/system";
import { onLoad, onPullDownRefresh, onReachBottom, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import AttitudePanel from "@/components/AttitudePanel/AttitudePanel.vue";
import SuccessPopup from "@/components/SuccessPopup/SuccessPopup.vue";
import CommentItem from "@/components/CommentItem/CommentItem.vue";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import { FILE_BASE_URL } from "@/utils/constants";
import { fetchTopicRatingItems as fetchTopicItemList } from "@/api/topic";
import { fetchTopicRatingItems, topicItemScore, topicItemComment, topicItemCommentList } from "@/api/topicItem";
import { formatDate } from "@/utils/date";
import { getShareToken } from "@/utils/common";
import { getShareReward } from "@/api/system";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const statusBarHeight = ref(getStatusBarHeight() || 44);
const hasMore = ref(true);
const currentPage = ref(1);
const currentAction = ref('');
const successPopupRef = ref(null);
const loginPopupRef = ref(null);
const attitudePanelRef = ref(null);
const currentTab = ref('hot'); // 'hot' 或 'new'
const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName);
const commentLoading = ref(false);
const isSendingComment = ref(false);
const commentError = ref('');
const pkPreview = ref(null);
const commentLoadText = computed(() => {
if (commentLoading.value && comments.value.length) return '加载中...';
if (!hasMore.value) return '没有更多了';
return '上拉加载更多';
});
const goBack = () => {
uni.navigateBack();
};
const openLoginPopup = (message = '请先登录后再操作') => {
loginPopupRef.value?.open();
if (message) {
uni.showToast({
title: message,
icon: 'none'
});
}
};
const detailData = ref({
id: '',
name: '',
score: '0.0',
avatar: '',
aiSummary: '加载中...',
userAction: "",
topicId: ''
});
const comments = ref([]);
const itemId = ref('');
const replyTarget = ref(null);
const buildAvatarUrl = (avatar) => {
if (!avatar) return 'https://api.dicebear.com/7.x/avataaars/svg?seed=fallback';
return String(avatar).startsWith('http') ? avatar : `${FILE_BASE_URL}${avatar}`;
};
const normalizeCommentItem = (item = {}, rootComment = null) => {
const user = item.user || {};
const replyToUser = item.replyToUser || {};
return {
id: item.id,
rootCommentId: item.rootCommentId || rootComment?.id || null,
parentCommentId: item.parentCommentId || rootComment?.id || null,
replyToCommentId: item.replyToCommentId || null,
name: user.nickname || '匿名用户',
avatar: buildAvatarUrl(user.avatar),
time: formatDate(item.createdAt) || '刚刚',
content: item.content || '',
likes: Number(item.likeCount || 0),
isLiked: !!item.isLiked,
badge: '',
badgeIcon: '',
userAction: item.userAction || '',
replyCount: Number(item.replyCount || 0),
replyToName: replyToUser.nickname || '',
replies: rootComment
? []
: (Array.isArray(item.replies) ? item.replies.map((reply) => normalizeCommentItem(reply, item)) : [])
};
};
const clearReplyTarget = () => {
replyTarget.value = null;
};
const handleReplyComment = (payload) => {
if (!isLoggedIn.value) {
openLoginPopup();
return;
}
replyTarget.value = {
rootCommentId: payload.rootCommentId,
parentCommentId: payload.parentCommentId,
replyToCommentId: payload.replyToCommentId,
replyToName: payload.replyToName || '',
content: payload.content || ''
};
};
const calcPkPercent = (leftScore, rightScore) => {
const left = Number(leftScore) || 0;
const right = Number(rightScore) || 0;
if (left <= 0 && right <= 0) {
return { leftPercent: 50, rightPercent: 50 };
}
const total = left + right || 1;
let leftPercent = Math.round((left / total) * 100);
leftPercent = Math.min(82, Math.max(18, leftPercent));
return {
leftPercent,
rightPercent: 100 - leftPercent
};
};
const buildPkPreview = (items) => {
const sortedItems = [...items].sort((a, b) => (Number(b.scoreAvg) || 0) - (Number(a.scoreAvg) || 0));
const currentIndex = sortedItems.findIndex((item) => String(item.id) === String(itemId.value));
if (currentIndex === -1) return null;
const currentItem = sortedItems[currentIndex];
const rival =
sortedItems[currentIndex === 0 ? 1 : currentIndex - 1] ||
sortedItems.find((item) => String(item.id) !== String(itemId.value));
if (!currentItem || !rival) return null;
const { leftPercent, rightPercent } = calcPkPercent(currentItem.scoreAvg, rival.scoreAvg);
return {
left: {
id: currentItem.id,
name: currentItem.name || '当前对象',
avatar: buildAvatarUrl(currentItem.avatarUrl),
scoreText: `${Number(currentItem.scoreAvg || 0).toFixed(1)}`
},
right: {
id: rival.id,
name: rival.name || '对手',
avatar: buildAvatarUrl(rival.avatarUrl),
scoreText: `${Number(rival.scoreAvg || 0).toFixed(1)}`
},
leftPercent,
rightPercent
};
};
const getPkPreviewData = async () => {
if (!detailData.value.topicId || !itemId.value) {
pkPreview.value = null;
return;
}
try {
const res = await fetchTopicItemList(detailData.value.topicId, 1);
const rawList = res?.data?.records || res?.records || res?.data?.list || res?.list || [];
const list = Array.isArray(rawList) ? rawList : [];
pkPreview.value = list.length >= 2 ? buildPkPreview(list) : null;
} catch (error) {
pkPreview.value = null;
}
};
const goToPk = () => {
if (!detailData.value.topicId || !itemId.value) return;
uni.navigateTo({
url: `/pages/rating/pk?topicId=${detailData.value.topicId}&itemId=${itemId.value}`
});
};
const handleActionChange = async (action) => {
if (!isLoggedIn.value) {
openLoginPopup();
return;
}
const originAction = detailData.value.userAction;
const originScore = detailData.value.score;
const isCancel = detailData.value.userAction === action.label;
try {
detailData.value.userAction = isCancel ? '' : action.label;
currentAction.value = detailData.value.userAction;
const res = await topicItemScore({
topicId: detailData.value.topicId,
itemId: itemId.value,
scoreType: action.scoreType
});
detailData.value.score = res?.scoreAvg || originScore;
// 如果是成功评分(非取消),展示特效弹窗
if (!isCancel && successPopupRef.value) {
successPopupRef.value.show(action);
}
uni.$trackRecord({
eventName: 'rating',
eventType: 'rating',
elementId: `item_${itemId.value}`,
elementContent: `评分项_${detailData.value.name}`,
customParams: {
scoreType: action.scoreType,
action: action.label,
topicId: detailData.value.topicId,
topicTitle: detailData.value.name,
score: detailData.value.score,
page: 'rating_detail'
}
});
} catch (error) {
detailData.value.userAction = originAction;
detailData.value.score = originScore;
currentAction.value = originAction;
}
};
const handleSendComment = async (payload) => {
if (!isLoggedIn.value) {
openLoginPopup();
return;
}
if (!detailData.value.topicId || !itemId.value || isSendingComment.value) return;
try {
isSendingComment.value = true;
uni.showLoading({ title: '发送中...' });
await topicItemComment({
topicId: detailData.value.topicId,
itemId: itemId.value,
content: payload.content,
parentCommentId: replyTarget.value?.parentCommentId || undefined,
replyToCommentId: replyTarget.value?.replyToCommentId || undefined
});
uni.hideLoading();
// 弹出成功弹窗
if (successPopupRef.value) {
successPopupRef.value.show({
emoji: '💬',
label: '你的锐评已发布'
}, 'comment');
}
// 刷新页面数据以获取最新评论
currentPage.value = 1;
hasMore.value = true;
uni.$trackRecord({
eventName: 'comment',
eventType: 'comment',
elementId: `item_${itemId.value}`,
elementContent: `评论项_${detailData.value.name}`,
customParams: {
comment: payload.content,
replyToCommentId: replyTarget.value?.replyToCommentId || '',
page: 'rating_detail'
}
});
attitudePanelRef.value?.resetComment?.();
clearReplyTarget();
await getCommentsData({ replace: true });
} catch (error) {
uni.hideLoading();
uni.showToast({
title: '发送失败',
icon: 'none'
});
} finally {
isSendingComment.value = false;
}
};
const switchTab = (tab) => {
if (currentTab.value === tab) return;
currentTab.value = tab;
currentPage.value = 1;
hasMore.value = true;
commentError.value = '';
clearReplyTarget();
getCommentsData({ replace: true });
};
const resolveCommentHasMore = (res, list) => {
const data = res?.data || res || {};
const hasNext = data?.hasNext || res?.hasNext;
if (typeof hasNext === 'boolean') return hasNext;
const pages = Number(data?.pages || res?.pages || 0);
if (pages > 0) return currentPage.value < pages;
const total = Number(data?.total || res?.total || 0);
if (total > 0) {
const size = Number(data?.size || data?.pageSize || res?.size || res?.pageSize || list.length || 1);
return currentPage.value * size < total;
}
return list.length > 0;
};
const getCommentsData = async ({ replace = false } = {}) => {
if (!detailData.value.topicId || !itemId.value || commentLoading.value) return;
try {
commentLoading.value = true;
commentError.value = '';
const orderBy = currentTab.value === 'hot' ? 1 : 2;
const res = await topicItemCommentList(detailData.value.topicId, itemId.value, currentPage.value, orderBy);
const rawList =
res?.data?.records ||
res?.data?.list ||
res?.records ||
res?.list ||
(Array.isArray(res?.data) ? res.data : []);
const list = Array.isArray(rawList) ? rawList : [];
const formattedList = list.map((item) => normalizeCommentItem(item));
if (currentPage.value === 1 || replace) {
comments.value = formattedList;
} else {
comments.value = [...comments.value, ...formattedList];
}
hasMore.value = resolveCommentHasMore(res, list);
} catch (error) {
console.error('获取评论列表失败:', error);
commentError.value = '请检查网络后重试';
if (currentPage.value > 1) {
currentPage.value -= 1;
}
} finally {
commentLoading.value = false;
}
};
const retryLoadComments = () => {
if (!comments.value.length) {
currentPage.value = 1;
}
clearReplyTarget();
getCommentsData({ replace: currentPage.value === 1 });
};
const handleCommentLikeChange = ({ id, parentId, isReply, isLiked, likes }) => {
comments.value = comments.value.map((item) => {
if (!isReply && item.id !== id) return item;
if (isReply && item.id !== parentId) return item;
if (isReply) {
return {
...item,
replies: (item.replies || []).map((reply) => {
if (reply.id !== id) return reply;
return {
...reply,
isLiked,
likes
};
})
};
}
return {
...item,
isLiked,
likes
};
});
};
const getDetailData = async () => {
if (!itemId.value) return;
try {
uni.showLoading({ title: '加载中' });
const res = await fetchTopicRatingItems(itemId.value);
const data = res?.data || res || {};
detailData.value = {
id: itemId.value,
name: data.name || '未知',
score: data.scoreAvg ? Number(data.scoreAvg).toFixed(1) : '0.0',
avatar: data.avatarUrl ? `${FILE_BASE_URL}${data.avatarUrl}` : '',
aiSummary: data.description || '暂无总结',
hotScore: data.hotScore || '0.00',
topicId: data.topicId || detailData.value.topicId || '',
ratingCount: data.scoreCount || 0,
userAction: data.userAction || ""
};
// 如果接口返回了当前用户的态度,初始化它
if (data.userAction) {
currentAction.value = data.userAction;
}
// 如果有评论数据,更新评论 (兼容老接口一并返回的情况,但推荐通过 getCommentsData 单独获取)
if (data.comments && Array.isArray(data.comments) && currentPage.value === 1) {
// 如果后端在详情里依然返回了一部分评论
// comments.value = data.comments;
}
// 拉取真实的评论列表
if (currentPage.value === 1) {
getCommentsData({ replace: true });
}
getPkPreviewData();
} catch (error) {
console.error('获取详情失败:', error);
uni.showToast({
title: '获取详情失败',
icon: 'none'
});
} finally {
uni.hideLoading();
uni.stopPullDownRefresh();
}
};
onLoad((options) => {
uni.$trackRecord({
eventName: "rating_item_detail_page_visit",
eventType: `visit`,
elementId: options.itemId
})
if (options.topicId) {
detailData.value.topicId = options.topicId;
}
if (options.itemId) {
itemId.value = options.itemId;
getDetailData();
}
});
const handleLoginSuccess = () => {
currentPage.value = 1;
hasMore.value = true;
comments.value = [];
commentError.value = '';
clearReplyTarget();
getDetailData();
};
onShareAppMessage(async () => {
const shareToken = await getShareToken("rating_item");
getShareReward({ scene: "rating_item" });
const itemName = detailData.value.name || "这个评分对象";
return {
title: `${itemName}」在夯拉评分里是什么段位?`,
path: `/pages/rating/detail?itemId=${itemId.value}&topicId=${detailData.value.topicId}&shareToken=${shareToken}`,
};
});
onShareTimeline(async () => {
const shareToken = await getShareToken("rating_item");
getShareReward({ scene: "rating_item_timeline" });
const itemName = detailData.value.name || "这个评分对象";
const score = detailData.value.score || "0.0";
return {
title: `${itemName}当前评分 ${score},来看看大家怎么评`,
query: `itemId=${itemId.value}&topicId=${detailData.value.topicId}&shareToken=${shareToken}`,
};
});
onPullDownRefresh(() => {
currentPage.value = 1;
hasMore.value = true;
comments.value = [];
commentError.value = '';
clearReplyTarget();
getDetailData();
});
onReachBottom(() => {
if (!hasMore.value || commentLoading.value) return;
currentPage.value += 1;
getCommentsData();
});
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f6fa;
padding-bottom: 40rpx;
}
/* 导航栏 */
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: #f5f6fa;
}
.nav-content {
height: 44px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 32rpx;
}
.nav-left {
display: flex;
align-items: center;
}
.back-icon {
width: 40rpx;
height: 40rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='19' y1='12' x2='5' y2='12'%3E%3C/line%3E%3Cpolyline points='12 19 5 12 12 5'%3E%3C/polyline%3E%3C/svg%3E");
background-size: cover;
margin-right: 16rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 700;
color: #111;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
}
.nav-right {
display: flex;
align-items: center;
}
.search-icon {
width: 40rpx;
height: 40rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232953ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");
background-size: cover;
margin-right: 24rpx;
}
.nav-avatar {
width: 52rpx;
height: 52rpx;
border-radius: 8rpx;
background-color: #eee;
}
.content-wrap {
padding: 24rpx 32rpx;
}
.card {
background-color: #fff;
border-radius: 32rpx;
padding: 32rpx;
margin-bottom: 24rpx;
}
/* 英雄卡片 */
.hero-card {
display: flex;
align-items: center;
}
.hero-avatar {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
margin-right: 32rpx;
border: 4rpx solid #fff;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.1);
}
.hero-info {
flex: 1;
}
.hero-name-row {
display: flex;
align-items: baseline;
margin-bottom: 12rpx;
}
.hero-name {
font-size: 44rpx;
font-weight: bold;
color: #111;
margin-right: 16rpx;
}
.hero-score {
font-size: 44rpx;
font-weight: 900;
color: #2953ff;
font-family: 'DIN Alternate', sans-serif;
}
.hero-meta {
display: flex;
align-items: center;
font-size: 24rpx;
color: #666;
}
.meta-rank {
margin-right: 24rpx;
font-weight: bold;
color: #d9a000;
}
.meta-heat {
display: flex;
align-items: center;
color: #666;
}
.meta-icon {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
flex-shrink: 0;
}
/* AI 观察员 */
.ai-card {
background: #ffffff;
border: 2rpx solid #e0d6ff;
border-radius: 32rpx;
padding: 32rpx;
margin-bottom: 24rpx;
}
.ai-header {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.ai-icon {
width: 36rpx;
height: 36rpx;
margin-right: 12rpx;
}
.ai-icon-img {
display: block;
}
.ai-title {
font-size: 28rpx;
font-weight: 800;
color: #7b46f1;
}
.ai-content {
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
/* 你的态度面板 */
/* 大家都在 PK */
.section-title {
font-size: 34rpx;
font-weight: 700;
color: #111;
margin-bottom: 32rpx;
display: block;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
}
.pk-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
}
.pk-header .section-title {
margin-bottom: 0;
}
.go-compare {
font-size: 24rpx;
color: #2953ff;
display: flex;
align-items: center;
}
.arrow-right {
width: 24rpx;
height: 24rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232953ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='9 18 15 12 9 6'%3E%3C/polyline%3E%3C/svg%3E");
background-size: cover;
}
.pk-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
padding: 0 40rpx;
}
.pk-person {
display: flex;
flex-direction: column;
align-items: center;
}
.pk-avatar {
width: 110rpx;
height: 110rpx;
border-radius: 50%;
border: 4rpx solid transparent;
margin-bottom: 12rpx;
}
.blue-border { border-color: #2953ff; }
.yellow-border { border-color: #ffc107; }
.pk-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.pk-score {
margin-top: 8rpx;
font-size: 22rpx;
font-weight: 700;
}
.vs-text {
font-size: 40rpx;
font-weight: 900;
color: #a1a1a1;
}
.pk-bar-wrap {
height: 16rpx;
border-radius: 100rpx;
display: flex;
overflow: hidden;
margin-bottom: 16rpx;
}
.pk-bar { height: 100%; }
.blue-bar { background-color: #4d44f1; }
.yellow-bar { background-color: #ffe066; }
.pk-data-text {
display: flex;
justify-content: space-between;
font-size: 24rpx;
font-weight: bold;
}
.blue-text { color: #4d44f1; }
.yellow-text { color: #333; }
/* 评论区 */
.comments-section {
padding: 32rpx;
}
.tabs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
}
.tabs {
display: flex;
gap: 40rpx;
}
.tab {
font-size: 32rpx;
color: #999;
font-weight: 500;
position: relative;
padding-bottom: 12rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
}
.tab.active {
color: #111;
font-size: 36rpx;
font-weight: 700; /* 稍微减轻字重从800改为700显得不那么生硬 */
}
.tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 32rpx;
height: 8rpx;
background: linear-gradient(90deg, #4d44f1, #8a78ff);
border-radius: 4rpx;
}
.filter-icon {
width: 40rpx;
height: 40rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='4' y1='21' x2='4' y2='14'%3E%3C/line%3E%3Cline x1='4' y1='10' x2='4' y2='3'%3E%3C/line%3E%3Cline x1='12' y1='21' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12' y2='3'%3E%3C/line%3E%3Cline x1='20' y1='21' x2='20' y2='16'%3E%3C/line%3E%3Cline x1='20' y1='12' x2='20' y2='3'%3E%3C/line%3E%3Cline x1='1' y1='14' x2='7' y2='14'%3E%3C/line%3E%3Cline x1='9' y1='8' x2='15' y2='8'%3E%3C/line%3E%3Cline x1='17' y1='16' x2='23' y2='16'%3E%3C/line%3E%3C/svg%3E");
background-size: cover;
transition: opacity 0.2s;
}
.filter-icon:active {
opacity: 0.7;
}
.comment-list {
display: flex;
flex-direction: column;
min-height: 400rpx;
}
.comment-state {
min-height: 400rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: #999;
padding: 56rpx 40rpx;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(247, 248, 255, 0.92) 0%, rgba(255, 255, 255, 0.98) 100%);
border-radius: 28rpx;
border: 2rpx solid #f1f2fb;
}
.comment-empty-visual {
position: relative;
width: 180rpx;
height: 180rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 28rpx;
}
.comment-empty-orbit {
position: absolute;
border-radius: 50%;
border: 2rpx solid rgba(123, 70, 241, 0.12);
}
.orbit-1 {
width: 180rpx;
height: 180rpx;
}
.orbit-2 {
width: 132rpx;
height: 132rpx;
border-color: rgba(77, 68, 241, 0.18);
}
.comment-empty-core {
width: 96rpx;
height: 96rpx;
border-radius: 32rpx;
background: linear-gradient(135deg, #7668ff, #4d44f1);
box-shadow: 0 18rpx 36rpx rgba(94, 83, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.comment-empty-emoji {
font-size: 44rpx;
}
.comment-state-title {
font-size: 32rpx;
color: #1f2430;
font-weight: 700;
margin-bottom: 14rpx;
letter-spacing: 0.5rpx;
}
.comment-state-desc {
font-size: 25rpx;
color: #70778a;
line-height: 1.7;
}
.comment-state-tip {
margin-top: 14rpx;
font-size: 22rpx;
color: #a7adbc;
line-height: 1.6;
}
.comment-state-btn {
margin-top: 24rpx;
height: 72rpx;
line-height: 72rpx;
padding: 0 32rpx;
border-radius: 999rpx;
background: #4d44f1;
color: #fff;
font-size: 26rpx;
}
.comment-state-btn::after {
border: none;
}
.load-more {
text-align: center;
padding: 40rpx 0 0;
color: #999;
font-size: 24rpx;
}
</style>