Compare commits

..

20 Commits

Author SHA1 Message Date
zzc
628c02752c fix: youhua 2026-06-10 01:37:27 +08:00
zzc
f3baf1447a fix: youhua 2026-06-10 01:35:10 +08:00
zzc
aa88eef08e fix: youhua 2026-06-10 01:24:41 +08:00
zzc
1e5bbbbf9e fix: youhua 2026-06-10 01:10:58 +08:00
zzc
f95fe36b5a fix: youhua 2026-06-10 00:58:01 +08:00
zzc
8f11ed9333 fix: youhua 2026-06-10 00:54:12 +08:00
zzc
3ae62897b6 fix: youhua 2026-06-10 00:47:06 +08:00
zzc
21bfa3efd8 fix: youhua 2026-06-10 00:45:45 +08:00
zzc
d325329af4 fix: not login 2026-06-10 00:41:11 +08:00
zzc
0383ad6be2 fix: rating 2026-06-10 00:34:12 +08:00
zzc
d129f1d38e fix: rating 2026-06-10 00:25:10 +08:00
zzc
829f9a07d4 fix: rating 2026-06-10 00:09:22 +08:00
zzc
1173a3e3a1 fix: rating 2026-06-10 00:07:08 +08:00
zzc
a98f82c113 fix: rating 2026-06-10 00:03:29 +08:00
zzc
0e0d9604e1 fix: rating 2026-06-09 23:59:13 +08:00
zzc
d4478cccd8 fix: rating 2026-06-09 23:55:11 +08:00
zzc
36dbf05c11 fix: rating 2026-06-08 19:08:13 +08:00
zzc
03cb2edf5e fix:rating 2026-06-08 10:57:20 +08:00
zzc
65cef99610 fix:rating 2026-06-08 10:34:47 +08:00
zzc
27c244f3cd fix:rating 2026-06-08 06:47:04 +08:00
23 changed files with 1385 additions and 615 deletions

View File

@@ -1,5 +1,16 @@
import { request } from "@/utils/request.js"; import { request } from "@/utils/request.js";
/**
* @description: 获取当前用户参与的话题数据
* @return {participant: number, comment: number, liked: number, label: string[]}
*/
export const fetchUserNums = async () => {
return request({
url: "/api/rating/personal/nums",
method: "GET",
});
};
export const sendFeedback = async (data) => { export const sendFeedback = async (data) => {
return request({ return request({
url: "/api/common/feedback", url: "/api/common/feedback",

View File

@@ -40,3 +40,11 @@ export const fetchTopicRatingItems = async (topicId, page = 1) => {
method: "GET", method: "GET",
}); });
}; };
// 获取当前用户参与的话题列表
export const fetchUserTopics = async (page = 1) => {
return request({
url: `/api/rating/personal/topic/list?page=${page}`,
method: "GET",
});
};

View File

@@ -42,4 +42,14 @@ export const fetchTopicRatingItems = async (itemId) => {
url: `/api/rating/topic/item/detail/${itemId}`, url: `/api/rating/topic/item/detail/${itemId}`,
method: "GET", method: "GET",
}); });
};
// 添加评分项
// data = { topicId: 'xxx', name: 'xxx', avatarUrl: 'xxx' }
export const topicItemAdd = async (data) => {
return request({
url: "/api/rating/topic/item/add",
method: "POST",
data,
});
}; };

View File

