fix: pk comment

This commit is contained in:
zzc
2026-06-15 23:14:05 +08:00
parent 88f4220432
commit b79e1e8204
5 changed files with 823 additions and 8 deletions

View File

@@ -65,7 +65,12 @@
<view class="menu-card">
<view class="menu-item" @tap="navTo('/pages/mine/topics')">
<view class="icon-left"><text class="icon-bookmark"></text></view>
<text class="menu-text">参与</text>
<text class="menu-text">我参与的话题</text>
<text class="menu-value" v-if="isLoggedIn">{{ userStats.participant }}</text>
</view>
<view class="menu-item" @tap="navTo('/pages/mine/pk')">
<view class="icon-left"><text class="icon-bookmark"></text></view>
<text class="menu-text">我参与的PK</text>
<text class="menu-value" v-if="isLoggedIn">{{ userStats.participant }}</text>
</view>
<view class="menu-item" @tap="navTo('comments')">
@@ -226,6 +231,11 @@ const navTo = (page) => {
return;
}
if (page === "/pages/mine/pk") {
uni.navigateTo({ url: page });
return;
}
uni.showToast({ title: "功能开发中", icon: "none" });
};
</script>

786
pages/mine/pk.vue Normal file
View File

@@ -0,0 +1,786 @@
<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">我参与的PK</text>
</view>
</view>
</view>
<view :style="{ height: statusBarHeight + 44 + 'px' }"></view>
<view class="content-wrap">
<!-- <view class="hero-card">
<view class="hero-main">
<text class="hero-badge">PK Record</text>
<text class="hero-title">我的站队记录</text>
<text class="hero-desc">回看你参与过的每一场 PK对比风向看看你是大众派还是眼光独到</text>
</view>
<view class="hero-stats">
<view class="hero-stat">
<text class="hero-stat-num">{{ isLoggedIn ? totalCount : "-" }}</text>
<text class="hero-stat-label">参与场次</text>
</view>
<view class="hero-stat-divider"></view>
<view class="hero-stat">
<text class="hero-stat-num">{{ supportWinCount }}</text>
<text class="hero-stat-label">站队领先</text>
</view>
</view>
</view> -->
<view v-if="!isLoggedIn" class="state-card">
<text class="state-title">登录后查看你的 PK 足迹</text>
<text class="state-desc">你参与过的 PK 对局站队结果和支持率都会记录在这里</text>
<button class="state-btn" @tap="openLoginPopup('请先登录')">立即登录</button>
</view>
<view v-else-if="loading && !pkList.length" class="state-card">
<text class="state-title">正在加载 PK 记录...</text>
<text class="state-desc">马上为你整理最近参与过的对决</text>
</view>
<view v-else-if="loadError && !pkList.length" class="state-card">
<text class="state-title">加载失败</text>
<text class="state-desc">{{ loadError }}</text>
<button class="state-btn" @tap="refreshList">重新加载</button>
</view>
<view v-else-if="!pkList.length" class="state-card empty-card">
<view class="empty-orbit orbit-1"></view>
<view class="empty-orbit orbit-2"></view>
<view class="empty-core">
<text class="empty-emoji"></text>
</view>
<text class="state-title">你还没有参与任何 PK</text>
<text class="state-desc"> PK 站挑一场神仙打架留下你的第一条站队记录吧</text>
<button class="state-btn" @tap="goToPkStation"> PK 站看看</button>
</view>
<view v-else class="pk-list">
<view
v-for="item in pkList"
:key="item.id"
class="pk-card"
@tap="goToPkDetail(item)"
>
<view class="pk-card-top">
<view class="pk-topic-wrap">
<text class="pk-topic-label">话题</text>
<text class="pk-topic-title">{{ item.topicTitle }}</text>
</view>
<view class="pk-time-chip">
<text class="pk-time-text">{{ item.votedAtText }}</text>
</view>
</view>
<view class="battle-stage">
<view class="fighter-card left" :class="{ selected: item.selfChooseSide === 'left' }">
<view class="fighter-badge" v-if="item.selfChooseSide === 'left'">我站这边</view>
<image class="fighter-avatar" :src="item.left.avatar" mode="aspectFill" />
<text class="fighter-name">{{ item.left.name }}</text>
<text class="fighter-votes">{{ item.leftVoteCount }} </text>
</view>
<view class="battle-middle">
<view class="vs-badge">VS</view>
<text class="battle-middle-text">{{ item.voteCount }} 人参与</text>
</view>
<view class="fighter-card right" :class="{ selected: item.selfChooseSide === 'right' }">
<view class="fighter-badge fighter-badge--right" v-if="item.selfChooseSide === 'right'">我站这边</view>
<image class="fighter-avatar" :src="item.right.avatar" mode="aspectFill" />
<text class="fighter-name">{{ item.right.name }}</text>
<text class="fighter-votes">{{ item.rightVoteCount }} </text>
</view>
</view>
<view class="support-bar">
<view class="support-fill left-fill" :style="{ width: `${item.leftPercent}%` }"></view>
<view class="support-fill right-fill" :style="{ width: `${item.rightPercent}%` }"></view>
</view>
<view class="support-row">
<text class="support-text left-text">{{ item.leftPercent }}% 支持 {{ item.left.name }}</text>
<text class="support-text right-text">{{ item.rightPercent }}% 支持 {{ item.right.name }}</text>
</view>
<view class="pk-card-footer">
<view class="choice-pill" :class="item.selfChooseSide === 'right' ? 'choice-pill--right' : ''">
<text class="choice-pill-label">我的选择</text>
<text class="choice-pill-value">{{ item.selfChooseName }}</text>
</view>
<text class="footer-link">继续查看</text>
</view>
</view>
</view>
</view>
<view v-if="pkList.length" class="load-more">
<text>{{ loading ? '加载中...' : (hasMore ? '上拉加载更多' : '没有更多了') }}</text>
</view>
<LoginPopup ref="loginPopupRef" @logind="handleLoginSuccess" />
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad, onPullDownRefresh, onReachBottom, onShow } from "@dcloudio/uni-app";
import { getStatusBarHeight } from "@/utils/system";
import { fetchUserPkList } from "@/api/pk";
import { FILE_BASE_URL } from "@/utils/constants";
import { formatDate } from "@/utils/date";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const loginPopupRef = ref(null);
const statusBarHeight = ref(getStatusBarHeight() || 44);
const pkList = ref([]);
const loading = ref(false);
const hasMore = ref(true);
const page = ref(1);
const totalCount = ref(0);
const loadError = ref("");
const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName);
const supportWinCount = computed(() => {
return pkList.value.filter((item) => {
if (item.selfChooseSide === "left") return item.leftPercent >= item.rightPercent;
if (item.selfChooseSide === "right") return item.rightPercent >= item.leftPercent;
return false;
}).length;
});
const buildAvatarUrl = (avatar) => {
if (!avatar) return "https://api.dicebear.com/7.x/avataaars/svg?seed=fallback";
const normalizedAvatar = String(avatar).trim();
return normalizedAvatar.startsWith("http") ? normalizedAvatar : `${FILE_BASE_URL}${normalizedAvatar}`;
};
const clampPercent = (value) => {
const number = Number(value);
if (!Number.isFinite(number)) return 0;
return Math.max(0, Math.min(100, Math.round(number)));
};
const normalizePercentPair = (item) => {
let leftPercent = clampPercent(item.leftPercent);
let rightPercent = clampPercent(item.rightPercent);
if (!leftPercent && !rightPercent) {
const leftVotes = Number(item.leftVoteCount || 0);
const rightVotes = Number(item.rightVoteCount || 0);
const totalVotes = leftVotes + rightVotes;
if (totalVotes > 0) {
leftPercent = Math.round((leftVotes / totalVotes) * 100);
rightPercent = 100 - leftPercent;
} else {
leftPercent = 50;
rightPercent = 50;
}
} else if (!leftPercent) {
leftPercent = 100 - rightPercent;
} else if (!rightPercent) {
rightPercent = 100 - leftPercent;
}
const total = leftPercent + rightPercent;
if (total && total !== 100) {
leftPercent = Math.round((leftPercent / total) * 100);
rightPercent = 100 - leftPercent;
}
return { leftPercent, rightPercent };
};
const normalizePkItem = (item = {}) => {
const { leftPercent, rightPercent } = normalizePercentPair(item);
const selfChooseSide =
item.selfChooseSide ||
(item.selfChooseItemId && item.selfChooseItemId === item.leftItemId
? "left"
: item.selfChooseItemId && item.selfChooseItemId === item.rightItemId
? "right"
: "");
const selfChooseName =
selfChooseSide === "right"
? (item.rightItemName || "右侧对象")
: selfChooseSide === "left"
? (item.leftItemName || "左侧对象")
: "未记录";
return {
id: item.id || "",
topicId: item.topicId || "",
topicTitle: item.topicTitle || "未知话题",
voteCount: Number(item.voteCount || 0),
leftVoteCount: Number(item.leftVoteCount || 0),
rightVoteCount: Number(item.rightVoteCount || 0),
selfChooseItemId: item.selfChooseItemId || "",
selfChooseSide,
selfChooseName,
leftPercent,
rightPercent,
votedAt: item.votedAt || "",
votedAtText: formatDate(item.votedAt, "YYYY-MM-DD HH:mm") || "刚刚参与",
left: {
id: item.leftItemId || "",
name: item.leftItemName || "左侧对象",
avatar: buildAvatarUrl(item.leftItemAvatar),
},
right: {
id: item.rightItemId || "",
name: item.rightItemName || "右侧对象",
avatar: buildAvatarUrl(item.rightItemAvatar),
},
};
};
const goBack = () => {
uni.navigateBack();
};
const openLoginPopup = (message = "请先登录") => {
loginPopupRef.value?.open();
if (message) {
uni.showToast({ title: message, icon: "none" });
}
};
const goToPkStation = () => {
uni.switchTab({
url: "/pages/pk/index",
});
};
const goToPkDetail = (item) => {
if (!item?.topicId) return;
const focusItemId = item.selfChooseItemId || item.left.id;
uni.navigateTo({
url: `/pages/rating/pk?topicId=${item.topicId}&itemId=${focusItemId}`,
});
};
const loadList = async (targetPage = 1) => {
if (!isLoggedIn.value || loading.value) return;
try {
loading.value = true;
loadError.value = "";
const res = await fetchUserPkList({ page: targetPage });
const data = res?.data || res || {};
const rawList = data.list || res?.list || [];
const list = Array.isArray(rawList) ? rawList : [];
const formattedList = list.map(normalizePkItem);
const nextTotalCount = Number(data.totalCount || res?.totalCount || 0);
totalCount.value = nextTotalCount;
pkList.value = targetPage === 1 ? formattedList : [...pkList.value, ...formattedList];
if (nextTotalCount > 0) {
hasMore.value = pkList.value.length < nextTotalCount;
} else {
hasMore.value = list.length > 0;
}
} catch (error) {
loadError.value = "网络开了点小差,稍后重试";
if (targetPage > 1) {
page.value -= 1;
}
} finally {
loading.value = false;
}
};
const refreshList = async () => {
page.value = 1;
hasMore.value = true;
await loadList(1);
};
const handleLoginSuccess = async () => {
await refreshList();
};
onLoad(() => {});
onShow(() => {
if (!isLoggedIn.value) {
pkList.value = [];
totalCount.value = 0;
hasMore.value = false;
return;
}
refreshList();
});
onPullDownRefresh(async () => {
if (isLoggedIn.value) {
await refreshList();
}
uni.stopPullDownRefresh();
});
onReachBottom(() => {
if (!isLoggedIn.value || !hasMore.value || loading.value) return;
page.value += 1;
loadList(page.value);
});
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background:
radial-gradient(circle at top, rgba(111, 90, 255, 0.12), transparent 30%),
linear-gradient(180deg, #f7f8ff 0%, #f6f7fb 42%, #ffffff 100%);
padding-bottom: 56rpx;
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(247, 248, 255, 0.88);
backdrop-filter: blur(24rpx);
}
.nav-content {
height: 44px;
display: flex;
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='%23202433' 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: 800;
color: #202433;
}
.content-wrap {
padding: 24rpx 32rpx 0;
}
.hero-card,
.pk-card,
.state-card {
background: rgba(255, 255, 255, 0.94);
border: 2rpx solid rgba(229, 232, 246, 0.92);
border-radius: 32rpx;
box-shadow: 0 18rpx 42rpx rgba(87, 92, 145, 0.08);
}
.hero-card {
padding: 32rpx;
margin-bottom: 24rpx;
}
.hero-main {
margin-bottom: 24rpx;
}
.hero-badge {
display: inline-flex;
align-items: center;
height: 44rpx;
padding: 0 18rpx;
border-radius: 999rpx;
background: rgba(92, 67, 245, 0.1);
color: #5c43f5;
font-size: 22rpx;
font-weight: 700;
margin-bottom: 18rpx;
}
.hero-title {
display: block;
font-size: 42rpx;
font-weight: 800;
color: #1f2432;
margin-bottom: 12rpx;
}
.hero-desc {
font-size: 25rpx;
color: #7b8191;
line-height: 1.7;
}
.hero-stats {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 20rpx 8rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, rgba(92, 67, 245, 0.08), rgba(92, 67, 245, 0.02));
}
.hero-stat {
flex: 1;
text-align: center;
}
.hero-stat-num {
display: block;
font-size: 38rpx;
font-weight: 800;
color: #202433;
margin-bottom: 8rpx;
}
.hero-stat-label {
font-size: 22rpx;
color: #8a90a0;
}
.hero-stat-divider {
width: 2rpx;
height: 52rpx;
background: rgba(206, 211, 229, 0.8);
}
.state-card {
position: relative;
overflow: hidden;
padding: 64rpx 40rpx;
text-align: center;
}
.state-title {
display: block;
font-size: 32rpx;
color: #232734;
font-weight: 800;
margin-bottom: 14rpx;
}
.state-desc {
display: block;
font-size: 25rpx;
color: #8a90a0;
line-height: 1.7;
margin-bottom: 28rpx;
}
.state-btn {
width: 240rpx;
height: 84rpx;
line-height: 84rpx;
border-radius: 999rpx;
background: #111827;
color: #fff;
font-size: 28rpx;
font-weight: 700;
}
.state-btn::after {
border: none;
}
.empty-card {
padding-top: 92rpx;
}
.empty-orbit {
position: absolute;
border-radius: 50%;
border: 2rpx dashed rgba(99, 102, 241, 0.16);
}
.orbit-1 {
width: 200rpx;
height: 200rpx;
top: 26rpx;
left: 50%;
transform: translateX(-50%);
}
.orbit-2 {
width: 140rpx;
height: 140rpx;
top: 56rpx;
left: 50%;
transform: translateX(-50%);
border-style: solid;
}
.empty-core {
width: 112rpx;
height: 112rpx;
margin: 0 auto 130rpx;
border-radius: 50%;
background: linear-gradient(135deg, #ffffff 0%, #eef2ff 100%);
box-shadow: 0 16rpx 34rpx rgba(99, 102, 241, 0.14);
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 2;
}
.empty-emoji {
font-size: 52rpx;
}
.pk-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.pk-card {
padding: 28rpx;
}
.pk-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 24rpx;
}
.pk-topic-wrap {
flex: 1;
min-width: 0;
}
.pk-topic-label {
display: block;
font-size: 22rpx;
color: #8a90a0;
margin-bottom: 8rpx;
}
.pk-topic-title {
display: block;
font-size: 30rpx;
line-height: 1.45;
color: #202433;
font-weight: 800;
}
.pk-time-chip {
flex-shrink: 0;
padding: 10rpx 16rpx;
border-radius: 999rpx;
background: rgba(17, 24, 39, 0.06);
}
.pk-time-text {
font-size: 22rpx;
color: #6b7280;
}
.battle-stage {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
margin-bottom: 22rpx;
}
.fighter-card {
position: relative;
width: 224rpx;
min-height: 276rpx;
border-radius: 28rpx;
padding: 30rpx 18rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #fafbff 0%, #ffffff 100%);
border: 2rpx solid transparent;
box-sizing: border-box;
}
.fighter-card.left {
border-color: rgba(92, 67, 245, 0.18);
}
.fighter-card.right {
border-color: rgba(255, 153, 102, 0.2);
}
.fighter-card.selected {
transform: translateY(-4rpx);
box-shadow: 0 14rpx 28rpx rgba(92, 67, 245, 0.12);
}
.fighter-badge {
position: absolute;
top: 16rpx;
left: 16rpx;
padding: 8rpx 14rpx;
border-radius: 999rpx;
background: rgba(92, 67, 245, 0.12);
color: #5c43f5;
font-size: 20rpx;
font-weight: 700;
}
.fighter-badge--right {
background: rgba(255, 141, 95, 0.14);
color: #ff7a45;
}
.fighter-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
border: 6rpx solid #fff;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.08);
margin-bottom: 18rpx;
}
.fighter-name {
font-size: 30rpx;
color: #1f2432;
font-weight: 800;
text-align: center;
margin-bottom: 10rpx;
}
.fighter-votes {
font-size: 22rpx;
color: #858b9b;
}
.battle-middle {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
}
.vs-badge {
width: 90rpx;
height: 90rpx;
border-radius: 50%;
background: linear-gradient(135deg, #6a5bff, #4d44f1);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 900;
box-shadow: 0 16rpx 30rpx rgba(92, 67, 245, 0.24);
margin-bottom: 16rpx;
}
.battle-middle-text {
font-size: 22rpx;
color: #8a90a0;
}
.support-bar {
height: 18rpx;
border-radius: 999rpx;
overflow: hidden;
display: flex;
background: #eceff8;
margin-bottom: 16rpx;
}
.support-fill {
height: 100%;
}
.left-fill {
background: linear-gradient(90deg, #6557ff, #4d44f1);
}
.right-fill {
background: linear-gradient(90deg, #ffb15f, #ff8d5f);
}
.support-row {
display: flex;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 22rpx;
}
.support-text {
font-size: 24rpx;
font-weight: 700;
}
.left-text {
color: #5c43f5;
}
.right-text {
color: #ff8d5f;
text-align: right;
}
.pk-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.choice-pill {
display: inline-flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 18rpx;
border-radius: 999rpx;
background: rgba(92, 67, 245, 0.1);
}
.choice-pill--right {
background: rgba(255, 141, 95, 0.12);
}
.choice-pill-label {
font-size: 22rpx;
color: #8a90a0;
}
.choice-pill-value {
font-size: 24rpx;
font-weight: 700;
color: #4d44f1;
}
.choice-pill--right .choice-pill-value {
color: #ff7a45;
}
.footer-link {
font-size: 24rpx;
font-weight: 700;
color: #111827;
}
.load-more {
text-align: center;
padding: 24rpx 0 40rpx;
color: #999;
font-size: 24rpx;
}
</style>