帖子详情页

main
abab2320 2 weeks ago
parent 8bf952c8ad
commit fffaecb0a3

@ -13,6 +13,8 @@
"@vue/shared": "^3.5.13",
"axios": "^1.8.3",
"element-plus": "^2.9.7",
"highlight.js": "^11.11.1",
"marked": "^15.0.12",
"vee-validate": "^4.15.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0",

@ -20,6 +20,12 @@ importers:
element-plus:
specifier: ^2.9.7
version: 2.9.7(vue@3.5.13(typescript@5.7.3))
highlight.js:
specifier: ^11.11.1
version: 11.11.1
marked:
specifier: ^15.0.12
version: 15.0.12
vee-validate:
specifier: ^4.15.0
version: 4.15.0(vue@3.5.13(typescript@5.7.3))
@ -588,6 +594,10 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
highlight.js@11.11.1:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@ -614,6 +624,11 @@ packages:
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
marked@15.0.12:
resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
engines: {node: '>= 18'}
hasBin: true
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@ -1416,6 +1431,8 @@ snapshots:
he@1.2.0: {}
highlight.js@11.11.1: {}
hookable@5.5.3: {}
immutable@5.1.2: {}
@ -1436,6 +1453,8 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
marked@15.0.12: {}
math-intrinsics@1.1.0: {}
memoize-one@6.0.0: {}

@ -86,7 +86,7 @@ button {
}
.card:hover {
transform: translateY(-5px);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}

@ -56,7 +56,7 @@
height: 70px;
width: 100%;
background: #ead1fb;
position:absolute;
position:fixed;
top: 0;
left:0;
padding:0;

@ -0,0 +1,63 @@
<template>
<div class="author-box">
<img class="avatar" :src="author.avatar" @click="toProfile" />
<div class="nickname">{{ author.name }}</div>
<div class="stats">
<span>xx 关注</span>
<span>xx 粉丝</span>
<span>xx 帖子</span>
</div>
<div class="other-posts">
<h4>这是帖主的其他帖子</h4>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
author: {
id: number,
name: string,
avatar: string,
bio: string,
},
}>()
function toProfile() {
window.location.href = '/not-found'
}
</script>
<style scoped lang="scss">
.author-box {
text-align: center;
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
margin-bottom: 8px;
cursor: pointer;
}
.nickname {
font-weight: bold;
margin-bottom: 8px;
}
.stats {
display: flex;
justify-content: space-around;
margin-bottom: 16px;
font-size: 14px;
}
.other-posts {
background: #fff;
border-radius: 8px;
padding: 10px;
font-size: 14px;
text-align: left;
}
}
</style>

@ -0,0 +1,90 @@
<template>
<div class="post-content">
<!-- 顶部信息 -->
<div class="post-header">
<img class="avatar" :src="post.author.avatar" @click="toProfile" />
<div class="info">
<div class="nickname">{{ post.author.name }}</div>
<div class="meta">发布日期 | IP 属地</div>
</div>
<button class="follow-btn btn-primary" @click="toProfile">+</button>
</div>
<el-divider />
<!-- markdown内容 -->
<MarkdownContent :content="post.content" />
<!-- 评论区 -->
<CommentInput />
</div>
</template>
<script setup lang="ts">
import { string } from 'yup';
import MarkdownContent from './PostContent/MarkdownContent.vue'
import CommentInput from './PostContent/CommentInput.vue'
import CommentList from './PostContent/CommentList.vue'
defineProps<{
post: {
id:number,
title:string,
content:string,
author: {
id: number,
name: string,
avatar: string,
bio: string,
},
tags:string[],
},
}>()
function toProfile() {
window.location.href = '/not-found'
}
</script>
<style scoped lang="scss">
.post-content{
.post-header {
display:flex;
min-height:70px;
align-items: center;
margin-bottom: 30px;
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
cursor: pointer;
}
.info {
margin-left: 15px;
.nickname {
font-size: 25px;
font-weight: bold;
}
.meta {
font-size: 15px;
}
}
.follow-btn {
width:100px;
height:50px;
margin-left: auto;
margin-right:50px;
}
}
}
</style>

