Files
rating/pages/rating/pk.vue
2026-06-12 01:26:41 +08:00

644 lines
16 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-back" @tap="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">PK 对决</text>
<view class="nav-placeholder"></view>
</view>
</view>
<view :style="{ height: statusBarHeight + 44 + 'px' }"></view>
<view class="content-wrap" v-if="leftItem && rightItem">
<view class="hero-card">
<text class="hero-badge">Battle Mode</text>
<text class="hero-title">{{ topicTitle || '夯拉评分 PK 对决' }}</text>
<text class="hero-subtitle">你更看好谁快速站队感受榜单碰撞的爽感</text>
</view>
<view class="battle-card">
<view class="battle-arena">
<view class="fighter-card left" :class="{ selected: selectedSide === 'left' }">
<image class="fighter-avatar" :src="leftItem.avatar" mode="aspectFill" />
<text class="fighter-name">{{ leftItem.name }}</text>
<text class="fighter-score">{{ leftItem.scoreText }}</text>
</view>
<view class="battle-center">
<view class="vs-badge">VS</view>
<text class="battle-tip">{{ scoreGapText }}</text>
</view>
<view class="fighter-card right" :class="{ selected: selectedSide === 'right' }">
<image class="fighter-avatar" :src="rightItem.avatar" mode="aspectFill" />
<text class="fighter-name">{{ rightItem.name }}</text>
<text class="fighter-score">{{ rightItem.scoreText }}</text>
</view>
</view>
<view class="support-bar">
<view class="support-fill left-fill" :style="{ width: `${supportData.leftPercent}%` }"></view>
<view class="support-fill right-fill" :style="{ width: `${supportData.rightPercent}%` }"></view>
</view>
<view class="support-text">
<text class="left-text">{{ supportData.leftPercent }}% 看好 {{ leftItem.name }}</text>
<text class="right-text">{{ supportData.rightPercent }}% 看好 {{ rightItem.name }}</text>
</view>
<view class="action-row">
<button class="support-btn left-btn" @tap="handleSupport('left')">我站 {{ leftItem.name }}</button>
<button class="support-btn right-btn" @tap="handleSupport('right')">我站 {{ rightItem.name }}</button>
</view>
</view>
<view class="insight-card">
<view class="insight-header">
<image class="insight-icon" src="/static/images/icon/robot.png" mode="aspectFit"></image>
<text class="insight-title">AI PK 分析</text>
</view>
<text class="insight-content">{{ battleInsight }}</text>
</view>
<view class="rivals-card" v-if="rivalOptions.length">
<view class="rivals-header">
<text class="rivals-title">换个对手继续 PK</text>
<text class="random-action" @tap="randomPickRival">随机换人</text>
</view>
<scroll-view scroll-x class="rivals-scroll" :show-scrollbar="false">
<view class="rivals-list">
<view
v-for="item in rivalOptions"
:key="item.id"
class="rival-chip"
:class="{ active: String(item.id) === String(rightItem.id) }"
@tap="changeRival(item.id)"
>
<image class="rival-avatar" :src="item.avatar" mode="aspectFill" />
<text class="rival-name">{{ item.name }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
<view v-else class="empty-wrap">
<text class="empty-title">暂时还无法开启 PK</text>
<text class="empty-desc">至少需要两个评分对象快去为榜单补充更多角色吧</text>
</view>
<LoginPopup ref="loginPopupRef" @logind="handleLoginSuccess" />
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import { getStatusBarHeight } from "@/utils/system";
import { fetchTopicDetail, fetchTopicRatingItems } from "@/api/topic";
import { FILE_BASE_URL } from "@/utils/constants";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import { useUserStore } from "@/stores/user";
import { getShareReward } from "@/api/system";
import { getShareToken } from "@/utils/common";
const userStore = useUserStore();
const statusBarHeight = ref(getStatusBarHeight() || 44);
const loginPopupRef = ref(null);
const topicId = ref("");
const focusItemId = ref("");
const topicTitle = ref("");
const items = ref([]);
const leftItemId = ref("");
const rightItemId = ref("");
const selectedSide = ref("");
const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName);
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 normalizeItem = (item) => ({
...item,
avatar: buildAvatarUrl(item.avatarUrl),
score: Number(item.scoreAvg || 0),
scoreText: `${Number(item.scoreAvg || 0).toFixed(1)}`,
});
const sortedItems = computed(() => [...items.value].sort((a, b) => b.score - a.score));
const leftItem = computed(() => sortedItems.value.find((item) => String(item.id) === String(leftItemId.value)) || null);
const rightItem = computed(() => sortedItems.value.find((item) => String(item.id) === String(rightItemId.value)) || null);
const calcPercent = (leftScore, rightScore) => {
const total = leftScore + rightScore;
if (!total) return { leftPercent: 50, rightPercent: 50 };
let leftPercent = Math.round((leftScore / total) * 100);
leftPercent = Math.max(18, Math.min(82, leftPercent));
if (selectedSide.value === "left") {
leftPercent = Math.min(88, leftPercent + 4);
} else if (selectedSide.value === "right") {
leftPercent = Math.max(12, leftPercent - 4);
}
return {
leftPercent,
rightPercent: 100 - leftPercent,
};
};
const supportData = computed(() => {
if (!leftItem.value || !rightItem.value) {
return { leftPercent: 50, rightPercent: 50 };
}
return calcPercent(leftItem.value.score, rightItem.value.score);
});
const scoreGapText = computed(() => {
if (!leftItem.value || !rightItem.value) return "火热对决";
const gap = Math.abs(leftItem.value.score - rightItem.value.score).toFixed(1);
if (gap === "0.0") return "势均力敌";
return `分差 ${gap}`;
});
const battleInsight = computed(() => {
if (!leftItem.value || !rightItem.value) return "当前榜单对象不足,暂时无法生成 PK 分析。";
if (leftItem.value.score === rightItem.value.score) {
return `${leftItem.value.name}${rightItem.value.name} 当前分数持平,这会是一场纯主观审美的大混战。`;
}
const leader = leftItem.value.score > rightItem.value.score ? leftItem.value : rightItem.value;
const follower = leader.id === leftItem.value.id ? rightItem.value : leftItem.value;
const gap = Math.abs(leftItem.value.score - rightItem.value.score).toFixed(1);
return `${leader.name} 目前在榜单分数上领先 ${gap} 分,但 ${follower.name} 更容易在 PK 模式里制造反转,适合拉好友一起站队。`;
});
const rivalOptions = computed(() => {
if (!leftItem.value) return [];
return sortedItems.value.filter((item) => String(item.id) !== String(leftItem.value.id));
});
const openLoginPopup = (message = "请先登录后再操作") => {
loginPopupRef.value?.open();
if (message) {
uni.showToast({ title: message, icon: "none" });
}
};
const initBattle = () => {
if (sortedItems.value.length < 2) {
leftItemId.value = "";
rightItemId.value = "";
return;
}
const currentIndex = sortedItems.value.findIndex((item) => String(item.id) === String(focusItemId.value));
const primary = currentIndex >= 0 ? sortedItems.value[currentIndex] : sortedItems.value[0];
const rival =
sortedItems.value[currentIndex > 0 ? currentIndex - 1 : 1] ||
sortedItems.value.find((item) => String(item.id) !== String(primary.id));
leftItemId.value = primary?.id || "";
rightItemId.value = rival?.id || "";
};
const loadPageData = async () => {
if (!topicId.value) return;
try {
const [detailRes, itemsRes] = await Promise.all([
fetchTopicDetail(topicId.value),
fetchTopicRatingItems(topicId.value, 1),
]);
const detail = detailRes?.data || detailRes || {};
topicTitle.value = detail.title || "";
const rawList = itemsRes?.data?.records || itemsRes?.records || itemsRes?.data?.list || itemsRes?.list || [];
items.value = (Array.isArray(rawList) ? rawList : []).map(normalizeItem);
initBattle();
} catch (error) {
items.value = [];
}
};
const goBack = () => {
uni.navigateBack({ delta: 1 });
};
const changeRival = (id) => {
if (String(id) === String(leftItemId.value)) return;
rightItemId.value = id;
selectedSide.value = "";
};
const randomPickRival = () => {
if (!rivalOptions.value.length) return;
const pool = rivalOptions.value.filter((item) => String(item.id) !== String(rightItemId.value));
const targetPool = pool.length ? pool : rivalOptions.value;
const index = Math.floor(Math.random() * targetPool.length);
changeRival(targetPool[index].id);
};
const handleSupport = (side) => {
if (!isLoggedIn.value) {
openLoginPopup("请先登录后再站队");
return;
}
selectedSide.value = side;
const targetName = side === "left" ? leftItem.value?.name : rightItem.value?.name;
uni.showToast({
title: `已站队 ${targetName}`,
icon: "success",
});
};
const handleLoginSuccess = () => {};
onLoad((options) => {
topicId.value = options.topicId || "";
focusItemId.value = options.itemId || "";
loadPageData();
});
onShareAppMessage(async () => {
const shareToken = await getShareToken("rating_pk");
getShareReward({ scene: "rating_pk" });
return {
title: leftItem.value && rightItem.value
? `你更站 ${leftItem.value.name} 还是 ${rightItem.value.name}`
: `来夯拉评分看看这场 PK 谁会赢`,
path: `/pages/rating/pk?topicId=${topicId.value}&itemId=${focusItemId.value}&shareToken=${shareToken}`,
};
});
onShareTimeline(async () => {
const shareToken = await getShareToken("rating_pk");
getShareReward({ scene: "rating_pk_timeline" });
return {
title: leftItem.value && rightItem.value
? `${leftItem.value.name} VS ${rightItem.value.name},你站谁?`
: `夯拉评分 PK 模式已开启,快来站队`,
query: `topicId=${topicId.value}&itemId=${focusItemId.value}&shareToken=${shareToken}`,
};
});
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background:
radial-gradient(circle at top, rgba(108, 92, 231, 0.16), transparent 32%),
linear-gradient(180deg, #f7f8ff 0%, #f8f9fe 38%, #ffffff 100%);
padding-bottom: 40rpx;
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(247, 248, 255, 0.84);
backdrop-filter: blur(22rpx);
}
.nav-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
}
.nav-back,
.nav-placeholder {
width: 88rpx;
}
.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%3Cpolyline points='15 18 9 12 15 6'%3E%3C/polyline%3E%3C/svg%3E");
background-size: cover;
}
.nav-title {
font-size: 32rpx;
font-weight: 800;
color: #262b37;
}
.content-wrap {
padding: 24rpx 32rpx;
}
.hero-card,
.battle-card,
.insight-card,
.rivals-card {
background: rgba(255, 255, 255, 0.92);
border: 2rpx solid rgba(229, 232, 246, 0.9);
border-radius: 32rpx;
box-shadow: 0 18rpx 40rpx rgba(88, 93, 146, 0.08);
margin-bottom: 24rpx;
}
.hero-card {
padding: 32rpx;
}
.hero-badge {
display: inline-flex;
height: 44rpx;
align-items: center;
padding: 0 16rpx;
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;
color: #1e2230;
font-weight: 800;
line-height: 1.35;
margin-bottom: 12rpx;
}
.hero-subtitle {
font-size: 26rpx;
color: #747b8c;
line-height: 1.7;
}
.battle-card {
padding: 32rpx 28rpx;
}
.battle-arena {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 28rpx;
}
.fighter-card {
width: 240rpx;
min-height: 300rpx;
padding: 28rpx 20rpx;
border-radius: 28rpx;
background: linear-gradient(180deg, #f8f9ff 0%, #ffffff 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 2rpx solid transparent;
}
.fighter-card.left {
border-color: rgba(92, 67, 245, 0.18);
}
.fighter-card.right {
border-color: rgba(255, 168, 92, 0.2);
}
.fighter-card.selected {
box-shadow: 0 16rpx 30rpx rgba(92, 67, 245, 0.14);
transform: translateY(-4rpx);
}
.fighter-avatar {
width: 128rpx;
height: 128rpx;
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: #202433;
font-weight: 800;
text-align: center;
margin-bottom: 10rpx;
}
.fighter-score {
font-size: 24rpx;
color: #81889b;
}
.battle-center {
display: flex;
flex-direction: column;
align-items: center;
}
.vs-badge {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #715eff, #4d44f1);
color: #fff;
font-size: 32rpx;
font-weight: 900;
letter-spacing: 1rpx;
box-shadow: 0 18rpx 32rpx rgba(92, 67, 245, 0.25);
margin-bottom: 16rpx;
}
.battle-tip {
font-size: 22rpx;
color: #81889b;
}
.support-bar {
height: 18rpx;
border-radius: 999rpx;
overflow: hidden;
display: flex;
margin-bottom: 16rpx;
background: #eef1fb;
}
.support-fill {
height: 100%;
}
.left-fill {
background: linear-gradient(90deg, #5f56f4, #7a71ff);
}
.right-fill {
background: linear-gradient(90deg, #ffb056, #ff8d5f);
}
.support-text {
display: flex;
justify-content: space-between;
gap: 20rpx;
font-size: 24rpx;
font-weight: 700;
margin-bottom: 28rpx;
}
.left-text {
color: #5c43f5;
}
.right-text {
color: #ff8d5f;
text-align: right;
}
.action-row {
display: flex;
gap: 18rpx;
}
.support-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 700;
}
.support-btn::after {
border: none;
}
.left-btn {
background: linear-gradient(135deg, #6556ff, #4d44f1);
color: #fff;
}
.right-btn {
background: linear-gradient(135deg, #ffb15f, #ff8d5f);
color: #fff;
}
.insight-card {
padding: 28rpx;
}
.insight-header {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.insight-icon {
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
}
.insight-title {
font-size: 28rpx;
font-weight: 800;
color: #4d44f1;
}
.insight-content {
font-size: 26rpx;
color: #4b5161;
line-height: 1.7;
}
.rivals-card {
padding: 28rpx;
}
.rivals-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.rivals-title {
font-size: 30rpx;
font-weight: 800;
color: #212533;
}
.random-action {
font-size: 24rpx;
color: #5c43f5;
font-weight: 700;
}
.rivals-scroll {
white-space: nowrap;
}
.rivals-list {
display: inline-flex;
gap: 18rpx;
}
.rival-chip {
width: 176rpx;
padding: 18rpx 16rpx;
border-radius: 24rpx;
background: #f7f8fd;
border: 2rpx solid transparent;
display: flex;
flex-direction: column;
align-items: center;
}
.rival-chip.active {
background: #f1efff;
border-color: rgba(92, 67, 245, 0.18);
}
.rival-avatar {
width: 76rpx;
height: 76rpx;
border-radius: 50%;
margin-bottom: 12rpx;
}
.rival-name {
font-size: 24rpx;
color: #3a3f4d;
font-weight: 700;
text-align: center;
}
.empty-wrap {
min-height: 60vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0 56rpx;
}
.empty-title {
font-size: 34rpx;
color: #232733;
font-weight: 800;
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 26rpx;
color: #81889b;
line-height: 1.7;
}
</style>