mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-21 14:30:59 +08:00
Compare commits
1 Commits
codex/add-
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9e996f291 |
@@ -53,14 +53,29 @@
|
||||
@click="handleContentClick"
|
||||
></div>
|
||||
<div class="article-footer-container">
|
||||
<ReactionsGroup v-model="comment.reactions" content-type="comment" :content-id="comment.id">
|
||||
<div class="make-reaction-item comment-reaction" @click="toggleEditor">
|
||||
<ReactionsGroup
|
||||
ref="commentReactionsGroupRef"
|
||||
v-model="comment.reactions"
|
||||
content-type="comment"
|
||||
:content-id="comment.id"
|
||||
/>
|
||||
<div class="comment-reaction-actions">
|
||||
<div
|
||||
class="reaction-action like-action"
|
||||
:class="{ selected: commentLikedByMe }"
|
||||
@click="toggleCommentLike"
|
||||
>
|
||||
<like v-if="!commentLikedByMe" />
|
||||
<like v-else theme="filled" />
|
||||
<span v-if="commentLikeCount" class="reaction-count">{{ commentLikeCount }}</span>
|
||||
</div>
|
||||
<div class="reaction-action comment-reaction" @click="toggleEditor">
|
||||
<comment-icon />
|
||||
</div>
|
||||
<div class="make-reaction-item copy-link" @click="copyCommentLink">
|
||||
<div class="reaction-action copy-link" @click="copyCommentLink">
|
||||
<link-icon />
|
||||
</div>
|
||||
</ReactionsGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-editor-wrapper" ref="editorWrapper">
|
||||
<CommentEditor
|
||||
@@ -156,6 +171,18 @@ const lightboxVisible = ref(false)
|
||||
const lightboxIndex = ref(0)
|
||||
const lightboxImgs = ref([])
|
||||
const loggedIn = computed(() => authState.loggedIn)
|
||||
const commentReactionsGroupRef = ref(null)
|
||||
const commentLikeCount = computed(
|
||||
() => (props.comment.reactions || []).filter((reaction) => reaction.type === 'LIKE').length,
|
||||
)
|
||||
const commentLikedByMe = computed(() =>
|
||||
(props.comment.reactions || []).some(
|
||||
(reaction) => reaction.type === 'LIKE' && reaction.user === authState.username,
|
||||
),
|
||||
)
|
||||
const toggleCommentLike = () => {
|
||||
commentReactionsGroupRef.value?.toggleReaction('LIKE')
|
||||
}
|
||||
const countReplies = (list) => list.reduce((sum, r) => sum + 1 + countReplies(r.reply || []), 0)
|
||||
const replyCount = computed(() => countReplies(props.comment.reply || []))
|
||||
const isCommentFromPostAuthor = computed(() => {
|
||||
@@ -365,6 +392,56 @@ const handleContentClick = (e) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.article-footer-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.comment-reaction-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.reaction-action {
|
||||
cursor: pointer;
|
||||
padding: 4px 10px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
opacity: 0.6;
|
||||
font-size: 18px;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.reaction-action:hover {
|
||||
opacity: 1;
|
||||
background-color: var(--normal-light-background-color);
|
||||
}
|
||||
|
||||
.reaction-action.like-action {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.reaction-action.selected {
|
||||
opacity: 1;
|
||||
background-color: var(--normal-light-background-color);
|
||||
}
|
||||
|
||||
.reaction-count {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reply-toggle {
|
||||
cursor: pointer;
|
||||
color: var(--primary-color);
|
||||
@@ -378,10 +455,6 @@ const handleContentClick = (e) => {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.comment-reaction:hover {
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.comment-highlight {
|
||||
animation: highlight 2s;
|
||||
}
|
||||
|
||||
@@ -35,18 +35,6 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="make-reaction-container">
|
||||
<div
|
||||
v-if="props.contentType !== 'message'"
|
||||
class="make-reaction-item like-reaction"
|
||||
@click="toggleReaction('LIKE')"
|
||||
>
|
||||
<like v-if="!userReacted('LIKE')" />
|
||||
<like v-else theme="filled" />
|
||||
<span class="reactions-count" v-if="likeCount">{{ likeCount }}</span>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="panelVisible"
|
||||
class="reactions-panel"
|
||||
@@ -102,8 +90,6 @@ const counts = computed(() => {
|
||||
})
|
||||
|
||||
const totalCount = computed(() => Object.values(counts.value).reduce((a, b) => a + b, 0))
|
||||
const likeCount = computed(() => counts.value['LIKE'] || 0)
|
||||
|
||||
const userReacted = (type) =>
|
||||
reactions.value.some((r) => r.type === type && r.user === authState.username)
|
||||
|
||||
@@ -152,7 +138,7 @@ const displayedReactions = computed(() => {
|
||||
.map((type) => ({ type }))
|
||||
})
|
||||
|
||||
const panelTypes = computed(() => sortedReactionTypes.value.filter((t) => t !== 'LIKE'))
|
||||
const panelTypes = computed(() => sortedReactionTypes.value)
|
||||
|
||||
const panelVisible = ref(false)
|
||||
let hideTimer = null
|
||||
@@ -246,6 +232,10 @@ const toggleReaction = async (type) => {
|
||||
onMounted(async () => {
|
||||
await initialize()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
toggleReaction,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -253,11 +243,7 @@ onMounted(async () => {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.reactions-viewer {
|
||||
@@ -295,32 +281,6 @@ onMounted(async () => {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.make-reaction-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.make-reaction-item {
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
opacity: 0.5;
|
||||
border-radius: 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.like-reaction {
|
||||
color: #ff0000;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.make-reaction-item:hover {
|
||||
background-color: #ffe2e2;
|
||||
}
|
||||
|
||||
.reactions-count {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -61,14 +61,31 @@
|
||||
@click="handleContentClick"
|
||||
></div>
|
||||
</div>
|
||||
<ReactionsGroup
|
||||
:model-value="item.reactions"
|
||||
content-type="message"
|
||||
:content-id="item.id"
|
||||
@update:modelValue="(v) => (item.reactions = v)"
|
||||
>
|
||||
<div @click="setReply(item)" class="reply-btn"><next /> 写个回复...</div>
|
||||
</ReactionsGroup>
|
||||
<div class="message-reaction-row">
|
||||
<ReactionsGroup
|
||||
:ref="(el) => setMessageReactionRef(item.id, el)"
|
||||
:model-value="item.reactions"
|
||||
content-type="message"
|
||||
:content-id="item.id"
|
||||
@update:modelValue="(v) => (item.reactions = v)"
|
||||
/>
|
||||
<div class="message-reaction-actions">
|
||||
<div
|
||||
class="reaction-action like-action"
|
||||
:class="{ selected: isMessageLiked(item) }"
|
||||
@click="toggleMessageLike(item)"
|
||||
>
|
||||
<like v-if="!isMessageLiked(item)" />
|
||||
<like v-else theme="filled" />
|
||||
<span v-if="getMessageLikeCount(item)" class="reaction-count">{{
|
||||
getMessageLikeCount(item)
|
||||
}}</span>
|
||||
</div>
|
||||
<div @click="setReply(item)" class="reaction-action reply-btn">
|
||||
<next /> 写个回复...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BaseTimeline>
|
||||
<div class="empty-container">
|
||||
@@ -180,6 +197,32 @@ function setReply(message) {
|
||||
replyTo.value = message
|
||||
}
|
||||
|
||||
const messageReactionRefs = new Map()
|
||||
function setMessageReactionRef(id, el) {
|
||||
if (el) {
|
||||
messageReactionRefs.set(id, el)
|
||||
} else {
|
||||
messageReactionRefs.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
function getMessageLikeCount(message) {
|
||||
return (message.reactions || []).filter((reaction) => reaction.type === 'LIKE').length
|
||||
}
|
||||
|
||||
function isMessageLiked(message) {
|
||||
const username = currentUser.value?.username
|
||||
if (!username) return false
|
||||
return (message.reactions || []).some(
|
||||
(reaction) => reaction.type === 'LIKE' && reaction.user === username,
|
||||
)
|
||||
}
|
||||
|
||||
function toggleMessageLike(message) {
|
||||
const group = messageReactionRefs.get(message.id)
|
||||
group?.toggleReaction('LIKE')
|
||||
}
|
||||
|
||||
/** 改造:滚动函数 —— smooth & instant */
|
||||
function scrollToBottomSmooth() {
|
||||
const el = messagesListEl.value
|
||||
@@ -710,6 +753,55 @@ function goBack() {
|
||||
background-color: var(--normal-light-background-color);
|
||||
}
|
||||
|
||||
.message-reaction-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.message-reaction-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.reaction-action {
|
||||
cursor: pointer;
|
||||
padding: 4px 10px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
opacity: 0.6;
|
||||
font-size: 16px;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.reaction-action:hover {
|
||||
opacity: 1;
|
||||
background-color: var(--normal-light-background-color);
|
||||
}
|
||||
|
||||
.reaction-action.like-action {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.reaction-action.selected {
|
||||
opacity: 1;
|
||||
background-color: var(--normal-light-background-color);
|
||||
}
|
||||
|
||||
.reaction-count {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reply-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -723,14 +815,8 @@ function goBack() {
|
||||
}
|
||||
|
||||
.reply-btn {
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
opacity: 0.6;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.reply-btn:hover {
|
||||
opacity: 1;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.active-reply {
|
||||
|
||||
@@ -92,11 +92,26 @@
|
||||
></div>
|
||||
|
||||
<div class="article-footer-container">
|
||||
<ReactionsGroup v-model="postReactions" content-type="post" :content-id="postId">
|
||||
<div class="make-reaction-item copy-link" @click="copyPostLink">
|
||||
<ReactionsGroup
|
||||
ref="postReactionsGroupRef"
|
||||
v-model="postReactions"
|
||||
content-type="post"
|
||||
:content-id="postId"
|
||||
/>
|
||||
<div class="article-footer-actions">
|
||||
<div
|
||||
class="reaction-action like-action"
|
||||
:class="{ selected: postLikedByMe }"
|
||||
@click="togglePostLike"
|
||||
>
|
||||
<like v-if="!postLikedByMe" />
|
||||
<like v-else theme="filled" />
|
||||
<span v-if="postLikeCount" class="reaction-count">{{ postLikeCount }}</span>
|
||||
</div>
|
||||
<div class="reaction-action copy-link" @click="copyPostLink">
|
||||
<link-icon />
|
||||
</div>
|
||||
</ReactionsGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,6 +238,18 @@ const postContent = ref('')
|
||||
const category = ref('')
|
||||
const tags = ref([])
|
||||
const postReactions = ref([])
|
||||
const postReactionsGroupRef = ref(null)
|
||||
const postLikeCount = computed(
|
||||
() => postReactions.value.filter((reaction) => reaction.type === 'LIKE').length,
|
||||
)
|
||||
const postLikedByMe = computed(() =>
|
||||
postReactions.value.some(
|
||||
(reaction) => reaction.type === 'LIKE' && reaction.user === authState.username,
|
||||
),
|
||||
)
|
||||
const togglePostLike = () => {
|
||||
postReactionsGroupRef.value?.toggleReaction('LIKE')
|
||||
}
|
||||
const comments = ref([])
|
||||
const changeLogs = ref([])
|
||||
const status = ref('PUBLISHED')
|
||||
@@ -366,9 +393,9 @@ const changeLogIcon = (l) => {
|
||||
return 'unlock'
|
||||
}
|
||||
} else if (l.type === 'PINNED') {
|
||||
if(l.newPinnedAt){
|
||||
if (l.newPinnedAt) {
|
||||
return 'pin'
|
||||
}else{
|
||||
} else {
|
||||
return 'clear-icon'
|
||||
}
|
||||
} else if (l.type === 'FEATURED') {
|
||||
@@ -1245,35 +1272,53 @@ onMounted(async () => {
|
||||
.article-footer-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 0px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.reactions-viewer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.reactions-viewer-item-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.reactions-viewer-item {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.make-reaction-container {
|
||||
.article-footer-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.copy-link:hover {
|
||||
.reaction-action {
|
||||
cursor: pointer;
|
||||
padding: 4px 10px;
|
||||
opacity: 0.6;
|
||||
border-radius: 10px;
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.reaction-action:hover {
|
||||
opacity: 1;
|
||||
background-color: var(--normal-light-background-color);
|
||||
}
|
||||
|
||||
.reaction-action.like-action {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.reaction-action.selected {
|
||||
opacity: 1;
|
||||
background-color: var(--normal-light-background-color);
|
||||
}
|
||||
|
||||
.reaction-count {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reaction-action.copy-link:hover {
|
||||
background-color: #e2e2e2;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user