@ -0,0 +1,133 @@
<template>
<div class="comment-section">
<!-- 评论输入框 -->
<div class="comment-input">
<img class="avatar" src="@/assets/images/默认头像.jpg" @click="goToProfile" />
<input v-model="newComment" placeholder="发布友善的评论" />
<button @click="submitComment">
<i class="icon-send">📨</i>
</button>
</div>
<!-- 评论操作栏 -->
<div class="action-bar">
<i class="icon">👍</i>
<i class="icon">📤</i>
<i class="icon"></i>
</div>
<!-- 评论列表 -->
<div class="comment-list">
<CommentList
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CommentList from './CommentList.vue'
import Avatar from '@/assets/images/默认头像.jpg';
interface Comment {
id: number
user: string
avatar: string
content: string
date: string
}
const newComment = ref('')
const comments = ref<Comment[]>([
{
id: 1,
user: '其他用户昵称',
avatar: Avatar,
content: '这是一条评论……',
date: '日期时间 IP',
},
{
id: 2,
user: '用户B',
avatar: Avatar,
content: '第二条评论',
date: '日期时间 IP',
},
])
function goToProfile() {
window.location.href = '/not-found'
}
function submitComment() {
if (!newComment.value.trim()) return
comments.value.push({
id: Date.now(),
user: '你',
avatar: Avatar,
content: newComment.value,
date: '刚刚',
})
newComment.value = ''
}
</script>
<style scoped lang="scss">
.comment-section {
margin-top: 24px;
.comment-input {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
}
input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 6px;
}
button {
color: white;
border: none;
border-radius: 6px;
padding: 6px 10px;
cursor: pointer;
}
}
.action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-bottom: 16px;
.icon {
cursor: pointer;
font-size: 20px;
&:hover {
}
}
}
.comment-list {
display: flex;
flex-direction: column;
gap: 12px;
}
}
</style>

@ -0,0 +1,84 @@
<template>
<div class="comment-item">
<img class="avatar" :src="comment.avatar" @click="toProfile" />
<div class="content-box">
<div class="user">{{ comment.user }}</div>
<div class="content">{{ comment.content }}</div>
<div class="meta">
<span class="date">{{ comment.date }}</span>
<span class="actions">
<i class="icon" title="点赞">👍</i>
<i class="icon" title="回复">💬</i>
<i class="icon" title="举报">🚩</i>
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
comment: {
id: number
user: string
avatar: string
content: string
date: string
}
}>()
function toProfile() {
window.location.href = '/not-found'
}
</script>
<style scoped lang="scss">
.comment-item {
display: flex;
gap: 10px;
padding: 12px;
border: 1px solid #eee;
border-radius: 6px;
background: #fff;
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
}
.content-box {
flex: 1;
.user {
font-weight: bold;
margin-bottom: 4px;
}
.content {
margin-bottom: 6px;
font-size: 14px;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 12px;
.actions {
display: flex;
gap: 10px;
.icon {
cursor: pointer;
&:hover {
}
}
}
}
}
}
</style>

@ -0,0 +1,29 @@
<template>
<div class="markdown-body">
<h1>这是 Markdown 标题</h1>
<p>这是一段内容这里是模拟 Markdown 的渲染文本</p>
<img src="@/assets/images/LogPage1.jpg" alt="预览图片" />
</div>
</template>
<style scoped lang="scss">
.markdown-body {
font-family: 'Georgia', serif;
line-height: 1.6;
h1 {
font-size: 24px;
margin-bottom: 12px;
}
p {
margin: 10px 0;
}
img {
max-width: 100%;
margin: 10px 0;
border-radius: 6px;
}
}
</style>

@ -0,0 +1,60 @@
<template>
<div class="post-sidebar">
<h2>分类</h2>
<ul class="top-tags">
<li v-for="tag in tags" :key="tag" @click="goTo(tag)">
{{ tag }}
</li>
</ul>
<el-divider />
<div class="sub-menu">
<div v-for="item in subs" :key="item" @click="goTo(item)">{{ item }}</div>
</div>
</div>
</template>
<script setup lang="ts">
const tags = ['计算机学院', '遥感学院', '电子信息学院']
const subs = ['综合', '最新', '热度最高', '用户']
//
function goTo(tag: string) {
// NotFound
window.location.href = '/not-found'
}
</script>
<style scoped lang="scss">
.post-sidebar {
font-size: 16px;
.top-tags {
margin-top: 8px;
padding: 0;
li {
cursor: pointer;
margin: 6px 0;
list-style-type: none;
padding: 0;
&:hover {
font-size:20px;
color:#8a63d2;
}
}
}
.sub-menu {
margin-top: 24px;
div {
margin: 6px 0;
cursor: pointer;
&:hover {
font-size:20px;
color:#8a63d2;
}
}
}
}
</style>

