Files
rating/pages/rating/index.vue
2026-06-01 23:55:03 +08:00

867 lines
21 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>
<view class="nav-logo-wrap">
<image class="nav-logo" src="/static/images/default-avatar.png" mode="aspectFill" />
</view>
<text class="nav-title">全民评分</text>
</view>
</view>
<!-- 顶部占位 -->
<view :style="{ height: statusBarHeight + 44 + 'px' }"></view>
<view class="content-wrap">
<!-- 头部信息 -->
<view class="topic-header">
<text class="topic-title">{{ topicData.title }}</text>
<view class="topic-stats">
<text class="stat-icon fire-icon"></text>
<text class="stat-text">热度 {{ topicData.hotScore }} w</text>
<text class="stat-text margin-left">{{ topicData.participantCount }}人参与</text>
</view>
</view>
<!-- AI 实时观察 -->
<view class="ai-observe">
<view class="ai-header">
<text class="ai-icon"></text>
<text class="ai-title">AI 实时观察</text>
</view>
<text class="ai-content">{{ topicData.aiSummary }}</text>
</view>
<!-- 领奖台 (Top 3) -->
<view class="podium-section" v-if="ratingItems && ratingItems.length >= 3">
<!-- 第二名 -->
<view class="podium-item rank-2" @tap="goToDetail(ratingItems[1].id)">
<view class="avatar-wrap">
<image class="avatar" :src="FILE_BASE_URL + ratingItems[1].avatarUrl" mode="aspectFill" />
<text class="rank-badge badge-2">2</text>
</view>
<view class="info-box box-2">
<text class="name">{{ ratingItems[1].name }}</text>
<text class="score">分数 {{ ratingItems[1].scoreAvg }}</text>
</view>
</view>
<!-- 第一名 -->
<view class="podium-item rank-1" @tap="goToDetail(ratingItems[0].id)">
<view class="avatar-wrap">
<image class="avatar avatar-1" :src="FILE_BASE_URL + ratingItems[0].avatarUrl" mode="aspectFill" />
<text class="rank-badge badge-1">1</text>
</view>
<view class="info-box box-1">
<text class="name name-1">{{ ratingItems[0].name }}</text>
<text class="score score-1">分数 {{ ratingItems[0].scoreAvg }}</text>
</view>
</view>
<!-- 第三名 -->
<view class="podium-item rank-3" @tap="goToDetail(ratingItems[2].id)">
<view class="avatar-wrap">
<image class="avatar" :src="FILE_BASE_URL + ratingItems[2].avatarUrl" mode="aspectFill" />
<text class="rank-badge badge-3">3</text>
</view>
<view class="info-box box-3">
<text class="name">{{ ratingItems[2].name }}</text>
<text class="score">分数 {{ ratingItems[2].scoreAvg }}</text>
</view>
</view>
</view>
<!-- 评分项列表 -->
<view class="rating-list">
<RatingCard
v-for="(item, index) in ratingItems"
:key="item.id"
:item="item"
@item-click="goToDetail"
@action-click="handleAction"
/>
</view>
<!-- 热门锐评 -->
<!-- <view class="comments-section">
<view class="section-header">
<text class="comment-icon"></text>
<text class="section-title">热门锐评</text>
</view>
<view class="comment-input-box">
<text class="edit-icon"></text>
<text class="placeholder">发表你的锐评...</text>
</view>
<view class="comment-list">
<view class="comment-item" v-for="comment in comments" :key="comment.id">
<view class="comment-user">
<image class="comment-avatar" :src="comment.avatar" mode="aspectFill" />
<text class="comment-name">{{ comment.name }}</text>
<text class="comment-time">{{ comment.time }}</text>
</view>
<view class="comment-content">
<text>{{ comment.content }}</text>
</view>
</view>
</view>
</view> -->
<!-- 加载更多 -->
<view class="load-more">
<text>{{ loading ? '加载中...' : (hasMore ? '上拉加载更多' : '没有更多了') }}</text>
</view>
</view>
<!-- 右下角悬浮添加按钮 -->
<view class="fab-btn" @tap="showAddPopup = true">
<text class="plus-icon">+</text>
</view>
<!-- 添加评分项弹窗 -->
<view class="popup-mask" v-if="showAddPopup" @tap="showAddPopup = false">
<view class="popup-content" @tap.stop>
<text class="popup-title">添加评分项</text>
<view class="form-item">
<text class="form-label">上传</text>
<view class="avatar-uploader" @tap="chooseAvatar">
<image v-if="newItem.avatar" :src="newItem.avatar" class="preview-avatar" mode="aspectFill" />
<view v-else class="upload-placeholder">
<text class="camera-icon"></text>
</view>
</view>
</view>
<view class="form-item">
<text class="form-label">名称</text>
<input class="form-input" type="text" v-model="newItem.name" placeholder="请输入名称" placeholder-class="input-placeholder" />
</view>
<view class="popup-actions">
<button class="btn-cancel" @tap="showAddPopup = false">取消</button>
<button class="btn-confirm" @tap="confirmAddItem">确定</button>
</view>
</view>
</view>
<!-- 评分成功特效弹窗 -->
<SuccessPopup ref="successPopupRef" />
</view>
</template>
<script setup>
import { ref } from 'vue';
import { getStatusBarHeight } from "@/utils/system";
import { onPullDownRefresh, onReachBottom, onLoad } from "@dcloudio/uni-app";
import { fetchTopicDetail, fetchTopicRatingItems } from "@/api/topic";
import { FILE_BASE_URL } from "@/utils/constants";
import RatingCard from "@/components/RatingCard/RatingCard.vue";
import SuccessPopup from "@/components/SuccessPopup/SuccessPopup.vue";
import { topicItemScore } from "@/api/topicItem";
// 状态栏高度处理
const statusBarHeight = ref(getStatusBarHeight() || 44);
const loading = ref(false);
const hasMore = ref(true);
const currentPage = ref(1);
const currentTopicId = ref(null);
const successPopupRef = ref(null);
const goBack = () => {
uni.navigateBack({
delta: 1
});
};
const topicData = ref({
title: '',
heat: '0',
participants: '0',
aiSummary: ''
});
const loadDetail = async (topicId) => {
try {
const res = await fetchTopicDetail(topicId);
const data = res?.data || res || {};
topicData.value = {
title: data.title || '',
heat: data.heat || '0',
participants: data.participants || '0',
aiSummary: data.aiSummary || '暂无总结',
participantCount: data.participantCount || '0',
hotScore: data.hotScore || '0'
};
} catch (e) {
console.error("获取主题详情失败:", e);
}
};
const showAddPopup = ref(false);
const newItem = ref({
name: '',
avatar: ''
});
const chooseAvatar = () => {
uni.chooseImage({
count: 1,
success: (res) => {
newItem.value.avatar = res.tempFilePaths[0];
}
});
};
const confirmAddItem = () => {
if (!newItem.value.name.trim()) {
uni.showToast({ title: '请输入名称', icon: 'none' });
return;
}
// 模拟添加
const newRatingItem = {
id: Date.now(),
rank: ratingItems.value.length + 1,
name: newItem.value.name,
avatar: newItem.value.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${Date.now()}`,
score: '0.0',
quote: '刚刚加入讨论,快来发表你的看法吧。',
userAction: ''
};
ratingItems.value.push(newRatingItem);
// 重置并关闭
newItem.value = { name: '', avatar: '' };
showAddPopup.value = false;
uni.showToast({ title: '添加成功', icon: 'success' });
};
const ratingItems = ref([]);
const loadItems = async (topicId, page = 1) => {
if (page === 1) {
ratingItems.value = [];
}
try {
loading.value = true;
const res = await fetchTopicRatingItems(topicId, page);
const list = res?.data?.records || res?.records || res?.list || [];
ratingItems.value = [...ratingItems.value, ...list];
hasMore.value = list.length >= 10; // 假设 pageSize 是 10
} catch (error) {
console.error("获取评分项列表失败:", error);
hasMore.value = false;
} finally {
loading.value = false;
}
};
onLoad((options) => {
uni.$trackRecord({
eventName: "rating_topic_detail_page_visit",
eventType: `visit`,
elementId: options.topicId
})
if (options.topicId) {
currentTopicId.value = options.topicId;
loadDetail(options.topicId);
loadItems(options.topicId, 1);
}
});
const mockComments = [
{
id: 1,
name: '史学家阿强',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=c1',
time: '刚刚',
content: '李世民唯一缺点是儿子太抽象。他在位时那种包容力和大局观,放眼整个历史都是炸裂的存在。'
},
{
id: 2,
name: '逻辑帝',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=c2',
time: '2小时前',
content: '秦始皇是开辟者李世民是完善者。一个是0到1的惊天动地一个是1到100的极致巅峰。'
}
];
const comments = ref([...mockComments]);
const handleAction = async (item, action) => {
const originAction = item.userAction;
const originScore = item.scoreAvg;
const isCancel = item.userAction === action.label;
try {
item.userAction = isCancel ? '' : action.label;
const res = await topicItemScore({
topicId: currentTopicId.value,
itemId: item.id,
scoreType: action.scoreType
});
item.scoreAvg = res?.scoreAvg || originScore;
// 如果是成功评分(非取消),展示特效弹窗
if (!isCancel && successPopupRef.value) {
successPopupRef.value.show(action);
}
} catch (error) {
item.userAction = originAction;
item.scoreAvg = originScore;
}
};
const goToDetail = (itemId) => {
uni.navigateTo({
url: `/pages/rating/detail?itemId=${itemId}&topicId=${currentTopicId.value}`
});
};
// 下拉刷新
onPullDownRefresh(async () => {
if (currentTopicId.value) {
currentPage.value = 1;
hasMore.value = true;
await Promise.all([
loadDetail(currentTopicId.value),
loadItems(currentTopicId.value, currentPage.value)
]);
}
// 模拟评论刷新(此处未接接口,暂时保留原有模拟刷新逻辑,去掉 mockRatingItems 的重置)
setTimeout(() => {
comments.value = [...mockComments];
uni.stopPullDownRefresh();
}, 1000);
});
// 上拉加载更多
onReachBottom(() => {
if (!hasMore.value || loading.value) return;
if (currentTopicId.value) {
currentPage.value += 1;
loadItems(currentTopicId.value, currentPage.value);
}
// 原有的评论模拟加载(根据需要决定是否保留)
setTimeout(() => {
const moreComments = mockComments.map(c => ({...c, id: c.id + comments.value.length}));
comments.value.push(...moreComments);
}, 1000);
});
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #fafbfe; /* 浅蓝灰色背景 */
padding-bottom: 120rpx; /* 留出底部 tabBar 空间 */
}
/* 导航栏 */
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: rgba(250, 251, 254, 0.95);
backdrop-filter: blur(20rpx);
}
.nav-content {
height: 44px;
display: flex;
align-items: center;
padding: 0 32rpx;
}
.nav-back {
padding: 10rpx 24rpx 10rpx 0;
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%3Cpolyline points='15 18 9 12 15 6'%3E%3C/polyline%3E%3C/svg%3E");
background-size: cover;
}
.nav-logo-wrap {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: linear-gradient(135deg, #001f3f, #0088a9);
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
}
.nav-logo {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
}
.nav-title {
font-size: 32rpx;
font-weight: bold;
color: #2953ff;
}
.content-wrap {
padding: 24rpx 32rpx;
}
/* 头部信息 */
.topic-header {
margin-bottom: 32rpx;
margin-top: 16rpx;
}
.topic-title {
font-size: 46rpx;
font-weight: 800;
color: #111;
display: block;
margin-bottom: 20rpx;
letter-spacing: 2rpx;
}
.topic-stats {
display: flex;
align-items: center;
}
.stat-icon.fire-icon {
width: 26rpx;
height: 26rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ff4d4f'%3E%3Cpath d='M19.48 13.03c-.02-.19-.13-.36-.29-.46-.17-.11-.38-.13-.56-.05-.98.42-2.12.3-2.92-.31-1.07-.81-1.55-2.22-1.28-3.75.12-.66.1-1.32-.06-1.97-.24-.95-1.1-1.6-2.09-1.58-1 .02-1.83.74-2 1.72-.25 1.55-.95 2.94-2 4.02-1.38 1.41-2.05 3.39-1.85 5.4.2 2.05 1.42 3.86 3.25 4.82 1.37.72 2.94 1.01 4.5.83 2.74-.32 5.06-2.28 5.86-4.95.23-.75.29-1.52.19-2.28-.06-.5-.21-.99-.41-1.45l-.34-.99zm-4.98 6.47c-1.37 0-2.5-1.13-2.5-2.5s1.13-2.5 2.5-2.5 2.5 1.13 2.5 2.5-1.13 2.5-2.5 2.5z'/%3E%3C/svg%3E");
background-size: cover;
margin-right: 12rpx;
}
.stat-text {
font-size: 24rpx;
color: #FF5A5F;
font-weight: 500;
}
.stat-text.margin-left {
margin-left: 32rpx;
color: #555;
font-weight: normal;
}
/* AI 实时观察 */
.ai-observe {
background: #ffffff;
border: 2rpx solid rgba(77, 68, 241, 0.2);
border-radius: 24rpx;
padding: 28rpx;
margin-bottom: 50rpx;
box-shadow: 0 8rpx 24rpx rgba(77, 68, 241, 0.03);
}
.ai-header {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.ai-icon {
width: 32rpx;
height: 32rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%234d44f1'%3E%3Cpath d='M12 2l2.4 5.6L20 10l-5.6 2.4L12 18l-2.4-5.6L4 10l5.6-2.4L12 2zm0 0'/%3E%3C/svg%3E");
background-size: cover;
margin-right: 12rpx;
}
.ai-title {
font-size: 28rpx;
font-weight: bold;
color: #4d44f1;
}
.ai-content {
font-size: 26rpx;
color: #444;
line-height: 1.6;
}
/* 领奖台 (Top 3) */
.podium-section {
display: flex;
justify-content: center;
align-items: flex-end;
height: 360rpx;
margin-bottom: 50rpx;
gap: 16rpx;
}
.podium-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.avatar-wrap {
position: relative;
z-index: 2;
margin-bottom: -44rpx; /* 将头像下移,覆盖在底座上 */
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
border: 4rpx solid #fff;
box-shadow: 0 8rpx 16rpx rgba(0,0,0,0.1);
background-color: #eee;
}
.avatar-1 {
width: 140rpx;
height: 140rpx;
border-color: #ffd700; /* 冠军金边 */
border-width: 6rpx;
}
.rank-badge {
position: absolute;
bottom: 4rpx;
right: -8rpx;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
color: #111;
font-size: 22rpx;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid #fff;
}
.badge-1 { background-color: #ffd700; width: 44rpx; height: 44rpx; right: -10rpx; bottom: 0; font-size: 26rpx; }
.badge-2 { background-color: #e2e8f0; }
.badge-3 { background-color: #e2e8f0; }
.info-box {
width: 200rpx;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 56rpx;
padding-bottom: 24rpx;
border-radius: 20rpx;
}
.box-1 {
width: 220rpx;
height: 190rpx;
background: linear-gradient(180deg, #6c5ce7 0%, #5a48eb 100%);
color: #fff;
box-shadow: 0 12rpx 32rpx rgba(108, 92, 231, 0.3);
}
.box-2 {
height: 150rpx;
background-color: #e4e7f3;
}
.box-3 {
height: 150rpx; /* 从 130rpx 调整到 150rpx */
background-color: #eef1f8;
}
.name {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.name-1 {
color: #fff;
font-size: 34rpx;
}
.score {
font-size: 24rpx;
color: #888;
}
.score-1 {
color: rgba(255,255,255,0.8);
}
/* 评分项列表 */
.rating-list {
margin-bottom: 40rpx;
}
/* 热门锐评 */
.comments-section {
margin-bottom: 40rpx;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 32rpx;
}
.comment-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%3Cpath d='M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'%3E%3C/path%3E%3C/svg%3E");
background-size: cover;
margin-right: 12rpx;
}
.section-title {
font-size: 36rpx;
font-weight: bold;
color: #111;
}
.comment-input-box {
background-color: #f0f2f7;
border-radius: 20rpx;
padding: 24rpx 32rpx;
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.edit-icon {
width: 36rpx;
height: 36rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23666'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E");
background-size: cover;
margin-right: 16rpx;
}
.placeholder {
font-size: 28rpx;
color: #888;
}
.comment-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.comment-item {
background-color: #f5f7fb;
border-radius: 28rpx;
padding: 28rpx;
}
.comment-user {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.comment-avatar {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
margin-right: 16rpx;
background-color: #eee;
}
.comment-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.comment-time {
font-size: 24rpx;
color: #999;
}
.comment-content {
font-size: 30rpx;
color: #333;
line-height: 1.6;
}
.load-more {
text-align: center;
padding: 30rpx 0;
color: #999;
font-size: 24rpx;
}
/* 悬浮添加按钮 */
.fab-btn {
position: fixed;
right: 40rpx;
bottom: 160rpx; /* 留出 tabBar 的距离 */
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #4d44f1, #2953ff);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(41, 83, 255, 0.4);
z-index: 99;
transition: transform 0.2s;
}
.fab-btn:active {
transform: scale(0.9);
}
.plus-icon {
color: #fff;
font-size: 60rpx;
font-weight: 300;
line-height: 1;
margin-top: -8rpx;
}
/* 弹窗样式 */
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
.popup-content {
width: 600rpx;
background-color: #fff;
border-radius: 32rpx;
padding: 40rpx;
box-sizing: border-box;
animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes popIn {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.popup-title {
font-size: 36rpx;
font-weight: bold;
color: #111;
text-align: center;
display: block;
margin-bottom: 40rpx;
}
.form-item {
margin-bottom: 32rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
display: block;
font-weight: 500;
}
.avatar-uploader {
width: 120rpx;
height: 120rpx;
border-radius: 20rpx;
background-color: #f5f6fa;
border: 2rpx dashed #d1d5db;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.preview-avatar {
width: 100%;
height: 100%;
}
.upload-placeholder {
display: flex;
align-items: center;
justify-content: center;
}
.camera-icon {
width: 48rpx;
height: 48rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z'%3E%3C/path%3E%3Ccircle cx='12' cy='13' r='4'%3E%3C/circle%3E%3C/svg%3E");
background-size: cover;
}
.form-input {
height: 80rpx;
background-color: #f5f6fa;
border-radius: 16rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
}
.input-placeholder {
color: #aaa;
}
.popup-actions {
display: flex;
justify-content: space-between;
margin-top: 48rpx;
gap: 24rpx;
}
.btn-cancel, .btn-confirm {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
border-radius: 100rpx;
font-size: 30rpx;
margin: 0;
}
.btn-cancel::after, .btn-confirm::after {
border: none;
}
.btn-cancel {
background-color: #f0f2f7;
color: #666;
}
.btn-confirm {
background: linear-gradient(135deg, #4d44f1, #2953ff);
color: #fff;
font-weight: bold;
}
</style>