1001 lines
20 KiB
Plaintext
1001 lines
20 KiB
Plaintext
<!-- 评论组件 - 支持多层级评论和回复 -->
|
||
<template>
|
||
<view class="comments-container">
|
||
<!-- 评论头部 -->
|
||
<view class="comments-header">
|
||
<view class="header-left">
|
||
<text class="comments-title">{{ $t('mt.comment.title') }} ({{ totalComments }})</text>
|
||
</view>
|
||
<view class="header-right">
|
||
<view class="sort-selector" @click="showSortOptions">
|
||
<text class="sort-text">{{ $t(currentSortText) }}</text>
|
||
<text class="sort-arrow">▼</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 评论输入框 -->
|
||
<view class="comment-input-section" v-if="showCommentInput">
|
||
<view class="input-header">
|
||
<view class="user-avatar">
|
||
<text class="avatar-text">{{ userAvatarText }}</text>
|
||
</view>
|
||
<view class="input-actions">
|
||
<view class="cancel-btn" @click="cancelComment">
|
||
<text class="cancel-text">{{ $t('mt.comment.cancel') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<textarea
|
||
class="comment-textarea"
|
||
v-model="commentContent"
|
||
:placeholder="$t('mt.comment.placeholder')"
|
||
:maxlength="500"
|
||
:auto-height="true"
|
||
@blur="handleInputBlur"
|
||
@focus="handleInputFocus">
|
||
</textarea>
|
||
<view class="input-footer">
|
||
<text class="char-count">{{ commentContent.length }}/500</text>
|
||
<view class="submit-btn" @click="submitComment" :class="{ disabled: !canSubmit }">
|
||
<text class="submit-text">{{ $t('mt.comment.submit') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 快速评论按钮 -->
|
||
<view class="quick-comment-btn" @click="showCommentInput = true" v-if="!showCommentInput">
|
||
<text class="quick-comment-text">{{ $t('mt.comment.quick') }}</text>
|
||
</view>
|
||
|
||
|
||
<!-- 加载状态 -->
|
||
<view class="loading-section" v-if="loading">
|
||
<text class="loading-text">{{ $t('mt.status.loading') }}</text>
|
||
</view>
|
||
|
||
<!-- 错误状态 -->
|
||
<view class="error-section" v-if="error !== null">
|
||
<text class="error-text">{{ error }}</text>
|
||
<view class="retry-btn" @click="retryLoad">
|
||
<text class="retry-text">{{ $t('mt.action.retry') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 评论列表 -->
|
||
<view class="comments-list" v-if="commentsList.length > 0">
|
||
<view
|
||
v-for="(comment, index) in commentsList"
|
||
:key="comment.id"
|
||
class="comment-item"
|
||
:class="{ 'is-reply': (comment.level ?? 0) > 0 }"
|
||
:style="{ marginLeft: ((comment.level ?? 0) * 20) + 'px' }">
|
||
|
||
<!-- 评论主体 -->
|
||
<view class="comment-main">
|
||
<view class="comment-avatar">
|
||
<text class="avatar-text">{{ (comment.user_name ?? '').substring(0, 1) }}</text>
|
||
</view>
|
||
<view class="comment-body">
|
||
<view class="comment-header">
|
||
<text class="author-name">{{ comment.user_name }}</text>
|
||
<text class="comment-time">{{ $t(formatRelativeTimeKey(comment.created_at)) }}</text>
|
||
</view>
|
||
<text class="comment-content">{{ comment.content }}</text>
|
||
<view class="comment-actions">
|
||
<view class="action-item" @click="toggleLike(comment)">
|
||
<text class="action-icon" :class="{ liked: comment.is_liked }">👍</text>
|
||
<text class="action-text">{{ comment.like_count }}</text>
|
||
</view>
|
||
<view class="action-item" @click="showReplyInput(comment)">
|
||
<text class="action-icon">💬</text>
|
||
<text class="action-text">{{ $t('mt.comment.reply') }}</text>
|
||
</view>
|
||
<view class="action-item" @click="showReportModal(comment)" v-if="comment?.is_author==false">
|
||
<text class="action-icon">⚠️</text>
|
||
<text class="action-text">{{ $t('mt.comment.report') }}</text>
|
||
</view>
|
||
<view class="action-item" @click="deleteComment(comment)" v-if="comment.is_author">
|
||
<text class="action-icon">🗑️</text>
|
||
<text class="action-text">{{ $t('mt.comment.delete') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 回复输入框 -->
|
||
<view class="reply-input-section" v-if="activeReplyId === comment.id">
|
||
<view class="reply-input-header">
|
||
<text class="reply-target">{{ $t('mt.comment.replyTo') }} @{{ comment.user_name }}:</text>
|
||
<view class="reply-cancel" @click="cancelReply">
|
||
<text class="cancel-text">×</text>
|
||
</view>
|
||
</view>
|
||
<textarea
|
||
class="reply-textarea"
|
||
v-model="replyContent"
|
||
:placeholder="$t('mt.comment.replyPlaceholder')"
|
||
:maxlength="300"
|
||
:auto-height="true">
|
||
</textarea>
|
||
<view class="reply-footer">
|
||
<text class="char-count">{{ replyContent.length }}/300</text>
|
||
<view class="reply-submit-btn" @click="submitReply(comment)" :class="{ disabled: !canSubmitReply }">
|
||
<text class="submit-text">{{ $t('mt.comment.reply') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 查看更多回复 -->
|
||
<view class="load-replies-btn" v-if="comment.reply_count > 0 && !isRepliesLoaded(comment)" @click="loadReplies(comment)">
|
||
<text class="load-replies-text">{{ $t('mt.comment.loadReplies', { count: comment.reply_count }) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view class="empty-section" v-if="commentsList.length === 0 && !loading && error === null">
|
||
<text class="empty-text">{{ $t('mt.comment.empty') }}</text>
|
||
</view>
|
||
|
||
<!-- 加载更多 -->
|
||
<view class="load-more-section" v-if="commentsList.length > 0 && hasMore">
|
||
<view class="load-more-btn" @click="loadMore" v-if="!loadingMore">
|
||
<text class="load-more-text">{{ $t('mt.comment.loadMore') }}</text>
|
||
</view>
|
||
<view class="loading-more" v-if="loadingMore">
|
||
<text class="loading-more-text">{{ $t('mt.loadingMore') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 排序选择弹窗 -->
|
||
<view class="modal-overlay" v-if="showSortModal" @click="hideSortOptions">
|
||
<view class="sort-modal" @click.stop="">
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ $t('mt.comment.sortTitle') }}</text>
|
||
<view class="modal-close" @click="hideSortOptions">
|
||
<text class="close-text">×</text>
|
||
</view>
|
||
</view>
|
||
<view class="sort-list">
|
||
<view
|
||
v-for="(option, index) in sortOptionsList"
|
||
:key="option.value"
|
||
class="sort-item"
|
||
:class="{ active: currentSort === option.value }"
|
||
@click="selectSortOption(option)">
|
||
<text class="sort-name">{{ $t(option.text )}}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 举报弹窗 -->
|
||
<view class="modal-overlay" v-if="showReportModalFlag" @click="hideReportModal">
|
||
<view class="report-modal" @click.stop="">
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ $t('mt.comment.reportTitle') }}</text>
|
||
<view class="modal-close" @click="hideReportModal">
|
||
<text class="close-text">×</text>
|
||
</view>
|
||
</view>
|
||
<view class="report-list">
|
||
<view
|
||
v-for="(type, index) in reportTypesList"
|
||
:key="type.value"
|
||
class="report-item"
|
||
@click="reportComment(type)">
|
||
<text class="report-text">{{ type.text }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import {
|
||
CommentData,
|
||
COMMENT_SORT_OPTIONS,
|
||
COMMENT_REPORT_TYPES,
|
||
formatRelativeTimeKey
|
||
} from './types.uts'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import { tt } from '@/utils/i18nfun.uts'
|
||
|
||
// 属性设计对齐 supadb.uvue,支持灵活配置
|
||
const props = defineProps({
|
||
// 业务主键
|
||
targetType: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
targetId: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
userId: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
userName: {
|
||
type: String,
|
||
default: '游客'
|
||
},
|
||
// 通用 supadb.uvue 风格属性
|
||
collection: {
|
||
type: String,
|
||
default: 'ak_comments'
|
||
},
|
||
filter: {
|
||
type: UTSJSONObject,
|
||
default: () => ({}),
|
||
},
|
||
field: {
|
||
type: String,
|
||
default: '*'
|
||
},
|
||
orderby: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
pageCurrent: {
|
||
type: Number,
|
||
default: 1
|
||
},
|
||
pageSize: {
|
||
type: Number,
|
||
default: 20
|
||
},
|
||
getcount: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
getone: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
loadtime: {
|
||
type: String,
|
||
default: 'auto'
|
||
},
|
||
// RPC 函数名
|
||
rpc: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// RPC 参数
|
||
params: {
|
||
type: UTSJSONObject,
|
||
default: () => ({})
|
||
}
|
||
})
|
||
|
||
// 状态变量
|
||
const loading = ref<boolean>(false)
|
||
const error = ref<string | null>(null)
|
||
const loadingMore = ref<boolean>(false)
|
||
const hasMore = ref<boolean>(true)
|
||
const currentPage = ref<number>(1)
|
||
const pageSize = ref<number>(20)
|
||
const totalComments = ref<number>(0)
|
||
|
||
// UI状态
|
||
const showCommentInput = ref<boolean>(false)
|
||
const showSortModal = ref<boolean>(false)
|
||
const showReportModalFlag = ref<boolean>(false)
|
||
const activeReplyId = ref<string>('')
|
||
const reportingCommentId = ref<string>('')
|
||
|
||
// 输入内容
|
||
const commentContent = ref<string>('')
|
||
const replyContent = ref<string>('')
|
||
|
||
// 排序和选项
|
||
const currentSort = ref<string>('created_at_desc')
|
||
const currentSortText = ref<string>('mt.comment.sort.latest')
|
||
const sortOptionsList = ref(COMMENT_SORT_OPTIONS)
|
||
const reportTypesList = ref(COMMENT_REPORT_TYPES)
|
||
|
||
// 数据列表
|
||
const commentsList = ref<Array<CommentData>>([])
|
||
const loadedReplies = ref<Array<string>>([]) // 已加载回复的评论ID
|
||
|
||
// 计算属性
|
||
const userAvatarText = computed((): string => {
|
||
return props.userName.substring(0, 1)
|
||
})
|
||
|
||
const canSubmit = computed((): boolean => {
|
||
return commentContent.value.trim().length > 0
|
||
})
|
||
|
||
const canSubmitReply = computed((): boolean => {
|
||
return replyContent.value.trim().length > 0
|
||
})
|
||
|
||
// 生命周期
|
||
// onMounted(() => {
|
||
// loadComments()
|
||
// })
|
||
|
||
|
||
// 加载评论
|
||
const loadComments = async () => {
|
||
console.log(props.targetId)
|
||
if (loading.value) return
|
||
loading.value = true
|
||
error.value = null
|
||
try {
|
||
// 构建查询
|
||
let query = supa.from('ak_comments')
|
||
.select('*',{count:'exact'})
|
||
.eq('target_type', props.targetType)
|
||
.eq('target_id', props.targetId)
|
||
.eq('status', 'active')
|
||
// 排序
|
||
const sortParts = currentSort.value.split('_')
|
||
const column = sortParts.slice(0, -1).join('_')
|
||
const direction = sortParts[sortParts.length - 1] === 'desc'
|
||
if (direction) {
|
||
query = query.order(column, { ascending: false })
|
||
} else {
|
||
query = query.order(column, { ascending: true })
|
||
}
|
||
// 分页
|
||
const start = (currentPage.value - 1) * pageSize.value
|
||
const end = start + pageSize.value - 1
|
||
query = query.range(start, end)
|
||
const result = await query.executeAs<CommentData>()
|
||
if (result.error != null) {
|
||
throw new Error(result.error?.message??' error')
|
||
}
|
||
|
||
if (result.data !== null) {
|
||
const data = result.data as CommentData[]
|
||
if (currentPage.value === 1) {
|
||
commentsList.value = data
|
||
} else {
|
||
for (let i: Int = 0; i < data.length; i++) {
|
||
commentsList.value.push(data[i])
|
||
}
|
||
}
|
||
hasMore.value = data.length === pageSize.value
|
||
if (currentPage.value === 1) {
|
||
totalComments.value = commentsList.value.length
|
||
}
|
||
}
|
||
} catch (e: any) {
|
||
error.value = '加载评论失败,请稍后重试'
|
||
console.error('Comments loading error:', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 提交评论
|
||
const submitComment = () => {
|
||
if (!canSubmit.value) return
|
||
|
||
// TODO: 调用API提交评论
|
||
uni.showToast({
|
||
title: '评论发表成功',
|
||
icon: 'success'
|
||
})
|
||
|
||
commentContent.value = ''
|
||
showCommentInput.value = false
|
||
|
||
// 刷新评论列表
|
||
currentPage.value = 1
|
||
loadComments()
|
||
}
|
||
|
||
// 显示回复输入框
|
||
const showReplyInput = (comment: CommentData) => {
|
||
activeReplyId.value = comment.id
|
||
replyContent.value = ''
|
||
}
|
||
|
||
// 取消回复
|
||
const cancelReply = () => {
|
||
activeReplyId.value = ''
|
||
replyContent.value = ''
|
||
}
|
||
|
||
// 提交回复
|
||
const submitReply = (comment: CommentData) => {
|
||
if (!canSubmitReply.value) return
|
||
|
||
// TODO: 调用API提交回复
|
||
uni.showToast({
|
||
title: '回复发表成功',
|
||
icon: 'success'
|
||
})
|
||
|
||
cancelReply()
|
||
|
||
// 刷新评论列表
|
||
currentPage.value = 1
|
||
loadComments()
|
||
}
|
||
|
||
// 点赞/取消点赞
|
||
const toggleLike = (comment: CommentData) => {
|
||
// TODO: 调用API切换点赞状态
|
||
const message = comment.is_liked === true ? '取消点赞' : '点赞成功'
|
||
uni.showToast({
|
||
title: message,
|
||
icon: 'success'
|
||
})
|
||
}
|
||
|
||
// 加载回复
|
||
const loadReplies = (comment: CommentData) => {
|
||
loadedReplies.value.push(comment.id)
|
||
|
||
// TODO: 加载该评论的回复
|
||
uni.showToast({
|
||
title: '加载回复功能开发中',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
// 检查回复是否已加载
|
||
const isRepliesLoaded = (comment: CommentData): boolean => {
|
||
return loadedReplies.value.includes(comment.id)
|
||
}
|
||
|
||
// 删除评论
|
||
const deleteComment = (comment: CommentData) => {
|
||
uni.showModal({
|
||
title: '确认删除',
|
||
content: '删除后不可恢复,确定要删除这条评论吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
// TODO: 调用API删除评论
|
||
uni.showToast({
|
||
title: '删除成功',
|
||
icon: 'success'
|
||
})
|
||
|
||
// 刷新评论列表
|
||
currentPage.value = 1
|
||
loadComments()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 举报相关
|
||
const showReportModal = (comment: CommentData) => {
|
||
reportingCommentId.value = comment.id
|
||
showReportModalFlag.value = true
|
||
}
|
||
|
||
const hideReportModal = () => {
|
||
showReportModalFlag.value = false
|
||
reportingCommentId.value = ''
|
||
}
|
||
|
||
const reportComment = (type: any) => {
|
||
// TODO: 调用API举报评论
|
||
uni.showToast({
|
||
title: '举报已提交',
|
||
icon: 'success'
|
||
})
|
||
|
||
hideReportModal()
|
||
}
|
||
|
||
// 排序相关
|
||
const showSortOptions = () => {
|
||
showSortModal.value = true
|
||
}
|
||
|
||
const hideSortOptions = () => {
|
||
showSortModal.value = false
|
||
}
|
||
|
||
import type { OptionItem } from './types.uts'
|
||
const selectSortOption = (option: OptionItem) => {
|
||
currentSort.value = option.value
|
||
currentSortText.value = option.text
|
||
hideSortOptions()
|
||
currentPage.value = 1
|
||
loadComments()
|
||
}
|
||
|
||
// 加载更多
|
||
const loadMore = () => {
|
||
if (loadingMore.value || !hasMore.value) return
|
||
|
||
loadingMore.value = true
|
||
currentPage.value += 1
|
||
|
||
setTimeout(() => {
|
||
loadComments()
|
||
loadingMore.value = false
|
||
}, 500)
|
||
}
|
||
|
||
// 重试加载
|
||
const retryLoad = () => {
|
||
error.value = null
|
||
loadComments()
|
||
}
|
||
|
||
// 取消评论
|
||
const cancelComment = () => {
|
||
commentContent.value = ''
|
||
showCommentInput.value = false
|
||
}
|
||
|
||
// 输入框事件
|
||
const handleInputFocus = () => {
|
||
// 输入框获得焦点时的处理
|
||
}
|
||
|
||
const handleInputBlur = () => {
|
||
// 输入框失去焦点时的处理
|
||
}
|
||
watch(props.targetId, (val:string): void => {
|
||
console.log(val)
|
||
if (typeof val === 'string' && val.length > 0) {
|
||
loadComments()
|
||
}
|
||
return;
|
||
}, { immediate: true })
|
||
|
||
</script>
|
||
|
||
<style>
|
||
.comments-container {
|
||
background-color: #ffffff;
|
||
padding: 16px;
|
||
}
|
||
|
||
.comments-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
padding-bottom: 12px;
|
||
border-bottom-width: 1px;
|
||
border-bottom-color: #e2e8f0;
|
||
}
|
||
|
||
.header-left {
|
||
flex: 1;
|
||
}
|
||
|
||
.comments-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.header-right {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.sort-selector {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 6px 12px;
|
||
background-color: #f1f5f9;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.sort-text {
|
||
font-size: 14px;
|
||
color: #475569;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.sort-arrow {
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
}
|
||
|
||
.comment-input-section {
|
||
background-color: #f8fafc;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
margin-bottom: 16px;
|
||
border-width: 1px;
|
||
border-color: #e2e8f0;
|
||
}
|
||
|
||
.input-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 16px;
|
||
background-color: #3b82f6;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.avatar-text {
|
||
font-size: 14px;
|
||
color: #ffffff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.input-actions {
|
||
flex-direction: row;
|
||
}
|
||
|
||
.cancel-btn {
|
||
padding: 6px 12px;
|
||
background-color: #f1f5f9;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.cancel-text {
|
||
font-size: 14px;
|
||
color: #64748b;
|
||
}
|
||
|
||
.comment-textarea {
|
||
width: 100%;
|
||
min-height: 80px;
|
||
padding: 12px;
|
||
background-color: #ffffff;
|
||
border-radius: 8px;
|
||
border-width: 1px;
|
||
border-color: #d1d5db;
|
||
font-size: 14px;
|
||
color: #1f2937;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.input-footer {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.char-count {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.submit-btn {
|
||
padding: 8px 16px;
|
||
background-color: #3b82f6;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.submit-btn.disabled {
|
||
background-color: #d1d5db;
|
||
}
|
||
|
||
.submit-text {
|
||
font-size: 14px;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.quick-comment-btn {
|
||
background-color: #f8fafc;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
margin-bottom: 16px;
|
||
border-width: 1px;
|
||
border-color: #e2e8f0;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.quick-comment-text {
|
||
font-size: 14px;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.loading-section, .error-section {
|
||
padding: 40px 16px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 14px;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.error-text {
|
||
font-size: 14px;
|
||
color: #ef4444;
|
||
margin-bottom: 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.retry-btn {
|
||
background-color: #3b82f6;
|
||
border-radius: 20px;
|
||
padding: 8px 24px;
|
||
}
|
||
|
||
.retry-text {
|
||
font-size: 14px;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.comments-list {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.comment-item {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.comment-item.is-reply {
|
||
border-left-width: 2px;
|
||
border-left-color: #e2e8f0;
|
||
padding-left: 12px;
|
||
}
|
||
|
||
.comment-main {
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.comment-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 20px;
|
||
background-color: #64748b;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.comment-body {
|
||
flex: 1;
|
||
}
|
||
|
||
.comment-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.author-name {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.comment-time {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.comment-content {
|
||
font-size: 14px;
|
||
color: #374151;
|
||
line-height: 20px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.comment-actions {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.action-item {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 4px 8px;
|
||
margin-right: 16px;
|
||
}
|
||
|
||
.action-icon {
|
||
font-size: 14px;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.action-icon.liked {
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
}
|
||
|
||
.reply-input-section {
|
||
margin-top: 12px;
|
||
margin-left: 52px;
|
||
background-color: #f8fafc;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.reply-input-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.reply-target {
|
||
font-size: 12px;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.reply-cancel {
|
||
width: 24px;
|
||
height: 24px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #f1f5f9;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.reply-textarea {
|
||
width: 100%;
|
||
min-height: 60px;
|
||
padding: 8px;
|
||
background-color: #ffffff;
|
||
border-radius: 6px;
|
||
border-width: 1px;
|
||
border-color: #d1d5db;
|
||
font-size: 14px;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.reply-footer {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.reply-submit-btn {
|
||
padding: 6px 12px;
|
||
background-color: #3b82f6;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.reply-submit-btn.disabled {
|
||
background-color: #d1d5db;
|
||
}
|
||
|
||
.load-replies-btn {
|
||
margin-top: 8px;
|
||
margin-left: 52px;
|
||
padding: 6px 12px;
|
||
background-color: #f1f5f9;
|
||
border-radius: 16px;
|
||
align-items: center;
|
||
}
|
||
|
||
.load-replies-text {
|
||
font-size: 12px;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.empty-section {
|
||
padding: 60px 16px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 16px;
|
||
color: #94a3b8;
|
||
text-align: center;
|
||
}
|
||
|
||
.load-more-section {
|
||
padding: 20px 16px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.load-more-btn {
|
||
background-color: #f1f5f9;
|
||
border-radius: 20px;
|
||
padding: 10px 24px;
|
||
border-width: 1px;
|
||
border-color: #e2e8f0;
|
||
}
|
||
|
||
.load-more-text {
|
||
font-size: 14px;
|
||
color: #475569;
|
||
}
|
||
|
||
.loading-more {
|
||
padding: 10px 24px;
|
||
}
|
||
|
||
.loading-more-text {
|
||
font-size: 14px;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.sort-modal, .report-modal {
|
||
background-color: #ffffff;
|
||
border-radius: 16px;
|
||
margin: 20px;
|
||
max-height: 500px;
|
||
min-width: 280px;
|
||
}
|
||
|
||
.modal-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
border-bottom-width: 1px;
|
||
border-bottom-color: #e2e8f0;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.modal-close {
|
||
width: 32px;
|
||
height: 32px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 16px;
|
||
background-color: #f1f5f9;
|
||
}
|
||
|
||
.close-text {
|
||
font-size: 20px;
|
||
color: #64748b;
|
||
}
|
||
|
||
.sort-list, .report-list {
|
||
max-height: 400px;
|
||
}
|
||
|
||
.sort-item, .report-item {
|
||
padding: 16px 20px;
|
||
border-bottom-width: 1px;
|
||
border-bottom-color: #f1f5f9;
|
||
}
|
||
|
||
.sort-item.active {
|
||
background-color: #eff6ff;
|
||
}
|
||
|
||
.sort-name, .report-text {
|
||
font-size: 16px;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.sort-item.active .sort-name {
|
||
color: #3b82f6;
|
||
}
|
||
</style>
|