Skip to content

Commit 8eb8af4

Browse files
committed
feat: Add article view and comments
1 parent 04c64a9 commit 8eb8af4

File tree

11 files changed

+1881
-0
lines changed

11 files changed

+1881
-0
lines changed

site/components/Comment.vue

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<template>
2+
<div>
3+
<div class="comment-header">
4+
评论
5+
<span v-if="commentCount > 0">({{ commentCount }})</span>
6+
</div>
7+
<comment-input
8+
v-if="mode === 'markdown'"
9+
ref="input"
10+
:entity-id="entityId"
11+
:entity-type="entityType"
12+
@created="commentCreated"
13+
/>
14+
<comment-text-input
15+
v-else
16+
ref="input"
17+
:entity-id="entityId"
18+
:entity-type="entityType"
19+
@created="commentCreated"
20+
/>
21+
22+
<comment-list
23+
ref="list"
24+
:entity-id="entityId"
25+
:entity-type="entityType"
26+
:comments-page="commentsPage"
27+
@reply="reply"
28+
/>
29+
</div>
30+
</template>
31+
32+
<script>
33+
import CommentList from '~/components/CommentList'
34+
import CommentInput from '~/components/CommentInput'
35+
import CommentTextInput from '~/components/CommentTextInput'
36+
export default {
37+
name: 'Comment',
38+
components: {
39+
CommentList,
40+
CommentInput,
41+
CommentTextInput,
42+
},
43+
props: {
44+
mode: {
45+
type: String,
46+
default: 'markdown',
47+
},
48+
entityType: {
49+
type: String,
50+
default: '',
51+
required: true,
52+
},
53+
entityId: {
54+
type: Number,
55+
default: 0,
56+
required: true,
57+
},
58+
commentsPage: {
59+
type: Object,
60+
default() {
61+
return {}
62+
},
63+
},
64+
commentCount: {
65+
type: Number,
66+
default: 0,
67+
},
68+
showAd: {
69+
type: Boolean,
70+
default: false,
71+
},
72+
},
73+
methods: {
74+
commentCreated(data) {
75+
this.$refs.list.append(data)
76+
},
77+
reply(quote) {
78+
this.$refs.input.reply(quote)
79+
},
80+
},
81+
}
82+
</script>
83+
<style lang="scss" scoped>
84+
.comment-header {
85+
display: flex;
86+
padding-top: 20px;
87+
margin: 0 10px;
88+
border-top: 1px solid rgba(228, 228, 228, 0.6);
89+
color: #6d6d6d;
90+
font-size: 16px;
91+
}
92+
</style>

site/components/CommentInput.vue

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<template>
2+
<div class="comment-form">
3+
<div v-if="isLogin" class="comment-create">
4+
<div ref="commentEditor" class="comment-input-wrapper">
5+
<div v-if="quote" class="comment-quote-info">
6+
回复:
7+
<label v-text="quote.user.nickname" />
8+
<i class="iconfont icon-close" alt="取消回复" @click="cancelReply" />
9+
</div>
10+
<markdown-editor
11+
ref="mdEditor"
12+
v-model="content"
13+
editor-id="createEditor"
14+
height="200px"
15+
placeholder="请发表你的观点..."
16+
@submit="create"
17+
/>
18+
</div>
19+
<div class="comment-button-wrapper">
20+
<span>Ctrl or ⌘ + Enter</span>
21+
<button
22+
class="button is-small is-success"
23+
@click="create"
24+
v-text="btnName"
25+
/>
26+
</div>
27+
</div>
28+
<div v-else class="comment-not-login">
29+
<div class="comment-login-div">
30+
31+
<a style="font-weight: 700;" @click="toLogin">登录</a>后发表观点
32+
</div>
33+
</div>
34+
</div>
35+
</template>
36+
37+
<script>
38+
import MarkdownEditor from '~/components/MarkdownEditor'
39+
40+
export default {
41+
components: {
42+
MarkdownEditor,
43+
},
44+
props: {
45+
entityType: {
46+
type: String,
47+
default: '',
48+
required: true,
49+
},
50+
entityId: {
51+
type: Number,
52+
default: 0,
53+
required: true,
54+
},
55+
},
56+
data() {
57+
return {
58+
content: '', // 内容
59+
sending: false, // 发送中
60+
quote: null, // 引用的对象
61+
}
62+
},
63+
computed: {
64+
btnName() {
65+
return this.sending ? '正在发表...' : '发表'
66+
},
67+
user() {
68+
return this.$store.state.user.current
69+
},
70+
isLogin() {
71+
return this.$store.state.user.current != null
72+
},
73+
},
74+
methods: {
75+
async create() {
76+
if (!this.content) {
77+
this.$message.error('请输入评论内容')
78+
return
79+
}
80+
if (this.sending) {
81+
console.log('正在发送中,请不要重复提交...')
82+
return
83+
}
84+
this.sending = true
85+
try {
86+
const data = await this.$axios.post('/api/comment/create', {
87+
entityType: this.entityType,
88+
entityId: this.entityId,
89+
content: this.content,
90+
quoteId: this.quote ? this.quote.commentId : '',
91+
})
92+
this.$emit('created', data)
93+
this.content = ''
94+
this.$refs.mdEditor.clear()
95+
this.quote = null
96+
} catch (e) {
97+
console.error(e)
98+
this.$message.error('评论失败:' + (e.message || e))
99+
} finally {
100+
this.sending = false
101+
}
102+
},
103+
reply(quote) {
104+
this.quote = quote
105+
this.$refs.commentEditor.scrollIntoView({
106+
block: 'start',
107+
behavior: 'smooth',
108+
})
109+
},
110+
cancelReply() {
111+
this.quote = null
112+
},
113+
toLogin() {
114+
this.$toSignin()
115+
},
116+
},
117+
}
118+
</script>
119+
120+
<style scoped lang="scss"></style>

