Files
rating/pages/mine/pk.vue
2026-06-15 23:37:33 +08:00

806 lines
19 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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">我参与的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?.id) return;
const cacheKey = `pk_detail_cache_${item.id}`;
uni.setStorageSync(cacheKey, {
id: item.id,
topicId: item.topicId,
topicTitle: item.topicTitle,
leftItemId: item.left.id,
leftItemName: item.left.name,
leftItemAvatar: item.left.avatar,
rightItemId: item.right.id,
rightItemName: item.right.name,
rightItemAvatar: item.right.avatar,
voteCount: item.voteCount,
leftVoteCount: item.leftVoteCount,
rightVoteCount: item.rightVoteCount,
selfChooseItemId: item.selfChooseItemId,
selfChooseSide: item.selfChooseSide,
leftPercent: item.leftPercent,
rightPercent: item.rightPercent,
votedAt: item.votedAt,
});
uni.navigateTo({
url: `/pages/pk/detail?pkId=${item.id}&cacheKey=${cacheKey}`,
});
};
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>