文件
shanghaipeiwan/pwuniapp/pages/community/index.vue
xcbyy 94cf6cdc69 perf: 清理无界缓存、合并收益走势查询、补齐组合索引并优化前端定时器/图片加载
- 移除只写不读的 verifyCodeCache(无界内存增长)

- 大神收益走势由 20 次逐日/逐周/逐月查询改为 3 次 GROUP BY 聚合

- 派单池地理编码改为并行,避免串行等待外部接口

- 补齐 orders/activities/participants/user_notifications/finance/withdrawals/posts 组合索引;修复 withdrawals 使用不存在 created_at 的失效索引

- 数据库连接池支持 MYSQL_CONNECTION_LIMIT/MYSQL_QUEUE_LIMIT

- 首页/弹窗定时器卸载清理,活动列表参与者按活动 id 查询,大厅组局按页取数,图片 lazy-load

- 121 套件/792 用例全绿;已部署并验证新索引与 9ms 走势查询
2026-10-23 15:31:37 +08:00

3490 行
93 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="community-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }" :style="navStyle">
<!-- 友圈广场骨架屏 (首屏加载中流光占位,杜绝视觉空白) -->
<skeleton-screen v-if="isLoading" type="community" />
<!-- 真实内容 (数据加载完成平滑展示) -->
<view v-else class="community-body-wrapper">
<!-- 顶部状态栏与标题区 (将发动态置于胶囊正下方,规避横向挤压) -->
<view class="community-header">
<!-- 第一行:主标题与胶囊水平对齐 -->
<view class="header-title-row">
<text class="page-title">友圈广场</text>
</view>
<!-- 第二行:副标题与发动态按钮 (位于胶囊正下方) -->
<view class="header-sub-row">
<text class="page-sub">同城搭子 · 车队组排 · 游戏高光</text>
<view class="publish-btn btn-active" @click="handlePublishPost">
<image class="publish-icon" :src="ICONS.editPen" mode="aspectFit" />
<text class="publish-text">发动态</text>
</view>
</view>
</view>
<!-- 友圈置顶头条公告 (跑马灯平滑微动与点击查看详情弹窗组合) -->
<view class="community-notice-bar btn-active" v-if="communityNotice && communityNotice.title" @click="openNoticeDetailModal">
<view class="notice-badge">{{ communityNotice.tag || '置顶推荐' }}</view>
<view class="notice-marquee-wrap">
<view class="notice-marquee-content">
<text class="notice-title-text">{{ communityNotice.title }}:{{ communityNotice.content }}</text>
<text class="notice-title-text marquee-clone">{{ communityNotice.title }}:{{ communityNotice.content }}</text>
</view>
</view>
<view class="notice-action-hint">
<text class="hint-text">详情</text>
<image class="hint-arrow" :src="ICONS.chevronRight" mode="aspectFit" />
</view>
</view>
<!-- 分类筛选栏 (高能矢量Tab标签与标准HOT角标) -->
<view class="feed-tabs-wrap">
<scroll-view class="feed-scroll" scroll-x :show-scrollbar="false">
<view class="tabs-flex">
<view
v-for="tab in feedTabs"
:key="tab.key"
class="tab-pill btn-active"
:class="{ active: currentTab === tab.key }"
@click="switchFeedTab(tab.key)"
>
<image v-if="tab.icon" class="tab-icon-svg" :src="tab.icon" mode="aspectFit" />
<text class="pill-text">{{ tab.name }}</text>
<view class="tab-badge" v-if="tab.hot">
<text class="badge-text">HOT</text>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 动态信息流列表 -->
<view class="posts-list">
<view v-if="filteredPosts.length === 0" class="posts-empty">
<text class="posts-empty-title">还没有动态</text>
<text class="posts-empty-desc">发第一条同城搭子动态,约上附近的小伙伴</text>
</view>
<view
v-for="(post, index) in filteredPosts"
:key="post.id"
class="post-card"
:style="{ animationDelay: `${Math.min(index, 8) * 45}ms` }"
@click="openPostDetail(post)"
>
<!-- 作者信息行 -->
<view class="author-row">
<image class="author-avatar btn-active" :src="post.avatar" mode="aspectFill" @click.stop="openAuthorProfile(post)" />
<view class="author-meta btn-active" @click.stop="openAuthorProfile(post)">
<view class="name-line">
<text class="author-name">{{ post.author }}</text>
<text class="author-tag" :class="post.tagType">{{ post.tag }}</text>
</view>
<view class="sub-line">
<text class="post-time">{{ post.time }}</text>
<text class="dot-sep">·</text>
<view class="distance-box">
<image class="dist-icon" :src="ICONS.mapPin" mode="aspectFit" />
<text class="dist-text">{{ post.location }} ({{ post.distance }})</text>
</view>
</view>
</view>
<view class="author-actions">
<template v-if="isPostOwner(post)">
<button
class="join-btn btn-active"
style="margin-right: 12rpx;"
@click.stop="handleManageApplies(post)"
>
申请({{ post.applyCount || 0 }})
</button>
<button
class="btn-delete-post btn-active"
@click.stop="handleDeletePost(post)"
>
删除
</button>
</template>
<button
v-else
class="join-btn btn-active"
:class="{ 'btn-applied': post.hasApplied && post.myApplyStatus !== 'REJECTED' }"
:disabled="post.hasApplied && post.myApplyStatus !== 'REJECTED'"
@click.stop="handleJoinParty(post)"
>
{{ post.myApplyStatus === 'ACCEPTED' ? '已同意上车' : (post.myApplyStatus === 'REJECTED' ? '重新申请' : (post.hasApplied ? '待房主确认' : (post.actionText || '聊一聊'))) }}
</button>
</view>
</view>
<!-- 动态文本内容 -->
<text class="post-content">{{ post.content }}</text>
<!-- 话题标签 -->
<view class="post-topics" v-if="post.topics && post.topics.length">
<text v-for="t in post.topics" :key="t" class="topic-item">#{{ t }}</text>
</view>
<!-- 图片九宫格 / 展位 -->
<view class="post-images" v-if="post.images && post.images.length">
<image
v-for="(img, idx) in post.images"
:key="idx"
class="post-img"
lazy-load
:src="img"
mode="aspectFill"
@click.stop="previewImage(post.images, idx)"
/>
</view>
<!-- 底部互动操作栏 -->
<view class="post-footer">
<view class="footer-left">
<view class="action-item btn-active" @click.stop="toggleLike(post)">
<image class="action-icon" :src="post.liked ? ICONS.heartFilled : ICONS.heart" mode="aspectFit" />
<text class="action-count" :class="{ liked: post.liked }">{{ post.likes }}</text>
</view>
<view class="action-item btn-active" @click.stop="handleComment(post)">
<image class="action-icon" :src="ICONS.headphones" mode="aspectFit" />
<text class="action-count">{{ post.comments }}</text>
</view>
</view>
<view class="footer-right">
<text class="view-tag">浏览 {{ post.views }} 次</text>
</view>
</view>
</view>
</view>
</view>
<!-- 发动态弹窗编辑器 (顶奢社交风格 Bottom Sheet,支持手柄/分段器/沉浸式输入/优雅九宫格) -->
<view class="publish-modal-mask" v-if="showPublishModal" @click="closePublishModal">
<view class="publish-modal-box" @click.stop="stopPropagation">
<!-- 抽屉顶部居中下拉把手 -->
<view class="sheet-drag-handle"></view>
<!-- 弹窗顶栏:标题 + 副标题 + 关闭按钮 -->
<view class="modal-header">
<view class="header-titles">
<text class="modal-title">发布友圈动态</text>
<text class="modal-sub">与附近 3km 的同城同好分享高光时刻</text>
</view>
<view class="close-btn btn-active" @click="closePublishModal">
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<!-- 分类分段控制器 (iOS / 现代 App Segmented Control 风格一体化滑块) -->
<view class="segment-tabs-container">
<view
v-for="cat in publishCategories"
:key="cat.key"
class="segment-item btn-active"
:class="{ active: newPostForm.category === cat.key }"
@click="selectPublishCategory(cat.key)"
>
<text class="segment-text">{{ cat.name }}</text>
</view>
</view>
<!-- 正文沉浸式输入区 -->
<view class="textarea-card">
<textarea
class="post-textarea"
v-model="newPostForm.content"
:placeholder="contentPlaceholder"
placeholder-class="ph"
maxlength="300"
:show-confirm-bar="false"
/>
<view class="textarea-corner-meta">
<view class="char-count-badge" :class="{ 'near-limit': newPostForm.content.length > 270 }">
<text class="current-len">{{ newPostForm.content.length }}</text>
<text class="max-len">/300</text>
</view>
</view>
</view>
<!-- 推荐热门话题 (微胶囊流式布局) -->
<view class="topics-section">
<view class="section-title-row">
<text class="title-text">推荐话题</text>
<text class="title-tip">最多选择3个话题标签</text>
</view>
<view class="topics-flex">
<view
v-for="topic in availableTopics"
:key="topic"
class="topic-chip btn-active"
:class="{ selected: isTopicSelected(topic) }"
@click="toggleTopic(topic)"
>
<text class="hash-tag">#</text>
<text class="topic-name">{{ topic }}</text>
</view>
</view>
</view>
<!-- 图片九宫格选择区 (小红书/朋友圈现代画廊式) -->
<view class="gallery-section">
<view class="gallery-grid">
<view
class="grid-img-item"
v-for="(img, idx) in newPostForm.images"
:key="idx"
>
<image class="thumb-img" :src="img" mode="aspectFill" @click="previewImage(newPostForm.images, idx)" />
<view class="remove-btn btn-active" @click.stop="removeImage(idx)">
<image class="remove-icon" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<view
class="add-img-tile btn-active"
v-if="newPostForm.images.length < 9"
@click="choosePostImage"
>
<view class="tile-icon-circle">
<image class="cam-icon" :src="ICONS.camera" mode="aspectFit" />
</view>
<text class="tile-label">添加图片</text>
<text class="tile-count">{{ newPostForm.images.length }}/9</text>
</view>
</view>
</view>
<!-- 定位信息栏 (轻量微胶囊) -->
<view class="meta-row">
<view class="location-pill">
<image class="loc-pin" :src="ICONS.mapPin" mode="aspectFit" />
<text class="loc-name">{{ newPostForm.location }}</text>
</view>
</view>
<!-- 底部发布操作栏 (双按钮高能对比) -->
<view class="modal-footer-row">
<button class="action-btn-cancel btn-active" @click="closePublishModal">取消</button>
<button
class="action-btn-submit btn-active"
:disabled="!newPostForm.content.trim()"
@click="submitPublishPost"
>
立即发布
</button>
</view>
</view>
</view>
<!-- 动态互动评论区抽屉组件 -->
<view class="comment-drawer-mask" v-if="commentDrawerVisible" @click="closeCommentDrawer">
<view class="comment-drawer-content" @click.stop="stopPropagation">
<view class="drawer-header">
<view class="drawer-header-left">
<text class="drawer-title">动态评论</text>
<text class="drawer-badge" v-if="activeCommentPost">({{ commentsList.length }})</text>
</view>
<view class="drawer-close" @click="closeCommentDrawer">
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<!-- 评论列表区域 -->
<scroll-view class="comments-scroll" scroll-y="true">
<view v-if="loadingComments" class="comments-loading">
<text class="loading-text">正在加载评论...</text>
</view>
<view v-else-if="commentsList.length === 0" class="comments-empty">
<image class="empty-icon" :src="ICONS.messageSquare" mode="aspectFit" />
<text class="empty-text">暂无搭子留言,快来抢占沙发!</text>
</view>
<view v-else class="comments-list">
<view v-for="item in commentsList" :key="item.id" class="comment-item">
<image class="comment-avatar btn-active" :src="item.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100'" mode="aspectFill" @click.stop="openAuthorProfile(item)" />
<view class="comment-body">
<view class="comment-top">
<text class="comment-author" @click.stop="openAuthorProfile(item)">{{ item.author }}</text>
<text class="comment-time">{{ item.createdAt ? '刚刚' : '近期' }}</text>
</view>
<text class="comment-content">{{ item.content }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部输入栏 -->
<view class="comment-input-bar">
<input
class="comment-input"
v-model="commentInput"
placeholder="友善互动,共创精彩搭子体验..."
placeholder-class="input-placeholder"
maxlength="200"
confirm-type="send"
@confirm="sendComment"
/>
<button
class="comment-send-btn btn-active"
:disabled="!commentInput.trim() || sendingComment"
@click="sendComment"
>
发送
</button>
</view>
</view>
</view>
<!-- 社区置顶公告与公约详情弹窗 -->
<view class="notice-modal-mask" v-if="noticeModalVisible" @click="closeNoticeDetailModal">
<view class="notice-modal-content" @click.stop="stopPropagation">
<view class="modal-header">
<view class="modal-header-left">
<view class="modal-badge">{{ communityNotice.tag || '置顶推荐' }}</view>
<text class="modal-title">{{ communityNotice.title }}</text>
</view>
<view class="modal-close btn-active" @click="closeNoticeDetailModal">
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<scroll-view class="notice-modal-body" scroll-y="true">
<!-- 公告引导前言 -->
<view class="notice-lead-card">
<image class="lead-icon" :src="ICONS.shield" mode="aspectFit" />
<text class="lead-text">{{ communityNotice.content }}</text>
</view>
<!-- 社区规范条款列表 -->
<view class="notice-rules-section">
<view class="rules-header">社区公约与安全守则</view>
<view class="rule-item">
<view class="rule-index">01</view>
<view class="rule-body">
<text class="rule-title">文明交友与真实组局</text>
<text class="rule-desc">严禁发布虚假、欺诈、低俗或人身攻击信息,发帖请明确时间、地点及AA分摊规则,共建健康同城氛围。</text>
</view>
</view>
<view class="rule-item">
<view class="rule-index">02</view>
<view class="rule-body">
<text class="rule-title">严禁私域引流与脱网交易</text>
<text class="rule-desc">动态与评论中严禁出现微信号、手机号、二维码等私下引流信息,脱网交易不受平台资金与安全保障。</text>
</view>
</view>
<view class="rule-item">
<view class="rule-index">03</view>
<view class="rule-body">
<text class="rule-title">全天候官方内容安全审查</text>
<text class="rule-desc">全平台已接入微信官方文本与图片内容安全识别系统,违规内容秒级拦截,涉嫌严重违规者将实施封号处置。</text>
</view>
</view>
<view class="rule-item">
<view class="rule-index">04</view>
<view class="rule-body">
<text class="rule-title">同城线下公开安全履约</text>
<text class="rule-desc">线下同城搭子组局倡导在正规商圈、餐馆等公开营业场所进行,现场凭借4位数字核销码签到核销。</text>
</view>
</view>
</view>
</scroll-view>
<view class="notice-modal-footer">
<button class="modal-confirm-btn btn-active" @click="closeNoticeDetailModal">已知晓并遵守公约</button>
</view>
</view>
</view>
<!-- 底部自定义导航栏 (发单人端:活动中心 / 我要发单 / 友圈 / 个人中心) -->
<AppTabBar current="community" />
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import AppTabBar from '@/components/CustomTabBar.vue';
import SkeletonScreen from '@/components/SkeletonScreen.vue';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { ensureLoginOrRedirect } from '@/common/role.js';
import { getNavBarInfo } from '@/common/nav.js';
import { getPosts, createPost, deletePost, likePost, viewPost, getPostComments, addPostComment, applyPostParty, getPostApplies, handlePostApply, uploadImage, getPlatformConfigs, getUserProfile, normalizeUserId } from '@/common/api.js';
import { useShare } from '@/common/share.js';
useShare({
title: '友圈广场 · 发现同城精彩动态与生活日常',
timelineTitle: '友圈广场 · 看看同城小伙伴的精彩日常',
path: '/pages/community/index'
});
const DEFAULT_USER_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120';
const isLoading = ref(true);
const currentThemeKey = ref(getCurrentThemeKey());
const navInfo = ref(getNavBarInfo());
const currentUserProfile = ref({});
const readCachedUserInfo = () => {
const pwInfo = uni.getStorageSync('PW_USER_INFO') || {};
const uInfo = uni.getStorageSync('userInfo') || {};
const uid = normalizeUserId();
const rawName = pwInfo.nickname || uInfo.nickname || '';
const nickname = (rawName && !['点击登录 / 注册', '电竞玩家 (我)'].includes(rawName)) ? rawName : '';
const avatar = pwInfo.avatar || uInfo.avatar || '';
return {
id: uid,
nickname,
avatar
};
};
const syncCurrentUserProfile = async () => {
const cached = readCachedUserInfo();
currentUserProfile.value = {
...currentUserProfile.value,
...cached
};
const storedUid = Number(uni.getStorageSync('userId'));
if (!storedUid) return currentUserProfile.value;
try {
const remote = await getUserProfile(storedUid);
if (remote && remote.nickname) {
const merged = {
...cached,
...remote,
nickname: remote.nickname,
avatar: remote.avatar || cached.avatar || DEFAULT_USER_AVATAR
};
currentUserProfile.value = merged;
uni.setStorageSync('PW_USER_INFO', merged);
uni.setStorageSync('userInfo', merged);
}
} catch (_) {}
return currentUserProfile.value;
};
const communityNotice = ref({
title: '友圈同城互动与公约置顶',
content: '欢迎来到同城搭子友圈!文明交友,发布真实组局诉求;平台全天候对动态与评论实施微信官方内容安全过滤,严禁广告引流及私下交易。',
tag: '置顶推荐',
showInBanner: true
});
const noticeModalVisible = ref(false);
const openNoticeDetailModal = () => {
noticeModalVisible.value = true;
};
const closeNoticeDetailModal = () => {
noticeModalVisible.value = false;
};
const availableTopics = ref([
'三里屯探店',
'铜锅涮肉AA',
'国服上分车队',
'饭搭子招募',
'工体纯K麦霸',
'同城搭子招募'
]);
const navStyle = computed(() => {
return {
'--safe-header-gap': `${navInfo.value.safeHeaderGap}px`,
'--capsule-right-space': `${navInfo.value.capsuleRightSpace}px`,
'--capsule-height': `${navInfo.value.capsuleHeight}px`
};
});
const onThemeChanged = (newTheme) => {
currentThemeKey.value = newTheme;
applyThemeToNativeUI(newTheme);
};
const loadInitialCommunity = async () => {
try {
await syncCurrentUserProfile();
await loadRemotePosts();
} finally {
setTimeout(() => {
isLoading.value = false;
}, 200);
}
};
onMounted(() => {
navInfo.value = getNavBarInfo();
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
uni.$on('theme-change', onThemeChanged);
loadInitialCommunity();
loadPlatformCommunityConfigs();
});
onShow(() => {
navInfo.value = getNavBarInfo();
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
uni.hideTabBar({ animation: false, fail: () => {} });
const cachedActive = uni.getStorageSync('ACTIVE_COMMUNITY_POST');
if (cachedActive && cachedActive.id) {
const idx = posts.value.findIndex(p => String(p.id) === String(cachedActive.id));
if (idx > -1) {
posts.value[idx] = {
...posts.value[idx],
...cachedActive
};
}
}
loadRemotePosts();
loadPlatformCommunityConfigs();
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
});
const loadRemotePosts = async () => {
try {
const list = await getPosts(currentTab.value);
{
let localLikedIds = uni.getStorageSync('COMMUNITY_LIKED_POST_IDS') || [];
if (!Array.isArray(localLikedIds)) localLikedIds = [];
let localAppliedIds = uni.getStorageSync('COMMUNITY_APPLIED_POST_IDS') || [];
if (!Array.isArray(localAppliedIds)) localAppliedIds = [];
let viewsMap = uni.getStorageSync('COMMUNITY_POST_VIEWS_MAP') || {};
if (typeof viewsMap !== 'object' || viewsMap === null) viewsMap = {};
posts.value = (list || []).map(item => {
const strId = String(item.id);
const serverStatus = item.myApplyStatus || null;
const isApplied = serverStatus ? (serverStatus !== 'REJECTED') : localAppliedIds.includes(strId);
const mergedViews = Math.max(Number(item.views) || 1, Number(viewsMap[strId]) || 0);
return {
...item,
views: mergedViews,
myApplyStatus: serverStatus || (isApplied ? 'PENDING' : null),
liked: item.liked !== undefined ? item.liked : localLikedIds.includes(strId),
hasApplied: isApplied,
actionText: serverStatus === 'ACCEPTED' ? '已同意上车' : (isApplied ? '待房主确认' : (item.actionText || '加入车队'))
};
});
}
} catch (err) {
console.warn('[Community] 获取动态列表失败,保持本地列表:', err.message);
}
};
const loadPlatformCommunityConfigs = async () => {
try {
const configs = await getPlatformConfigs();
if (configs && typeof configs === 'object') {
if (configs.community_notice && configs.community_notice.title) {
communityNotice.value = configs.community_notice;
}
if (Array.isArray(configs.community_topics) && configs.community_topics.length > 0) {
availableTopics.value = configs.community_topics.map((t) => t.name || t);
}
}
} catch (e) {
console.warn('[Community] 获取平台话题公告异常降级:', e);
}
};
const feedTabs = ref([
{ key: 'all', name: '全部推荐', icon: ICONS.gameAll, hot: false },
{ key: 'partner', name: '同城搭子', icon: ICONS.teamUp, hot: true },
{ key: 'game', name: '车队开黑', icon: ICONS.gameCrosshair, hot: false },
{ key: 'highlight', name: '战绩高光', icon: ICONS.gameTrophy, hot: false }
]);
const currentTab = ref('all');
const syncInitialPostStates = (rawPosts) => {
try {
let localLikedIds = uni.getStorageSync('COMMUNITY_LIKED_POST_IDS') || [];
if (!Array.isArray(localLikedIds)) localLikedIds = [];
let localAppliedIds = uni.getStorageSync('COMMUNITY_APPLIED_POST_IDS') || [];
if (!Array.isArray(localAppliedIds)) localAppliedIds = [];
let viewsMap = uni.getStorageSync('COMMUNITY_POST_VIEWS_MAP') || {};
if (typeof viewsMap !== 'object' || viewsMap === null) viewsMap = {};
return rawPosts.map(p => {
const strId = String(p.id);
const isApplied = localAppliedIds.includes(strId);
const mergedViews = Math.max(Number(p.views) || 1, Number(viewsMap[strId]) || 0);
return {
...p,
views: mergedViews,
liked: localLikedIds.includes(strId) ? true : p.liked,
hasApplied: isApplied,
actionText: isApplied ? '已申请' : p.actionText
};
});
} catch (_) {
return rawPosts;
}
};
const syncInitialLikedState = syncInitialPostStates;
const posts = ref([]);
const filteredPosts = computed(() => {
if (currentTab.value === 'all') return posts.value;
return posts.value.filter(p => p.category === currentTab.value);
});
const switchFeedTab = (key) => {
currentTab.value = key;
};
const toggleLike = async (post) => {
if (!ensureLoginOrRedirect('/pages/community/index')) {
return;
}
const willLike = !post.liked;
post.liked = willLike;
post.likes += willLike ? 1 : -1;
// 1. 同步更新本地存储的点赞记录 (保障切页、断网或刷新时不丢失状态)
let localLikedIds = uni.getStorageSync('COMMUNITY_LIKED_POST_IDS') || [];
if (!Array.isArray(localLikedIds)) localLikedIds = [];
const strId = String(post.id);
if (willLike) {
if (!localLikedIds.includes(strId)) localLikedIds.push(strId);
} else {
localLikedIds = localLikedIds.filter(id => id !== strId);
}
uni.setStorageSync('COMMUNITY_LIKED_POST_IDS', localLikedIds);
uni.showToast({
title: willLike ? '已点赞' : '已取消点赞',
icon: 'none',
duration: 1000
});
if (post.id) {
try {
const res = await likePost(post.id, willLike ? 'like' : 'unlike');
if (res && res.likes !== undefined) {
post.likes = res.likes;
}
if (res && res.liked !== undefined) {
post.liked = res.liked;
if (res.liked && !localLikedIds.includes(strId)) {
localLikedIds.push(strId);
uni.setStorageSync('COMMUNITY_LIKED_POST_IDS', localLikedIds);
} else if (!res.liked && localLikedIds.includes(strId)) {
localLikedIds = localLikedIds.filter(id => id !== strId);
uni.setStorageSync('COMMUNITY_LIKED_POST_IDS', localLikedIds);
}
}
} catch (err) {
// 回滚乐观更新,避免"已点赞"却未落盘
console.warn('[Community] 点赞落盘异常:', err.message);
post.liked = !willLike;
post.likes += willLike ? -1 : 1;
let revertIds = uni.getStorageSync('COMMUNITY_LIKED_POST_IDS') || [];
if (!Array.isArray(revertIds)) revertIds = [];
if (willLike) {
revertIds = revertIds.filter(id => id !== strId);
} else if (!revertIds.includes(strId)) {
revertIds.push(strId);
}
uni.setStorageSync('COMMUNITY_LIKED_POST_IDS', revertIds);
uni.showToast({ title: (err && err.message) || '操作失败,请重试', icon: 'none' });
}
}
};
// 评论区抽屉与动态详情跳转
const commentDrawerVisible = ref(false);
const activeCommentPost = ref(null);
const commentsList = ref([]);
const commentInput = ref('');
const loadingComments = ref(false);
const sendingComment = ref(false);
const loadPostCommentsList = async (post) => {
if (!post) return;
loadingComments.value = true;
try {
const list = await getPostComments(post.id);
commentsList.value = Array.isArray(list) ? list : [];
} catch (_) {
commentsList.value = [];
} finally {
loadingComments.value = false;
}
};
const openAuthorProfile = (target) => {
if (!target) return;
const authorName = target.author || target.nickname || '同城达人';
let targetId = target.expertId || target.companionId || target.userId;
if (!targetId) {
uni.showToast({ title: '该用户信息不完整', icon: 'none' });
return;
}
const authorSnapshot = {
id: targetId,
userId: target.userId || targetId,
nickname: authorName,
avatar: target.avatar || DEFAULT_USER_AVATAR,
rankTag: target.tag || '同城认证达人',
levelTag: '实名认证',
game: target.category === 'game' ? '车队开黑' : (target.category === 'highlight' ? '战绩高光' : '同城搭子'),
city: target.location || '',
bio: target.content || '',
photos: Array.isArray(target.images) && target.images.length > 0 ? target.images : [target.avatar || DEFAULT_USER_AVATAR]
};
uni.setStorageSync('ACTIVE_AUTHOR_PROFILE', authorSnapshot);
const qName = encodeURIComponent(authorSnapshot.nickname);
const qAvatar = encodeURIComponent(authorSnapshot.avatar);
const qTag = encodeURIComponent(authorSnapshot.rankTag);
const qCity = encodeURIComponent(authorSnapshot.city);
uni.navigateTo({
url: `/pages/expert/detail?id=${encodeURIComponent(String(targetId))}&name=${qName}&avatar=${qAvatar}&tag=${qTag}&city=${qCity}`
});
};
const openPostDetail = (post) => {
if (!post) return;
const strId = String(post.id);
const nextViews = (Number(post.views) || 0) + 1;
post.views = nextViews;
let viewsMap = uni.getStorageSync('COMMUNITY_POST_VIEWS_MAP') || {};
if (typeof viewsMap !== 'object' || viewsMap === null) viewsMap = {};
viewsMap[strId] = Math.max(Number(viewsMap[strId] || 0), nextViews);
uni.setStorageSync('COMMUNITY_POST_VIEWS_MAP', viewsMap);
uni.setStorageSync('ACTIVE_COMMUNITY_POST', { ...post, views: viewsMap[strId] });
viewPost(post.id).then((res) => {
if (res && res.views !== undefined) {
post.views = Math.max(Number(post.views) || 0, Number(res.views));
}
}).catch(() => {});
uni.navigateTo({
url: `/pages/community/detail?id=${post.id}`
});
};
const handleComment = async (post) => {
if (!ensureLoginOrRedirect('/pages/community/index')) {
return;
}
activeCommentPost.value = post;
commentDrawerVisible.value = true;
commentInput.value = '';
await loadPostCommentsList(post);
};
const closeCommentDrawer = () => {
commentDrawerVisible.value = false;
activeCommentPost.value = null;
commentsList.value = [];
commentInput.value = '';
};
const sendComment = async () => {
if (!ensureLoginOrRedirect('/pages/community/index')) {
return;
}
if (!commentInput.value.trim()) {
uni.showToast({ title: '请输入评论内容', icon: 'none' });
return;
}
if (!activeCommentPost.value) return;
sendingComment.value = true;
const content = commentInput.value.trim();
const profile = currentUserProfile.value.nickname
? currentUserProfile.value
: await syncCurrentUserProfile();
const uid = normalizeUserId();
const authorName = profile.nickname || '同城用户';
const authorAvatar = profile.avatar || DEFAULT_USER_AVATAR;
try {
const res = await addPostComment(activeCommentPost.value.id, {
userId: uid,
author: authorName,
avatar: authorAvatar,
content
});
commentsList.value.push({
id: res ? res.id : `cmt_${Date.now()}`,
userId: uid,
author: (res && res.author) || authorName,
avatar: (res && res.avatar) || authorAvatar,
content,
createdAt: '刚刚'
});
if (activeCommentPost.value.comments !== undefined) {
activeCommentPost.value.comments += 1;
}
commentInput.value = '';
uni.showToast({ title: '评论发表成功', icon: 'success' });
} catch (err) {
uni.showToast({ title: err.message || '发表失败,请重试', icon: 'none' });
} finally {
sendingComment.value = false;
}
};
const isPostOwner = (post) => {
const cur = currentUserProfile.value.nickname ? currentUserProfile.value : readCachedUserInfo();
return post && (
Number(post.userId) === Number(cur.id || 0) ||
post.author === '电竞玩家 (我)' ||
(cur.nickname && post.author === cur.nickname) ||
post.isMine === true
);
};
const handleDeletePost = (post) => {
if (!ensureLoginOrRedirect('/pages/community/index')) {
return;
}
uni.showModal({
title: '删除动态确认',
content: '确定要删除这条动态吗?删除后将无法恢复。',
confirmText: '确定删除',
cancelText: '取消',
confirmColor: '#EF4444',
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({ title: '正在删除...' });
await deletePost(post.id);
posts.value = posts.value.filter(p => p.id !== post.id);
uni.hideLoading();
uni.showToast({ title: '动态已删除', icon: 'success' });
} catch (err) {
uni.hideLoading();
uni.showToast({ title: (err && err.message) || '删除失败,请重试', icon: 'none' });
}
}
}
});
};
const handleManageApplies = async (post) => {
if (!ensureLoginOrRedirect('/pages/community/index')) {
return;
}
uni.showLoading({ title: '加载申请列表...' });
try {
const list = await getPostApplies(post.id);
uni.hideLoading();
if (!list || list.length === 0) {
uni.showToast({ title: '暂无搭子上车申请', icon: 'none' });
return;
}
const itemList = list.slice(0, 6).map(a => {
const name = a.applicant_name || a.applicantName || '同城搭子';
const st = a.status === 'ACCEPTED' ? '已同意' : (a.status === 'REJECTED' ? '已婉拒' : '待确认');
return `${name} [${st}] - 点击审批`;
});
uni.showActionSheet({
itemList,
success: (res) => {
const target = list[res.tapIndex];
if (!target) return;
const name = target.applicant_name || target.applicantName || '同城搭子';
uni.showModal({
title: `审批搭子申请`,
content: `申请人:${name}\n请选择是否同意该搭子加入车队/同城局:`,
confirmText: '同意上车',
cancelText: '婉拒申请',
confirmColor: '#10B981',
success: async (mRes) => {
const newStatus = mRes.confirm ? 'ACCEPTED' : (mRes.cancel ? 'REJECTED' : null);
if (!newStatus) return;
try {
await handlePostApply(target.id, newStatus);
uni.showToast({
title: newStatus === 'ACCEPTED' ? '已同意搭子上车' : '已婉拒该申请',
icon: 'success'
});
loadRemotePosts();
} catch (err) {
uni.showToast({ title: err.message || '操作失败', icon: 'none' });
}
}
});
}
});
} catch (err) {
uni.hideLoading();
uni.showToast({ title: '加载申请列表失败', icon: 'none' });
}
};
const handleJoinParty = (post) => {
if (!ensureLoginOrRedirect('/pages/community/index')) {
return;
}
if (post.hasApplied && post.myApplyStatus !== 'REJECTED') {
uni.showToast({
title: post.myApplyStatus === 'ACCEPTED' ? '房主已同意您的上车申请' : '已提交过申请,请等待房主确认',
icon: 'none'
});
return;
}
uni.showModal({
title: '搭子互动申请',
content: `确定向【${post.author}】发起同城约伴 / 上车请求?`,
confirmText: '立即发起',
confirmColor: '#6366F1',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '正在提交申请...' });
const profile = currentUserProfile.value.nickname
? currentUserProfile.value
: await syncCurrentUserProfile();
const uid = normalizeUserId();
const applicantName = profile.nickname || '同城用户';
const applicantAvatar = profile.avatar || DEFAULT_USER_AVATAR;
try {
await applyPostParty(post.id, {
applicantId: uid,
applicantName,
applicantAvatar
});
} catch (err) {
uni.hideLoading();
uni.showToast({ title: (err && err.message) || '申请失败,请重试', icon: 'none' });
return;
}
post.hasApplied = true;
post.myApplyStatus = 'PENDING';
post.actionText = '待房主确认';
let localAppliedIds = uni.getStorageSync('COMMUNITY_APPLIED_POST_IDS') || [];
if (!Array.isArray(localAppliedIds)) localAppliedIds = [];
const strId = String(post.id);
if (!localAppliedIds.includes(strId)) {
localAppliedIds.push(strId);
uni.setStorageSync('COMMUNITY_APPLIED_POST_IDS', localAppliedIds);
}
uni.hideLoading();
uni.showToast({
title: '申请已送达,等待房主确认!',
icon: 'success'
});
}
}
});
};
const previewImage = (imgs, index) => {
uni.previewImage({
urls: imgs,
current: index
});
};
// 发动态功能模块
const showPublishModal = ref(false);
const publishCategories = [
{ key: 'partner', name: '同城搭子' },
{ key: 'game', name: '车队开黑' },
{ key: 'highlight', name: '生活随笔' }
];
const newPostForm = ref({
category: 'partner',
content: '',
selectedTopics: [],
images: [],
location: ''
});
const contentPlaceholder = computed(() => {
if (newPostForm.value.category === 'partner') {
return '分享你的同城聚会计划、想去吃的美食或找搭子需求(如:今晚三里屯涮肉AA,差2人)...';
}
if (newPostForm.value.category === 'game') {
return '发布你的开黑车队信息、段位要求或开黑时间(如:王者荣耀上分车队,缺软辅妹子/中单)...';
}
return '记录你今天的高光时刻、生活随笔或心情打卡...';
});
const isTopicSelected = (t) => {
return newPostForm.value.selectedTopics.includes(t);
};
const toggleTopic = (t) => {
const idx = newPostForm.value.selectedTopics.indexOf(t);
if (idx > -1) {
newPostForm.value.selectedTopics.splice(idx, 1);
} else {
if (newPostForm.value.selectedTopics.length >= 3) {
uni.showToast({ title: '最多添加3个话题标签', icon: 'none' });
return;
}
newPostForm.value.selectedTopics.push(t);
}
};
const selectPublishCategory = (key) => {
newPostForm.value.category = key;
};
const choosePostImage = () => {
const remainCount = 9 - newPostForm.value.images.length;
if (remainCount <= 0) return;
uni.chooseImage({
count: remainCount,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
newPostForm.value.images = newPostForm.value.images.concat(res.tempFilePaths);
}
});
};
const removeImage = (idx) => {
newPostForm.value.images.splice(idx, 1);
};
const openPublishModal = (catKey = 'partner') => {
if (!ensureLoginOrRedirect('/pages/community/index')) {
return;
}
newPostForm.value.category = catKey;
showPublishModal.value = true;
};
const closePublishModal = () => {
showPublishModal.value = false;
};
const stopPropagation = (e) => {
if (e && typeof e.stopPropagation === 'function') {
e.stopPropagation();
}
};
const handlePublishPost = () => {
if (!ensureLoginOrRedirect('/pages/community/index')) {
return;
}
uni.showActionSheet({
itemList: ['发同城美食搭子', '发开黑车队招募', '发生活高光随笔'],
success: (res) => {
const catMap = ['partner', 'game', 'highlight'];
openPublishModal(catMap[res.tapIndex] || 'partner');
},
fail: () => {
// 若用户点击灰色蒙层取消 ActionSheet,直接拉起默认搭子发布器
openPublishModal('partner');
}
});
};
const submitPublishPost = async () => {
const content = newPostForm.value.content.trim();
if (!content) {
uni.showToast({
title: '请输入动态正文内容',
icon: 'none'
});
return;
}
const tagMap = {
partner: { text: '同城搭子', type: 'warm-tag', action: '加入车队' },
game: { text: '国服大神', type: 'cool-tag', action: '立即上车' },
highlight: { text: '生活达人', type: 'gold-tag', action: '聊一聊' }
};
const currentMeta = tagMap[newPostForm.value.category] || tagMap.partner;
uni.showLoading({ title: '正在上传与发布...' });
const profile = currentUserProfile.value.nickname
? currentUserProfile.value
: await syncCurrentUserProfile();
const uid = normalizeUserId();
const authorName = profile.nickname || '同城用户';
const authorAvatar = profile.avatar || '';
let finalImages = [];
try {
if (newPostForm.value.images.length > 0) {
finalImages = await Promise.all(
newPostForm.value.images.map(img => uploadImage(img))
);
}
} catch (uploadErr) {
console.warn('[Community] 图片上传失败:', uploadErr);
uni.hideLoading();
uni.showToast({ title: '图片上传失败,请重试后再发布', icon: 'none' });
return;
}
const postPayload = {
userId: uid,
author: authorName,
avatar: authorAvatar,
tag: currentMeta.text,
tagType: currentMeta.type,
location: newPostForm.value.location,
content: content,
topics: [...newPostForm.value.selectedTopics],
images: finalImages,
actionText: currentMeta.action,
category: newPostForm.value.category
};
try {
const res = await createPost(postPayload);
if (!res || !res.id) {
throw new Error('发布失败:服务端未返回动态编号');
}
const createdPost = {
id: res.id,
userId: uid,
author: (res && res.author) || postPayload.author,
avatar: (res && res.avatar) || postPayload.avatar,
tag: currentMeta.text,
tagType: currentMeta.type,
location: postPayload.location,
distance: '',
time: '刚刚',
content: content,
topics: postPayload.topics,
images: finalImages,
actionText: currentMeta.action,
likes: 0,
liked: false,
comments: 0,
views: 1,
category: postPayload.category
};
// 即时将新发布的动态插入列表顶端
posts.value.unshift(createdPost);
// 若当前 Tab 过滤了该分类,自动切换为全部或当前分类,确保用户即刻看到
if (currentTab.value !== 'all' && currentTab.value !== newPostForm.value.category) {
currentTab.value = 'all';
}
// 关闭弹窗并重置
closePublishModal();
newPostForm.value.content = '';
newPostForm.value.images = [];
newPostForm.value.selectedTopics = ['三里屯探店'];
uni.hideLoading();
uni.showToast({
title: '动态发布成功!',
icon: 'success',
duration: 1800
});
// 平滑滚动回顶部
uni.pageScrollTo({
scrollTop: 0,
duration: 300
});
} catch (err) {
uni.hideLoading();
uni.showToast({
title: err.message || '发布失败,请重试',
icon: 'none',
duration: 2500
});
}
};
</script>
<style lang="scss">
.community-container {
min-height: 100vh;
background-color: #0A0F1D;
color: #F8FAFC;
padding: 0 28rpx 24rpx;
box-sizing: border-box;
.community-header {
display: flex;
flex-direction: column;
padding-top: var(--safe-header-gap, 50rpx);
margin-bottom: 20rpx;
.header-title-row {
display: flex;
align-items: center;
height: var(--capsule-height, 64rpx);
/* #ifdef MP-WEIXIN */
padding-right: calc(var(--capsule-right-space, 210rpx));
/* #endif */
.page-title {
font-size: 40rpx;
font-weight: 800;
color: #FFFFFF;
letter-spacing: 1rpx;
line-height: 1;
}
}
.header-sub-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10rpx;
min-height: 56rpx;
.page-sub {
font-size: 22rpx;
color: #94A3B8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
margin-right: 16rpx;
}
.publish-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 0 28rpx;
height: 58rpx;
border-radius: 29rpx;
box-shadow: 0 6rpx 18rpx rgba(99, 102, 241, 0.36);
flex-shrink: 0;
box-sizing: border-box;
.publish-icon {
width: 28rpx;
height: 28rpx;
flex-shrink: 0;
display: block;
}
.publish-text {
font-size: 25rpx;
font-weight: 700;
color: #FFFFFF;
line-height: 1;
letter-spacing: 1rpx;
white-space: nowrap;
}
}
}
}
.community-notice-bar {
display: flex;
align-items: center;
background: rgba(99, 102, 241, 0.12);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 14rpx;
padding: 12rpx 16rpx 12rpx 20rpx;
margin-bottom: 20rpx;
gap: 14rpx;
cursor: pointer;
box-sizing: border-box;
.notice-badge {
font-size: 20rpx;
font-weight: 700;
color: #FFFFFF;
background: linear-gradient(135deg, #F43F5E, #E11D48);
padding: 4rpx 14rpx;
border-radius: 6rpx;
flex-shrink: 0;
letter-spacing: 0.5rpx;
}
.notice-marquee-wrap {
flex: 1;
overflow: hidden;
white-space: nowrap;
position: relative;
height: 36rpx;
line-height: 36rpx;
.notice-marquee-content {
display: inline-flex;
align-items: center;
white-space: nowrap;
animation: marqueeScroll 24s linear infinite;
.notice-title-text {
font-size: 24rpx;
font-weight: 600;
color: #F1F5F9;
padding-right: 48rpx;
}
.marquee-clone {
font-size: 24rpx;
font-weight: 600;
color: #F1F5F9;
padding-right: 48rpx;
}
}
}
.notice-action-hint {
display: flex;
align-items: center;
gap: 2rpx;
flex-shrink: 0;
padding-left: 6rpx;
.hint-text {
font-size: 20rpx;
color: #818CF8;
font-weight: 600;
}
.hint-arrow {
width: 22rpx;
height: 22rpx;
opacity: 0.8;
}
}
}
@keyframes marqueeScroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.notice-modal-mask {
position: fixed;
inset: 0;
z-index: 999;
background: rgba(0, 0, 0, 0.68);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
.notice-modal-content {
width: 100%;
max-height: 82vh;
background: #151D30;
border: 1px solid rgba(99, 102, 241, 0.35);
border-radius: 28rpx;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.45);
display: flex;
flex-direction: column;
overflow: hidden;
.modal-header {
padding: 32rpx 32rpx 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
.modal-header-left {
display: flex;
align-items: center;
gap: 14rpx;
flex: 1;
overflow: hidden;
.modal-badge {
font-size: 20rpx;
font-weight: 700;
color: #FFFFFF;
background: linear-gradient(135deg, #F43F5E, #E11D48);
padding: 4rpx 14rpx;
border-radius: 6rpx;
flex-shrink: 0;
}
.modal-title {
font-size: 30rpx;
font-weight: 700;
color: #FFFFFF;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.modal-close {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.06);
border-radius: 28rpx;
flex-shrink: 0;
.close-icon {
width: 28rpx;
height: 28rpx;
}
}
}
.notice-modal-body {
max-height: 58vh;
padding: 24rpx 32rpx;
box-sizing: border-box;
.notice-lead-card {
display: flex;
align-items: flex-start;
gap: 16rpx;
background: rgba(99, 102, 241, 0.15);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 24rpx;
.lead-icon {
width: 36rpx;
height: 36rpx;
flex-shrink: 0;
margin-top: 4rpx;
}
.lead-text {
font-size: 24rpx;
line-height: 1.6;
color: #E2E8F0;
font-weight: 500;
}
}
.notice-rules-section {
.rules-header {
font-size: 26rpx;
font-weight: 700;
color: #94A3B8;
margin-bottom: 16rpx;
letter-spacing: 0.5rpx;
}
.rule-item {
display: flex;
align-items: flex-start;
gap: 16rpx;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
.rule-index {
width: 44rpx;
height: 44rpx;
border-radius: 12rpx;
background: rgba(99, 102, 241, 0.2);
color: #818CF8;
font-size: 22rpx;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.rule-body {
flex: 1;
.rule-title {
display: block;
font-size: 26rpx;
font-weight: 700;
color: #FFFFFF;
margin-bottom: 6rpx;
}
.rule-desc {
display: block;
font-size: 22rpx;
line-height: 1.5;
color: #94A3B8;
}
}
}
}
}
.notice-modal-footer {
padding: 20rpx 32rpx 32rpx;
border-top: 1px solid rgba(255, 255, 255, 0.08);
.modal-confirm-btn {
width: 100%;
height: 84rpx;
line-height: 84rpx;
border-radius: 42rpx;
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
color: #FFFFFF;
font-size: 28rpx;
font-weight: 700;
box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.35);
margin: 0;
}
}
}
}
.feed-tabs-wrap {
margin-bottom: 24rpx;
width: 100%;
.feed-scroll {
white-space: nowrap;
width: 100%;
box-sizing: border-box;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
.tabs-flex {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 16rpx;
padding: 16rpx 10rpx 10rpx;
.tab-pill {
display: inline-flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
white-space: nowrap;
position: relative;
padding: 14rpx 28rpx;
background: #151D30;
border-radius: 32rpx;
border: 1px solid rgba(255, 255, 255, 0.06);
transition: all 0.15s ease-out;
.tab-icon-svg {
width: 28rpx;
height: 28rpx;
margin-right: 10rpx;
flex-shrink: 0;
}
.pill-text {
font-size: 24rpx;
color: #94A3B8;
font-weight: 500;
white-space: nowrap;
}
.tab-badge {
position: absolute;
top: -10rpx;
right: -8rpx;
height: 26rpx;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #FF3B30 0%, #FF2D55 100%);
border: 2rpx solid #0A0F1D;
padding: 0 10rpx;
border-radius: 14rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 45, 85, 0.5);
z-index: 3;
pointer-events: none;
.badge-text {
color: #FFFFFF;
font-size: 16rpx;
font-weight: 800;
font-style: italic;
line-height: 1;
letter-spacing: 1rpx;
}
}
&.active {
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
border-color: #818CF8;
box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.45);
.pill-text {
color: #FFFFFF;
font-weight: 600;
}
}
}
}
}
.posts-list {
display: flex;
flex-direction: column;
gap: 24rpx;
.posts-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10rpx;
padding: 80rpx 24rpx;
border-radius: 24rpx;
border: 1rpx dashed rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.03);
.posts-empty-title {
font-size: 28rpx;
font-weight: 600;
color: #E2E8F0;
}
.posts-empty-desc {
font-size: 24rpx;
color: #94A3B8;
}
}
.post-card {
background: #151D30;
border-radius: 24rpx;
padding: 24rpx;
border: 1rpx solid rgba(255, 255, 255, 0.06);
.author-row {
display: flex;
align-items: center;
margin-bottom: 18rpx;
.author-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: 2rpx solid #334155;
margin-right: 16rpx;
}
.author-meta {
flex: 1;
.name-line {
display: flex;
align-items: center;
gap: 12rpx;
.author-name {
font-size: 28rpx;
font-weight: 700;
color: #F8FAFC;
}
.author-tag {
font-size: 18rpx;
padding: 2rpx 10rpx;
border-radius: 8rpx;
font-weight: 600;
&.warm-tag {
background: rgba(245, 158, 11, 0.15);
color: #F59E0B;
border: 1rpx solid rgba(245, 158, 11, 0.3);
}
&.cool-tag {
background: rgba(99, 102, 241, 0.15);
color: #818CF8;
border: 1rpx solid rgba(99, 102, 241, 0.3);
}
&.gold-tag {
background: rgba(236, 72, 153, 0.15);
color: #F472B6;
border: 1rpx solid rgba(236, 72, 153, 0.3);
}
}
}
.sub-line {
display: flex;
align-items: center;
gap: 8rpx;
margin-top: 4rpx;
.post-time, .dot-sep {
font-size: 20rpx;
color: #64748B;
}
.distance-box {
display: flex;
align-items: center;
gap: 4rpx;
.dist-icon {
width: 20rpx;
height: 20rpx;
}
.dist-text {
font-size: 20rpx;
color: #38BDF8;
}
}
}
}
.author-actions {
display: flex;
align-items: center;
gap: 12rpx;
.btn-delete-post {
background: rgba(239, 68, 68, 0.12);
color: #EF4444;
border: 1rpx solid rgba(239, 68, 68, 0.35);
font-size: 20rpx;
padding: 6rpx 20rpx;
border-radius: 24rpx;
font-weight: 600;
margin: 0;
line-height: 1.5;
}
.join-btn {
background: rgba(99, 102, 241, 0.15);
color: #818CF8;
border: 1rpx solid rgba(99, 102, 241, 0.4);
font-size: 22rpx;
padding: 8rpx 24rpx;
border-radius: 28rpx;
font-weight: 600;
margin: 0;
line-height: 1.5;
&.btn-applied {
background: rgba(148, 163, 184, 0.15);
color: #94A3B8;
border-color: rgba(148, 163, 184, 0.3);
opacity: 0.8;
}
}
}
}
.post-content {
font-size: 26rpx;
line-height: 1.6;
color: #E2E8F0;
display: block;
margin-bottom: 14rpx;
}
.post-topics {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-bottom: 16rpx;
.topic-item {
font-size: 22rpx;
color: #818CF8;
background: rgba(99, 102, 241, 0.1);
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
}
.post-images {
display: flex;
gap: 12rpx;
margin-bottom: 18rpx;
.post-img {
width: 200rpx;
height: 200rpx;
border-radius: 16rpx;
}
}
.post-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 16rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.05);
.footer-left {
display: flex;
align-items: center;
gap: 32rpx;
.action-item {
display: flex;
align-items: center;
gap: 8rpx;
.action-icon {
width: 28rpx;
height: 28rpx;
}
.action-count {
font-size: 22rpx;
color: #94A3B8;
&.liked {
color: #EF4444;
font-weight: 700;
}
}
}
}
.view-tag {
font-size: 20rpx;
color: #64748B;
}
}
}
}
/* 发动态弹窗编辑器样式 (现代高级 Bottom Sheet 社交画廊风格) */
.publish-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.72);
backdrop-filter: blur(10px);
z-index: 99999;
display: flex;
align-items: flex-end;
justify-content: center;
.publish-modal-box {
width: 100%;
max-width: 750rpx;
max-height: 86vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
background: #151D30;
border-top-left-radius: 40rpx;
border-top-right-radius: 40rpx;
padding: 24rpx 32rpx calc(36rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
border-top: 1rpx solid rgba(255, 255, 255, 0.12);
box-shadow: 0 -16rpx 48rpx rgba(0, 0, 0, 0.55);
animation: modalSlideUp 0.28s cubic-bezier(0.16, 1, 0.3, 1);
/* 居中下拉把手 */
.sheet-drag-handle {
width: 72rpx;
height: 8rpx;
border-radius: 4rpx;
background: rgba(255, 255, 255, 0.22);
margin: 0 auto 28rpx;
}
/* 弹窗顶栏 */
.modal-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 28rpx;
.header-titles {
display: flex;
flex-direction: column;
gap: 6rpx;
.modal-title {
font-size: 34rpx;
font-weight: 800;
color: #F8FAFC;
letter-spacing: 0.5rpx;
}
.modal-sub {
font-size: 22rpx;
color: #94A3B8;
font-weight: 400;
}
}
.close-btn {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
.close-icon {
width: 24rpx;
height: 24rpx;
}
}
}
/* 分类分段控制器 */
.segment-tabs-container {
display: flex;
background: rgba(255, 255, 255, 0.05);
padding: 6rpx;
border-radius: 20rpx;
margin-bottom: 24rpx;
border: 1rpx solid rgba(255, 255, 255, 0.05);
.segment-item {
flex: 1;
text-align: center;
padding: 14rpx 0;
border-radius: 16rpx;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
.segment-text {
font-size: 24rpx;
font-weight: 500;
color: #94A3B8;
}
&.active {
background: linear-gradient(135deg, #6366F1, #8B5CF6);
box-shadow: 0 4rpx 14rpx rgba(99, 102, 241, 0.35);
.segment-text {
color: #FFFFFF;
font-weight: 700;
}
}
}
}
/* 沉浸式文本输入卡片 */
.textarea-card {
background: #0A0F1D;
border: 1rpx solid rgba(255, 255, 255, 0.08);
border-radius: 24rpx;
padding: 22rpx;
position: relative;
margin-bottom: 26rpx;
box-sizing: border-box;
.post-textarea {
width: 100%;
height: 200rpx;
font-size: 26rpx;
line-height: 1.6;
color: #F8FAFC;
}
.ph {
color: #64748B;
font-size: 25rpx;
}
.textarea-corner-meta {
display: flex;
justify-content: flex-end;
margin-top: 8rpx;
.char-count-badge {
font-size: 20rpx;
color: #64748B;
background: rgba(255, 255, 255, 0.05);
padding: 4rpx 14rpx;
border-radius: 12rpx;
font-variant-numeric: tabular-nums;
&.near-limit {
color: #F87171;
background: rgba(248, 113, 113, 0.12);
}
.current-len {
font-weight: 600;
}
.max-len {
opacity: 0.65;
}
}
}
}
/* 话题流式微胶囊区 */
.topics-section {
margin-bottom: 26rpx;
.section-title-row {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 14rpx;
.title-text {
font-size: 24rpx;
font-weight: 700;
color: #CBD5E1;
}
.title-tip {
font-size: 20rpx;
color: #64748B;
}
}
.topics-flex {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
.topic-chip {
display: inline-flex;
align-items: center;
gap: 4rpx;
padding: 10rpx 22rpx;
border-radius: 24rpx;
background: rgba(99, 102, 241, 0.08);
border: 1rpx solid rgba(99, 102, 241, 0.2);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
.hash-tag {
font-size: 22rpx;
font-weight: 700;
color: #818CF8;
opacity: 0.8;
}
.topic-name {
font-size: 22rpx;
font-weight: 500;
color: #C7D2FE;
}
&.selected {
background: #6366F1;
border-color: #6366F1;
box-shadow: 0 4rpx 14rpx rgba(99, 102, 241, 0.35);
.hash-tag {
color: #FFFFFF;
opacity: 1;
}
.topic-name {
color: #FFFFFF;
font-weight: 700;
}
}
}
}
}
/* 画廊九宫格图片区 */
.gallery-section {
margin-bottom: 26rpx;
.gallery-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
.grid-img-item {
position: relative;
width: 100%;
aspect-ratio: 1;
border-radius: 18rpx;
overflow: hidden;
background: #0A0F1D;
.thumb-img {
width: 100%;
height: 100%;
}
.remove-btn {
position: absolute;
top: 10rpx;
right: 10rpx;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(4px);
border: 2rpx solid rgba(255, 255, 255, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
.remove-icon {
width: 18rpx;
height: 18rpx;
}
}
}
.add-img-tile {
aspect-ratio: 1;
border-radius: 18rpx;
background: rgba(255, 255, 255, 0.04);
border: 2rpx dashed rgba(255, 255, 255, 0.16);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
transition: all 0.2s;
.tile-icon-circle {
width: 58rpx;
height: 58rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
display: flex;
align-items: center;
justify-content: center;
.cam-icon {
width: 30rpx;
height: 30rpx;
}
}
.tile-label {
font-size: 20rpx;
font-weight: 600;
color: #CBD5E1;
}
.tile-count {
font-size: 18rpx;
color: #64748B;
}
}
}
}
/* 定位信息微胶囊 */
.meta-row {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.location-pill {
display: inline-flex;
align-items: center;
gap: 8rpx;
background: rgba(56, 189, 248, 0.08);
border: 1rpx solid rgba(56, 189, 248, 0.2);
padding: 8rpx 20rpx;
border-radius: 30rpx;
.loc-pin {
width: 22rpx;
height: 22rpx;
}
.loc-name {
font-size: 22rpx;
font-weight: 500;
color: #38BDF8;
}
}
}
/* 底部操作按钮栏 */
.modal-footer-row {
display: flex;
gap: 20rpx;
align-items: center;
.action-btn-cancel {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
background: rgba(255, 255, 255, 0.08);
color: #94A3B8;
font-size: 28rpx;
font-weight: 600;
border: none;
text-align: center;
transition: all 0.2s;
}
.action-btn-submit {
flex: 2;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
background: linear-gradient(135deg, #6366F1, #8B5CF6);
color: #FFFFFF;
font-size: 28rpx;
font-weight: 700;
border: none;
text-align: center;
box-shadow: 0 6rpx 20rpx rgba(99, 102, 241, 0.4);
transition: all 0.2s;
&[disabled] {
opacity: 0.45;
box-shadow: none;
}
}
}
}
}
@keyframes modalSlideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
/* 浅色暖阳 (Warm Theme) 适配 */
&.theme-warm {
background-color: #FAF7F2;
color: #292524;
.community-header {
.page-title {
color: #292524;
}
.page-sub {
color: #78716C;
}
.publish-btn {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
border-color: rgba(255, 255, 255, 0.28);
box-shadow: 0 6rpx 18rpx rgba(234, 88, 12, 0.28);
}
}
.community-notice-bar {
background: #FFFFFF;
border: 1px solid #FED7AA;
box-shadow: 0 2rpx 10rpx rgba(234, 88, 12, 0.06);
.notice-badge {
background: linear-gradient(135deg, #EA580C, #F97316);
color: #FFFFFF;
font-weight: 700;
}
.notice-marquee-wrap {
.notice-marquee-content {
.notice-title-text, .marquee-clone {
color: #292524;
font-weight: 600;
}
}
}
.notice-action-hint {
.hint-text {
color: #EA580C;
}
}
}
.notice-modal-mask {
background: rgba(28, 25, 23, 0.6);
.notice-modal-content {
background: #FFFFFF;
border: 1px solid #F0ECE4;
box-shadow: 0 16rpx 48rpx rgba(234, 88, 12, 0.15);
.modal-header {
border-bottom-color: #F5F5F4;
.modal-header-left {
.modal-badge {
background: linear-gradient(135deg, #EA580C, #F97316);
}
.modal-title {
color: #1C1917;
}
}
.modal-close {
background: #F5F5F4;
}
}
.notice-modal-body {
.notice-lead-card {
background: #FFF7ED;
border-color: #FED7AA;
.lead-text {
color: #C2410C;
}
}
.notice-rules-section {
.rules-header {
color: #78716C;
}
.rule-item {
background: #FDFBF7;
border-color: #F0ECE4;
.rule-index {
background: #FFF7ED;
color: #EA580C;
}
.rule-body {
.rule-title {
color: #1C1917;
}
.rule-desc {
color: #78716C;
}
}
}
}
}
.notice-modal-footer {
border-top-color: #F5F5F4;
.modal-confirm-btn {
background: linear-gradient(135deg, #EA580C 0%, #C2410C 100%);
box-shadow: 0 4rpx 16rpx rgba(234, 88, 12, 0.35);
}
}
}
}
.feed-tabs-wrap {
.tabs-flex {
.tab-pill {
background: #FFFFFF;
border: 1px solid #F0ECE4;
.pill-text {
color: #57534E;
}
.tab-badge {
border: 2rpx solid #FFFFFF;
}
&.active {
background: linear-gradient(135deg, rgba(234, 88, 12, 0.12), rgba(249, 115, 22, 0.05));
border-color: #EA580C;
box-shadow: none;
.pill-text {
color: #EA580C;
font-weight: 700;
}
}
}
}
}
.posts-list {
.post-card {
background: #FFFFFF;
border: 1rpx solid #E7E5E4;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
.author-row {
.author-meta {
.name-line .author-name {
color: #292524;
}
}
.join-btn {
background: rgba(234, 88, 12, 0.1);
color: #EA580C;
border-color: rgba(234, 88, 12, 0.3);
&.btn-applied {
background: #F1F5F9;
color: #94A3B8;
border-color: #CBD5E1;
}
}
}
.post-content {
color: #44403C;
}
.post-topics .topic-item {
color: #EA580C;
background: rgba(234, 88, 12, 0.08);
}
.post-footer {
border-top-color: #F5F5F4;
}
}
}
.publish-modal-mask {
background: rgba(0, 0, 0, 0.45);
.publish-modal-box {
background: #FFFFFF;
border-top-color: #F0ECE4;
box-shadow: 0 -16rpx 48rpx rgba(28, 25, 23, 0.08);
.sheet-drag-handle {
background: #E7E5E4;
}
.modal-header {
.modal-title {
color: #1C1917;
}
.modal-sub {
color: #A8A29E;
}
.close-btn {
background: #F5F5F4;
}
}
.segment-tabs-container {
background: #F5F5F4;
border-color: #E7E5E4;
.segment-item {
.segment-text {
color: #78716C;
}
&.active {
background: linear-gradient(135deg, #EA580C, #F59E0B);
box-shadow: 0 4rpx 14rpx rgba(234, 88, 12, 0.25);
.segment-text {
color: #FFFFFF;
font-weight: 700;
}
}
}
}
.textarea-card {
background: #FAF7F2;
border-color: #EAE6DF;
.post-textarea {
color: #1C1917;
}
.ph {
color: #A8A29E;
}
.textarea-corner-meta .char-count-badge {
background: #F0ECE4;
color: #78716C;
&.near-limit {
color: #DC2626;
background: rgba(220, 38, 38, 0.1);
}
}
}
.topics-section {
.section-title-row {
.title-text {
color: #1C1917;
}
.title-tip {
color: #A8A29E;
}
}
.topics-flex .topic-chip {
background: #FAF7F2;
border-color: #E7E5E4;
.hash-tag {
color: #EA580C;
}
.topic-name {
color: #57534E;
}
&.selected {
background: #EA580C;
border-color: #EA580C;
box-shadow: 0 4rpx 14rpx rgba(234, 88, 12, 0.25);
.hash-tag,
.topic-name {
color: #FFFFFF;
}
}
}
}
.gallery-section {
.gallery-grid {
.grid-img-item {
background: #FAF7F2;
.remove-btn {
background: rgba(41, 37, 36, 0.85);
border-color: #FFFFFF;
}
}
.add-img-tile {
background: #FAF7F2;
border-color: #D6D3D1;
.tile-icon-circle {
background: #F0ECE4;
}
.tile-label {
color: #57534E;
}
.tile-count {
color: #A8A29E;
}
}
}
}
.meta-row .location-pill {
background: #FAF7F2;
border-color: #E7E5E4;
.loc-name {
color: #EA580C;
}
}
.modal-footer-row {
.action-btn-cancel {
background: #F5F5F4;
color: #78716C;
}
.action-btn-submit {
background: linear-gradient(135deg, #EA580C, #F59E0B);
box-shadow: 0 6rpx 20rpx rgba(234, 88, 12, 0.35);
}
}
}
}
}
}
.comment-drawer-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
z-index: 999;
display: flex;
align-items: flex-end;
justify-content: center;
}
.comment-drawer-content {
width: 100%;
max-width: 600px;
height: 65vh;
background: #151D30;
border-top-left-radius: 36rpx;
border-top-right-radius: 36rpx;
display: flex;
flex-direction: column;
border: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: none;
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.5);
animation: slideUp 0.25s ease-out;
.drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 36rpx 20rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
.drawer-header-left {
display: flex;
align-items: baseline;
gap: 12rpx;
.drawer-title {
font-size: 32rpx;
font-weight: 700;
color: #F8FAFC;
}
.drawer-badge {
font-size: 26rpx;
color: #94A3B8;
font-weight: 500;
}
}
.drawer-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
.close-icon {
width: 36rpx;
height: 36rpx;
}
}
}
.comments-scroll {
flex: 1;
overflow-y: auto;
padding: 24rpx 36rpx;
.comments-loading,
.comments-empty {
padding: 80rpx 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
.empty-icon {
width: 80rpx;
height: 80rpx;
opacity: 0.5;
}
.loading-text,
.empty-text {
font-size: 26rpx;
color: #94A3B8;
}
}
.comments-list {
display: flex;
flex-direction: column;
gap: 28rpx;
.comment-item {
display: flex;
gap: 20rpx;
.comment-avatar {
width: 68rpx;
height: 68rpx;
border-radius: 50%;
flex-shrink: 0;
}
.comment-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.comment-top {
display: flex;
justify-content: space-between;
align-items: center;
.comment-author {
font-size: 26rpx;
font-weight: 600;
color: #E2E8F0;
}
.comment-time {
font-size: 22rpx;
color: #64748B;
}
}
.comment-content {
font-size: 26rpx;
color: #CBD5E1;
line-height: 1.5;
word-break: break-all;
}
}
}
}
}
.comment-input-bar {
padding: 20rpx 36rpx calc(24rpx + env(safe-area-inset-bottom));
background: #0E1628;
border-top: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
align-items: center;
gap: 20rpx;
.comment-input {
flex: 1;
height: 72rpx;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 36rpx;
padding: 0 28rpx;
font-size: 26rpx;
color: #F8FAFC;
}
.comment-send-btn {
width: 120rpx;
height: 72rpx;
line-height: 72rpx;
font-size: 26rpx;
font-weight: 600;
border-radius: 36rpx;
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: #FFFFFF;
text-align: center;
padding: 0;
box-shadow: 0 4rpx 12rpx rgba(99, 102, 241, 0.35);
&:disabled {
opacity: 0.5;
}
}
}
}
.theme-warm {
.comment-drawer-content {
background: #FFFFFF;
border-color: #E7E5E4;
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.1);
.drawer-header {
border-bottom-color: #F5F2EC;
.drawer-header-left {
.drawer-title {
color: #1C1917;
}
.drawer-badge {
color: #78716C;
}
}
}
.comments-scroll {
.comments-list {
.comment-item {
.comment-body {
.comment-top {
.comment-author {
color: #1C1917;
}
.comment-time {
color: #A8A29E;
}
}
.comment-content {
color: #44403C;
}
}
}
}
}
.comment-input-bar {
background: #FDFBF7;
border-top-color: #E7E5E4;
.comment-input {
background: #F5F2EC;
border-color: #E7E5E4;
color: #1C1917;
}
.comment-send-btn {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
box-shadow: 0 4rpx 12rpx rgba(234, 88, 12, 0.35);
}
}
}
}
.post-detail-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.72);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
z-index: 1010;
display: flex;
align-items: flex-end;
justify-content: center;
}
.post-detail-modal-box {
width: 100%;
max-width: 600px;
height: 82vh;
background: #151D30;
border-top-left-radius: 40rpx;
border-top-right-radius: 40rpx;
display: flex;
flex-direction: column;
border: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: none;
box-shadow: 0 -12rpx 40rpx rgba(0, 0, 0, 0.55);
animation: slideUp 0.25s ease-out;
.sheet-drag-handle {
width: 72rpx;
height: 8rpx;
background: rgba(255, 255, 255, 0.18);
border-radius: 4rpx;
margin: 16rpx auto 8rpx;
flex-shrink: 0;
}
.detail-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 36rpx 24rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
flex-shrink: 0;
.detail-header-left {
display: flex;
align-items: center;
gap: 16rpx;
.detail-modal-title {
font-size: 32rpx;
font-weight: 700;
color: #F8FAFC;
}
.detail-category-badge {
font-size: 22rpx;
font-weight: 600;
color: #818CF8;
background: rgba(99, 102, 241, 0.16);
padding: 4rpx 16rpx;
border-radius: 20rpx;
}
}
.detail-close-btn {
width: 56rpx;
height: 56rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.06);
display: flex;
align-items: center;
justify-content: center;
.close-icon {
width: 32rpx;
height: 32rpx;
}
}
}
.detail-modal-scroll {
flex: 1;
overflow-y: auto;
padding: 28rpx 36rpx;
box-sizing: border-box;
.detail-author-card {
display: flex;
align-items: center;
margin-bottom: 28rpx;
.author-avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
margin-right: 20rpx;
border: 2rpx solid rgba(255, 255, 255, 0.15);
flex-shrink: 0;
}
.author-meta {
flex: 1;
min-width: 0;
.name-line {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
.author-name {
font-size: 30rpx;
font-weight: 700;
color: #F8FAFC;
}
.author-tag {
font-size: 20rpx;
padding: 2rpx 12rpx;
border-radius: 8rpx;
font-weight: 600;
&.warm-tag {
color: #F59E0B;
background: rgba(245, 158, 11, 0.15);
}
&.cool-tag {
color: #818CF8;
background: rgba(99, 102, 241, 0.15);
}
&.gold-tag {
color: #FBBF24;
background: rgba(251, 191, 36, 0.15);
}
}
}
.sub-line {
display: flex;
align-items: center;
font-size: 22rpx;
color: #94A3B8;
.dot-sep {
margin: 0 8rpx;
}
.distance-box {
display: flex;
align-items: center;
color: #38BDF8;
.dist-icon {
width: 22rpx;
height: 22rpx;
margin-right: 4rpx;
}
}
}
}
.author-actions {
margin-left: 16rpx;
flex-shrink: 0;
.join-btn {
height: 60rpx;
line-height: 58rpx;
padding: 0 28rpx;
border-radius: 30rpx;
font-size: 24rpx;
font-weight: 600;
color: #818CF8;
background: rgba(99, 102, 241, 0.15);
border: 1px solid rgba(99, 102, 241, 0.35);
margin: 0;
&.btn-applied {
color: #94A3B8;
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.12);
}
}
}
}
.detail-content-box {
margin-bottom: 24rpx;
.detail-content-text {
font-size: 30rpx;
line-height: 1.7;
color: #E2E8F0;
word-break: break-all;
}
}
.detail-topics-row {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-bottom: 28rpx;
.detail-topic-pill {
font-size: 24rpx;
font-weight: 500;
color: #818CF8;
background: rgba(99, 102, 241, 0.12);
padding: 6rpx 18rpx;
border-radius: 12rpx;
}
}
.detail-images-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
margin-bottom: 28rpx;
.detail-post-img {
width: 100%;
height: 280rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.04);
&.single-img {
grid-column: span 2;
height: 380rpx;
}
}
}
.detail-stats-strip {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
border-top: 1px solid rgba(255, 255, 255, 0.06);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
margin-bottom: 28rpx;
.stats-left {
display: flex;
align-items: center;
font-size: 24rpx;
color: #94A3B8;
.stats-dot {
margin: 0 12rpx;
}
}
.detail-like-pill {
display: flex;
align-items: center;
gap: 10rpx;
padding: 10rpx 24rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
.like-icon {
width: 30rpx;
height: 30rpx;
}
.like-txt {
font-size: 24rpx;
color: #CBD5E1;
font-weight: 600;
}
&.liked {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.35);
.like-txt {
color: #EF4444;
}
}
}
}
.detail-comments-section {
padding-bottom: 40rpx;
.comments-section-header {
display: flex;
align-items: baseline;
gap: 10rpx;
margin-bottom: 24rpx;
.section-title {
font-size: 28rpx;
font-weight: 700;
color: #F8FAFC;
}
.section-count {
font-size: 24rpx;
color: #94A3B8;
}
}
.comments-loading,
.comments-empty {
padding: 48rpx 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
.empty-icon {
width: 64rpx;
height: 64rpx;
opacity: 0.45;
}
.loading-text,
.empty-text {
font-size: 24rpx;
color: #94A3B8;
}
}
.comments-list {
display: flex;
flex-direction: column;
gap: 24rpx;
.comment-item {
display: flex;
gap: 18rpx;
.comment-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
flex-shrink: 0;
}
.comment-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
.comment-top {
display: flex;
justify-content: space-between;
align-items: center;
.comment-author {
font-size: 26rpx;
font-weight: 600;
color: #E2E8F0;
}
.comment-time {
font-size: 22rpx;
color: #64748B;
}
}
.comment-content {
font-size: 26rpx;
color: #CBD5E1;
line-height: 1.5;
word-break: break-all;
}
}
}
}
}
}
.detail-bottom-bar {
padding: 20rpx 36rpx calc(24rpx + env(safe-area-inset-bottom));
background: #0E1628;
border-top: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
align-items: center;
gap: 20rpx;
flex-shrink: 0;
.detail-comment-input {
flex: 1;
height: 72rpx;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 36rpx;
padding: 0 28rpx;
font-size: 26rpx;
color: #F8FAFC;
}
.detail-send-btn {
width: 120rpx;
height: 72rpx;
line-height: 72rpx;
font-size: 26rpx;
font-weight: 600;
border-radius: 36rpx;
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: #FFFFFF;
text-align: center;
padding: 0;
margin: 0;
box-shadow: 0 4rpx 12rpx rgba(99, 102, 241, 0.35);
&:disabled {
opacity: 0.5;
}
}
}
}
.theme-warm {
.post-detail-modal-box {
background: #FFFFFF;
border-color: #E7E5E4;
box-shadow: 0 -12rpx 40rpx rgba(28, 25, 23, 0.12);
.sheet-drag-handle {
background: #E7E5E4;
}
.detail-modal-header {
border-bottom-color: #F5F2EC;
.detail-header-left {
.detail-modal-title {
color: #1C1917;
}
.detail-category-badge {
color: #EA580C;
background: rgba(234, 88, 12, 0.1);
}
}
.detail-close-btn {
background: #F5F5F4;
}
}
.detail-modal-scroll {
.detail-author-card {
.author-meta .name-line .author-name {
color: #1C1917;
}
.author-actions .join-btn {
background: rgba(234, 88, 12, 0.1);
color: #EA580C;
border-color: rgba(234, 88, 12, 0.3);
&.btn-applied {
background: #F1F5F9;
color: #94A3B8;
border-color: #CBD5E1;
}
}
}
.detail-content-box .detail-content-text {
color: #292524;
}
.detail-topics-row .detail-topic-pill {
color: #EA580C;
background: rgba(234, 88, 12, 0.08);
}
.detail-stats-strip {
border-top-color: #F5F2EC;
border-bottom-color: #F5F2EC;
.detail-like-pill {
background: #F5F2EC;
border-color: #E7E5E4;
.like-txt {
color: #57534E;
}
&.liked {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
.like-txt {
color: #EF4444;
}
}
}
}
.detail-comments-section {
.comments-section-header .section-title {
color: #1C1917;
}
.comments-list .comment-item .comment-body {
.comment-top .comment-author {
color: #1C1917;
}
.comment-content {
color: #44403C;
}
}
}
}
.detail-bottom-bar {
background: #FDFBF7;
border-top-color: #E7E5E4;
.detail-comment-input {
background: #F5F2EC;
border-color: #E7E5E4;
color: #1C1917;
}
.detail-send-btn {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
box-shadow: 0 4rpx 12rpx rgba(234, 88, 12, 0.35);
}
}
}
}
/* ===== Premium visual layer: 友圈广场 ===== */
.community-container {
background:
radial-gradient(110% 40% at 50% -6%, rgba(99, 102, 241, 0.14), transparent 62%),
var(--surface-0);
.community-header {
background: rgba(7, 11, 20, 0.86);
backdrop-filter: blur(18px);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.04);
}
.page-title {
font-size: 42rpx;
font-weight: 800;
letter-spacing: 0.5rpx;
}
.page-sub {
color: #7C89A6;
letter-spacing: 0.3rpx;
}
.publish-btn {
background: linear-gradient(135deg, #818CF8 0%, #6366F1 60%, #7C3AED 100%);
border-radius: 999rpx;
box-shadow: 0 10rpx 24rpx rgba(99, 102, 241, 0.28);
}
.post-card {
background: linear-gradient(180deg, rgba(22, 31, 51, 0.96) 0%, rgba(13, 20, 36, 0.98) 100%);
border: 1rpx solid var(--hairline);
border-radius: 26rpx;
box-shadow: 0 12rpx 30rpx rgba(0, 0, 0, 0.2);
animation: communityCardEnter 0.5s var(--ease-out) both;
.author-avatar {
border: 2rpx solid rgba(255, 255, 255, 0.12);
}
.author-name {
font-size: 27rpx;
font-weight: 700;
}
.post-content {
font-size: 27rpx;
line-height: 1.65;
color: #D5DDEC;
}
.topic-item {
color: #A5B4FC;
background: rgba(99, 102, 241, 0.1);
border: 1rpx solid rgba(99, 102, 241, 0.18);
border-radius: 999rpx;
padding: 4rpx 12rpx;
}
.post-footer {
border-top-color: rgba(255, 255, 255, 0.045);
}
}
.posts-empty {
background: rgba(255, 255, 255, 0.02);
border: 1rpx dashed rgba(255, 255, 255, 0.09);
border-radius: 24rpx;
}
}
.community-container.theme-warm {
background:
radial-gradient(100% 38% at 50% -6%, rgba(234, 88, 12, 0.06), transparent 62%),
#FDFBF7;
.community-header {
background: rgba(253, 251, 247, 0.92);
border-bottom-color: #F0ECE4;
}
.page-title {
color: #1C1917;
}
.page-sub {
color: #A8A29E;
}
.post-card {
background: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 12rpx 28rpx rgba(28, 25, 23, 0.05);
.post-content {
color: #44403C;
}
.topic-item {
color: #C2410C;
background: #FFF7ED;
border-color: #FED7AA;
}
.post-footer {
border-top-color: #F5F2EC;
}
}
}
@keyframes communityCardEnter {
from {
opacity: 0;
transform: translateY(14rpx) scale(0.99);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
</style>