Files
OpenIsle/open-isle-cli/src/views/PostPageView.vue

500 lines
12 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="post-page-container">
<div v-if="isWaitingFetchingPost" class="loading-container">
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
</div>
<div v-else class="post-page-main-container" ref="mainContainer" @scroll="onScroll">
<div class="article-title-container">
<div class="article-title">{{ title }}</div>
<div class="article-info-container">
<div class="article-info-item">
<img class="article-info-item-img" :src="category.smallIcon" alt="category">
<div class="article-info-item-text">{{ category.name }}</div>
</div>
<div class="article-tags-container">
<div class="article-info-item" v-for="tag in tags" :key="tag">
<img class="article-info-item-img" :src="tag.smallIcon" alt="tag">
<div class="article-info-item-text">{{ tag.name }}</div>
</div>
</div>
</div>
</div>
<div class="info-content-container" ref="postItems">
<div class="user-avatar-container">
<div class="user-avatar-item">
<img class="user-avatar-item-img" src="https://picsum.photos/200/200" alt="avatar">
</div>
</div>
<div class="info-content">
<div class="info-content-header">
<div class="user-name">Nagisa77</div>
<div class="post-time">{{ postTime }}</div>
</div>
<div class="info-content-text" v-html="renderMarkdown(postContent)"></div>
<div class="article-footer-container">
<div class="reactions-container">
<div class="reactions-viewer">
<div class="reactions-viewer-item-container">
<div class="reactions-viewer-item">
🤣
</div>
<div class="reactions-viewer-item">
</div>
<div class="reactions-viewer-item">
👏
</div>
</div>
<div class="reactions-count">1882</div>
</div>
<div class="make-reaction-container">
<div class="make-reaction-item like-reaction">
<i class="far fa-heart"></i>
</div>
<div class="make-reaction-item copy-link" @click="copyPostLink">
<i class="fas fa-link"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<CommentEditor @submit="postComment" :loading="isWaitingPostingComment" />
<div class="comments-container">
<CommentItem v-for="comment in comments" :key="comment.id" :comment="comment" :level="0" ref="postItems" />
</div>
</div>
<div class="post-page-scroller-container">
<div class="scroller">
<div v-if="isWaitingFetchingPost" class="scroller-time">loading...</div>
<div v-else class="scroller-time">{{ postTime }}</div>
<div class="scroller-middle">
<input type="range" class="scroller-range" :max="totalPosts" :min="1" v-model.number="currentIndex"
@input="onSliderInput" />
<div class="scroller-index">{{ currentIndex }}/{{ totalPosts }}</div>
</div>
<div v-if="isWaitingFetchingPost" class="scroller-time">loading...</div>
<div v-else class="scroller-time">{{ lastReplyTime }}</div>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import CommentItem from '../components/CommentItem.vue'
import CommentEditor from '../components/CommentEditor.vue'
import { renderMarkdown } from '../utils/markdown'
import { API_BASE_URL, toast } from '../main'
import { getToken } from '../utils/auth'
import { hatch } from 'ldrs'
hatch.register()
export default {
name: 'PostPageView',
components: { CommentItem, CommentEditor },
setup() {
const route = useRoute()
const postId = route.params.id
const title = ref('')
const postContent = ref('')
const category = ref('')
const tags = ref([])
const comments = ref([])
const isWaitingFetchingPost = ref(false);
const isWaitingPostingComment = ref(false);
const postTime = ref('')
const postItems = ref([])
const mainContainer = ref(null)
const currentIndex = ref(1)
const mapComment = c => ({
id: c.id,
userName: c.author,
time: new Date(c.createdAt).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' }),
avatar: 'https://picsum.photos/200/200',
text: c.content,
reply: (c.replies || []).map(mapComment)
})
const fetchPost = async () => {
try {
isWaitingFetchingPost.value = true;
const res = await fetch(`${API_BASE_URL}/api/posts/${postId}`)
isWaitingFetchingPost.value = false;
if (!res.ok) return
const data = await res.json()
postContent.value = data.content
title.value = data.title
category.value = data.category
tags.value = data.tags || []
comments.value = (data.comments || []).map(mapComment)
postTime.value = new Date(data.createdAt).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' })
} catch (e) {
console.error(e)
}
}
const totalPosts = computed(() => comments.value.length + 1)
const lastReplyTime = computed(() =>
comments.value.length ? comments.value[comments.value.length - 1].time : postTime.value
)
const updateCurrentIndex = () => {
const scrollTop = mainContainer.value ? mainContainer.value.scrollTop : 0
for (let i = 0; i < postItems.value.length; i++) {
const el = postItems.value[i].$el
if (el.offsetTop + el.offsetHeight > scrollTop) {
currentIndex.value = i + 1
break
}
}
}
const onSliderInput = () => {
const target = postItems.value[currentIndex.value - 1]?.$el
if (target && mainContainer.value) {
mainContainer.value.scrollTo({ top: target.offsetTop, behavior: 'instant' })
}
}
const postComment = async (text) => {
if (!text.trim()) return
isWaitingPostingComment.value = true
const token = getToken()
if (!token) {
toast.error('请先登录')
isWaitingPostingComment.value = false
return
}
try {
const res = await fetch(`${API_BASE_URL}/api/posts/${postId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ content: text })
})
if (res.ok) {
const data = await res.json()
comments.value.push(mapComment(data))
} else {
toast.error('评论失败')
}
} catch (e) {
toast.error('评论失败')
} finally {
isWaitingPostingComment.value = false
}
}
const copyPostLink = () => {
navigator.clipboard.writeText(location.href.split('#')[0])
}
onMounted(() => {
fetchPost()
updateCurrentIndex()
const hash = location.hash
if (hash.startsWith('#comment-')) {
const id = hash.substring('#comment-'.length)
const el = document.getElementById('comment-' + id)
if (el && mainContainer.value) {
mainContainer.value.scrollTo({ top: el.offsetTop, behavior: 'instant' })
el.classList.add('comment-highlight')
setTimeout(() => el.classList.remove('comment-highlight'), 2000)
}
}
})
return {
postContent,
title,
category,
tags,
comments,
postTime,
lastReplyTime,
postItems,
mainContainer,
currentIndex,
totalPosts,
postComment,
onSliderInput,
onScroll: updateCurrentIndex,
copyPostLink,
renderMarkdown,
isWaitingFetchingPost,
isWaitingPostingComment
}
}
}
</script>
<style>
.post-page-container {
display: flex;
flex-direction: row;
height: calc(100vh - var(--header-height));
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
width: 85%;
}
.post-page-main-container {
overflow-y: auto;
scrollbar-width: none;
padding: 20px;
height: calc(100% - 40px);
width: calc(85% - 40px);
}
.post-page-scroller-container {
display: flex;
flex-direction: column;
width: 15%;
}
.scroller {
margin-top: 20px;
margin-left: 20px;
}
.scroller-time {
font-size: 14px;
opacity: 0.5;
}
.scroller-middle {
margin: 10px 0;
margin-left: 10px;
display: flex;
flex-direction: row;
gap: 8px;
}
.scroller-range {
writing-mode: vertical-rl;
direction: ltr;
height: 300px;
width: 2px;
-webkit-appearance: none;
background: transparent;
}
.scroller-range::-webkit-slider-runnable-track {
width: 1px;
height: 100%;
background-color: var(--scroller-background-color);
}
.scroller-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 6px;
height: 60px;
right: 2px;
border-radius: 3px;
background-color: var(--scroller-background-color);
cursor: pointer;
}
.scroller-range::-moz-range-track {
width: 2px;
height: 100%;
background-color: #ccc;
border-radius: 1px;
}
.scroller-range::-moz-range-thumb {
width: 10px;
height: 10px;
background-color: #333;
border-radius: 50%;
cursor: pointer;
}
.scroller-index {
font-size: 17px;
font-weight: bold;
margin-top: 10px;
}
.article-title-container {
display: flex;
flex-direction: column;
}
.article-title {
font-size: 30px;
font-weight: bold;
}
.article-info-container {
display: flex;
flex-direction: row;
margin-top: 10px;
gap: 10px;
align-items: center;
}
.article-info-item {
display: flex;
flex-direction: row;
gap: 5px;
align-items: center;
}
.article-tags-container {
display: flex;
flex-direction: row;
gap: 10px;
}
.article-info-item {
display: flex;
flex-direction: row;
gap: 5px;
align-items: center;
font-size: 14px;
padding: 2px 4px;
background-color: #f0f0f0;
}
.article-info-item-img {
width: 16px;
height: 16px;
}
.info-content-container {
margin-top: 20px;
display: flex;
flex-direction: row;
gap: 10px;
padding: 0px;
border-bottom: 1px solid #e2e2e2;
}
.user-avatar-container {}
.user-avatar-item {
width: 50px;
height: 50px;
}
.user-avatar-item-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
.info-content {
display: flex;
width: 100%;
flex-direction: column;
gap: 10px;
}
.info-content-header {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
justify-content: space-between;
}
.user-name {
font-size: 16px;
font-weight: bold;
opacity: 0.7;
}
.post-time {
font-size: 14px;
opacity: 0.5;
}
.info-content-text {
font-size: 16px;
line-height: 1.8;
opacity: 0.7;
width: 100%;
}
.article-footer-container {
display: flex;
flex-direction: row;
gap: 10px;
margin-top: 60px;
}
.reactions-container {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
width: 100%;
justify-content: space-between;
}
.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;
}
.reactions-count {
font-size: 16px;
opacity: 0.5;
}
.make-reaction-container {
display: flex;
flex-direction: row;
gap: 10px;
}
.make-reaction-item {
cursor: pointer;
padding: 10px;
border-radius: 50%;
opacity: 0.5;
font-size: 20px;
}
.like-reaction {
color: #ff0000;
}
.like-reaction:hover {
background-color: #ffe2e2;
}
.copy-link:hover {
background-color: #e2e2e2;
}
</style>