From 4f7613f71c7bcd4e68eb8f2c5de10351897debcf Mon Sep 17 00:00:00 2001 From: zzc <1761997216@qq.com> Date: Thu, 11 Jun 2026 15:51:45 +0800 Subject: [PATCH] feat: pk --- api/pk.js | 21 ++ manifest.json | 2 +- pages.json | 22 ++ pages/pk/index.vue | 660 ++++++++++++++++++++++++++++++++++++++++ pages/rating/detail.vue | 105 ++++++- pages/rating/index.vue | 132 ++++++++ pages/rating/pk.vue | 643 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 1574 insertions(+), 11 deletions(-) create mode 100644 api/pk.js create mode 100644 pages/pk/index.vue create mode 100644 pages/rating/pk.vue diff --git a/api/pk.js b/api/pk.js new file mode 100644 index 0000000..fcf2a0a --- /dev/null +++ b/api/pk.js @@ -0,0 +1,21 @@ +import { request } from "@/utils/request.js"; + +// 获取随机一组PK主题 +export const fetchRandomTopicList = async () => { + return request({ + url: "/api/rating/pk/random", + method: "GET", + }); +}; + +/** + * 用户投票 + * @param { pkId, itemId } + */ +export const vote = async (data) => { + return request({ + url: "/api/rating/pk/vote", + method: "POST", + data, + }); +}; diff --git a/manifest.json b/manifest.json index b9d7797..f46886f 100644 --- a/manifest.json +++ b/manifest.json @@ -50,7 +50,7 @@ "quickapp" : {}, /* 小程序特有相关 */ "mp-weixin" : { - "appid" : "wx8d0a4bfb94c9a7f0", + "appid" : "wx146a2e83b50f9f75", "__usePrivacyCheck__" : true, "setting" : { "urlCheck" : false diff --git a/pages.json b/pages.json index b14ed03..cd9ac59 100644 --- a/pages.json +++ b/pages.json @@ -49,6 +49,14 @@ "backgroundColor": "#fbfbfb" } }, + { + "path": "pages/pk/index", + "style": { + "navigationBarTitleText": "PK站", + "navigationStyle": "custom", + "enablePullDownRefresh": true + } + }, { "path": "pages/webview/index", "style": { @@ -80,6 +88,14 @@ "enablePullDownRefresh": true } }, + { + "path": "pages/rating/pk", + "style": { + "navigationBarTitleText": "PK 对决", + "navigationStyle": "custom", + "enablePullDownRefresh": false + } + }, { "path": "pages/mine/topics", "style": { @@ -112,6 +128,12 @@ "iconPath": "static/images/tabBar/home.png", "selectedIconPath": "static/images/tabBar/home_s.png" }, + { + "text": "PK站", + "pagePath": "pages/pk/index", + "iconPath": "static/images/tabBar/creation.png", + "selectedIconPath": "static/images/tabBar/creation_s.png" + }, { "text": "发布", "pagePath": "pages/release/index", diff --git a/pages/pk/index.vue b/pages/pk/index.vue new file mode 100644 index 0000000..0b374f7 --- /dev/null +++ b/pages/pk/index.vue @@ -0,0 +1,660 @@ + + + + + diff --git a/pages/rating/detail.vue b/pages/rating/detail.vue index 52edf19..ea3f97e 100644 --- a/pages/rating/detail.vue +++ b/pages/rating/detail.vue @@ -55,31 +55,33 @@ /> - + @@ -144,6 +146,7 @@ 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"; @@ -162,6 +165,7 @@ const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nic 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 '没有更多了'; @@ -195,6 +199,80 @@ const detailData = ref({ const comments = ref([]); const itemId = ref(''); +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 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(); @@ -420,6 +498,7 @@ const getDetailData = async () => { if (currentPage.value === 1) { getCommentsData({ replace: true }); } + getPkPreviewData(); } catch (error) { console.error('获取详情失败:', error); uni.showToast({ @@ -737,6 +816,12 @@ onReachBottom(() => { color: #333; } +.pk-score { + margin-top: 8rpx; + font-size: 22rpx; + font-weight: 700; +} + .vs-text { font-size: 40rpx; font-weight: 900; diff --git a/pages/rating/index.vue b/pages/rating/index.vue index 537c653..8b2c645 100644 --- a/pages/rating/index.vue +++ b/pages/rating/index.vue @@ -37,6 +37,30 @@ {{ topicData.aiSummary }} + + + + PK 对决模式 + 快速站队,看谁更能打 + + + 进入 PK + + + + + + + {{ pkPair.left.name }} + + VS + + + {{ pkPair.right.name }} + + + + @@ -180,6 +204,10 @@ const currentTopicId = ref(null); const successPopupRef = ref(null); const loginPopupRef = ref(null); const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName); +const pkPair = computed(() => ({ + left: ratingItems.value?.[0] || null, + right: ratingItems.value?.[1] || null +})); const goBack = () => { uni.navigateBack({ @@ -373,6 +401,13 @@ const goToDetail = (itemId) => { }); }; +const goToPk = (itemId = '') => { + if (!currentTopicId.value) return; + uni.navigateTo({ + url: `/pages/rating/pk?topicId=${currentTopicId.value}${itemId ? `&itemId=${itemId}` : ''}` + }); +}; + const handleAddClick = () => { if (!isLoggedIn.value) { openLoginPopup(); @@ -582,6 +617,103 @@ onReachBottom(() => { line-height: 1.6; } +.pk-entry-card { + background: linear-gradient(135deg, #ffffff 0%, #f7f4ff 52%, #eef1ff 100%); + border: 2rpx solid rgba(99, 88, 255, 0.14); + border-radius: 28rpx; + padding: 28rpx; + margin-bottom: 42rpx; + box-shadow: 0 12rpx 30rpx rgba(91, 84, 223, 0.08); +} + +.pk-entry-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24rpx; +} + +.pk-entry-title { + display: block; + font-size: 32rpx; + font-weight: 800; + color: #1d2233; + margin-bottom: 8rpx; +} + +.pk-entry-subtitle { + font-size: 24rpx; + color: #7d8495; +} + +.pk-entry-action { + display: flex; + align-items: center; + color: #5c43f5; + font-size: 24rpx; + font-weight: 700; +} + +.pk-entry-arrow { + width: 24rpx; + height: 24rpx; + margin-left: 6rpx; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235c43f5' 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-entry-battle { + display: flex; + align-items: center; + justify-content: space-between; +} + +.pk-entry-user { + width: 220rpx; + display: flex; + flex-direction: column; + align-items: center; +} + +.pk-entry-avatar { + width: 104rpx; + height: 104rpx; + border-radius: 50%; + border: 4rpx solid #fff; + box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.08); + margin-bottom: 14rpx; +} + +.pk-entry-user.left .pk-entry-avatar { + border-color: rgba(92, 67, 245, 0.22); +} + +.pk-entry-user.right .pk-entry-avatar { + border-color: rgba(255, 154, 76, 0.22); +} + +.pk-entry-name { + font-size: 26rpx; + color: #2a2f3c; + font-weight: 700; + text-align: center; +} + +.pk-entry-vs { + width: 88rpx; + height: 88rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #6f5aff, #4d44f1); + color: #fff; + font-size: 30rpx; + font-weight: 900; + letter-spacing: 1rpx; + box-shadow: 0 14rpx 28rpx rgba(92, 67, 245, 0.22); +} + /* 领奖台 (Top 3) */ .podium-section { display: flex; diff --git a/pages/rating/pk.vue b/pages/rating/pk.vue new file mode 100644 index 0000000..e20aed1 --- /dev/null +++ b/pages/rating/pk.vue @@ -0,0 +1,643 @@ + + + + +