Files
rating/pages/index/index.vue
2026-05-21 06:56:42 +08:00

608 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="home-container">
<!-- 顶部固定栏 (使用 padding-top 避开状态栏) -->
<view class="header-section" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="header-content">
<image
class="user-avatar"
:src="userInfo.avatarUrl || '/static/images/default-avatar.png'"
mode="aspectFill"
@tap="handleLogin"
/>
<view class="header-texts">
<text class="title">全民评分</text>
<text class="subtitle">看看全国网友怎么评分</text>
</view>
</view>
<!-- 顶部切换 tab -->
<scroll-view class="category-tabs" scroll-x :show-scrollbar="false">
<view class="tabs-wrapper">
<view
class="tab-item"
:class="{ active: currentCategoryId === category.id }"
v-for="category in categories"
:key="category.id"
@tap="switchCategory(category.id)"
>
<text class="tab-text">{{ category.title }}</text>
<view class="tab-line" v-if="currentCategoryId === category.id"></view>
</view>
</view>
</scroll-view>
</view>
<!-- 占位防止固定头部遮挡内容 (增加 tab 的高度) -->
<view :style="{ height: statusBarHeight + 104 + 'px' }"></view>
<!-- 列表内容 -->
<view class="topic-list">
<view class="topic-card" v-for="topic in topicList" :key="topic.id">
<!-- 卡片头部标题与统计信息 -->
<view class="topic-header">
<text class="topic-title">{{ topic.title }}</text>
<view class="topic-stats">
<view class="stat-item">
<text class="stat-icon icon-group"></text>
<text>{{ topic.participants }}人参与</text>
</view>
<view class="stat-item heat">
<text class="stat-icon icon-fire"></text>
<text>热度 {{ topic.heat }}</text>
</view>
</view>
</view>
<!-- 排行榜列表 -->
<view class="rank-list">
<view
class="rank-item"
v-for="(item, index) in topic.items"
:key="item.id"
@tap="goToRating(topic.id, item.id)"
>
<text class="rank-num" :class="'rank-' + (index + 1)">{{ index + 1 }}</text>
<image class="item-avatar" :src="item.avatar" mode="aspectFill"></image>
<text class="item-name">{{ item.name }}</text>
<text class="item-score" :class="'score-' + (index + 1)">{{ item.score }}</text>
</view>
</view>
<!-- AI 总结 -->
<view class="ai-summary">
<text class="ai-icon icon-robot"></text>
<view class="summary-text">
<text class="summary-label">AI总结</text>
<text class="summary-content">{{ topic.aiSummary }}</text>
</view>
</view>
<!-- 标签列表 -->
<view class="tag-list">
<view class="tag" v-for="(tag, tIndex) in topic.tags" :key="tIndex">
<text>#{{ tag }}</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more">
<text>{{ loading ? '加载中...' : (hasMore ? '上拉加载更多' : '没有更多了') }}</text>
</view>
</view>
<!-- 登录弹窗 -->
<LoginPopup @logind="handleLoginSuccess" />
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { getStatusBarHeight } from "@/utils/system";
import {
onShareAppMessage,
onShareTimeline,
onShow,
onPullDownRefresh,
onReachBottom,
onLoad,
} from "@dcloudio/uni-app";
import { useUserStore } from "@/stores/user";
import { getShareReward } from "@/api/system";
import { fetchTopicList, fetchTopicCategories } from "@/api/topic";
import { getShareToken, saveViewRequest } from "@/utils/common.js";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import adManager from "@/utils/adManager.js";
const userStore = useUserStore();
const statusBarHeight = ref(getStatusBarHeight());
const userInfo = computed(() => userStore?.userInfo || {});
const loading = ref(false);
const hasMore = ref(true);
const currentCategoryId = ref('');
const categories = ref([]);
const currentPage = ref(1);
const loadCategories = async () => {
try {
const res = await fetchTopicCategories();
// 提取真实数据数组
const list = res?.data?.list || res?.list || res || [];
// 增加一个“全部”的默认选项id 置空
categories.value = [{ id: '', title: '全部' }, ...list];
// 初始化选中的ID为“全部”
if (!currentCategoryId.value && categories.value.length > 0) {
currentCategoryId.value = categories.value[0].id;
}
// 加载初始列表
loadTopicList(currentCategoryId.value, currentPage.value);
} catch (error) {
console.error("获取分类失败:", error);
// 兜底数据
categories.value = [
{ id: '', title: '全部' },
{ id: 1, title: '历史' },
{ id: 2, title: '数码' },
];
// 使用兜底也尝试加载一次
loadTopicList(currentCategoryId.value, currentPage.value);
}
};
const loadTopicList = async (categoryId, page = 1) => {
if (page === 1) {
topicList.value = [];
}
try {
loading.value = true;
const res = await fetchTopicList(categoryId, page);
// 提取真实数据数组
const list = res?.list || [];
// 合并数据
topicList.value = [...topicList.value, ...list];
hasMore.value = list.length >= 5;
} catch (error) {
console.error("获取主题列表失败:", error);
hasMore.value = false;
} finally {
loading.value = false;
}
};
const switchCategory = (categoryId) => {
if (currentCategoryId.value === categoryId) return;
currentCategoryId.value = categoryId;
// 切换分类时重新加载数据
currentPage.value = 1;
hasMore.value = true;
loadTopicList(categoryId, currentPage.value);
};
const mockData = [
{
id: 1,
title: '你心中的千古一帝',
participants: '23.5万',
heat: '98k',
items: [
{ id: 1, name: '李世民', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=1', score: '9.8' },
{ id: 2, name: '秦始皇', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=2', score: '9.5' },
{ id: 3, name: '康熙', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=3', score: '9.2' },
],
aiSummary: '多数用户认为李世民综合能力最强,兼具文治武功。',
tags: ['历史', '皇帝', '争议']
},
{
id: 2,
title: '国产手机系统谁最强',
participants: '15万',
heat: '76k',
items: [
{ id: 1, name: 'iOS', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=4', score: '8.5' },
{ id: 2, name: 'HyperOS', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=5', score: '8.2' },
{ id: 3, name: 'OriginOS', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=6', score: '8.0' },
],
aiSummary: '系统流畅度与动效是用户关注的核心,底层优化成为评分关键。',
tags: ['数码', '手机', '测评']
},
{
id: 3,
title: '最值得定居的城市',
participants: '12.8万',
heat: '62k',
items: [
{ id: 1, name: '成都', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=7', score: '9.2' },
{ id: 2, name: '杭州', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=8', score: '8.9' },
{ id: 3, name: '厦门', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=9', score: '8.7' },
],
aiSummary: '宜居性与就业机会是主要博弈点,慢节奏生活深受青睐。',
tags: ['城市', '生活', '推荐']
}
];
const topicList = ref([...mockData]);
const goToRating = (topicId, itemId) => {
uni.navigateTo({
url: `/pages/rating/index?topicId=${topicId}&itemId=${itemId}`
});
};
const handleLogin = () => {
if (!userInfo.value.nickName) {
uni.$emit("show-login-popup");
}
};
let firstLoad = true;
const initData = () => {
if (adManager.isAdShowing) return; // 防止广告页返回时立即重新渲染引发 insertTextView:fail
};
onLoad(async (options) => {
uni.$trackRecord({
eventName: "index_page_visit",
eventType: `visit`,
})
if (options.shareToken) {
saveViewRequest(options.shareToken, "index");
}
await loadCategories(); // 初始化时加载分类
await loadTopicList(currentCategoryId.value); // 初始化时加载主题列表
initData();
});
onShow(() => {
if (firstLoad) {
firstLoad = false;
return;
}
initData();
});
const handleLoginSuccess = () => {
};
// 下拉刷新
onPullDownRefresh(async () => {
currentPage.value = 1;
hasMore.value = true;
await loadTopicList(currentCategoryId.value, currentPage.value);
uni.stopPullDownRefresh();
});
// 上拉加载更多
onReachBottom(() => {
if (!hasMore.value || loading.value) return;
currentPage.value += 1;
loadTopicList(currentCategoryId.value, currentPage.value);
});
onShareAppMessage(async () => {
const shareToken = await getShareToken("index");
getShareReward();
return {
title: "全民评分",
path: "/pages/index/index?shareToken=" + shareToken,
};
});
onShareTimeline(async () => {
const shareToken = await getShareToken("index");
getShareReward();
return {
title: "全民评分",
query: `shareToken=${shareToken}`,
};
});
</script>
<style lang="scss" scoped>
.home-container {
min-height: 100vh;
background-color: #f5f6fa;
box-sizing: border-box;
}
.header-section {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background-color: rgba(245, 246, 250, 0.95);
backdrop-filter: blur(20rpx);
padding-bottom: 20rpx;
}
.header-content {
display: flex;
align-items: center;
padding: 0 32rpx;
height: 120rpx;
}
.header-texts {
display: flex;
flex-direction: column;
justify-content: center;
}
.title {
font-size: 44rpx;
font-weight: 800;
color: #111;
margin-bottom: 6rpx;
}
.subtitle {
font-size: 24rpx;
color: #666;
}
.user-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
border: 2rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
margin-right: 24rpx; /* 头像在左边时,增加右侧间距 */
}
/* Tab 样式 */
.category-tabs {
width: 100%;
white-space: nowrap;
height: 88rpx;
}
.tabs-wrapper {
display: inline-flex;
padding: 0 16rpx;
height: 100%;
align-items: center;
}
.tab-item {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 24rpx;
height: 100%;
position: relative;
}
.tab-text {
font-size: 30rpx;
color: #666;
transition: all 0.3s ease;
}
.tab-item.active .tab-text {
font-size: 34rpx;
font-weight: bold;
color: #111;
}
.tab-line {
position: absolute;
bottom: 12rpx;
width: 32rpx;
height: 6rpx;
background-color: #2953ff;
border-radius: 4rpx;
transition: all 0.3s ease;
}
.topic-list {
padding: 24rpx 32rpx;
}
.topic-card {
background-color: #ffffff;
border-radius: 40rpx; /* 增加圆角,更圆润 */
padding: 36rpx; /* 稍微增加内边距,让内容更呼吸 */
margin-bottom: 36rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.04); /* 更柔和、扩散的阴影 */
transition: transform 0.3s ease, box-shadow 0.3s ease; /* 增加过渡动画 */
}
.topic-card:active {
transform: scale(0.98); /* 点击时微缩放效果 */
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
}
.topic-header {
margin-bottom: 32rpx;
}
.topic-title {
font-size: 38rpx; /* 稍微调大标题 */
font-weight: 800; /* 加粗标题 */
color: #1a1a1a;
margin-bottom: 16rpx;
display: block;
letter-spacing: 1rpx; /* 增加字间距 */
}
.topic-stats {
display: flex;
align-items: center;
}
.stat-item {
display: flex;
align-items: center;
font-size: 24rpx;
color: #888; /* 稍微调浅次要文本颜色 */
margin-right: 32rpx;
background-color: #f8f9fa; /* 增加极浅的背景色块 */
padding: 6rpx 16rpx;
border-radius: 100rpx; /* 完全圆角的统计标签 */
}
.stat-item.heat {
color: #ff4d4f;
background-color: #fff1f0; /* 热度专属浅红背景 */
}
.stat-icon {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
display: inline-block;
background-size: cover;
}
.icon-group {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23999999'%3E%3Cpath d='M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z'/%3E%3C/svg%3E");
}
.icon-fire {
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");
}
.rank-list {
margin-bottom: 32rpx;
background-color: #fafbfc; /* 增加极浅的灰底色作为容器 */
border-radius: 32rpx;
padding: 16rpx 24rpx;
}
.rank-item {
display: flex;
align-items: center;
padding: 16rpx 0;
border-bottom: 2rpx dashed rgba(0, 0, 0, 0.03); /* 柔和的虚线分割 */
}
.rank-item:last-child {
border-bottom: none;
}
.rank-num {
width: 44rpx;
font-size: 32rpx;
font-weight: 800;
text-align: center;
margin-right: 16rpx;
font-style: italic; /* 数字增加倾斜感,更动感 */
}
.rank-1 {
color: #2953ff;
font-size: 36rpx; /* 第一名数字更大 */
}
.rank-2 {
color: #ff8f00; /* 第二名橘色 */
}
.rank-3 {
color: #ffc107; /* 第三名黄色 */
}
.item-avatar {
width: 76rpx;
height: 76rpx;
border-radius: 50%;
margin-right: 24rpx;
background-color: #f0f0f0;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.06); /* 头像增加微阴影 */
border: 4rpx solid #fff; /* 头像增加白边 */
}
.item-name {
flex: 1;
font-size: 32rpx;
color: #2c2c2c;
font-weight: 500;
}
.item-score {
font-size: 36rpx;
font-weight: 800;
font-family: 'DIN Alternate', sans-serif; /* 推荐使用数字字体,如果没有则回退到 sans-serif */
}
.score-1 {
color: #2953ff;
}
.score-2 {
color: #ff8f00;
}
.score-3 {
color: #ffc107;
}
.ai-summary {
display: flex;
align-items: flex-start;
padding: 24rpx 32rpx;
background: linear-gradient(135deg, #f8f6ff 0%, #ffffff 100%); /* 增加极浅的渐变背景 */
border: 2rpx solid rgba(157, 91, 254, 0.15); /* 边框更加柔和透明 */
border-radius: 24rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(157, 91, 254, 0.04); /* 增加紫色的微光阴影 */
}
.ai-icon {
width: 36rpx;
height: 36rpx;
margin-right: 16rpx;
margin-top: 2rpx;
flex-shrink: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237B46F1'%3E%3Cpath d='M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h5a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h5V5.73A2 2 0 0 1 10 4a2 2 0 0 1 2-2zm-3 8a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm6 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z'/%3E%3C/svg%3E");
}
.summary-text {
flex: 1;
font-size: 28rpx;
line-height: 1.6;
}
.summary-label {
color: #7b46f1; /* 柔和一点的紫色 */
font-weight: 800;
}
.summary-content {
color: #4a4a4a;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.tag {
background-color: #f4eaff;
padding: 6rpx 20rpx;
border-radius: 100rpx;
}
.tag text {
color: #9d5bfe;
font-size: 22rpx;
}
.load-more {
text-align: center;
padding: 30rpx 0;
color: #999;
font-size: 24rpx;
}
</style>