feat: repley

This commit is contained in:
zzc
2026-06-15 08:56:18 +08:00
parent cd1595373b
commit 082cd011d2
4 changed files with 369 additions and 89 deletions

View File

@@ -30,13 +30,25 @@ export const topicItemCommentLike = async (data) => {
}; };
// 获取评论列表 // 获取评论列表
export const topicItemCommentList = async (topicId, itemId, page = 1, orderBy = 1) => { export const topicItemCommentList = async (topicId, itemId, page = 1, orderBy = 'hot') => {
return request({ return request({
url: `/api/rating/topic/comment/item/list?topicId=${topicId}&itemId=${itemId}&page=${page}&orderBy=${orderBy}`, url: `/api/rating/topic/comment/item/list?topicId=${topicId}&itemId=${itemId}&page=${page}&orderBy=${orderBy}`,
method: "GET", method: "GET",
}); });
}; };
/*
* 获取评论回复列表
* @param {rootCommentId: string, page: number} data
*/
export const topicItemCommentReplyList = async (data) => {
return request({
url: "/api/rating/topic/comment/replies",
method: "GET",
data,
});
};
// 获取评分项详情 // 获取评分项详情
export const fetchTopicRatingItems = async (itemId) => { export const fetchTopicRatingItems = async (itemId) => {
return request({ return request({

View File

@@ -15,13 +15,6 @@
<text class="action-name">{{ action.label }}</text> <text class="action-name">{{ action.label }}</text>
</view> </view>
</view> </view>
<view v-if="replyTarget" class="reply-banner">
<view class="reply-banner-main">
<text class="reply-banner-label">正在回复</text>
<text class="reply-banner-user">@{{ replyTarget.replyToName || replyTarget.name }}</text>
</view>
<text class="reply-banner-cancel" @tap="handleCancelReply">取消</text>
</view>
<view class="quick-comment"> <view class="quick-comment">
<input <input
class="comment-input" class="comment-input"
@@ -49,14 +42,10 @@ const props = defineProps({
sending: { sending: {
type: Boolean, type: Boolean,
default: false default: false
},
replyTarget: {
type: Object,
default: null
} }
}); });
const emit = defineEmits(['action-change', 'send-comment', 'cancel-reply']); const emit = defineEmits(['action-change', 'send-comment']);
const currentAction = ref(props.initialAction); const currentAction = ref(props.initialAction);
@@ -66,12 +55,7 @@ watch(() => props.initialAction, (newVal) => {
}); });
const commentText = ref(''); const commentText = ref('');
const inputPlaceholder = computed(() => { const inputPlaceholder = computed(() => '说点什么吧...');
if (props.replyTarget?.replyToName || props.replyTarget?.name) {
return `回复 @${props.replyTarget.replyToName || props.replyTarget.name}...`;
}
return '说点什么吧...';
});
const actions = [ const actions = [
{ label: '拉', value: 'terrible', emoji: '👎', score: 2, scoreType:1 }, { label: '拉', value: 'terrible', emoji: '👎', score: 2, scoreType:1 },
@@ -101,10 +85,6 @@ const handleSend = () => {
}); });
}; };
const handleCancelReply = () => {
emit('cancel-reply');
};
const resetComment = () => { const resetComment = () => {
commentText.value = ''; commentText.value = '';
}; };
@@ -178,42 +158,6 @@ defineExpose({
padding: 8rpx 8rpx 8rpx 32rpx; padding: 8rpx 8rpx 8rpx 32rpx;
} }
.reply-banner {
margin-bottom: 18rpx;
padding: 16rpx 20rpx;
border-radius: 18rpx;
background: linear-gradient(135deg, rgba(29, 78, 216, 0.08) 0%, rgba(124, 58, 237, 0.08) 100%);
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.reply-banner-main {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10rpx;
}
.reply-banner-label {
font-size: 22rpx;
color: #6b7280;
}
.reply-banner-user {
font-size: 24rpx;
font-weight: 700;
color: #4338ca;
}
.reply-banner-cancel {
flex-shrink: 0;
font-size: 24rpx;
font-weight: 600;
color: #111827;
}
.comment-input { .comment-input {
flex: 1; flex: 1;
height: 72rpx; height: 72rpx;

View File

@@ -22,7 +22,15 @@
<view class="c-actions"> <view class="c-actions">
<text class="c-action-btn" @tap="emitReply(localComment)">回复</text> <text class="c-action-btn" @tap="emitReply(localComment)">回复</text>
<text v-if="localComment.replyCount" class="c-action-count">{{ localComment.replyCount }} 条回复</text> <text
v-if="localComment.replyCount && !localComment.repliesLoaded && !localComment.repliesLoading"
class="c-action-link"
@tap="emit('toggle-replies', localComment)"
>
查看 {{ localComment.replyCount }} 条回复
</text>
<text v-else-if="localComment.repliesLoading" class="c-action-count">回复加载中...</text>
<text v-else-if="localComment.replyCount" class="c-action-count">{{ localComment.replyCount }} 条回复</text>
</view> </view>
<view v-if="localComment.replies && localComment.replies.length" class="reply-panel"> <view v-if="localComment.replies && localComment.replies.length" class="reply-panel">
@@ -54,6 +62,17 @@
</view> </view>
</view> </view>
</view> </view>
<view class="reply-panel-footer">
<text
v-if="localComment.repliesHasMore && !localComment.repliesLoading"
class="reply-more-btn"
@tap="emit('more-replies', localComment)"
>
查看更多回复
</text>
<text v-else-if="localComment.repliesLoading" class="reply-more-loading">加载更多中...</text>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -73,7 +92,7 @@ const props = defineProps({
} }
}); });
const emit = defineEmits(['need-login', 'like-change', 'reply']); const emit = defineEmits(['need-login', 'like-change', 'reply', 'toggle-replies', 'more-replies']);
const userStore = useUserStore(); const userStore = useUserStore();
const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName); const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName);
@@ -97,6 +116,10 @@ const buildCommentState = (comment = {}) => ({
likes: Number(comment.likes || 0), likes: Number(comment.likes || 0),
isLiked: !!comment.isLiked, isLiked: !!comment.isLiked,
replyCount: Number(comment.replyCount || 0), replyCount: Number(comment.replyCount || 0),
repliesLoading: !!comment.repliesLoading,
repliesLoaded: !!comment.repliesLoaded,
repliesHasMore: !!comment.repliesHasMore,
repliesPage: Number(comment.repliesPage || 1),
replies: Array.isArray(comment.replies) ? comment.replies.map(buildReplyItem) : [] replies: Array.isArray(comment.replies) ? comment.replies.map(buildReplyItem) : []
}); });
@@ -335,13 +358,15 @@ const handleLike = async (target = null) => {
.c-action-btn, .c-action-btn,
.reply-btn, .reply-btn,
.c-action-count { .c-action-count,
.c-action-link {
font-size: 22rpx; font-size: 22rpx;
color: #8b8f98; color: #8b8f98;
} }
.c-action-btn, .c-action-btn,
.reply-btn { .reply-btn,
.c-action-link {
font-weight: 600; font-weight: 600;
} }
@@ -423,4 +448,19 @@ const handleLike = async (target = null) => {
width: 28rpx; width: 28rpx;
height: 28rpx; height: 28rpx;
} }
.reply-panel-footer {
padding: 8rpx 0 10rpx 72rpx;
}
.reply-more-btn,
.reply-more-loading {
font-size: 22rpx;
color: #4f46e5;
font-weight: 600;
}
.reply-more-loading {
color: #9ca3af;
}
</style> </style>

