Files
rating/pages/rating/index.vue
2026-06-22 15:33:26 +08:00

1088 lines
26 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/logo.jpg" 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">
<image class="stat-icon stat-icon-img" src="/static/images/icon/fire.png" mode="aspectFit"></image>
<text class="stat-text">热度 {{ topicData.hotScore }} w</text>
<image class="stat-icon stat-icon-img stat-icon-gap" src="/static/images/icon/group.png" mode="aspectFit"></image>
<text class="stat-text stat-text-participant">{{ topicData.participantCount }}人参与</text>
</view>
</view>
<!-- AI 实时观察 -->
<view class="ai-observe">
<view class="ai-header">
<image class="ai-icon ai-icon-img" src="/static/images/icon/robot.png" mode="aspectFit"></image>
<text class="ai-title">AI 实时观察</text>
</view>
<text class="ai-content">{{ topicData.aiSummary }}</text>
</view>
<!-- <view v-if="pkPair.left && pkPair.right" class="pk-entry-card" @tap="goToPk()">
<view class="pk-entry-header">
<view class="pk-entry-title-wrap">
<text class="pk-entry-title">PK 对决模式</text>
<text class="pk-entry-subtitle">快速站队看谁更能打</text>
</view>
<view class="pk-entry-action">
<text>进入 PK</text>
<text class="pk-entry-arrow"></text>
</view>
</view>
<view class="pk-entry-battle">
<view class="pk-entry-user left">
<image class="pk-entry-avatar" :src="FILE_BASE_URL + pkPair.left.avatarUrl" mode="aspectFill" />
<text class="pk-entry-name">{{ pkPair.left.name }}</text>
</view>
<view class="pk-entry-vs">VS</view>
<view class="pk-entry-user right">
<image class="pk-entry-avatar" :src="FILE_BASE_URL + pkPair.right.avatarUrl" mode="aspectFill" />
<text class="pk-entry-name">{{ pkPair.right.name }}</text>
</view>
</view>
</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="handleAddClick">
<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" />
<LoginPopup ref="loginPopupRef" @logind="handleLoginSuccess" />
</view>
</template>
<script setup>
import { computed, ref } from 'vue';
import { getStatusBarHeight } from "@/utils/system";
import { onPullDownRefresh, onReachBottom, onLoad, onShareAppMessage, onShareTimeline } 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 LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import { topicItemScore, topicItemAdd } from "@/api/topicItem";
import { uploadImage } from "@/utils/common";
import { getShareToken } from "@/utils/common";
import { getShareReward } from "@/api/system";
import { useUserStore } from "@/stores/user";
// 状态栏高度处理
const userStore = useUserStore();
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 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({
delta: 1
});
};
const openLoginPopup = (message = '请先登录后再操作') => {
loginPopupRef.value?.open();
if (message) {
uni.showToast({ title: message, icon: 'none' });
}
};
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 = () => {
if (!isLoggedIn.value) {
openLoginPopup();
return;
}
uni.chooseImage({
count: 1,
success: (res) => {
newItem.value.avatar = res.tempFilePaths[0];
}
});
};
const confirmAddItem = async () => {
if (!isLoggedIn.value) {
openLoginPopup();
return;
}
if (!newItem.value.name.trim()) {
uni.showToast({ title: '请输入名称', icon: 'none' });
return;
}
if (!newItem.value.avatar) {
uni.showToast({ title: '请上传头像', icon: 'none' });
return;
}
try {
uni.showLoading({ title: '添加中...' });
// 1. 先通过 uni.uploadFile 上传头像图片
const uploadRes = await uploadImage(newItem.value.avatar);
// 2. 将上传成功后的图片 URL 和其他数据一起提交给添加接口
await topicItemAdd({
topicId: currentTopicId.value,
name: newItem.value.name,
avatarUrl: uploadRes
});
uni.hideLoading();
// uni.showToast({ title: '添加成功', icon: 'success' });
if (successPopupRef.value) {
successPopupRef.value.show({
emoji: '🎉',
label: '请耐心等待管理员审核'
}, 'release');
}
// 重置表单并关闭弹窗
newItem.value = { name: '', avatar: '' };
showAddPopup.value = false;
// 重新加载列表数据
currentPage.value = 1;
hasMore.value = true;
loadItems(currentTopicId.value, 1);
} catch (error) {
uni.hideLoading();
uni.showToast({ title: '添加失败,请重试', icon: 'none' });
console.error('添加评分项失败:', error);
}
};
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) {
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 handleAction = async (item, action) => {
if (!isLoggedIn.value) {
openLoginPopup();
return;
}
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);
}
uni.$trackRecord({
eventName: 'rating',
eventType: 'rating',
elementId: `item_${item.id}`,
elementContent: `评分项_${item.name}`,
customParams: {
scoreType: action.scoreType,
action: action.label,
topicId: currentTopicId.value,
topicTitle: topicData.value.title,
score: item.scoreAvg,
page: 'rating'
}
});
} catch (error) {
item.userAction = originAction;
item.scoreAvg = originScore;
}
};
const goToDetail = (itemId) => {
uni.$trackRecord({
eventName: 'jumpto_detail',
eventType: 'jump',
elementId: `item_${itemId}`,
elementContent: `评分项${itemId}`,
});
uni.navigateTo({
url: `/pages/rating/detail?itemId=${itemId}&topicId=${currentTopicId.value}`
});
};
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();
return;
}
showAddPopup.value = true;
};
const handleLoginSuccess = async () => {
if (!currentTopicId.value) return;
currentPage.value = 1;
hasMore.value = true;
await Promise.all([
loadDetail(currentTopicId.value),
loadItems(currentTopicId.value, 1)
]);
};
onShareAppMessage(async () => {
const shareToken = await getShareToken("rating_topic");
getShareReward({ scene: "rating_topic" });
const topName = ratingItems.value?.[0]?.name;
return {
title: topName
? `${topicData.value.title || "夯拉评分"}」正在热评,${topName}暂时领先`
: `${topicData.value.title || "夯拉评分"}」正在热评,快来参与打分`,
path: `/pages/rating/index?topicId=${currentTopicId.value}&shareToken=${shareToken}`,
};
});
onShareTimeline(async () => {
const shareToken = await getShareToken("rating_topic");
getShareReward({ scene: "rating_topic_timeline" });
const topName = ratingItems.value?.[0]?.name;
return {
title: topName
? `${topName}在「${topicData.value.title || "夯拉评分"}」里冲到第一了`
: `${topicData.value.title || "夯拉评分"}」榜单已开启,来看看大家怎么评`,
query: `topicId=${currentTopicId.value}&shareToken=${shareToken}`,
};
});
// 下拉刷新
onPullDownRefresh(async () => {
if (currentTopicId.value) {
currentPage.value = 1;
hasMore.value = true;
await Promise.all([
loadDetail(currentTopicId.value),
loadItems(currentTopicId.value, currentPage.value)
]);
}
uni.stopPullDownRefresh();
});
// 上拉加载更多
onReachBottom(() => {
if (!hasMore.value || loading.value) return;
if (currentTopicId.value) {
currentPage.value += 1;
loadItems(currentTopicId.value, currentPage.value);
}
});
</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%;
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 {
width: 26rpx;
height: 26rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.stat-icon-gap {
margin-left: 32rpx;
}
.stat-text {
font-size: 24rpx;
color: #FF5A5F;
font-weight: 500;
}
.stat-text-participant {
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;
margin-right: 12rpx;
}
.ai-icon-img {
display: block;
}
.ai-title {
font-size: 28rpx;
font-weight: bold;
color: #4d44f1;
}
.ai-content {
font-size: 26rpx;
color: #444;
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;
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>