- ADMIN_TOKEN_SECRET 的随机化闸门由 NODE_ENV 改为 SECURE_MODE:线上以 development 运行时不再使用公开默认密钥(该密钥可伪造管理员/用户 token) - 运行模式与安全开关常量前移到模块顶部,避免鉴权常量初始化顺序问题 - 启动自检与回归用例同步(109 套件/720 用例全绿)
1261 行
36 KiB
Vue
1261 行
36 KiB
Vue
<template>
|
||
<view class="post-detail-page" :class="['theme-' + currentThemeKey]" :style="navStyle">
|
||
<!-- 顶部沉浸式自定义导航栏 -->
|
||
<view class="custom-nav-bar" :style="{ height: (navInfo.totalHeaderHeight || 88) + 'px' }">
|
||
<view class="nav-content" :style="{ height: (navInfo.capsuleHeight || 32) + 'px', top: (navInfo.capsuleTop || 48) + 'px' }">
|
||
<view class="btn-nav-back btn-active" @click="handleBack">
|
||
<image class="nav-back-icon" :src="ICONS.chevronLeft" mode="aspectFit" />
|
||
</view>
|
||
<view class="nav-title-wrap">
|
||
<text class="nav-title">动态详情</text>
|
||
</view>
|
||
<view class="nav-capsule-placeholder"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 顶部导航栏高度占位(避免内容被固定导航栏遮挡) -->
|
||
<view class="nav-bar-placeholder" :style="{ height: (navInfo.totalHeaderHeight || 88) + 'px' }"></view>
|
||
|
||
<!-- 主体内容区 -->
|
||
<view class="detail-scroll-view">
|
||
<view class="detail-main-wrap" v-if="post">
|
||
<!-- 动态主卡片 -->
|
||
<view class="detail-post-card">
|
||
<!-- 作者与组局操作行 -->
|
||
<view class="detail-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 || getPostCategoryName(post.category) }}</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 || '1.0km' }})</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.applyCount || 0 }})
|
||
</button>
|
||
<button
|
||
class="btn-delete-post btn-active"
|
||
@click.stop="handleDeletePost"
|
||
>
|
||
删除
|
||
</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.myApplyStatus === 'ACCEPTED' ? '已同意上车' : (post.myApplyStatus === 'REJECTED' ? '重新申请' : (post.hasApplied ? '待房主确认' : (post.actionText || '加入车队'))) }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 动态完整正文 -->
|
||
<view class="detail-content-box">
|
||
<text class="detail-content-text">{{ post.content }}</text>
|
||
</view>
|
||
|
||
<!-- 话题标签 -->
|
||
<view class="detail-topics-row" v-if="post.topics && post.topics.length">
|
||
<text v-for="t in post.topics" :key="t" class="detail-topic-pill">#{{ t }}</text>
|
||
</view>
|
||
|
||
<!-- 高清图片展示 -->
|
||
<view class="detail-images-grid" v-if="post.images && post.images.length">
|
||
<image
|
||
v-for="(img, idx) in post.images"
|
||
:key="idx"
|
||
class="detail-post-img"
|
||
:class="{ 'single-img': post.images.length === 1 }"
|
||
:src="img"
|
||
mode="aspectFill"
|
||
@click.stop="previewImage(post.images, idx)"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 浏览与点赞统计栏 -->
|
||
<view class="detail-stats-strip">
|
||
<view class="stats-left">
|
||
<text class="stats-item-txt">浏览 {{ post.views || 1 }} 次</text>
|
||
<text class="stats-dot">·</text>
|
||
<text class="stats-item-txt">{{ post.likes || 0 }} 人觉得很赞</text>
|
||
</view>
|
||
<view
|
||
class="detail-like-pill btn-active"
|
||
:class="{ liked: post.liked }"
|
||
@click.stop="toggleLike"
|
||
>
|
||
<image class="like-icon" :src="post.liked ? ICONS.heartFilled : ICONS.heart" mode="aspectFit" />
|
||
<text class="like-txt">{{ post.liked ? '已赞' : '点赞' }} {{ post.likes || 0 }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 评论互动列表卡 -->
|
||
<view class="detail-comments-card">
|
||
<view class="comments-section-header">
|
||
<text class="section-title">搭子互动留言</text>
|
||
<text class="section-count">({{ commentsList.length }})</text>
|
||
</view>
|
||
|
||
<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>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部固定评论与互动栏 -->
|
||
<view class="detail-bottom-bar" v-if="post">
|
||
<input
|
||
class="detail-comment-input"
|
||
v-model="commentInput"
|
||
placeholder="写下你的留言或约伴诉求..."
|
||
placeholder-class="input-placeholder"
|
||
maxlength="200"
|
||
confirm-type="send"
|
||
@confirm="sendComment"
|
||
/>
|
||
<view
|
||
class="bottom-like-btn btn-active"
|
||
:class="{ liked: post.liked }"
|
||
@click="toggleLike"
|
||
>
|
||
<image class="like-svg" :src="post.liked ? ICONS.heartFilled : ICONS.heart" mode="aspectFit" />
|
||
</view>
|
||
<button
|
||
class="detail-send-btn btn-active"
|
||
:disabled="!commentInput.trim() || sendingComment"
|
||
@click="sendComment"
|
||
>
|
||
发送
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue';
|
||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||
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,
|
||
deletePost,
|
||
likePost,
|
||
getPostComments,
|
||
addPostComment,
|
||
applyPostParty,
|
||
getPostApplies,
|
||
handlePostApply,
|
||
getUserProfile
|
||
} from '@/common/api.js';
|
||
|
||
const DEFAULT_USER_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120';
|
||
const currentThemeKey = ref(getCurrentThemeKey());
|
||
const navInfo = ref(getNavBarInfo());
|
||
const postId = ref(null);
|
||
const post = ref(null);
|
||
const commentsList = ref([]);
|
||
const commentInput = ref('');
|
||
const loadingComments = ref(false);
|
||
const sendingComment = ref(false);
|
||
const currentUserProfile = ref({});
|
||
|
||
const readCachedUserInfo = () => {
|
||
const pwInfo = uni.getStorageSync('PW_USER_INFO') || {};
|
||
const uInfo = uni.getStorageSync('userInfo') || {};
|
||
const uid = Number(uni.getStorageSync('userId')) || Number(pwInfo.id) || Number(uInfo.id) || 1001;
|
||
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 normalizeCommentItem = (item) => {
|
||
if (!item) return item;
|
||
const cur = currentUserProfile.value.nickname ? currentUserProfile.value : readCachedUserInfo();
|
||
const isMyComment =
|
||
(item.userId && Number(item.userId) === Number(cur.id || 1001)) ||
|
||
item.author === '电竞玩家 (我)';
|
||
return {
|
||
...item,
|
||
author: (isMyComment && cur.nickname)
|
||
? cur.nickname
|
||
: ((item.author && item.author !== '电竞玩家 (我)') ? item.author : (cur.nickname || '同城局长')),
|
||
avatar: (isMyComment && cur.avatar)
|
||
? cur.avatar
|
||
: ((item.avatar && !item.avatar.includes('photo-1535713875002-d1d0cf377fde'))
|
||
? item.avatar
|
||
: (cur.avatar || item.avatar || DEFAULT_USER_AVATAR))
|
||
};
|
||
};
|
||
|
||
const navStyle = computed(() => {
|
||
const info = navInfo.value || {};
|
||
return {
|
||
'--safe-header-gap': `${info.totalHeaderHeight || 88}px`,
|
||
'--capsule-right-space': `${info.capsuleRightSpace || 0}px`,
|
||
'--capsule-height': `${info.capsuleHeight || 32}px`
|
||
};
|
||
});
|
||
|
||
const getPostCategoryName = (catKey) => {
|
||
const map = {
|
||
partner: '同城搭子',
|
||
game: '车队开黑',
|
||
highlight: '战绩高光'
|
||
};
|
||
return map[catKey] || '同城动态';
|
||
};
|
||
|
||
const isPostOwner = (target) => {
|
||
const cur = currentUserProfile.value.nickname ? currentUserProfile.value : readCachedUserInfo();
|
||
return target && (
|
||
Number(target.userId) === Number(cur.id || 1001) ||
|
||
target.author === '电竞玩家 (我)' ||
|
||
(cur.nickname && target.author === cur.nickname) ||
|
||
target.isMine === true
|
||
);
|
||
};
|
||
|
||
const syncPostLocalState = (rawPost) => {
|
||
if (!rawPost) return null;
|
||
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 = {};
|
||
|
||
const strId = String(rawPost.id);
|
||
const serverStatus = rawPost.myApplyStatus || null;
|
||
const isApplied = serverStatus ? (serverStatus !== 'REJECTED') : localAppliedIds.includes(strId);
|
||
const mergedViews = Math.max(Number(rawPost.views) || 1, Number(viewsMap[strId]) || 0);
|
||
|
||
return {
|
||
...rawPost,
|
||
views: mergedViews,
|
||
myApplyStatus: serverStatus || (isApplied ? 'PENDING' : null),
|
||
liked: localLikedIds.includes(strId) ? true : Boolean(rawPost.liked),
|
||
hasApplied: isApplied,
|
||
actionText: serverStatus === 'ACCEPTED' ? '已同意上车' : (isApplied ? '待房主确认' : (rawPost.actionText || '加入车队'))
|
||
};
|
||
};
|
||
|
||
const loadPostDetailData = async () => {
|
||
await syncCurrentUserProfile();
|
||
// 优先读取本地缓存的当前点击动态,实现零闪烁秒开
|
||
const cached = uni.getStorageSync('ACTIVE_COMMUNITY_POST');
|
||
if (cached && (!postId.value || String(cached.id) === String(postId.value))) {
|
||
post.value = syncPostLocalState(cached);
|
||
}
|
||
|
||
try {
|
||
const list = await getPosts('all');
|
||
if (Array.isArray(list) && list.length > 0) {
|
||
const matched = postId.value
|
||
? list.find(item => String(item.id) === String(postId.value))
|
||
: list[0];
|
||
if (matched) {
|
||
const currentViews = post.value && post.value.views ? Math.max(Number(post.value.views), Number(matched.views || 0)) : (Number(matched.views || 0) + 1);
|
||
post.value = syncPostLocalState({
|
||
...matched,
|
||
views: currentViews
|
||
});
|
||
uni.setStorageSync('ACTIVE_COMMUNITY_POST', post.value);
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('[PostDetail] 获取远端动态详情降级:', err.message);
|
||
}
|
||
|
||
if (post.value) {
|
||
await loadComments(post.value.id);
|
||
}
|
||
};
|
||
|
||
const loadComments = async (id) => {
|
||
loadingComments.value = true;
|
||
try {
|
||
const list = await getPostComments(id);
|
||
if (list && list.length > 0) {
|
||
commentsList.value = list.map(normalizeCommentItem);
|
||
} else {
|
||
commentsList.value = [
|
||
{
|
||
id: `cmt_seed_${id}`,
|
||
author: (post.value && post.value.author) || '小甜橙',
|
||
avatar: (post.value && post.value.avatar) || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120',
|
||
content: '欢迎各位小伙伴在评论区留言约伴!一起开黑/探店~',
|
||
createdAt: '刚刚'
|
||
}
|
||
];
|
||
}
|
||
} catch (_) {
|
||
commentsList.value = [];
|
||
} finally {
|
||
loadingComments.value = false;
|
||
}
|
||
};
|
||
|
||
const handleBack = () => {
|
||
const pages = getCurrentPages();
|
||
if (pages.length > 1) {
|
||
uni.navigateBack();
|
||
} else {
|
||
uni.reLaunch({
|
||
url: '/pages/community/index'
|
||
});
|
||
}
|
||
};
|
||
|
||
const openAuthorProfile = (target) => {
|
||
if (!target) return;
|
||
const authorName = target.author || target.nickname || '同城达人';
|
||
let targetId = target.expertId || target.companionId || target.userId;
|
||
if (!targetId) {
|
||
if (authorName === '小甜橙') targetId = 2005;
|
||
else if (authorName.includes('暗影刺客')) targetId = 2004;
|
||
else if (authorName === '苏苏学姐') targetId = 2006;
|
||
else targetId = target.id || 101;
|
||
}
|
||
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 previewImage = (imgs, index) => {
|
||
uni.previewImage({
|
||
urls: imgs,
|
||
current: index
|
||
});
|
||
};
|
||
|
||
const toggleLike = async () => {
|
||
if (!post.value) return;
|
||
if (!ensureLoginOrRedirect(`/pages/community/detail?id=${post.value.id}`)) {
|
||
return;
|
||
}
|
||
const willLike = !post.value.liked;
|
||
post.value.liked = willLike;
|
||
post.value.likes = Math.max(0, (Number(post.value.likes) || 0) + (willLike ? 1 : -1));
|
||
|
||
let localLikedIds = uni.getStorageSync('COMMUNITY_LIKED_POST_IDS') || [];
|
||
if (!Array.isArray(localLikedIds)) localLikedIds = [];
|
||
const strId = String(post.value.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.setStorageSync('ACTIVE_COMMUNITY_POST', post.value);
|
||
|
||
uni.showToast({
|
||
title: willLike ? '已点赞' : '已取消点赞',
|
||
icon: 'none',
|
||
duration: 1000
|
||
});
|
||
|
||
try {
|
||
const res = await likePost(post.value.id, willLike ? 'like' : 'unlike');
|
||
if (res && res.likes !== undefined) {
|
||
post.value.likes = res.likes;
|
||
}
|
||
if (res && res.liked !== undefined) {
|
||
post.value.liked = res.liked;
|
||
}
|
||
uni.setStorageSync('ACTIVE_COMMUNITY_POST', post.value);
|
||
} catch (err) {
|
||
// 回滚乐观更新,避免"已点赞"却未落盘
|
||
console.warn('[PostDetail] 点赞同步异常:', err.message);
|
||
post.value.liked = !willLike;
|
||
post.value.likes = Math.max(0, (Number(post.value.likes) || 0) + (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.setStorageSync('ACTIVE_COMMUNITY_POST', post.value);
|
||
uni.showToast({ title: (err && err.message) || '操作失败,请重试', icon: 'none' });
|
||
}
|
||
};
|
||
|
||
const sendComment = async () => {
|
||
if (!post.value) return;
|
||
if (!ensureLoginOrRedirect(`/pages/community/detail?id=${post.value.id}`)) {
|
||
return;
|
||
}
|
||
if (!commentInput.value.trim()) {
|
||
uni.showToast({ title: '请输入评论内容', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
sendingComment.value = true;
|
||
const content = commentInput.value.trim();
|
||
const profile = currentUserProfile.value.nickname
|
||
? currentUserProfile.value
|
||
: await syncCurrentUserProfile();
|
||
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
|
||
const authorName = profile.nickname || '同城局长';
|
||
const authorAvatar = profile.avatar || DEFAULT_USER_AVATAR;
|
||
|
||
try {
|
||
const res = await addPostComment(post.value.id, {
|
||
userId: uid,
|
||
author: authorName,
|
||
avatar: authorAvatar,
|
||
content
|
||
});
|
||
|
||
commentsList.value.push(normalizeCommentItem({
|
||
id: res ? res.id : `cmt_${Date.now()}`,
|
||
userId: uid,
|
||
author: (res && res.author) || authorName,
|
||
avatar: (res && res.avatar) || authorAvatar,
|
||
content,
|
||
createdAt: '刚刚'
|
||
}));
|
||
|
||
post.value.comments = (Number(post.value.comments) || 0) + 1;
|
||
uni.setStorageSync('ACTIVE_COMMUNITY_POST', post.value);
|
||
commentInput.value = '';
|
||
uni.showToast({ title: '评论发表成功', icon: 'success' });
|
||
} catch (err) {
|
||
uni.showToast({ title: err.message || '发表失败', icon: 'none' });
|
||
} finally {
|
||
sendingComment.value = false;
|
||
}
|
||
};
|
||
|
||
const handleJoinParty = () => {
|
||
if (!post.value) return;
|
||
if (!ensureLoginOrRedirect(`/pages/community/detail?id=${post.value.id}`)) {
|
||
return;
|
||
}
|
||
if (post.value.hasApplied && post.value.myApplyStatus !== 'REJECTED') {
|
||
uni.showToast({
|
||
title: post.value.myApplyStatus === 'ACCEPTED' ? '已同意上车' : '请等待房主确认',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
uni.showModal({
|
||
title: '搭子互动申请',
|
||
content: `确定向【${post.value.author}】发起同城约伴 / 上车请求?`,
|
||
confirmText: '立即发起',
|
||
cancelText: '取消',
|
||
confirmColor: '#6366F1',
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
uni.showLoading({ title: '正在提交...' });
|
||
const profile = currentUserProfile.value.nickname
|
||
? currentUserProfile.value
|
||
: await syncCurrentUserProfile();
|
||
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
|
||
const applicantName = profile.nickname || '同城局长';
|
||
const applicantAvatar = profile.avatar || DEFAULT_USER_AVATAR;
|
||
try {
|
||
await applyPostParty(post.value.id, {
|
||
applicantId: uid,
|
||
applicantName,
|
||
applicantAvatar
|
||
});
|
||
} catch (err) {
|
||
uni.hideLoading();
|
||
uni.showToast({ title: (err && err.message) || '申请失败,请重试', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
post.value.hasApplied = true;
|
||
post.value.myApplyStatus = 'PENDING';
|
||
post.value.actionText = '待房主确认';
|
||
let localAppliedIds = uni.getStorageSync('COMMUNITY_APPLIED_POST_IDS') || [];
|
||
if (!Array.isArray(localAppliedIds)) localAppliedIds = [];
|
||
const strId = String(post.value.id);
|
||
if (!localAppliedIds.includes(strId)) {
|
||
localAppliedIds.push(strId);
|
||
uni.setStorageSync('COMMUNITY_APPLIED_POST_IDS', localAppliedIds);
|
||
}
|
||
uni.setStorageSync('ACTIVE_COMMUNITY_POST', post.value);
|
||
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '申请已送达',
|
||
icon: 'success'
|
||
});
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
const handleManageApplies = async () => {
|
||
if (!post.value) return;
|
||
uni.showLoading({ title: '加载申请...' });
|
||
try {
|
||
const list = await getPostApplies(post.value.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'
|
||
});
|
||
loadPostDetailData();
|
||
} catch (err) {
|
||
uni.showToast({ title: err.message || '操作失败', icon: 'none' });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
});
|
||
} catch (err) {
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '加载失败', icon: 'none' });
|
||
}
|
||
};
|
||
|
||
const handleDeletePost = () => {
|
||
if (!post.value) return;
|
||
uni.showModal({
|
||
title: '删除动态确认',
|
||
content: '确定要删除这条动态吗?删除后将无法恢复。',
|
||
confirmText: '确定删除',
|
||
cancelText: '取消',
|
||
confirmColor: '#EF4444',
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
try {
|
||
uni.showLoading({ title: '正在删除...' });
|
||
await deletePost(post.value.id);
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '动态已删除', icon: 'success' });
|
||
setTimeout(() => {
|
||
handleBack();
|
||
}, 500);
|
||
} catch (err) {
|
||
uni.hideLoading();
|
||
uni.showToast({ title: (err && err.message) || '删除失败,请重试', icon: 'none' });
|
||
}
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
onLoad((options) => {
|
||
if (options && options.id) {
|
||
postId.value = options.id;
|
||
}
|
||
loadPostDetailData();
|
||
});
|
||
|
||
onShow(() => {
|
||
navInfo.value = getNavBarInfo();
|
||
currentThemeKey.value = getCurrentThemeKey();
|
||
applyThemeToNativeUI(currentThemeKey.value);
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.post-detail-page {
|
||
min-height: 100vh;
|
||
background-color: #0A0F1D;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.custom-nav-bar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: var(--safe-header-gap);
|
||
background: rgba(10, 15, 29, 0.92);
|
||
backdrop-filter: blur(14px);
|
||
-webkit-backdrop-filter: blur(14px);
|
||
z-index: 1000;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||
|
||
.nav-content {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: var(--capsule-height);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 24rpx;
|
||
|
||
.btn-nav-back {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 32rpx;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.nav-back-icon {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
}
|
||
}
|
||
|
||
.nav-title-wrap {
|
||
position: absolute;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
text-align: center;
|
||
pointer-events: none;
|
||
|
||
.nav-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #F8FAFC;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
}
|
||
|
||
.nav-capsule-placeholder {
|
||
width: var(--capsule-right-space);
|
||
}
|
||
}
|
||
}
|
||
|
||
.nav-bar-placeholder {
|
||
height: var(--safe-header-gap);
|
||
width: 100%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.detail-scroll-view {
|
||
flex: 1;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.detail-main-wrap {
|
||
padding: 24rpx 28rpx calc(160rpx + env(safe-area-inset-bottom));
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.detail-post-card {
|
||
background: #151D30;
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 28rpx;
|
||
padding: 32rpx 28rpx;
|
||
box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.25);
|
||
|
||
.detail-author-row {
|
||
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.14);
|
||
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;
|
||
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 {
|
||
display: flex;
|
||
align-items: center;
|
||
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);
|
||
}
|
||
}
|
||
|
||
.btn-delete-post {
|
||
height: 60rpx;
|
||
line-height: 58rpx;
|
||
padding: 0 22rpx;
|
||
border-radius: 30rpx;
|
||
font-size: 22rpx;
|
||
color: #EF4444;
|
||
background: rgba(239, 68, 68, 0.12);
|
||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||
margin: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.detail-content-box {
|
||
margin-bottom: 24rpx;
|
||
|
||
.detail-content-text {
|
||
font-size: 30rpx;
|
||
line-height: 1.75;
|
||
color: #F1F5F9;
|
||
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: 300rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
|
||
&.single-img {
|
||
grid-column: span 2;
|
||
height: 420rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.detail-stats-strip {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding-top: 24rpx;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||
|
||
.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 26rpx;
|
||
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-card {
|
||
background: #151D30;
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 28rpx;
|
||
padding: 32rpx 28rpx;
|
||
box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.25);
|
||
|
||
.comments-section-header {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 10rpx;
|
||
margin-bottom: 28rpx;
|
||
|
||
.section-title {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #F8FAFC;
|
||
}
|
||
|
||
.section-count {
|
||
font-size: 24rpx;
|
||
color: #94A3B8;
|
||
}
|
||
}
|
||
|
||
.comments-loading,
|
||
.comments-empty {
|
||
padding: 60rpx 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
|
||
.empty-icon {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
opacity: 0.45;
|
||
}
|
||
|
||
.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;
|
||
padding-bottom: 24rpx;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||
|
||
.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.55;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
&:last-child .comment-body {
|
||
border-bottom: none;
|
||
padding-bottom: 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.detail-bottom-bar {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
padding: 20rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
|
||
background: rgba(14, 22, 40, 0.96);
|
||
backdrop-filter: blur(12px);
|
||
-webkit-backdrop-filter: blur(12px);
|
||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 18rpx;
|
||
z-index: 990;
|
||
|
||
.detail-comment-input {
|
||
flex: 1;
|
||
height: 76rpx;
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 38rpx;
|
||
padding: 0 28rpx;
|
||
font-size: 26rpx;
|
||
color: #F8FAFC;
|
||
}
|
||
|
||
.bottom-like-btn {
|
||
width: 76rpx;
|
||
height: 76rpx;
|
||
border-radius: 38rpx;
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
|
||
.like-svg {
|
||
width: 38rpx;
|
||
height: 38rpx;
|
||
}
|
||
|
||
&.liked {
|
||
background: rgba(239, 68, 68, 0.15);
|
||
border-color: rgba(239, 68, 68, 0.35);
|
||
}
|
||
}
|
||
|
||
.detail-send-btn {
|
||
width: 132rpx;
|
||
height: 76rpx;
|
||
line-height: 76rpx;
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
border-radius: 38rpx;
|
||
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||
color: #FFFFFF;
|
||
text-align: center;
|
||
padding: 0;
|
||
margin: 0;
|
||
box-shadow: 0 4rpx 14rpx rgba(99, 102, 241, 0.35);
|
||
|
||
&:disabled {
|
||
opacity: 0.5;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 暖阳晨曦模式适配 (theme-warm)
|
||
.post-detail-page.theme-warm {
|
||
background-color: #F8F6F0;
|
||
|
||
.custom-nav-bar {
|
||
background: rgba(248, 246, 240, 0.95);
|
||
border-bottom-color: #E7E5E4;
|
||
|
||
.nav-content {
|
||
.btn-nav-back {
|
||
background: #FFFFFF;
|
||
border: 1px solid #E7E5E4;
|
||
}
|
||
|
||
.nav-title-wrap .nav-title {
|
||
color: #1C1917;
|
||
}
|
||
}
|
||
}
|
||
|
||
.detail-post-card {
|
||
background: #FFFFFF;
|
||
border-color: #E7E5E4;
|
||
box-shadow: 0 4rpx 20rpx rgba(28, 25, 23, 0.04);
|
||
|
||
.detail-author-row {
|
||
.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;
|
||
|
||
.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-card {
|
||
background: #FFFFFF;
|
||
border-color: #E7E5E4;
|
||
box-shadow: 0 4rpx 20rpx rgba(28, 25, 23, 0.04);
|
||
|
||
.comments-section-header .section-title {
|
||
color: #1C1917;
|
||
}
|
||
|
||
.comments-list .comment-item .comment-body {
|
||
border-bottom-color: #F5F2EC;
|
||
|
||
.comment-top .comment-author {
|
||
color: #1C1917;
|
||
}
|
||
.comment-content {
|
||
color: #44403C;
|
||
}
|
||
}
|
||
}
|
||
|
||
.detail-bottom-bar {
|
||
background: rgba(253, 251, 247, 0.96);
|
||
border-top-color: #E7E5E4;
|
||
|
||
.detail-comment-input {
|
||
background: #F5F2EC;
|
||
border-color: #E7E5E4;
|
||
color: #1C1917;
|
||
}
|
||
|
||
.bottom-like-btn {
|
||
background: #F5F2EC;
|
||
border-color: #E7E5E4;
|
||
|
||
&.liked {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
border-color: rgba(239, 68, 68, 0.3);
|
||
}
|
||
}
|
||
|
||
.detail-send-btn {
|
||
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
|
||
box-shadow: 0 4rpx 14rpx rgba(234, 88, 12, 0.35);
|
||
}
|
||
}
|
||
}
|
||
</style>
|