site/components/CommentList.vue

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<template>
2+
<div class="comments">
3+
<load-more
4+
v-if="commentsPage"
5+
ref="commentsLoadMore"
6+
v-slot="{ results }"
7+
:init-data="commentsPage"
8+
:params="{ entityType: entityType, entityId: entityId }"
9+
url="/api/comment/list"
10+
>
11+
<ul>
12+
<li
13+
v-for="(comment, index) in results"
14+
:key="comment.commentId"
15+
class="comment"
16+
itemprop="comment"
17+
itemscope
18+
itemtype="http://schema.org/Comment"
19+
>
20+
<adsbygoogle
21+
v-if="showAd && (index + 1) % 3 === 0 && index !== 0"
22+
ad-slot="4980294904"
23+
ad-format="fluid"
24+
ad-layout-key="-ht-19-1m-3j+mu"
25+
/>
26+
<div class="comment-avatar">
27+
<avatar :user="comment.user" size="35" />
28+
</div>
29+
<div class="comment-meta">
30+
<span
31+
class="comment-nickname"
32+
itemprop="creator"
33+
itemscope
34+
itemtype="http://schema.org/Person"
35+
>
36+
<a :href="'/user/' + comment.user.id" itemprop="name">
37+
{{ comment.user.nickname }}
38+
</a>
39+
</span>
40+
<span class="comment-time">
41+
<time
42+
:datetime="
43+
comment.createTime | formatDate('yyyy-MM-ddTHH:mm:ss')
44+
"
45+
itemprop="datePublished"
46+
>{{ comment.createTime | prettyDate }}</time
47+
>
48+
</span>
49+
<span class="comment-reply">
50+
<a @click="reply(comment)">回复</a>
51+
</span>
52+
</div>
53+
<div class="comment-content content">
54+
<blockquote v-if="comment.quote" class="comment-quote">
55+
<div class="comment-quote-user">
56+
<avatar :user="comment.quote.user" size="20" />
57+
<a class="quote-nickname">{{ comment.quote.user.nickname }}</a>
58+
<span class="quote-time">
59+
{{ comment.quote.createTime | prettyDate }}
60+
</span>
61+
</div>
62+
<div
63+
v-lazy-container="{ selector: 'img' }"
64+
itemprop="text"
65+
v-html="comment.quote.content"
66+
/>
67+
</blockquote>
68+
<p
69+
v-lazy-container="{ selector: 'img' }"
70+
v-html="comment.content"
71+
/>
72+
</div>
73+
</li>
74+
</ul>
75+
</load-more>
76+
</div>
77+
</template>
78+
79+
<script>
80+
import Avatar from '~/components/Avatar'
81+
import LoadMore from '~/components/LoadMore'
82+
83+
export default {
84+
components: {
85+
Avatar,
86+
LoadMore,
87+
},
88+
props: {
89+
entityType: {
90+
type: String,
91+
default: '',
92+
required: true,
93+
},
94+
entityId: {
95+
type: Number,
96+
default: 0,
97+
required: true,
98+
},
99+
commentsPage: {
100+
type: Object,
101+
default() {
102+
return {}
103+
},
104+
},
105+
showAd: {
106+
type: Boolean,
107+
default: false,
108+
},
109+
},
110+
computed: {
111+
user() {
112+
return this.$store.state.user.current
113+
},
114+
isLogin() {
115+
return this.$store.state.user.current != null
116+
},
117+
},
118+
methods: {
119+
append(data) {
120+
if (!data) return
121+
122+
this.$refs.commentsLoadMore.unshiftResults(data)
123+
},
124+
reply(quote) {
125+
if (!this.isLogin) {
126+
this.$toSignin()
127+
}
128+
this.$emit('reply', quote)
129+
},
130+
cancelReply() {
131+
this.quote = null
132+
},
133+
},
134+
}
135+
</script>
136+
137+
<style scoped lang="scss"></style>

0 commit comments

Comments
 (0)