@@ -20,10 +20,13 @@
class="comment-input" class="comment-input"
type="text" type="text"
v-model="commentText" v-model="commentText"
:disabled="sending"
placeholder="说点什么吧..." placeholder="说点什么吧..."
placeholder-class="input-placeholder" placeholder-class="input-placeholder"
/> />
<button class="send-btn" @tap="handleSend">发送</button> <button class="send-btn" :class="{ 'is-disabled': sending }" :disabled="sending" @tap="handleSend">
{{ sending ? '发送中...' : '发送' }}
</button>
</view> </view>
</view> </view>
</template> </template>
@@ -35,6 +38,10 @@ const props = defineProps({
initialAction: { initialAction: {
type: String, type: String,
default: '' default: ''
},
sending: {
type: Boolean,
default: false
} }
}); });
@@ -62,19 +69,28 @@ const handleAction = (action) => {
}; };
const handleSend = () => { const handleSend = () => {
if (!commentText.value.trim()) { const content = commentText.value.trim();
if (!content) {
uni.showToast({ uni.showToast({
title: '请输入评论内容', title: '请输入评论内容',
icon: 'none' icon: 'none'
}); });
return; return;
} }
if (props.sending) return;
emit('send-comment', { emit('send-comment', {
action: currentAction.value, action: currentAction.value,
content: commentText.value content
}); });
commentText.value = ''; // 发送后清空
}; };
const resetComment = () => {
commentText.value = '';
};
defineExpose({
resetComment
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -162,7 +178,11 @@ const handleSend = () => {
margin: 0; margin: 0;
} }
.send-btn.is-disabled {
opacity: 0.7;
}
.send-btn::after { .send-btn::after {
border: none; border: none;
} }
</style> </style>

View File

@@ -24,9 +24,10 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import ActionBadge from "@/components/ActionBadge/ActionBadge.vue"; import ActionBadge from "@/components/ActionBadge/ActionBadge.vue";
import { topicItemCommentLike } from "@/api/topicItem"; import { topicItemCommentLike } from "@/api/topicItem";
import { useUserStore } from "@/stores/user";
const props = defineProps({ const props = defineProps({
comment: { comment: {
@@ -36,6 +37,10 @@ const props = defineProps({
} }
}); });
const emit = defineEmits(['need-login', 'like-change']);
const userStore = useUserStore();
const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName);
const isLiked = ref(props.comment.isLiked || false); const isLiked = ref(props.comment.isLiked || false);
const currentLikes = ref(props.comment.likes || 0); const currentLikes = ref(props.comment.likes || 0);
let isLiking = false; // 防抖标志 let isLiking = false; // 防抖标志
@@ -49,6 +54,11 @@ watch(() => props.comment.isLiked, (newVal) => {
}); });
const handleLike = async () => { const handleLike = async () => {
if (!isLoggedIn.value) {
emit('need-login');
return;
}
if (isLiking) return; if (isLiking) return;
isLiking = true; isLiking = true;
@@ -60,10 +70,26 @@ const handleLike = async () => {
} else { } else {
currentLikes.value -= 1; currentLikes.value -= 1;
} }
emit('like-change', {
id: props.comment.id,
isLiked: isLiked.value,
likes: currentLikes.value
});
try { try {
// 调用点赞接口 (点赞和取消点赞为同一个接口) // 调用点赞接口 (点赞和取消点赞为同一个接口)
await topicItemCommentLike({ commentId: props.comment.id }); await topicItemCommentLike({ commentId: props.comment.id });
// 记录点赞事件
uni.$trackRecord({
eventName: 'like_comment',
eventType: 'like',
elementId: `comment_${props.comment.id}`,
elementContent: `评论项_${props.comment.name}`,
customParams: {
comment: props.comment.content,
page: 'rating_detail'
}
});
} catch (error) { } catch (error) {
// 接口调用失败,回滚 UI 状态 // 接口调用失败,回滚 UI 状态
isLiked.value = !isLiked.value; isLiked.value = !isLiked.value;
@@ -72,6 +98,11 @@ const handleLike = async () => {
} else { } else {
currentLikes.value -= 1; currentLikes.value -= 1;
} }
emit('like-change', {
id: props.comment.id,
isLiked: isLiked.value,
likes: currentLikes.value
});
uni.showToast({ title: '操作失败', icon: 'none' }); uni.showToast({ title: '操作失败', icon: 'none' });
} finally { } finally {
isLiking = false; isLiking = false;
@@ -194,4 +225,4 @@ const handleLike = async () => {
line-height: 1.65; line-height: 1.65;
word-break: break-all; word-break: break-all;
} }
</style> </style>

View File

@@ -12,8 +12,9 @@
<view class="sparkle s-2"></view> <view class="sparkle s-2"></view>
<view class="sparkle s-3"></view> <view class="sparkle s-3"></view>
</view> </view>
<text class="title">{{ type === 'comment' ? '评论成功' : '评分成功' }}</text> <text class="title">{{ type === 'comment' ? '评论成功' : type === 'release' ? '已提交审核' : '评分成功' }}</text>
<text class="subtitle" v-if="type === 'comment'">{{ actionData?.label || '你的锐评已发布' }}</text> <text class="subtitle" v-if="type === 'comment'">{{ actionData?.label || '你的锐评已发布' }}</text>
<text class="subtitle" v-else-if="type === 'release'">{{ actionData?.label || '请耐心等待管理员审核' }}</text>
<text class="subtitle" v-else>你已将TA评价为{{ actionData?.label || '...' }}</text> <text class="subtitle" v-else>你已将TA评价为{{ actionData?.label || '...' }}</text>
</view> </view>
</view> </view>

View File

@@ -5,11 +5,11 @@
<text class="topic-title">{{ topic.title }}</text> <text class="topic-title">{{ topic.title }}</text>
<view class="topic-stats"> <view class="topic-stats">
<view class="stat-item"> <view class="stat-item">
<text class="stat-icon icon-group"></text> <image class="stat-icon stat-icon-img" src="/static/images/icon/group.png" mode="aspectFit"></image>
<text>{{ topic.participantCount }}人参与</text> <text>{{ topic.participantCount }}人参与</text>
</view> </view>
<view class="stat-item heat"> <view class="stat-item heat">
<text class="stat-icon icon-fire"></text> <image class="stat-icon stat-icon-img" src="/static/images/icon/fire.png" mode="aspectFit"></image>
<text>热度 {{ topic.hotScore }} w</text> <text>热度 {{ topic.hotScore }} w</text>
</view> </view>
</view> </view>
@@ -23,7 +23,13 @@
:key="item.id" :key="item.id"
@tap="handleItemTap(item.id)" @tap="handleItemTap(item.id)"
> >
<text class="rank-num" :class="'rank-' + (index + 1)">{{ index + 1 }}</text> <image
v-if="index < 3"
class="rank-icon"
:src="`/static/images/icon/${index + 1}.png`"
mode="aspectFit"
/>
<text v-else class="rank-num" :class="'rank-' + (index + 1)">{{ index + 1 }}</text>
<image class="item-avatar" :src="FILE_BASE_URL + item.avatarUrl" mode="aspectFill"></image> <image class="item-avatar" :src="FILE_BASE_URL + item.avatarUrl" mode="aspectFill"></image>
<text class="item-name">{{ item.name }}</text> <text class="item-name">{{ item.name }}</text>
<text class="item-score" :class="'score-' + (index + 1)">{{ item.scoreAvg }}</text> <text class="item-score" :class="'score-' + (index + 1)">{{ item.scoreAvg }}</text>
@@ -32,7 +38,7 @@
<!-- AI 总结 --> <!-- AI 总结 -->
<view class="ai-summary" v-if="topic.aiSummary"> <view class="ai-summary" v-if="topic.aiSummary">
<text class="ai-icon icon-robot"></text> <image class="ai-icon ai-icon-img" src="/static/images/icon/robot.png" mode="aspectFit"></image>
<view class="summary-text"> <view class="summary-text">
<text class="summary-label">AI总结</text> <text class="summary-label">AI总结</text>
<text class="summary-content">{{ topic.aiSummary }}</text> <text class="summary-content">{{ topic.aiSummary }}</text>
@@ -117,12 +123,8 @@ const handleItemTap = (itemId) => {
background-size: cover; background-size: cover;
} }
.icon-group { .stat-icon-img {
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"); flex-shrink: 0;
}
.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 { .rank-list {
@@ -152,6 +154,13 @@ const handleItemTap = (itemId) => {
font-style: italic; font-style: italic;
} }
.rank-icon {
width: 44rpx;
height: 44rpx;
margin-right: 16rpx;
flex-shrink: 0;
}
.rank-1 { .rank-1 {
color: #2953ff; color: #2953ff;
font-size: 40rpx; font-size: 40rpx;
@@ -209,7 +218,10 @@ const handleItemTap = (itemId) => {
margin-right: 12rpx; margin-right: 12rpx;
margin-top: 4rpx; margin-top: 4rpx;
flex-shrink: 0; 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"); }
.ai-icon-img {
display: block;
} }
.summary-text { .summary-text {
@@ -244,4 +256,4 @@ const handleItemTap = (itemId) => {
font-size: 22rpx; font-size: 22rpx;
font-weight: 500; font-weight: 500;
} }
</style> </style>

View File

@@ -3,7 +3,7 @@
{ {
"path": "pages/index/index", "path": "pages/index/index",
"style": { "style": {
"navigationBarTitleText": "全民评分", "navigationBarTitleText": "夯拉评分",
"enablePullDownRefresh": true, "enablePullDownRefresh": true,
"navigationStyle": "custom", "navigationStyle": "custom",
"backgroundColor": "#FFFFFF" "backgroundColor": "#FFFFFF"
@@ -67,7 +67,7 @@
{ {
"path": "pages/rating/index", "path": "pages/rating/index",
"style": { "style": {
"navigationBarTitleText": "全民评分", "navigationBarTitleText": "夯拉评分",
"navigationStyle": "custom", "navigationStyle": "custom",
"enablePullDownRefresh": true "enablePullDownRefresh": true
} }
@@ -79,6 +79,15 @@
"navigationStyle": "custom", "navigationStyle": "custom",
"enablePullDownRefresh": true "enablePullDownRefresh": true
} }
},
{
"path": "pages/mine/topics",
"style": {
"navigationBarTitleText": "我参与的话题",
"navigationStyle": "custom",
"enablePullDownRefresh": true,
"onReachBottomDistance": 50
}
} }
], ],
"globalStyle": { "globalStyle": {

View File

@@ -10,7 +10,7 @@
@tap="handleLogin" @tap="handleLogin"
/> />
<view class="header-texts"> <view class="header-texts">
<text class="title">全民评分</text> <text class="title">夯拉评分</text>
<text class="subtitle">看看全国网友怎么评分</text> <text class="subtitle">看看全国网友怎么评分</text>
</view> </view>
</view> </view>
@@ -120,8 +120,13 @@ const loadTopicList = async (categoryId, page = 1) => {
try { try {
loading.value = true; loading.value = true;
const res = await fetchTopicList(categoryId, page); const res = await fetchTopicList(categoryId, page);
// 提取真实数据数组 const rawList =
const list = res?.list || []; res?.data?.records ||
res?.data?.list ||
res?.records ||
res?.list ||
(Array.isArray(res?.data) ? res.data : []);
const list = Array.isArray(rawList) ? rawList : [];
// 合并数据 // 合并数据
topicList.value = [...topicList.value, ...list]; topicList.value = [...topicList.value, ...list];
hasMore.value = list.length >= 5; hasMore.value = list.length >= 5;
@@ -136,58 +141,31 @@ const loadTopicList = async (categoryId, page = 1) => {
const switchCategory = (categoryId) => { const switchCategory = (categoryId) => {
if (currentCategoryId.value === categoryId) return; if (currentCategoryId.value === categoryId) return;
currentCategoryId.value = categoryId; currentCategoryId.value = categoryId;
uni.$trackRecord({
// 切换分类时重新加载数据 eventName: 'switch_category',
eventType: 'click',
elementId: `category_${categoryId}`,
elementContent: categories.value.find(c => c.id === categoryId)?.title || '未知分类',
});
// 切换分类时重新加载数据
currentPage.value = 1; currentPage.value = 1;
hasMore.value = true; hasMore.value = true;
loadTopicList(categoryId, currentPage.value); loadTopicList(categoryId, currentPage.value);
}; };
const mockData = [ const topicList = ref([]);
{
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 getCurrentCategoryTitle = () => {
return categories.value.find((category) => category.id === currentCategoryId.value)?.title || "推荐";
};
const goToRating = (topicId, itemId) => { const goToRating = (topicId, itemId) => {
uni.$trackRecord({
eventName: 'jumpto_rating',
eventType: 'jump',
elementId: `topic_${topicId}`,
elementContent: `主题${topicId}`,
});
uni.navigateTo({ uni.navigateTo({
url: `/pages/rating/index?topicId=${topicId}&itemId=${itemId}` url: `/pages/rating/index?topicId=${topicId}&itemId=${itemId}`
}); });
@@ -226,7 +204,11 @@ onShow(() => {
initData(); initData();
}); });
const handleLoginSuccess = () => { const handleLoginSuccess = async () => {
currentPage.value = 1;
hasMore.value = true;
await loadCategories();
await loadTopicList(currentCategoryId.value, currentPage.value);
}; };
// 下拉刷新 // 下拉刷新
@@ -247,8 +229,9 @@ onReachBottom(() => {
onShareAppMessage(async () => { onShareAppMessage(async () => {
const shareToken = await getShareToken("index"); const shareToken = await getShareToken("index");
getShareReward(); getShareReward();
const categoryTitle = getCurrentCategoryTitle();
return { return {
title: "全民评分", title: `夯拉评分 | ${categoryTitle}榜单正在热议`,
path: "/pages/index/index?shareToken=" + shareToken, path: "/pages/index/index?shareToken=" + shareToken,
}; };
}); });
@@ -256,8 +239,9 @@ onShareAppMessage(async () => {
onShareTimeline(async () => { onShareTimeline(async () => {
const shareToken = await getShareToken("index"); const shareToken = await getShareToken("index");
getShareReward(); getShareReward();
const categoryTitle = getCurrentCategoryTitle();
return { return {
title: "全民评分", title: `${categoryTitle}圈都在玩夯拉评分,来看看谁最夯`,
query: `shareToken=${shareToken}`, query: `shareToken=${shareToken}`,
}; };
}); });

View File

@@ -5,7 +5,7 @@
class="nav-bar" class="nav-bar"
:style="{ paddingTop: navBarTop + 'px', height: navBarHeight + 'px' }" :style="{ paddingTop: navBarTop + 'px', height: navBarHeight + 'px' }"
> >
<!-- <text class="nav-title">我的</text> --> <!-- Optional title -->
</view> </view>
<scroll-view <scroll-view
@@ -14,85 +14,76 @@
:style="{ paddingTop: navBarTop + navBarHeight + 'px' }" :style="{ paddingTop: navBarTop + navBarHeight + 'px' }"
> >
<view class="content-wrap"> <view class="content-wrap">
<!-- User Card --> <!-- User Card -->
<view class="user-card" @tap="handleUserClick"> <view class="user-card">
<view class="avatar-box"> <view class="avatar-container" @tap="handleUserClick">
<image :src="userInfo.avatarUrl" class="avatar" mode="aspectFill" /> <view class="avatar-ring">
<!-- <view class="red-badge"><text class="fire">🔥</text></view> --> <image :src="userInfo.avatarUrl" class="avatar" mode="aspectFill" />
</view>
<view class="user-info">
<view class="row-1">
<text class="nickname">{{ userInfo.nickName }}</text>
<view class="vip-tag" v-if="isLoggedIn && userInfo.isVip">
<text class="vip-text">VIP 祥瑞会员</text>
</view>
</view>
<view class="row-2" v-if="isLoggedIn">
<!-- <text class="arrow-icon"></text> -->
<!-- <text class="stats-text"
>已发送 <text class="num">3</text> 条新春祝福</text
> -->
</view>
<view class="row-2" v-else>
<text class="stats-text">点击登录解锁更多功能</text>
</view> </view>
</view> </view>
<view class="card-arrow"></view>
</view> <text class="nickname" @tap="handleUserClick">{{ userInfo.nickName }}</text>
<!-- VIP Banner --> <view class="tags-container" v-if="isLoggedIn && userStats.label && userStats.label.length">
<view v-if="!isIos" class="vip-banner" @tap="navTo('vip')"> <text class="sparkle-icon"></text>
<view class="vip-left"> <text class="tags-text">{{ userStats.label.join(' | ') }}</text>
<view class="vip-icon-box"> </view>
<uni-icons type="vip-filled" size="32" color="#5d4037" /> <view class="tags-container" v-else-if="!isLoggedIn">
<text class="tags-text">点击登录解锁更多功能</text>
</view>
<view class="stats-container">
<view class="stat-item">
<text class="stat-num">{{ isLoggedIn ? userStats.participant : '-' }}</text>
<text class="stat-label">参与话题</text>
</view> </view>
<view class="vip-content"> <view class="stat-divider"></view>
<view class="vip-title-row"> <view class="stat-item">
<text class="vip-title">祥瑞会员中心</text> <text class="stat-num">{{ isLoggedIn ? userStats.comment : '-' }}</text>
<view class="diamond-tag">DIAMOND</view> <text class="stat-label">评论数</text>
</view> </view>
<text class="vip-subtitle">开通会员解锁全部特权</text> <view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-num">{{ isLoggedIn ? userStats.liked : '-' }}</text>
<text class="stat-label">获赞</text>
</view> </view>
</view> </view>
<view class="vip-right">
<text class="open-text">立即开通</text> <view class="action-buttons">
<text class="arrow"></text> <button class="btn-primary" @tap="navTo('profile')">编辑资料</button>
<button v-if="isLoggedIn" class="btn-secondary" open-type="share">
<text class="share-icon-btn"></text> 分享主页
</button>
<button v-else class="btn-secondary" @tap="openLoginPopup('请先登录后再分享主页')">
<text class="share-icon-btn"></text> 分享主页
</button>
</view> </view>
</view> </view>
<!-- My Creations --> <!-- Menu Group 1 -->
<view class="section-title">我的xx</view> <view class="menu-card">
<view class="menu-group"> <view class="menu-item" @tap="navTo('/pages/mine/topics')">
<view class="menu-item" @tap="navTo('avatar')"> <view class="icon-left"><text class="icon-bookmark"></text></view>
<view class="icon-box pink-bg"><text></text></view> <text class="menu-text">我的参与</text>
<text class="menu-text">我的专属头像</text> <text class="menu-value" v-if="isLoggedIn">{{ userStats.participant }}</text>
<text class="arrow"></text> </view>
<view class="menu-item" @tap="navTo('comments')">
<view class="icon-left"><text class="icon-document"></text></view>
<text class="menu-text">我的评论</text>
<text class="menu-value" v-if="isLoggedIn">{{ userStats.comment }}</text>
</view> </view>
</view> </view>
<!-- Other Actions --> <!-- Menu Group 2 -->
<view class="menu-group mt-30"> <view class="menu-card">
<button class="menu-item share-btn" open-type="share">
<view class="icon-left"><text class="share-icon">🔗</text></view>
<text class="menu-text">分享给好友</text>
<text class="arrow"></text>
</button>
<view class="menu-item" @tap="navTo('feedback')"> <view class="menu-item" @tap="navTo('feedback')">
<view class="icon-left"><text class="feedback-icon">📝</text></view> <view class="icon-left"><text class="icon-feedback"></text></view>
<text class="menu-text">意见反馈</text> <text class="menu-text">意见反馈</text>
<text class="arrow"></text> <text class="arrow"></text>
</view> </view>
<view class="menu-item" @tap="navTo('help')">
<view class="icon-left"><text class="help-icon"></text></view>
<text class="menu-text">使用说明 / 帮助</text>
<text class="arrow"></text>
</view>
</view> </view>
<!-- Footer -->
<!-- <view class="version-info">2026 丙午马年 · 精美助手 v1.0.2</view> -->
<view style="height: 120rpx"></view> <view style="height: 120rpx"></view>
</view> </view>
</scroll-view> </scroll-view>
@@ -105,9 +96,10 @@
<script setup> <script setup>
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { onShareAppMessage, onLoad } from "@dcloudio/uni-app"; import { onShareAppMessage, onShareTimeline, onLoad, onShow } from "@dcloudio/uni-app";
import { getShareToken } from "@/utils/common"; import { getShareToken } from "@/utils/common";
import { getShareReward } from "@/api/system"; import { getShareReward } from "@/api/system";
import { fetchUserNums } from "@/api/mine";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue"; import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
const userStore = useUserStore(); const userStore = useUserStore();
@@ -118,8 +110,7 @@ const navBarTop = ref(20);
const navBarHeight = ref(44); const navBarHeight = ref(44);
// User Info // User Info
const defaultAvatarUrl = const defaultAvatarUrl = "https://api.dicebear.com/7.x/avataaars/svg?seed=fallback";
"https://file.lihailezzc.com/resource/d9b329082b32f8305101f708593a4882.png";
const userInfo = computed(() => ({ const userInfo = computed(() => ({
nickName: userStore.userInfo.nickName || "点击登录", nickName: userStore.userInfo.nickName || "点击登录",
@@ -127,32 +118,76 @@ const userInfo = computed(() => ({
isVip: userStore.userInfo.isVip || false, isVip: userStore.userInfo.isVip || false,
})); }));
const isLoggedIn = computed(() => !!userStore.userInfo.nickName); const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo.nickName);
const isIos = computed(() => uni.getSystemInfoSync().osName === "ios"); // User Stats
const userStats = ref({
participant: 0,
comment: 0,
liked: 0,
label: []
});
onLoad((options) => { const loadUserStats = async () => {
if (!isLoggedIn.value) {
userStats.value = {
participant: 0,
comment: 0,
liked: 0,
label: []
};
return;
}
try {
const res = await fetchUserNums();
const data = res?.data || res || {};
userStats.value = {
participant: data.participant || 0,
comment: data.comment || 0,
liked: data.liked || 0,
label: data.label || []
};
} catch (error) {
console.error("获取用户数据失败:", error);
}
};
onLoad(() => {
const sysInfo = uni.getSystemInfoSync(); const sysInfo = uni.getSystemInfoSync();
navBarTop.value = sysInfo.statusBarHeight; navBarTop.value = sysInfo.statusBarHeight;
navBarHeight.value = 44; navBarHeight.value = 44;
}); });
onShow(() => {
loadUserStats();
});
onShareAppMessage(async () => { onShareAppMessage(async () => {
const shareToken = await getShareToken("mine"); const shareToken = await getShareToken("mine");
getShareReward({ scene: "mine" }); getShareReward({ scene: "mine" });
const nickName = isLoggedIn.value ? userInfo.value.nickName : "我";
return { return {
title: "分享", title: `${nickName}也在玩夯拉评分来看看TA的主页`,
path: "/pages/index/index?shareToken=" + shareToken, path: "/pages/mine/mine?shareToken=" + shareToken,
imageUrl: imageUrl: "https://file.lihailezzc.com/resource/8dd026d76ef7a63d123b7fd698fb989b.png",
"https://file.lihailezzc.com/resource/8dd026d76ef7a63d123b7fd698fb989b.png", };
});
onShareTimeline(async () => {
const shareToken = await getShareToken("mine");
getShareReward({ scene: "mine_timeline" });
const nickName = isLoggedIn.value ? userInfo.value.nickName : "我";
const participant = userStats.value.participant || 0;
return {
title: `${nickName}已参与 ${participant} 个话题,来夯拉评分一起围观`,
query: `shareToken=${shareToken}`,
}; };
}); });
const handleUserClick = () => { const handleUserClick = () => {
if (!isLoggedIn.value) { if (!isLoggedIn.value) {
loginPopupRef.value.open(); openLoginPopup("请先登录");
} else { } else {
// Navigate to profile details
uni.navigateTo({ uni.navigateTo({
url: "/pages/mine/profile", url: "/pages/mine/profile",
}); });
@@ -160,40 +195,37 @@ const handleUserClick = () => {
}; };
const handleLogind = async () => { const handleLogind = async () => {
// Logic after successful login if needed loadUserStats();
};
const openLoginPopup = (message = "请先登录") => {
loginPopupRef.value.open();
if (message) {
uni.showToast({ title: message, icon: "none" });
}
}; };
const navTo = (page) => { const navTo = (page) => {
if (!isLoggedIn.value) { if (!isLoggedIn.value) {
loginPopupRef.value.open(); openLoginPopup("请先登录");
uni.showToast({ title: "请先登录", icon: "none" });
return; return;
} }
if (page === "avatar") { if (page === "profile") {
uni.navigateTo({ uni.navigateTo({ url: "/pages/mine/profile" });
url: "/pages/mine/avatar",
});
return; return;
} }
if (page === "feedback") { if (page === "feedback") {
uni.navigateTo({ uni.navigateTo({ url: "/pages/feedback/index" });
url: "/pages/feedback/index",
});
return; return;
} }
if (page === "help") {
uni.navigateTo({ if (page === "/pages/mine/topics") {
url: "/pages/mine/help", uni.navigateTo({ url: page });
});
return;
}
if (page === "vip") {
uni.navigateTo({
url: "/pages/mine/vip",
});
return; return;
} }
uni.showToast({ title: "功能开发中", icon: "none" }); uni.showToast({ title: "功能开发中", icon: "none" });
}; };
</script> </script>
@@ -201,7 +233,7 @@ const navTo = (page) => {
<style lang="scss" scoped> <style lang="scss" scoped>
.mine-page { .mine-page {
min-height: 100vh; min-height: 100vh;
background: #f9f9f9; background-color: #f8f9fc;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -214,297 +246,225 @@ const navTo = (page) => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #f9f9f9; background: transparent;
}
.nav-title {
font-size: 32rpx;
font-weight: bold;
color: #000;
} }
.content-scroll { .content-scroll {
height: 100vh; height: 100vh;
} }
.content-wrap { .content-wrap {
padding: 20rpx 30rpx 40rpx; padding: 20rpx 32rpx 40rpx;
} }
/* User Card */ /* User Card */
.user-card { .user-card {
background: #fff; background: #fff;
border-radius: 30rpx; border-radius: 40rpx;
padding: 40rpx; padding: 60rpx 40rpx 40rpx;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
margin-bottom: 40rpx; margin-bottom: 32rpx;
position: relative; box-shadow: 0 16rpx 40rpx rgba(77, 68, 241, 0.04);
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.03);
} }
.avatar-box {
position: relative; .avatar-container {
margin-right: 30rpx; margin-bottom: 24rpx;
} }
.avatar {
width: 120rpx; .avatar-ring {
height: 120rpx; width: 176rpx;
border-radius: 50%; height: 176rpx;
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.red-badge {
position: absolute;
top: 0;
right: -10rpx;
width: 40rpx;
height: 40rpx;
background: #ff3b30;
border-radius: 50%; border-radius: 50%;
padding: 6rpx;
background: linear-gradient(135deg, #7B46F1, #4d44f1, #00d2ff);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 2rpx solid #fff;
}
.fire {
font-size: 24rpx;
color: #fff;
} }
.user-info { .avatar {
flex: 1; width: 100%;
height: 100%;
border-radius: 50%;
border: 4rpx solid #fff;
background-color: #fff;
} }
.row-1 {
display: flex; .nickname {
align-items: center; font-size: 40rpx;
font-weight: 800;
color: #111;
margin-bottom: 12rpx; margin-bottom: 12rpx;
} }
.nickname {
font-size: 36rpx; .tags-container {
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.vip-tag {
background: #ffecec;
padding: 4rpx 16rpx;
border-radius: 999rpx;
}
.vip-text {
color: #ff3b30;
font-size: 20rpx;
font-weight: bold;
}
.row-2 {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 48rpx;
} }
.arrow-icon {
color: #ff3b30; .sparkle-icon {
font-size: 20rpx; font-size: 28rpx;
color: #7B46F1;
margin-right: 8rpx; margin-right: 8rpx;
} }
.stats-text {
.tags-text {
font-size: 24rpx; font-size: 24rpx;
color: #666; color: #666;
} }
.num {
font-weight: bold;
color: #333;
margin: 0 4rpx;
}
.card-arrow {
font-size: 40rpx;
color: #ccc;
margin-left: 10rpx;
}
/* VIP Banner */ .stats-container {
.vip-banner {
background: linear-gradient(135deg, #2e2424 0%, #1a1616 100%);
border-radius: 30rpx;
padding: 30rpx 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 40rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
}
.vip-left {
display: flex;
align-items: center;
}
.vip-icon-box {
width: 80rpx;
height: 80rpx;
background: #fceea9;
border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 24rpx; width: 100%;
border: 4rpx solid #bfa46f; margin-bottom: 48rpx;
} }
.vip-content { .stat-item {
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
} }
.vip-title-row { .stat-num {
display: flex; font-size: 40rpx;
align-items: center; font-weight: 800;
color: #111;
margin-bottom: 8rpx; margin-bottom: 8rpx;
} }
.vip-title { .stat-label {
font-size: 32rpx; font-size: 24rpx;
font-weight: bold; color: #888;
color: #fceea9;
margin-right: 12rpx;
} }
.diamond-tag { .stat-divider {
background: rgba(252, 238, 169, 0.15); width: 2rpx;
color: #fceea9; height: 40rpx;
font-size: 18rpx; background-color: #f0f0f0;
padding: 2rpx 8rpx;
border-radius: 6rpx;
border: 1px solid rgba(252, 238, 169, 0.4);
} }
.vip-subtitle { .action-buttons {
font-size: 22rpx;
color: rgba(252, 238, 169, 0.6);
}
.vip-right {
display: flex;
align-items: center;
}
.open-text {
font-size: 26rpx;
color: #fceea9;
margin-right: 4rpx;
font-weight: 500;
}
.vip-banner .arrow {
color: #fceea9;
font-size: 32rpx;
}
/* Section Title */
.section-title {
font-size: 26rpx;
font-weight: bold;
color: #999;
margin-bottom: 20rpx;
padding-left: 10rpx;
}
/* Menu Group */
.menu-group {
background: #fff;
border-radius: 24rpx;
overflow: hidden;
margin-bottom: 40rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
}
.menu-group.mt-30 {
margin-top: 30rpx;
}
.menu-item {
display: flex;
align-items: center;
padding: 30rpx;
background: #fff;
position: relative;
}
.menu-item:active {
background: #f9f9f9;
}
/* For button reset */
.menu-item.share-btn {
width: 100%; width: 100%;
text-align: left; display: flex;
line-height: normal; flex-direction: column;
border-radius: 0; gap: 20rpx;
} }
.menu-item.share-btn::after {
.btn-primary {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #5b54df, #453bc7);
color: #fff;
font-size: 30rpx;
font-weight: 600;
border-radius: 100rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 20rpx rgba(77, 68, 241, 0.2);
}
.btn-primary::after {
border: none; border: none;
} }
.icon-box { .btn-secondary {
width: 72rpx; width: 100%;
height: 72rpx; height: 88rpx;
border-radius: 16rpx; background-color: #f3f2ff;
color: #453bc7;
font-size: 30rpx;
font-weight: 600;
border-radius: 100rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-secondary::after {
border: none;
}
.share-icon-btn {
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='none' stroke='%23453bc7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='5' r='3'%3E%3C/circle%3E%3Ccircle cx='6' cy='12' r='3'%3E%3C/circle%3E%3Ccircle cx='18' cy='19' r='3'%3E%3C/circle%3E%3Cline x1='8.59' y1='13.51' x2='15.42' y2='17.49'%3E%3C/line%3E%3Cline x1='15.41' y1='6.51' x2='8.59' y2='10.49'%3E%3C/line%3E%3C/svg%3E");
background-size: cover;
margin-right: 12rpx;
}
/* Menu Card */
.menu-card {
background: #fff;
border-radius: 32rpx;
padding: 16rpx 0;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.02);
}
.menu-item {
display: flex;
align-items: center;
padding: 32rpx 40rpx;
position: relative;
}
.menu-item:active {
background: #fcfcfc;
}
.icon-left {
width: 48rpx;
height: 48rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 24rpx; margin-right: 24rpx;
font-size: 36rpx;
}
.red-bg {
background: #fff0f0;
color: #ff3b30;
}
.orange-bg {
background: #fff8e6;
color: #ff9500;
}
.pink-bg {
background: #fff0f5;
color: #ff2d55;
}
.yellow-bg {
background: #fffae0;
color: #ffcc00;
}
.gray-bg {
background: #f5f5f5;
color: #666;
} }
.icon-left { .icon-bookmark {
width: 40rpx; width: 40rpx;
display: flex; height: 40rpx;
justify-content: center; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%234d44f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z'%3E%3C/path%3E%3C/svg%3E");
margin-right: 24rpx; background-size: cover;
margin-left: 16rpx;
} }
.share-icon,
.help-icon, .icon-document {
.feedback-icon { width: 40rpx;
font-size: 36rpx; height: 40rpx;
color: #666; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%237B46F1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'%3E%3C/path%3E%3Cpolyline points='14 2 14 8 20 8'%3E%3C/polyline%3E%3Cline x1='16' y1='13' x2='8' y2='13'%3E%3C/line%3E%3Cline x1='16' y1='17' x2='8' y2='17'%3E%3C/line%3E%3Cpolyline points='10 9 9 9 8 9'%3E%3C/polyline%3E%3C/svg%3E");
background-size: cover;
}
.icon-feedback {
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='%23453bc7' 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%3Cline x1='9' y1='10' x2='15' y2='10'%3E%3C/line%3E%3C/svg%3E");
background-size: cover;
} }
.menu-text { .menu-text {
flex: 1; flex: 1;
font-size: 28rpx; font-size: 30rpx;
color: #333; color: #333;
font-weight: 500; font-weight: 500;
} }
.new-badge {
background: #ff3b30; .menu-value {
color: #fff;
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 8rpx;
margin-right: 16rpx;
}
.arrow {
color: #ccc;
font-size: 32rpx; font-size: 32rpx;
color: #999;
font-family: 'DIN Alternate', -apple-system, sans-serif;
} }
/* Version Info */ .arrow {
.version-info {
text-align: center;
color: #ccc; color: #ccc;
font-size: 22rpx; font-size: 36rpx;
margin-top: 60rpx; margin-left: 12rpx;
} }
</style> </style>

236
pages/mine/topics.vue Normal file
View File

@@ -0,0 +1,236 @@
<template>
<view class="page-container">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @tap="goBack">
<text class="back-icon"></text>
<text class="nav-title">我参与的话题</text>
</view>
</view>
</view>
<!-- 占位防止内容被导航栏遮挡 -->
<view :style="{ height: statusBarHeight + 44 + 'px' }"></view>
<!-- 话题列表 -->
<view class="topic-list-wrap">
<view v-if="topics.length > 0">
<TopicCard
v-for="(item, index) in topics"
:key="item.id || index"
:topic="item"
@card-click="goToTopicDetail(item.id)"
/>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && topics.length === 0">
<text class="empty-icon">📝</text>
<text class="empty-text">你还没有参与任何话题</text>
<view class="empty-btn" @tap="goToHome">去逛逛</view>
</view>
</view>
<!-- 加载更多指示器 -->
<view class="load-more" v-if="topics.length > 0">
<text>{{ loading ? '加载中...' : (hasMore ? '上拉加载更多' : '没有更多了') }}</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { getStatusBarHeight } from "@/utils/system";
import { onLoad, onPullDownRefresh, onReachBottom, onShow } from "@dcloudio/uni-app";
import { fetchUserTopics } from "@/api/topic";
import TopicCard from "@/components/TopicCard/TopicCard.vue";
const statusBarHeight = ref(getStatusBarHeight() || 44);
const topics = ref([]);
const loading = ref(false);
const hasMore = ref(true);
const currentPage = ref(1);
const pageSize = 5; // 每次获取5个
const goBack = () => {
uni.navigateBack();
};
const goToHome = () => {
uni.switchTab({
url: '/pages/index/index'
});
};
const goToTopicDetail = (topicId) => {
uni.navigateTo({
url: `/pages/rating/index?topicId=${topicId}`
});
};
const loadData = async (page = 1) => {
if (loading.value) return;
try {
loading.value = true;
const res = await fetchUserTopics(page);
const list = res?.data?.records || res?.records || res?.list || [];
// 如果是第一页,先清空原有数据
if (page === 1) {
topics.value = [];
}
// 构造适合 TopicCard 渲染的数据格式
const formattedList = list.map(item => ({
id: item.topicId || item.id,
title: item.topicName || item.title || '未知话题',
image: item.image || item.coverUrl || 'https://api.dicebear.com/7.x/shapes/svg?seed=' + (item.topicId || item.id), // 给个默认图防止报错
views: item.participantCount || item.views || 0,
items: item.items || item.ratingItems || []
}));
topics.value = [...topics.value, ...formattedList];
// 判断是否有下一页
hasMore.value = list.length >= pageSize;
} catch (error) {
console.error("获取参与话题列表失败:", error);
uni.showToast({
title: '获取列表失败',
icon: 'none'
});
if (page === 1) {
hasMore.value = false;
}
} finally {
loading.value = false;
}
};
onLoad(() => {
// onLoad 只触发一次
});
onShow(() => {
// 每次进入页面都刷新,确保数据是最新的(如果刚才在详情页做了评分)
loadData(1);
});
onPullDownRefresh(async () => {
currentPage.value = 1;
hasMore.value = true;
await loadData(1);
uni.stopPullDownRefresh();
});
onReachBottom(() => {
if (!hasMore.value || loading.value) return;
currentPage.value += 1;
loadData(currentPage.value);
});
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f6fa;
padding-bottom: 60rpx;
}
/* 导航栏 */
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: #f5f6fa;
backdrop-filter: blur(20rpx);
}
.nav-content {
height: 44px;
display: flex;
align-items: center;
padding: 0 32rpx;
}
.nav-left {
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%3Cline x1='19' y1='12' x2='5' y2='12'%3E%3C/line%3E%3Cpolyline points='12 19 5 12 12 5'%3E%3C/polyline%3E%3C/svg%3E");
background-size: cover;
margin-right: 16rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 700;
color: #111;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
}
/* 列表区 */
.topic-list-wrap {
padding: 24rpx 32rpx;
display: flex;
flex-direction: column;
gap: 24rpx; /* 组件之间的间距 */
}
/* 覆盖 TopicCard 的外边距,让外层控制间距 */
:deep(.topic-card) {
margin-bottom: 24rpx !important;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 160rpx 0;
}
.empty-icon {
font-size: 100rpx;
margin-bottom: 24rpx;
opacity: 0.8;
}
.empty-text {
font-size: 28rpx;
color: #888;
margin-bottom: 48rpx;
}
.empty-btn {
background: linear-gradient(135deg, #4d44f1, #2953ff);
color: #fff;
font-size: 28rpx;
font-weight: 600;
padding: 16rpx 64rpx;
border-radius: 100rpx;
box-shadow: 0 8rpx 24rpx rgba(41, 83, 255, 0.25);
}
.empty-btn:active {
transform: scale(0.95);
transition: transform 0.2s;
}
/* 加载更多 */
.load-more {
text-align: center;
padding: 20rpx 0 40rpx;
color: #999;
font-size: 24rpx;
}
</style>

View File

@@ -5,7 +5,7 @@
<view class="nav-content"> <view class="nav-content">
<view class="nav-left" @tap="goBack"> <view class="nav-left" @tap="goBack">
<text class="back-icon"></text> <text class="back-icon"></text>
<text class="nav-title">全民评分</text> <text class="nav-title">夯拉评分</text>
</view> </view>
<view class="nav-right"> <view class="nav-right">
<text class="search-icon"></text> <text class="search-icon"></text>
@@ -27,7 +27,10 @@
</view> </view>
<view class="hero-meta"> <view class="hero-meta">
<text class="meta-rank">🥇 No.1</text> <text class="meta-rank">🥇 No.1</text>
<text class="meta-heat">🔥 {{ detailData.hotScore }}w 热度</text> <view class="meta-heat">
<image class="meta-icon" src="/static/images/icon/fire.png" mode="aspectFit"></image>
<text>{{ detailData.hotScore }}w 热度</text>
</view>
<text class="meta-count" style="margin-left: 20rpx; color: #999;">{{ detailData.ratingCount }} 人评分</text> <text class="meta-count" style="margin-left: 20rpx; color: #999;">{{ detailData.ratingCount }} 人评分</text>
</view> </view>
</view> </view>
@@ -36,7 +39,7 @@
<!-- AI 观察员 --> <!-- AI 观察员 -->
<view class="ai-card"> <view class="ai-card">
<view class="ai-header"> <view class="ai-header">
<text class="ai-icon"></text> <image class="ai-icon ai-icon-img" src="/static/images/icon/robot.png" mode="aspectFit"></image>
<text class="ai-title">AI观察员</text> <text class="ai-title">AI观察员</text>
</view> </view>
<text class="ai-content">{{ detailData.aiSummary }}</text> <text class="ai-content">{{ detailData.aiSummary }}</text>
@@ -44,13 +47,15 @@
<!-- 你的态度 --> <!-- 你的态度 -->
<AttitudePanel <AttitudePanel
ref="attitudePanelRef"
:initial-action="currentAction" :initial-action="currentAction"
:sending="isSendingComment"
@action-change="handleActionChange" @action-change="handleActionChange"
@send-comment="handleSendComment" @send-comment="handleSendComment"
/> />
<!-- 大家都在 PK --> <!-- 大家都在 PK -->
<view class="pk-panel card"> <!-- <view class="pk-panel card">
<view class="pk-header"> <view class="pk-header">
<text class="section-title">大家都在 PK</text> <text class="section-title">大家都在 PK</text>
<view class="go-compare">去对比<text class="arrow-right"></text></view> <view class="go-compare">去对比<text class="arrow-right"></text></view>
@@ -74,7 +79,7 @@
<text class="blue-text">63% 支持</text> <text class="blue-text">63% 支持</text>
<text class="yellow-text">37% 支持</text> <text class="yellow-text">37% 支持</text>
</view> </view>
</view> </view> -->
<!-- 评论区 --> <!-- 评论区 -->
<view class="comments-section card"> <view class="comments-section card">
@@ -86,52 +91,97 @@
<!-- <text class="filter-icon"></text> --> <!-- <text class="filter-icon"></text> -->
</view> </view>
<view class="comment-list" style="min-height: 400rpx;"> <view class="comment-list">
<view v-if="commentLoading && !comments.length" class="comment-state">
<text class="comment-state-title">评论加载中...</text>
<text class="comment-state-desc">正在拉取大家的锐评</text>
</view>
<view v-else-if="commentError && !comments.length" class="comment-state">
<text class="comment-state-title">评论加载失败</text>
<text class="comment-state-desc">{{ commentError }}</text>
<button class="comment-state-btn" @tap="retryLoadComments">重新加载</button>
</view>
<view v-else-if="!comments.length" class="comment-state">
<view class="comment-empty-visual">
<view class="comment-empty-orbit orbit-1"></view>
<view class="comment-empty-orbit orbit-2"></view>
<view class="comment-empty-core">
<text class="comment-empty-emoji">💬</text>
</view>
</view>
<text class="comment-state-title">还没有评论</text>
<text class="comment-state-desc">来发第一条锐评带动一下讨论气氛</text>
<text class="comment-state-tip">你的观点可能就是这条榜单最有意思的开始</text>
</view>
<CommentItem <CommentItem
v-for="(comment, index) in comments" v-for="(comment, index) in comments"
:key="comment.id" :key="comment.id"
:comment="comment" :comment="comment"
@need-login="openLoginPopup"
@like-change="handleCommentLikeChange"
/> />
</view> </view>
<!-- 加载更多 --> <!-- 加载更多 -->
<view class="load-more"> <view v-if="comments.length" class="load-more">
<text>{{ loading ? '加载中...' : (hasMore ? '上拉加载更多' : '没有更多了') }}</text> <text>{{ commentLoadText }}</text>
</view> </view>
</view> </view>
</view> </view>
<!-- 评分成功特效弹窗 --> <!-- 评分成功特效弹窗 -->
<SuccessPopup ref="successPopupRef" /> <SuccessPopup ref="successPopupRef" />
<LoginPopup ref="loginPopupRef" @logind="handleLoginSuccess" />
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { computed, ref } from 'vue';
import { getStatusBarHeight } from "@/utils/system"; import { getStatusBarHeight } from "@/utils/system";
import { onLoad, onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app"; import { onLoad, onPullDownRefresh, onReachBottom, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import AttitudePanel from "@/components/AttitudePanel/AttitudePanel.vue"; import AttitudePanel from "@/components/AttitudePanel/AttitudePanel.vue";
import SuccessPopup from "@/components/SuccessPopup/SuccessPopup.vue"; import SuccessPopup from "@/components/SuccessPopup/SuccessPopup.vue";
import ActionBadge from "@/components/ActionBadge/ActionBadge.vue";
import CommentItem from "@/components/CommentItem/CommentItem.vue"; import CommentItem from "@/components/CommentItem/CommentItem.vue";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import { FILE_BASE_URL } from "@/utils/constants"; import { FILE_BASE_URL } from "@/utils/constants";
import { fetchTopicRatingItems, topicItemScore, topicItemComment, topicItemCommentList } from "@/api/topicItem"; import { fetchTopicRatingItems, topicItemScore, topicItemComment, topicItemCommentList } from "@/api/topicItem";
import { formatDate } from "@/utils/date"; import { formatDate } from "@/utils/date";
import { getShareToken } from "@/utils/common";
import { getShareReward } from "@/api/system";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const statusBarHeight = ref(getStatusBarHeight() || 44); const statusBarHeight = ref(getStatusBarHeight() || 44);
const loading = ref(false);
const hasMore = ref(true); const hasMore = ref(true);
const currentPage = ref(1); const currentPage = ref(1);
const currentAction = ref(''); const currentAction = ref('');
const successPopupRef = ref(null); const successPopupRef = ref(null);
const loginPopupRef = ref(null);
const attitudePanelRef = ref(null);
const currentTab = ref('hot'); // 'hot' 或 'new' const currentTab = ref('hot'); // 'hot' 或 'new'
const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName);
const commentLoading = ref(false);
const isSendingComment = ref(false);
const commentError = ref('');
const commentLoadText = computed(() => {
if (commentLoading.value && comments.value.length) return '加载中...';
if (!hasMore.value) return '没有更多了';
return '上拉加载更多';
});
const goBack = () => { const goBack = () => {
uni.navigateBack(); uni.navigateBack();
}; };
const openLoginPopup = (message = '请先登录后再操作') => {
loginPopupRef.value?.open();
if (message) {
uni.showToast({
title: message,
icon: 'none'
});
}
};
const detailData = ref({ const detailData = ref({
id: '', id: '',
name: '', name: '',
@@ -146,6 +196,11 @@ const comments = ref([]);
const itemId = ref(''); const itemId = ref('');
const handleActionChange = async (action) => { const handleActionChange = async (action) => {
if (!isLoggedIn.value) {
openLoginPopup();
return;
}
const originAction = detailData.value.userAction; const originAction = detailData.value.userAction;
const originScore = detailData.value.score; const originScore = detailData.value.score;
const isCancel = detailData.value.userAction === action.label; const isCancel = detailData.value.userAction === action.label;
@@ -165,6 +220,20 @@ const handleActionChange = async (action) => {
if (!isCancel && successPopupRef.value) { if (!isCancel && successPopupRef.value) {
successPopupRef.value.show(action); successPopupRef.value.show(action);
} }
uni.$trackRecord({
eventName: 'rating',
eventType: 'rating',
elementId: `item_${itemId.value}`,
elementContent: `评分项_${detailData.value.name}`,
customParams: {
scoreType: action.scoreType,
action: action.label,
topicId: detailData.value.topicId,
topicTitle: detailData.value.name,
score: detailData.value.score,
page: 'rating_detail'
}
});
} catch (error) { } catch (error) {
detailData.value.userAction = originAction; detailData.value.userAction = originAction;
detailData.value.score = originScore; detailData.value.score = originScore;
@@ -173,9 +242,15 @@ const handleActionChange = async (action) => {
}; };
const handleSendComment = async (payload) => { const handleSendComment = async (payload) => {
if (!detailData.value.topicId || !itemId.value) return; if (!isLoggedIn.value) {
openLoginPopup();
return;
}
if (!detailData.value.topicId || !itemId.value || isSendingComment.value) return;
try { try {
isSendingComment.value = true;
uni.showLoading({ title: '发送中...' }); uni.showLoading({ title: '发送中...' });
await topicItemComment({ await topicItemComment({
topicId: detailData.value.topicId, topicId: detailData.value.topicId,
@@ -195,14 +270,26 @@ const handleSendComment = async (payload) => {
// 刷新页面数据以获取最新评论 // 刷新页面数据以获取最新评论
currentPage.value = 1; currentPage.value = 1;
hasMore.value = true; hasMore.value = true;
comments.value = []; uni.$trackRecord({
getCommentsData(); eventName: 'comment',
eventType: 'comment',
elementId: `item_${itemId.value}`,
elementContent: `评论项_${detailData.value.name}`,
customParams: {
comment: payload.content,
page: 'rating_detail'
}
});
attitudePanelRef.value?.resetComment?.();
await getCommentsData({ replace: true });
} catch (error) { } catch (error) {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({
title: '发送失败', title: '发送失败',
icon: 'none' icon: 'none'
}); });
} finally {
isSendingComment.value = false;
} }
}; };
@@ -211,23 +298,49 @@ const switchTab = (tab) => {
currentTab.value = tab; currentTab.value = tab;
currentPage.value = 1; currentPage.value = 1;
hasMore.value = true; hasMore.value = true;
// comments.value = []; // 移除这行,避免数据清空导致高度塌陷 commentError.value = '';
getCommentsData(true); // 传入标识表示是切换榜单 getCommentsData({ replace: true });
}; };
const getCommentsData = async (isSwitchTab = false) => { const resolveCommentHasMore = (res, list) => {
if (!detailData.value.topicId || !itemId.value || loading.value) return; const data = res?.data || res || {};
const hasNext = data?.hasNext || res?.hasNext;
if (typeof hasNext === 'boolean') return hasNext;
const pages = Number(data?.pages || res?.pages || 0);
if (pages > 0) return currentPage.value < pages;
const total = Number(data?.total || res?.total || 0);
if (total > 0) {
const size = Number(data?.size || data?.pageSize || res?.size || res?.pageSize || list.length || 1);
return currentPage.value * size < total;
}
return list.length > 0;
};
const getCommentsData = async ({ replace = false } = {}) => {
if (!detailData.value.topicId || !itemId.value || commentLoading.value) return;
try { try {
loading.value = true; commentLoading.value = true;
const orderBy = currentTab.value; commentError.value = '';
const orderBy = currentTab.value === 'hot' ? 1 : 2;
const res = await topicItemCommentList(detailData.value.topicId, itemId.value, currentPage.value, orderBy); const res = await topicItemCommentList(detailData.value.topicId, itemId.value, currentPage.value, orderBy);
const list = res?.list || []; const rawList =
res?.data?.records ||
res?.data?.list ||
res?.records ||
res?.list ||
(Array.isArray(res?.data) ? res.data : []);
const list = Array.isArray(rawList) ? rawList : [];
const formattedList = list.map(item => ({ const formattedList = list.map(item => ({
id: item.id, id: item.id,
name: item.user?.nickname || '匿名用户', name: item.user?.nickname || '匿名用户',
avatar: item.user?.avatar ? item.user.avatar : 'https://api.dicebear.com/7.x/avataaars/svg?seed=fallback', avatar: item.user?.avatar
? (String(item.user.avatar).startsWith('http') ? item.user.avatar : `${FILE_BASE_URL}${item.user.avatar}`)
: 'https://api.dicebear.com/7.x/avataaars/svg?seed=fallback',
time: formatDate(item.createdAt) || '刚刚', // 如果后端返回了时间字段可以替换 time: formatDate(item.createdAt) || '刚刚', // 如果后端返回了时间字段可以替换
content: item.content, content: item.content,
likes: item.likeCount || 0, likes: item.likeCount || 0,
@@ -237,21 +350,42 @@ const getCommentsData = async (isSwitchTab = false) => {
userAction: item.userAction || "" // 夯/顶级/人上人/NPC/拉 userAction: item.userAction || "" // 夯/顶级/人上人/NPC/拉
})); }));
if (currentPage.value === 1 || isSwitchTab) { if (currentPage.value === 1 || replace) {
comments.value = formattedList; comments.value = formattedList;
} else { } else {
comments.value = [...comments.value, ...formattedList]; comments.value = [...comments.value, ...formattedList];
} }
// 假设每页10条如果返回的少于10条则没有更多了 hasMore.value = resolveCommentHasMore(res, list);
hasMore.value = list.length >= 6;
} catch (error) { } catch (error) {
console.error('获取评论列表失败:', error); console.error('获取评论列表失败:', error);
commentError.value = '请检查网络后重试';
if (currentPage.value > 1) {
currentPage.value -= 1;
}
} finally { } finally {
loading.value = false; commentLoading.value = false;
} }
}; };
const retryLoadComments = () => {
if (!comments.value.length) {
currentPage.value = 1;
}
getCommentsData({ replace: currentPage.value === 1 });
};
const handleCommentLikeChange = ({ id, isLiked, likes }) => {
comments.value = comments.value.map((item) => {
if (item.id !== id) return item;
return {
...item,
isLiked,
likes
};
});
};
const getDetailData = async () => { const getDetailData = async () => {
if (!itemId.value) return; if (!itemId.value) return;
try { try {
@@ -284,7 +418,7 @@ const getDetailData = async () => {
// 拉取真实的评论列表 // 拉取真实的评论列表
if (currentPage.value === 1) { if (currentPage.value === 1) {
getCommentsData(); getCommentsData({ replace: true });
} }
} catch (error) { } catch (error) {
console.error('获取详情失败:', error); console.error('获取详情失败:', error);
@@ -313,15 +447,45 @@ onLoad((options) => {
} }
}); });
const handleLoginSuccess = () => {
currentPage.value = 1;
hasMore.value = true;
comments.value = [];
commentError.value = '';
getDetailData();
};
onShareAppMessage(async () => {
const shareToken = await getShareToken("rating_item");
getShareReward({ scene: "rating_item" });
const itemName = detailData.value.name || "这个评分对象";
return {
title: `${itemName}」在夯拉评分里是什么段位?`,
path: `/pages/rating/detail?itemId=${itemId.value}&topicId=${detailData.value.topicId}&shareToken=${shareToken}`,
};
});
onShareTimeline(async () => {
const shareToken = await getShareToken("rating_item");
getShareReward({ scene: "rating_item_timeline" });
const itemName = detailData.value.name || "这个评分对象";
const score = detailData.value.score || "0.0";
return {
title: `${itemName}当前评分 ${score},来看看大家怎么评`,
query: `itemId=${itemId.value}&topicId=${detailData.value.topicId}&shareToken=${shareToken}`,
};
});
onPullDownRefresh(() => { onPullDownRefresh(() => {
currentPage.value = 1; currentPage.value = 1;
hasMore.value = true; hasMore.value = true;
comments.value = []; comments.value = [];
commentError.value = '';
getDetailData(); getDetailData();
}); });
onReachBottom(() => { onReachBottom(() => {
if (!hasMore.value || loading.value) return; if (!hasMore.value || commentLoading.value) return;
currentPage.value += 1; currentPage.value += 1;
getCommentsData(); getCommentsData();
}); });
@@ -456,9 +620,18 @@ onReachBottom(() => {
} }
.meta-heat { .meta-heat {
display: flex;
align-items: center;
color: #666; color: #666;
} }
.meta-icon {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
flex-shrink: 0;
}
/* AI 观察员 */ /* AI 观察员 */
.ai-card { .ai-card {
background: #ffffff; background: #ffffff;
@@ -477,11 +650,13 @@ onReachBottom(() => {
.ai-icon { .ai-icon {
width: 36rpx; width: 36rpx;
height: 36rpx; height: 36rpx;
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");
background-size: cover;
margin-right: 12rpx; margin-right: 12rpx;
} }
.ai-icon-img {
display: block;
}
.ai-title { .ai-title {
font-size: 28rpx; font-size: 28rpx;
font-weight: 800; font-weight: 800;
@@ -650,6 +825,100 @@ onReachBottom(() => {
.comment-list { .comment-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 400rpx;
}
.comment-state {
min-height: 400rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: #999;
padding: 56rpx 40rpx;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(247, 248, 255, 0.92) 0%, rgba(255, 255, 255, 0.98) 100%);
border-radius: 28rpx;
border: 2rpx solid #f1f2fb;
}
.comment-empty-visual {
position: relative;
width: 180rpx;
height: 180rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 28rpx;
}
.comment-empty-orbit {
position: absolute;
border-radius: 50%;
border: 2rpx solid rgba(123, 70, 241, 0.12);
}
.orbit-1 {
width: 180rpx;
height: 180rpx;
}
.orbit-2 {
width: 132rpx;
height: 132rpx;
border-color: rgba(77, 68, 241, 0.18);
}
.comment-empty-core {
width: 96rpx;
height: 96rpx;
border-radius: 32rpx;
background: linear-gradient(135deg, #7668ff, #4d44f1);
box-shadow: 0 18rpx 36rpx rgba(94, 83, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.comment-empty-emoji {
font-size: 44rpx;
}
.comment-state-title {
font-size: 32rpx;
color: #1f2430;
font-weight: 700;
margin-bottom: 14rpx;
letter-spacing: 0.5rpx;
}
.comment-state-desc {
font-size: 25rpx;
color: #70778a;
line-height: 1.7;
}
.comment-state-tip {
margin-top: 14rpx;
font-size: 22rpx;
color: #a7adbc;
line-height: 1.6;
}
.comment-state-btn {
margin-top: 24rpx;
height: 72rpx;
line-height: 72rpx;
padding: 0 32rpx;
border-radius: 999rpx;
background: #4d44f1;
color: #fff;
font-size: 26rpx;
}
.comment-state-btn::after {
border: none;
} }
.load-more { .load-more {

View File

@@ -7,9 +7,9 @@
<text class="back-icon"></text> <text class="back-icon"></text>
</view> </view>
<view class="nav-logo-wrap"> <view class="nav-logo-wrap">
<image class="nav-logo" src="/static/images/default-avatar.png" mode="aspectFill" /> <image class="nav-logo" src="/static/images/logo.jpg" mode="aspectFill" />
</view> </view>
<text class="nav-title">全民评分</text> <text class="nav-title">夯拉评分</text>
</view> </view>
</view> </view>
@@ -21,16 +21,17 @@
<view class="topic-header"> <view class="topic-header">
<text class="topic-title">{{ topicData.title }}</text> <text class="topic-title">{{ topicData.title }}</text>
<view class="topic-stats"> <view class="topic-stats">
<text class="stat-icon fire-icon"></text> <image class="stat-icon stat-icon-img" src="/static/images/icon/fire.png" mode="aspectFit"></image>
<text class="stat-text">热度 {{ topicData.hotScore }} w</text> <text class="stat-text">热度 {{ topicData.hotScore }} w</text>
<text class="stat-text margin-left">{{ topicData.participantCount }}人参与</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>
</view> </view>
<!-- AI 实时观察 --> <!-- AI 实时观察 -->
<view class="ai-observe"> <view class="ai-observe">
<view class="ai-header"> <view class="ai-header">
<text class="ai-icon"></text> <image class="ai-icon ai-icon-img" src="/static/images/icon/robot.png" mode="aspectFit"></image>
<text class="ai-title">AI 实时观察</text> <text class="ai-title">AI 实时观察</text>
</view> </view>
<text class="ai-content">{{ topicData.aiSummary }}</text> <text class="ai-content">{{ topicData.aiSummary }}</text>
@@ -118,7 +119,7 @@
</view> </view>
<!-- 右下角悬浮添加按钮 --> <!-- 右下角悬浮添加按钮 -->
<view class="fab-btn" @tap="showAddPopup = true"> <view class="fab-btn" @tap="handleAddClick">
<text class="plus-icon">+</text> <text class="plus-icon">+</text>
</view> </view>
@@ -150,19 +151,26 @@
<!-- 评分成功特效弹窗 --> <!-- 评分成功特效弹窗 -->
<SuccessPopup ref="successPopupRef" /> <SuccessPopup ref="successPopupRef" />
<LoginPopup ref="loginPopupRef" @logind="handleLoginSuccess" />
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { computed, ref } from 'vue';
import { getStatusBarHeight } from "@/utils/system"; import { getStatusBarHeight } from "@/utils/system";
import { onPullDownRefresh, onReachBottom, onLoad } from "@dcloudio/uni-app"; import { onPullDownRefresh, onReachBottom, onLoad, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import { fetchTopicDetail, fetchTopicRatingItems } from "@/api/topic"; import { fetchTopicDetail, fetchTopicRatingItems } from "@/api/topic";
import { FILE_BASE_URL } from "@/utils/constants"; import { FILE_BASE_URL } from "@/utils/constants";
import RatingCard from "@/components/RatingCard/RatingCard.vue"; import RatingCard from "@/components/RatingCard/RatingCard.vue";
import SuccessPopup from "@/components/SuccessPopup/SuccessPopup.vue"; import SuccessPopup from "@/components/SuccessPopup/SuccessPopup.vue";
import { topicItemScore } from "@/api/topicItem"; 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 statusBarHeight = ref(getStatusBarHeight() || 44);
const loading = ref(false); const loading = ref(false);
@@ -170,6 +178,8 @@ const hasMore = ref(true);
const currentPage = ref(1); const currentPage = ref(1);
const currentTopicId = ref(null); const currentTopicId = ref(null);
const successPopupRef = ref(null); const successPopupRef = ref(null);
const loginPopupRef = ref(null);
const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName);
const goBack = () => { const goBack = () => {
uni.navigateBack({ uni.navigateBack({
@@ -177,6 +187,13 @@ const goBack = () => {
}); });
}; };
const openLoginPopup = (message = '请先登录后再操作') => {
loginPopupRef.value?.open();
if (message) {
uni.showToast({ title: message, icon: 'none' });
}
};
const topicData = ref({ const topicData = ref({
title: '', title: '',
heat: '0', heat: '0',
@@ -208,6 +225,11 @@ const newItem = ref({
}); });
const chooseAvatar = () => { const chooseAvatar = () => {
if (!isLoggedIn.value) {
openLoginPopup();
return;
}
uni.chooseImage({ uni.chooseImage({
count: 1, count: 1,
success: (res) => { success: (res) => {
@@ -216,30 +238,51 @@ const chooseAvatar = () => {
}); });
}; };
const confirmAddItem = () => { const confirmAddItem = async () => {
if (!isLoggedIn.value) {
openLoginPopup();
return;
}
if (!newItem.value.name.trim()) { if (!newItem.value.name.trim()) {
uni.showToast({ title: '请输入名称', icon: 'none' }); uni.showToast({ title: '请输入名称', icon: 'none' });
return; return;
} }
if (!newItem.value.avatar) {
uni.showToast({ title: '请上传头像', icon: 'none' });
return;
}
// 模拟添加 try {
const newRatingItem = { uni.showLoading({ title: '添加中...' });
id: Date.now(),
rank: ratingItems.value.length + 1, // 1. 先通过 uni.uploadFile 上传头像图片
name: newItem.value.name, const uploadRes = await uploadImage(newItem.value.avatar);
avatar: newItem.value.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${Date.now()}`,
score: '0.0', // 2. 将上传成功后的图片 URL 和其他数据一起提交给添加接口
quote: '刚刚加入讨论,快来发表你的看法吧。', await topicItemAdd({
userAction: '' topicId: currentTopicId.value,
}; name: newItem.value.name,
avatarUrl: uploadRes
ratingItems.value.push(newRatingItem); });
// 重置并关闭 uni.hideLoading();
newItem.value = { name: '', avatar: '' }; uni.showToast({ title: '添加成功', icon: 'success' });
showAddPopup.value = false;
// 重置表单并关闭弹窗
uni.showToast({ title: '添加成功', icon: 'success' }); 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 ratingItems = ref([]);
@@ -256,7 +299,6 @@ const loadItems = async (topicId, page = 1) => {
ratingItems.value = [...ratingItems.value, ...list]; ratingItems.value = [...ratingItems.value, ...list];
hasMore.value = list.length >= 10; // 假设 pageSize 是 10 hasMore.value = list.length >= 10; // 假设 pageSize 是 10
} catch (error) { } catch (error) {
console.error("获取评分项列表失败:", error);
hasMore.value = false; hasMore.value = false;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -276,26 +318,12 @@ onLoad((options) => {
} }
}); });
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 handleAction = async (item, action) => {
if (!isLoggedIn.value) {
openLoginPopup();
return;
}
const originAction = item.userAction; const originAction = item.userAction;
const originScore = item.scoreAvg; const originScore = item.scoreAvg;
const isCancel = item.userAction === action.label; const isCancel = item.userAction === action.label;
@@ -313,6 +341,20 @@ const handleAction = async (item, action) => {
if (!isCancel && successPopupRef.value) { if (!isCancel && successPopupRef.value) {
successPopupRef.value.show(action); 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) { } catch (error) {
item.userAction = originAction; item.userAction = originAction;
item.scoreAvg = originScore; item.scoreAvg = originScore;
@@ -320,11 +362,59 @@ const handleAction = async (item, action) => {
}; };
const goToDetail = (itemId) => { const goToDetail = (itemId) => {
uni.$trackRecord({
eventName: 'jumpto_detail',
eventType: 'jump',
elementId: `item_${itemId}`,
elementContent: `评分项${itemId}`,
});
uni.navigateTo({ uni.navigateTo({
url: `/pages/rating/detail?itemId=${itemId}&topicId=${currentTopicId.value}` url: `/pages/rating/detail?itemId=${itemId}&topicId=${currentTopicId.value}`
}); });
}; };
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 () => { onPullDownRefresh(async () => {
if (currentTopicId.value) { if (currentTopicId.value) {
@@ -335,11 +425,7 @@ onPullDownRefresh(async () => {
loadItems(currentTopicId.value, currentPage.value) loadItems(currentTopicId.value, currentPage.value)
]); ]);
} }
// 模拟评论刷新(此处未接接口,暂时保留原有模拟刷新逻辑,去掉 mockRatingItems 的重置) uni.stopPullDownRefresh();
setTimeout(() => {
comments.value = [...mockComments];
uni.stopPullDownRefresh();
}, 1000);
}); });
// 上拉加载更多 // 上拉加载更多
@@ -349,12 +435,6 @@ onReachBottom(() => {
currentPage.value += 1; currentPage.value += 1;
loadItems(currentTopicId.value, currentPage.value); loadItems(currentTopicId.value, currentPage.value);
} }
// 原有的评论模拟加载(根据需要决定是否保留)
setTimeout(() => {
const moreComments = mockComments.map(c => ({...c, id: c.id + comments.value.length}));
comments.value.push(...moreComments);
}, 1000);
}); });
</script> </script>
@@ -400,7 +480,6 @@ onReachBottom(() => {
width: 56rpx; width: 56rpx;
height: 56rpx; height: 56rpx;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(135deg, #001f3f, #0088a9);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -443,12 +522,15 @@ onReachBottom(() => {
align-items: center; align-items: center;
} }
.stat-icon.fire-icon { .stat-icon {
width: 26rpx; width: 26rpx;
height: 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; margin-right: 12rpx;
flex-shrink: 0;
}
.stat-icon-gap {
margin-left: 32rpx;
} }
.stat-text { .stat-text {
@@ -457,8 +539,7 @@ onReachBottom(() => {
font-weight: 500; font-weight: 500;
} }
.stat-text.margin-left { .stat-text-participant {
margin-left: 32rpx;
color: #555; color: #555;
font-weight: normal; font-weight: normal;
} }
@@ -482,11 +563,13 @@ onReachBottom(() => {
.ai-icon { .ai-icon {
width: 32rpx; width: 32rpx;
height: 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; margin-right: 12rpx;
} }
.ai-icon-img {
display: block;
}
.ai-title { .ai-title {
font-size: 28rpx; font-size: 28rpx;
font-weight: bold; font-weight: bold;

View File

@@ -16,6 +16,7 @@
<view class="content-wrap"> <view class="content-wrap">
<view class="topic-editor card-block"> <view class="topic-editor card-block">
<view class="editor-badge">创建新话题</view>
<textarea <textarea
v-model="formData.title" v-model="formData.title"
class="topic-input" class="topic-input"
@@ -24,14 +25,7 @@
placeholder-class="topic-placeholder" placeholder-class="topic-placeholder"
auto-height auto-height
/> />
<!-- <textarea <text class="topic-helper">标题越明确越容易吸引大家参与评分和评论</text>
v-model="formData.description"
class="desc-input"
maxlength="120"
placeholder="简单介绍一下这个评分话题..."
placeholder-class="desc-placeholder"
auto-height
/> -->
</view> </view>
<!-- <view class="cover-card" :class="{ 'has-cover': formData.coverUrl }" @tap="chooseCover"> <!-- <view class="cover-card" :class="{ 'has-cover': formData.coverUrl }" @tap="chooseCover">
@@ -47,20 +41,29 @@
</view> </view>
</view> --> </view> -->
<view class="category-group"> <view class="category-group card-block">
<view <view class="section-meta">
v-for="category in categories" <text class="category-label">话题分类</text>
:key="category.id" <text class="section-helper">选择一个更贴近内容的圈层</text>
class="category-chip" </view>
:class="{ active: formData.categoryId === category.id }" <view class="category-list">
@tap="formData.categoryId = category.id" <view
> v-for="category in categories"
{{ category.title }} :key="category.id"
class="category-chip"
:class="{ active: formData.categoryId === category.id }"
@tap="formData.categoryId = category.id"
>
{{ category.title }}
</view>
</view> </view>
</view> </view>
<view class="section-header"> <view class="section-header">
<text class="section-title">评分对象</text> <view class="section-title-wrap">
<text class="section-title">评分对象</text>
<text class="section-subtitle">至少保留 1 最多添加 20 </text>
</view>
<text class="section-count">{{ participants.length }}/20</text> <text class="section-count">{{ participants.length }}/20</text>
</view> </view>
@@ -101,17 +104,19 @@
</view> </view>
<view class="participant-actions"> <view class="participant-actions">
<!-- <text class="drag-icon"></text> --> <!-- <text class="drag-icon"></text> -->
<text class="delete-icon" @tap="removeParticipant(item.id)">🗑</text> <image class="delete-icon-img" src="/static/images/icon/remove.png" @tap="removeParticipant(item.id)" mode="aspectFit"></image>
</view> </view>
</view> </view>
<view class="add-participant" @tap="addParticipant"> <view class="add-participant" @tap="addParticipant">
<text class="add-plus">+</text> <view class="add-plus-wrap">
<text class="add-plus">+</text>
</view>
<text class="add-text">添加评分对象</text> <text class="add-text">添加评分对象</text>
</view> </view>
</view> </view>
<view class="settings-card card-block"> <!-- <view class="settings-card card-block">
<view class="setting-row"> <view class="setting-row">
<view class="setting-info"> <view class="setting-info">
<text class="setting-title">允许用户锐评</text> <text class="setting-title">允许用户锐评</text>
@@ -147,17 +152,21 @@
@change="formData.anonymous = $event.detail.value" @change="formData.anonymous = $event.detail.value"
/> />
</view> </view>
</view> </view> -->
</view> </view>
<view class="footer-bar"> <view class="footer-bar">
<button class="submit-btn" @tap="handleSubmit">发布全民评分</button> <button class="submit-btn" @tap="handleSubmit">发布夯拉评分</button>
</view> </view>
<!-- 成功特效弹窗 -->
<SuccessPopup ref="successPopupRef" />
<LoginPopup ref="loginPopupRef" @logind="handleLoginSuccess" />
</view> </view>
</template> </template>
<script setup> <script setup>
import { reactive, ref } from "vue"; import { computed, reactive, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app"; import { onLoad } from "@dcloudio/uni-app";
import { getStatusBarHeight } from "@/utils/system"; import { getStatusBarHeight } from "@/utils/system";
import { releaseTopic, fetchTopicCategories } from "@/api/topic"; import { releaseTopic, fetchTopicCategories } from "@/api/topic";
@@ -166,9 +175,16 @@ import {
uploadImage, uploadImage,
chooseAndUploadImage, chooseAndUploadImage,
} from "@/utils/common.js"; } from "@/utils/common.js";
import SuccessPopup from "@/components/SuccessPopup/SuccessPopup.vue";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import { FILE_BASE_URL } from "@/utils/constants"; import { FILE_BASE_URL } from "@/utils/constants";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const successPopupRef = ref(null);
const loginPopupRef = ref(null);
const statusBarHeight = ref(getStatusBarHeight() || 0); const statusBarHeight = ref(getStatusBarHeight() || 0);
const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName);
const categories = ref([]); const categories = ref([]);
@@ -219,7 +235,21 @@ const goBack = () => {
smartNavigateBack(); smartNavigateBack();
}; };
const openLoginPopup = (message = "请先登录后再操作") => {
loginPopupRef.value?.open();
if (message) {
uni.showToast({
title: message,
icon: "none",
});
}
};
const handlePreview = () => { const handlePreview = () => {
if (!isLoggedIn.value) {
openLoginPopup("请先登录后再预览");
return;
}
uni.showToast({ uni.showToast({
title: "预览功能待接入", title: "预览功能待接入",
icon: "none", icon: "none",
@@ -238,6 +268,10 @@ const chooseCover = async () => {
}; };
const chooseParticipantAvatar = async (index) => { const chooseParticipantAvatar = async (index) => {
if (!isLoggedIn.value) {
openLoginPopup("请先登录后再上传头像");
return;
}
try { try {
const urls = await chooseAndUploadImage({ count: 1 }); const urls = await chooseAndUploadImage({ count: 1 });
if (urls && urls.length > 0) { if (urls && urls.length > 0) {
@@ -294,6 +328,10 @@ const removeParticipant = (id) => {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!isLoggedIn.value) {
openLoginPopup("请先登录后再发起评分");
return;
}
if (!formData.title.trim()) { if (!formData.title.trim()) {
uni.showToast({ title: '请输入话题', icon: 'none' }); uni.showToast({ title: '请输入话题', icon: 'none' });
return; return;
@@ -321,14 +359,44 @@ const handleSubmit = async () => {
participants: participants.value, participants: participants.value,
}; };
await releaseTopic(payload); await releaseTopic(payload);
uni.hideLoading(); // 记录发布事件
uni.showToast({ uni.$trackRecord({
title: "发布成功", eventName: 'release_topic',
icon: "success", eventType: 'release',
elementContent: `话题_${payload.title}`,
customParams: {
title: payload.title,
categoryId: payload.categoryId,
}
}); });
smartNavigateBack({ delay: 1500 }); uni.hideLoading();
if (successPopupRef.value) {
successPopupRef.value.show({
emoji: '🎉',
label: '请耐心等待管理员审核'
}, 'release');
}
// 初始化表单内容
Object.assign(formData, {
title: "",
description: "",
coverUrl: "",
categoryId: "",
});
// 初始化评分对象列表为默认的一个空项,并且赋予唯一的 ID
participants.value = [
{ id: Date.now(), name: "", desc: "", avatar: "" }
];
// 延迟跳转,等待弹窗展示完成
setTimeout(() => {
smartNavigateBack();
}, 2000);
} catch (error) { } catch (error) {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({
@@ -338,14 +406,23 @@ const handleSubmit = async () => {
console.error("发布失败:", error); console.error("发布失败:", error);
} }
}; };
const handleLoginSuccess = () => {
if (categories.value.length === 0) {
loadCategories();
}
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.page-container { .page-container {
min-height: 100vh; min-height: 100vh;
background: #f8f9fc; /* 整体背景调亮 */ background:
radial-gradient(circle at top, rgba(111, 90, 255, 0.1), transparent 34%),
linear-gradient(180deg, #f7f8ff 0%, #f6f7fb 28%, #ffffff 100%);
padding-bottom: 180rpx; padding-bottom: 180rpx;
box-sizing: border-box; box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Segoe UI, Arial, Roboto, "PingFang SC", "miui", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
} }
.nav-bar { .nav-bar {
@@ -354,8 +431,9 @@ const handleSubmit = async () => {
left: 0; left: 0;
right: 0; right: 0;
z-index: 100; z-index: 100;
background: rgba(244, 245, 251, 0.96); background: rgba(247, 248, 255, 0.82);
backdrop-filter: blur(18rpx); backdrop-filter: blur(24rpx);
border-bottom: 1rpx solid rgba(133, 142, 194, 0.08);
} }
.nav-content { .nav-content {
@@ -363,7 +441,7 @@ const handleSubmit = async () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 24rpx; padding: 0 28rpx;
} }
.nav-side { .nav-side {
@@ -385,61 +463,68 @@ const handleSubmit = async () => {
.nav-title { .nav-title {
font-size: 34rpx; font-size: 34rpx;
font-weight: 700; font-weight: 800;
color: #171717; color: #16161d;
letter-spacing: 1rpx;
} }
.preview-text { .preview-text {
font-size: 28rpx; font-size: 28rpx;
color: #5f63ff; color: #5c43f5;
font-weight: 600;
} }
.content-wrap { .content-wrap {
padding: 24rpx; padding: 28rpx 24rpx 0;
} }
/* 统一的卡片风格 */
.card-block { .card-block {
background: #ffffff; background: #ffffff;
border-radius: 36rpx; /* 增大圆角,与设计图更统一 */ border-radius: 34rpx;
box-shadow: 0 4rpx 20rpx rgba(72, 82, 130, 0.03); /* 柔和的阴影 */ box-shadow: 0 18rpx 40rpx rgba(84, 88, 132, 0.08);
margin-bottom: 32rpx; /* 增加块级间距 */ margin-bottom: 28rpx;
border: 2rpx solid #eef0f7; /* 统一的极淡边框 */ border: 1rpx solid rgba(222, 227, 246, 0.9);
} }
.topic-editor { .topic-editor {
padding: 32rpx; /* 增加内边距 */ padding: 28rpx 28rpx 30rpx;
margin-bottom: 24rpx; background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(245, 246, 255, 0.92));
} }
.topic-input, .editor-badge {
.desc-input { display: inline-flex;
width: 100%; align-items: center;
min-height: 52rpx; height: 44rpx;
color: #232323; padding: 0 18rpx;
border-radius: 999rpx;
margin-bottom: 22rpx;
background: rgba(92, 67, 245, 0.08);
color: #5c43f5;
font-size: 22rpx;
font-weight: 700;
} }
.topic-input { .topic-input {
font-size: 44rpx; /* 稍微减小标题字号,显得更精致 */ width: 100%;
font-weight: 800; /* 加粗 */ min-height: 76rpx;
margin-bottom: 16rpx; /* 增加行距 */ color: #191a22;
} font-size: 48rpx;
font-weight: 800;
.desc-input { line-height: 1.3;
font-size: 28rpx;
color: #666;
line-height: 1.5;
} }
.topic-placeholder { .topic-placeholder {
font-size: 44rpx; font-size: 48rpx;
font-weight: 800; font-weight: 800;
color: #c1c2d3; color: #c6cadc;
} }
.desc-placeholder { .topic-helper {
font-size: 28rpx; margin-top: 18rpx;
color: #b8b9c9; display: block;
font-size: 24rpx;
line-height: 1.6;
color: #8a8fa7;
} }
.cover-card { .cover-card {
@@ -483,29 +568,53 @@ const handleSubmit = async () => {
} }
.category-group { .category-group {
padding: 28rpx;
}
.section-meta {
margin-bottom: 20rpx;
}
.category-label {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #1c1d24;
}
.section-helper {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #9196ac;
}
.category-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16rpx; gap: 16rpx;
margin-bottom: 24rpx;
} }
.category-chip { .category-chip {
min-width: 88rpx; min-width: 88rpx;
padding: 14rpx 32rpx; /* 增加内边距 */ padding: 16rpx 30rpx;
border-radius: 999rpx; border-radius: 999rpx;
background: #e9ecf5; /* 背景调浅一些,更接近设计图 */ background: #f3f5fc;
color: #5c6176; /* 字体颜色调浅 */ color: #5f647d;
font-size: 28rpx; /* 字体调大 */ font-size: 28rpx;
font-weight: 500; font-weight: 600;
text-align: center; text-align: center;
transition: all 0.2s ease; border: 1rpx solid transparent;
transition: all 0.22s ease;
} }
.category-chip.active { .category-chip.active {
background: linear-gradient(135deg, #6c4af2, #883cf3); /* 更接近设计图的紫色渐变 */ background: linear-gradient(135deg, #5e43f4, #7d4dff);
color: #fff; color: #fff;
font-weight: 700; font-weight: 700;
box-shadow: 0 8rpx 20rpx rgba(108, 74, 242, 0.3); /* 调整阴影颜色 */ border-color: rgba(116, 85, 255, 0.24);
box-shadow: 0 12rpx 26rpx rgba(101, 74, 245, 0.25);
transform: translateY(-2rpx);
} }
.tag-box { .tag-box {
@@ -555,16 +664,27 @@ const handleSubmit = async () => {
.section-header { .section-header {
display: flex; display: flex;
align-items: center; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
margin-bottom: 24rpx; /* 增加底部间距 */ margin-bottom: 20rpx;
padding: 0 8rpx; /* 标题与内容对齐 */ padding: 0 8rpx;
}
.section-title-wrap {
display: flex;
flex-direction: column;
} }
.section-title { .section-title {
font-size: 34rpx; /* 调整大小 */ font-size: 34rpx;
font-weight: 800; /* 加粗 */ font-weight: 800;
color: #171717; color: #1b1c24;
}
.section-subtitle {
margin-top: 8rpx;
font-size: 24rpx;
color: #9297ad;
} }
.section-count { .section-count {
@@ -572,10 +692,11 @@ const handleSubmit = async () => {
height: 46rpx; height: 46rpx;
line-height: 46rpx; line-height: 46rpx;
text-align: center; text-align: center;
border-radius: 14rpx; border-radius: 999rpx;
background: #e6e9f7; background: rgba(92, 67, 245, 0.1);
color: #5c6176; color: #5c43f5;
font-size: 26rpx; font-size: 26rpx;
font-weight: 700;
} }
.participant-list { .participant-list {
@@ -587,11 +708,15 @@ const handleSubmit = async () => {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: #fff; background: #fff;
border-radius: 36rpx; /* 更圆润的边角 */ border-radius: 32rpx;
padding: 32rpx 28rpx; /* 增加内边距 */ padding: 28rpx 24rpx;
margin-bottom: 24rpx; margin-bottom: 24rpx;
border: 2rpx solid #eef0f7; /* 增加细微的描边,更接近设计图质感 */ border: 1rpx solid #edf0fb;
box-shadow: 0 4rpx 20rpx rgba(72, 82, 130, 0.03); /* 阴影调弱 */ box-shadow: 0 12rpx 28rpx rgba(72, 82, 130, 0.05);
}
.participant-card:active {
transform: scale(0.995);
} }
.participant-main { .participant-main {
@@ -602,21 +727,23 @@ const handleSubmit = async () => {
.participant-avatar-wrap { .participant-avatar-wrap {
margin-right: 20rpx; margin-right: 20rpx;
position: relative;
} }
.participant-avatar { .participant-avatar {
width: 92rpx; width: 96rpx;
height: 92rpx; height: 96rpx;
border-radius: 50%; border-radius: 50%;
background: #e8ebf7; background: #eceffc;
box-shadow: 0 8rpx 18rpx rgba(95, 99, 255, 0.12);
} }
.participant-avatar.empty { .participant-avatar.empty {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 2rpx dashed #b5bad0; /* 加深虚线颜色 */ border: 2rpx dashed #c8cde1;
background: #f8f9fc; /* 空状态背景色 */ background: linear-gradient(180deg, #fafbff, #f3f5fc);
} }
.camera-small { .camera-small {
@@ -636,65 +763,64 @@ const handleSubmit = async () => {
} }
.participant-name { .participant-name {
height: 42rpx; height: 44rpx;
font-size: 30rpx; font-size: 30rpx;
font-weight: 700; font-weight: 700;
color: #202020; color: #1c1d24;
margin-bottom: 10rpx;
}
.participant-desc {
height: 38rpx;
font-size: 24rpx;
color: #676c7f;
} }
.participant-placeholder { .participant-placeholder {
color: #b6b9c7; color: #b7bbcd;
} }
.participant-actions { .participant-actions {
width: 56rpx; width: 72rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 16rpx;
margin-left: 16rpx; margin-left: 16rpx;
} }
.drag-icon { .delete-icon-img {
font-size: 34rpx; width: 40rpx;
color: #b2b4c5; height: 40rpx;
line-height: 1; padding: 12rpx;
} border-radius: 50%;
background: #fff5f5;
.delete-icon {
font-size: 32rpx; /* 图标放大 */
color: #ff6b6b; /* 明确的删除红色 */
line-height: 1;
padding: 8rpx; /* 增加点击区域 */
} }
.add-participant { .add-participant {
height: 120rpx; /* 增加高度 */ height: 120rpx;
border: 2rpx dashed #c0c4d6; /* 边框颜色更明显 */ border: 2rpx dashed #cfd5e8;
border-radius: 36rpx; /* 圆角一致 */ border-radius: 32rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #888d9e; /* 字体颜色微调 */ color: #737993;
gap: 16rpx; /* 间距变大 */ gap: 16rpx;
background: #f8f9fc; /* 添加底色 */ background: rgba(255, 255, 255, 0.72);
}
.add-plus-wrap {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(92, 67, 245, 0.1);
} }
.add-plus { .add-plus {
font-size: 38rpx; font-size: 34rpx;
line-height: 1; line-height: 1;
color: #5c43f5;
} }
.add-text { .add-text {
font-size: 28rpx; font-size: 28rpx;
font-weight: 600;
} }
.settings-card { .settings-card {
@@ -738,19 +864,20 @@ const handleSubmit = async () => {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
padding: 24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom)); padding: 24rpx 24rpx calc(28rpx + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba(244, 245, 251, 0) 0%, rgba(244, 245, 251, 0.98) 34%); background: linear-gradient(180deg, rgba(247, 248, 255, 0) 0%, rgba(247, 248, 255, 0.94) 26%, rgba(247, 248, 255, 1) 100%);
} }
.submit-btn { .submit-btn {
height: 100rpx; height: 104rpx;
line-height: 100rpx; line-height: 104rpx;
border-radius: 999rpx; border-radius: 999rpx;
background: linear-gradient(135deg, #5c43f5, #7a2cf3); /* 更接近设计图的深紫色 */ background: linear-gradient(135deg, #5c43f5, #6f55ff 58%, #8b66ff);
color: #fff; color: #fff;
font-size: 34rpx; font-size: 34rpx;
font-weight: 700; font-weight: 800;
box-shadow: 0 16rpx 32rpx rgba(100, 60, 240, 0.3); /* 加强底部按钮阴影 */ letter-spacing: 1rpx;
box-shadow: 0 18rpx 36rpx rgba(98, 69, 246, 0.34);
} }
.submit-btn::after { .submit-btn::after {

BIN
static/images/icon/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
static/images/icon/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
static/images/icon/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
static/images/icon/fire.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
static/images/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -154,6 +154,15 @@ export const getShareToken = async (scene, targetId = "") => {
return shareTokenRes?.shareToken || ""; return shareTokenRes?.shareToken || "";
}; };
/**
* 记录用户行为事件
* @param {Object} event 事件对象,包含事件名称和参数
* eventName 事件名称,例如 'switchCategory'
* eventType 事件类型,例如 'click',可选
* elementId 事件元素 ID例如 'category_123',可选
* elementContent 事件元素内容,例如 '分类1',可选
* customParams 自定义参数,例如 { categoryId: 123 },可选
*/
export const trackRecord = (event) => { export const trackRecord = (event) => {
const pages = getCurrentPages(); const pages = getCurrentPages();
// 核心修复在小程序刚启动或页面尚未压栈时pages 可能为空数组 // 核心修复在小程序刚启动或页面尚未压栈时pages 可能为空数组