@ -46,6 +46,11 @@ const routes:Array<RouteRecordRaw> = [
path:'/uniLifeHome',
name: 'ForumHome',
component: ForumHome,
},
{
path:'/post/:id',
name:'PostDetail',
component: () => import('@/views/PostDetailPage.vue'),
}
];

@ -63,6 +63,27 @@ const posts = [
tags: ['Ant Design', '设计语言', '蚂蚁金服'],
excerpt: '段落示意:这是帖子的部分具体内容……',
link: '/post/2'
},
{
id: 3,
title: '蚂蚁金服设计平台简介',
tags: ['Ant Design', '设计语言', '蚂蚁金服'],
excerpt: '段落示意:这是帖子的部分具体内容……',
link: '/post/3'
},
{
id: 4,
title: '蚂蚁金服设计平台简介',
tags: ['Ant Design', '设计语言', '蚂蚁金服'],
excerpt: '段落示意:这是帖子的部分具体内容……',
link: '/post/4'
},
{
id: 5,
title: '蚂蚁金服设计平台简介',
tags: ['Ant Design', '设计语言', '蚂蚁金服'],
excerpt: '段落示意:这是帖子的部分具体内容……',
link: '/post/5'
}
]
</script>
@ -71,8 +92,7 @@ const posts = [
.forum-home {
display: flex;
width:92%;
height:98%;
padding-top:75px;
padding-top:750px;
gap: 40px; // 🔧
@ -80,6 +100,7 @@ const posts = [
flex: 3;
display: flex;
flex-direction: column;
margin-right: 350px;
gap: 30px; // 🔧
background: linear-gradient(to bottom right, #f7f1ff, #ffffff);
}
@ -120,7 +141,10 @@ const posts = [
.right-section {
flex: 1;
min-width: 240px;
position:fixed;
margin-left:75%;
height: 830px;
min-width: 350px;
padding: 16px;
background-color: #f9f7ff;
border-radius: 8px;

@ -0,0 +1,77 @@
<template>
<div class = "post-detail-page">
<!-- 左侧分类 包含分类 -->
<SidebarCategory class="sidebar card" />
<!-- 中间内容 帖子的具体内容-->
<PostContent :post="post" class="content card" />
<!-- 右侧作者信息 -->
<AuthorInfo class="author-info card" :author="post.author" />
</div>
</template>
<script setup lang = "ts">
import SidebarCategory from '@/components/PostDetailPage/SidebarCategory.vue'
import PostContent from '@/components/PostDetailPage/PostContent.vue'
import AuthorInfo from '@/components/PostDetailPage/AuthorInfor.vue'
import { useRoute } from 'vue-router'
import {ref, onMounted} from 'vue'
import Avatar from '@/assets/images/默认头像.jpg'
const route = useRoute()
const postId = ref<number>(parseInt(route.params.id as string))
const post = ref({
id: postId.value,
title: '帖子标题示例',
content: '这是帖子的详细内容支持HTML或markdown',
author: {
id: 1,
name: '张三',
avatar: Avatar,
bio: '一名热爱分享的大学生'
},
tags: ['Vue3', '论坛开发', '学习笔记']
})
//
onMounted(() => {
// TODO: fetch(`/api/post/${postId.value}`)
})
</script>
<style scoped lang="scss">
.post-detail-page {
display:flex;
padding-top:550px;
width:92%;
gap:16px;
.sidebar{
flex:1;
position:sticky;
top:20px;
height: fit-content;
border-radius: 8px;
padding: 16px;
flex-shrink: 0;
}
.content{
flex: 8;
padding: 16px;
border-radius: 8px;
}
.author-info{
flex:2;
position:sticky;
flex-shrink:0;
height:fit-content;
border-radius: 8px;
padding: 16px;
}
}
</style>

@ -66,4 +66,9 @@ pnpm install @element-plus/icons-vue
```cmd
pnpm add -D sass-embedded
```
### 适配Markdown
```
pnpm add marked highlight.js
```
Loading…
Cancel
Save