View File

@@ -50,10 +50,8 @@
ref="attitudePanelRef" ref="attitudePanelRef"
:initial-action="currentAction" :initial-action="currentAction"
:sending="isSendingComment" :sending="isSendingComment"
:reply-target="replyTarget"
@action-change="handleActionChange" @action-change="handleActionChange"
@send-comment="handleSendComment" @send-comment="handleSendComment"
@cancel-reply="clearReplyTarget"
/> />
<!-- 大家都在 PK --> <!-- 大家都在 PK -->
@@ -124,6 +122,8 @@
@need-login="openLoginPopup" @need-login="openLoginPopup"
@like-change="handleCommentLikeChange" @like-change="handleCommentLikeChange"
@reply="handleReplyComment" @reply="handleReplyComment"
@toggle-replies="handleToggleReplies"
@more-replies="handleLoadMoreReplies"
/> />
</view> </view>
@@ -135,22 +135,62 @@
</view> </view>
<!-- 评分成功特效弹窗 --> <!-- 评分成功特效弹窗 -->
<view
v-if="replyPopupVisible"
class="reply-popup-mask"
@tap="closeReplyPopup"
></view>
<view
v-if="replyPopupVisible"
class="reply-popup"
:style="{ bottom: `${replyKeyboardHeight}px` }"
>
<view class="reply-popup-header">
<view class="reply-popup-user">
<text class="reply-popup-label">回复</text>
<text class="reply-popup-name">@{{ replyTarget?.replyToName || 'Ta' }}</text>
</view>
<text class="reply-popup-close" @tap="closeReplyPopup">取消</text>
</view>
<view class="reply-popup-input-wrap">
<input
class="reply-popup-input"
v-model="replyContent"
:focus="replyInputFocus"
:disabled="isSendingReply"
:adjust-position="false"
confirm-type="send"
:placeholder="replyPlaceholder"
placeholder-class="reply-popup-placeholder"
@confirm="handleSendReply"
/>
<button
class="reply-popup-send"
:class="{ disabled: isSendingReply || !replyContent.trim() }"
:disabled="isSendingReply || !replyContent.trim()"
@tap="handleSendReply"
>
{{ isSendingReply ? '发送中' : '发送' }}
</button>
</view>
</view>
<SuccessPopup ref="successPopupRef" /> <SuccessPopup ref="successPopupRef" />
<LoginPopup ref="loginPopupRef" @logind="handleLoginSuccess" /> <LoginPopup ref="loginPopupRef" @logind="handleLoginSuccess" />
</view> </view>
</template> </template>
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, nextTick, ref } from 'vue';
import { getStatusBarHeight } from "@/utils/system"; import { getStatusBarHeight } from "@/utils/system";
import { onLoad, onPullDownRefresh, onReachBottom, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app"; import { onLoad, onPullDownRefresh, onReachBottom, onShareAppMessage, onShareTimeline, onUnload } from "@dcloudio/uni-app";
import AttitudePanel from "@/components/AttitudePanel/AttitudePanel.vue"; import AttitudePanel from "@/components/AttitudePanel/AttitudePanel.vue";
import SuccessPopup from "@/components/SuccessPopup/SuccessPopup.vue"; import SuccessPopup from "@/components/SuccessPopup/SuccessPopup.vue";
import CommentItem from "@/components/CommentItem/CommentItem.vue"; import CommentItem from "@/components/CommentItem/CommentItem.vue";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue"; import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import { FILE_BASE_URL } from "@/utils/constants"; import { FILE_BASE_URL } from "@/utils/constants";
import { fetchTopicRatingItems as fetchTopicItemList } from "@/api/topic"; import { fetchTopicRatingItems as fetchTopicItemList } from "@/api/topic";
import { fetchTopicRatingItems, topicItemScore, topicItemComment, topicItemCommentList } from "@/api/topicItem"; import { fetchTopicRatingItems, topicItemScore, topicItemComment, topicItemCommentList, topicItemCommentReplyList } from "@/api/topicItem";
import { formatDate } from "@/utils/date"; import { formatDate } from "@/utils/date";
import { getShareToken } from "@/utils/common"; import { getShareToken } from "@/utils/common";
import { getShareReward } from "@/api/system"; import { getShareReward } from "@/api/system";
@@ -202,10 +242,17 @@ const detailData = ref({
const comments = ref([]); const comments = ref([]);
const itemId = ref(''); const itemId = ref('');
const replyTarget = ref(null); const replyTarget = ref(null);
const replyPopupVisible = ref(false);
const replyInputFocus = ref(false);
const replyContent = ref('');
const replyKeyboardHeight = ref(0);
const isSendingReply = ref(false);
let keyboardHeightListener = null;
const buildAvatarUrl = (avatar) => { const buildAvatarUrl = (avatar) => {
if (!avatar) return 'https://api.dicebear.com/7.x/avataaars/svg?seed=fallback'; if (!avatar) return 'https://api.dicebear.com/7.x/avataaars/svg?seed=fallback';
return String(avatar).startsWith('http') ? avatar : `${FILE_BASE_URL}${avatar}`; const normalizedAvatar = String(avatar).trim();
return normalizedAvatar.startsWith('http') ? normalizedAvatar : `${FILE_BASE_URL}${normalizedAvatar}`;
}; };
const normalizeCommentItem = (item = {}, rootComment = null) => { const normalizeCommentItem = (item = {}, rootComment = null) => {
@@ -227,9 +274,11 @@ const normalizeCommentItem = (item = {}, rootComment = null) => {
userAction: item.userAction || '', userAction: item.userAction || '',
replyCount: Number(item.replyCount || 0), replyCount: Number(item.replyCount || 0),
replyToName: replyToUser.nickname || '', replyToName: replyToUser.nickname || '',
replies: rootComment repliesLoading: false,
? [] repliesLoaded: !!rootComment,
: (Array.isArray(item.replies) ? item.replies.map((reply) => normalizeCommentItem(reply, item)) : []) repliesHasMore: false,
repliesPage: 1,
replies: rootComment ? [] : []
}; };
}; };
@@ -237,6 +286,51 @@ const clearReplyTarget = () => {
replyTarget.value = null; replyTarget.value = null;
}; };
const replyPlaceholder = computed(() => {
return replyTarget.value?.replyToName
? `回复 @${replyTarget.value.replyToName}...`
: '回复一下这条评论';
});
const patchCommentItem = (commentId, updater) => {
comments.value = comments.value.map((comment) => {
if (comment.id !== commentId) return comment;
return updater(comment);
});
};
const resolveReplyHasMore = (res, list, currentPage) => {
const data = res?.data || res || {};
const hasNext = data?.hasNext || res?.hasNext;
if (typeof hasNext === 'boolean') return hasNext;
const pages = Number(data?.pages || res?.pages || 0);
if (pages > 0) return currentPage < pages;
const total = Number(data?.totalCount || data?.total || res?.totalCount || res?.total || 0);
if (total > 0) {
return currentPage * list.length < total || list.length < total;
}
return list.length > 0;
};
const openReplyPopup = async () => {
replyPopupVisible.value = true;
await nextTick();
replyInputFocus.value = false;
await nextTick();
replyInputFocus.value = true;
};
const closeReplyPopup = () => {
replyPopupVisible.value = false;
replyInputFocus.value = false;
replyKeyboardHeight.value = 0;
replyContent.value = '';
clearReplyTarget();
};
const handleReplyComment = (payload) => { const handleReplyComment = (payload) => {
if (!isLoggedIn.value) { if (!isLoggedIn.value) {
openLoginPopup(); openLoginPopup();
@@ -250,6 +344,7 @@ const handleReplyComment = (payload) => {
replyToName: payload.replyToName || '', replyToName: payload.replyToName || '',
content: payload.content || '' content: payload.content || ''
}; };
openReplyPopup();
}; };
const calcPkPercent = (leftScore, rightScore) => { const calcPkPercent = (leftScore, rightScore) => {
@@ -367,23 +462,27 @@ const handleActionChange = async (action) => {
} }
}; };
const handleSendComment = async (payload) => { const submitComment = async ({ content, parentCommentId, replyToCommentId, successMode = 'comment' }) => {
if (!isLoggedIn.value) { if (!isLoggedIn.value) {
openLoginPopup(); openLoginPopup();
return; return;
} }
if (!detailData.value.topicId || !itemId.value || isSendingComment.value) return; if (!detailData.value.topicId || !itemId.value) return false;
try { try {
isSendingComment.value = true; if (successMode === 'reply') {
isSendingReply.value = true;
} else {
isSendingComment.value = true;
}
uni.showLoading({ title: '发送中...' }); uni.showLoading({ title: '发送中...' });
await topicItemComment({ await topicItemComment({
topicId: detailData.value.topicId, topicId: detailData.value.topicId,
itemId: itemId.value, itemId: itemId.value,
content: payload.content, content,
parentCommentId: replyTarget.value?.parentCommentId || undefined, parentCommentId: parentCommentId || undefined,
replyToCommentId: replyTarget.value?.replyToCommentId || undefined replyToCommentId: replyToCommentId || undefined
}); });
uni.hideLoading(); uni.hideLoading();
@@ -391,7 +490,7 @@ const handleSendComment = async (payload) => {
if (successPopupRef.value) { if (successPopupRef.value) {
successPopupRef.value.show({ successPopupRef.value.show({
emoji: '💬', emoji: '💬',
label: '你的锐评已发布' label: successMode === 'reply' ? '回复已发送' : '你的锐评已发布'
}, 'comment'); }, 'comment');
} }
@@ -404,32 +503,64 @@ const handleSendComment = async (payload) => {
elementId: `item_${itemId.value}`, elementId: `item_${itemId.value}`,
elementContent: `评论项_${detailData.value.name}`, elementContent: `评论项_${detailData.value.name}`,
customParams: { customParams: {
comment: payload.content, comment: content,
replyToCommentId: replyTarget.value?.replyToCommentId || '', replyToCommentId: replyToCommentId || '',
page: 'rating_detail' page: 'rating_detail'
} }
}); });
attitudePanelRef.value?.resetComment?.(); return true;
clearReplyTarget();
await getCommentsData({ replace: true });
} catch (error) { } catch (error) {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({
title: '发送失败', title: '发送失败',
icon: 'none' icon: 'none'
}); });
return false;
} finally { } finally {
isSendingComment.value = false; isSendingComment.value = false;
isSendingReply.value = false;
} }
}; };
const handleSendComment = async (payload) => {
if (isSendingComment.value) return;
const success = await submitComment({
content: payload.content,
successMode: 'comment'
});
if (!success) return;
currentPage.value = 1;
hasMore.value = true;
attitudePanelRef.value?.resetComment?.();
await getCommentsData({ replace: true });
};
const handleSendReply = async () => {
const content = replyContent.value.trim();
if (!content) {
uni.showToast({ title: '请输入回复内容', icon: 'none' });
return;
}
if (!replyTarget.value || isSendingReply.value) return;
const success = await submitComment({
content,
parentCommentId: replyTarget.value.parentCommentId,
replyToCommentId: replyTarget.value.replyToCommentId,
successMode: 'reply'
});
if (!success) return;
closeReplyPopup();
await getCommentsData({ replace: true });
};
const switchTab = (tab) => { const switchTab = (tab) => {
if (currentTab.value === tab) return; if (currentTab.value === tab) return;
currentTab.value = tab; currentTab.value = tab;
currentPage.value = 1; currentPage.value = 1;
hasMore.value = true; hasMore.value = true;
commentError.value = ''; commentError.value = '';
clearReplyTarget(); closeReplyPopup();
getCommentsData({ replace: true }); getCommentsData({ replace: true });
}; };
@@ -456,7 +587,7 @@ const getCommentsData = async ({ replace = false } = {}) => {
try { try {
commentLoading.value = true; commentLoading.value = true;
commentError.value = ''; commentError.value = '';
const orderBy = currentTab.value === 'hot' ? 1 : 2; const orderBy = currentTab.value;
const res = await topicItemCommentList(detailData.value.topicId, itemId.value, currentPage.value, orderBy); const res = await topicItemCommentList(detailData.value.topicId, itemId.value, currentPage.value, orderBy);
const rawList = const rawList =
res?.data?.records || res?.data?.records ||
@@ -490,10 +621,59 @@ const retryLoadComments = () => {
if (!comments.value.length) { if (!comments.value.length) {
currentPage.value = 1; currentPage.value = 1;
} }
clearReplyTarget(); closeReplyPopup();
getCommentsData({ replace: currentPage.value === 1 }); getCommentsData({ replace: currentPage.value === 1 });
}; };
const loadReplies = async (comment, page = 1) => {
if (!comment?.id) return;
patchCommentItem(comment.id, (item) => ({
...item,
repliesLoading: true
}));
try {
const res = await topicItemCommentReplyList({
rootCommentId: comment.id,
page
});
const data = res?.data || res || {};
const rawList = data?.list || res?.list || [];
const list = Array.isArray(rawList) ? rawList : [];
const normalizedReplies = list.map((reply) => normalizeCommentItem(reply, comment));
const hasMoreReplies = resolveReplyHasMore(res, list, page);
patchCommentItem(comment.id, (item) => ({
...item,
repliesLoading: false,
repliesLoaded: true,
repliesPage: page,
repliesHasMore: hasMoreReplies,
replies: page === 1 ? normalizedReplies : [...(item.replies || []), ...normalizedReplies]
}));
} catch (error) {
patchCommentItem(comment.id, (item) => ({
...item,
repliesLoading: false
}));
uni.showToast({
title: '加载回复失败',
icon: 'none'
});
}
};
const handleToggleReplies = (comment) => {
if (comment.repliesLoading || comment.repliesLoaded) return;
loadReplies(comment, 1);
};
const handleLoadMoreReplies = (comment) => {
if (comment.repliesLoading || !comment.repliesHasMore) return;
loadReplies(comment, Number(comment.repliesPage || 1) + 1);
};
const handleCommentLikeChange = ({ id, parentId, isReply, isLiked, likes }) => { const handleCommentLikeChange = ({ id, parentId, isReply, isLiked, likes }) => {
comments.value = comments.value.map((item) => { comments.value = comments.value.map((item) => {
if (!isReply && item.id !== id) return item; if (!isReply && item.id !== id) return item;
@@ -569,6 +749,12 @@ const getDetailData = async () => {
}; };
onLoad((options) => { onLoad((options) => {
if (uni.onKeyboardHeightChange) {
keyboardHeightListener = (res) => {
replyKeyboardHeight.value = res?.height || 0;
};
uni.onKeyboardHeightChange(keyboardHeightListener);
}
uni.$trackRecord({ uni.$trackRecord({
eventName: "rating_item_detail_page_visit", eventName: "rating_item_detail_page_visit",
eventType: `visit`, eventType: `visit`,
@@ -588,7 +774,7 @@ const handleLoginSuccess = () => {
hasMore.value = true; hasMore.value = true;
comments.value = []; comments.value = [];
commentError.value = ''; commentError.value = '';
clearReplyTarget(); closeReplyPopup();
getDetailData(); getDetailData();
}; };
@@ -618,10 +804,16 @@ onPullDownRefresh(() => {
hasMore.value = true; hasMore.value = true;
comments.value = []; comments.value = [];
commentError.value = ''; commentError.value = '';
clearReplyTarget(); closeReplyPopup();
getDetailData(); getDetailData();
}); });
onUnload(() => {
if (uni.offKeyboardHeightChange && keyboardHeightListener) {
uni.offKeyboardHeightChange(keyboardHeightListener);
}
});
onReachBottom(() => { onReachBottom(() => {
if (!hasMore.value || commentLoading.value) return; if (!hasMore.value || commentLoading.value) return;
currentPage.value += 1; currentPage.value += 1;
@@ -1071,4 +1263,96 @@ onReachBottom(() => {
color: #999; color: #999;
font-size: 24rpx; font-size: 24rpx;
} }
.reply-popup-mask {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.24);
z-index: 998;
}
.reply-popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
padding: 24rpx 24rpx calc(env(safe-area-inset-bottom) + 24rpx);
background: #ffffff;
border-radius: 28rpx 28rpx 0 0;
box-shadow: 0 -12rpx 36rpx rgba(15, 23, 42, 0.12);
}
.reply-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 20rpx;
}
.reply-popup-user {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10rpx;
}
.reply-popup-label {
font-size: 24rpx;
color: #6b7280;
}
.reply-popup-name {
font-size: 26rpx;
font-weight: 700;
color: #111827;
}
.reply-popup-close {
font-size: 24rpx;
color: #8b8f98;
}
.reply-popup-input-wrap {
display: flex;
align-items: center;
gap: 16rpx;
}
.reply-popup-input {
flex: 1;
height: 84rpx;
padding: 0 28rpx;
border-radius: 24rpx;
background: #f4f6fb;
font-size: 28rpx;
color: #1f2937;
box-sizing: border-box;
}
.reply-popup-placeholder {
color: #9ca3af;
}
.reply-popup-send {
min-width: 132rpx;
height: 84rpx;
line-height: 84rpx;
margin: 0;
padding: 0 24rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #111827 0%, #374151 100%);
color: #ffffff;
font-size: 26rpx;
font-weight: 700;
}
.reply-popup-send.disabled {
opacity: 0.45;
}
.reply-popup-send::after {
border: none;
}
</style> </style>