@ -0,0 +1,170 @@ |
||||
<template> |
||||
<br /><br /> |
||||
<hr /> |
||||
<h3>发表评论</h3> |
||||
<!-- 评论多行文本输入控件 --> |
||||
<textarea |
||||
v-model="message" |
||||
:placeholder="placeholder" |
||||
name="comment" |
||||
id="comment-area" |
||||
cols="60" |
||||
rows="10" |
||||
></textarea> |
||||
<div> |
||||
<button @click="submit" class="submitBtn">发布</button> |
||||
</div> |
||||
|
||||
<br /> |
||||
<p>已有 {{ comments.length }} 条评论</p> |
||||
<hr /> |
||||
|
||||
<!-- 渲染所有评论内容 --> |
||||
<div v-for="comment in comments" :key="comment.id"> |
||||
<div class="comments"> |
||||
<div> |
||||
<span class="username"> |
||||
{{ comment.author.username }} |
||||
</span> |
||||
于 |
||||
<span class="created"> |
||||
{{ formatted_time(comment.created) }} |
||||
</span> |
||||
<span v-if="comment.parent"> |
||||
对 |
||||
<span class="parent"> |
||||
{{ comment.parent.author.username }} |
||||
</span> |
||||
</span> |
||||
说道: |
||||
</div> |
||||
<div class="content"> |
||||
{{ comment.content }} |
||||
</div> |
||||
<div> |
||||
<button class="commentBtn" @click="replyTo(comment)"> |
||||
回复 |
||||
</button> |
||||
</div> |
||||
</div> |
||||
<hr /> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import $ from 'jquery'; |
||||
import authorization from '@/utils/authorization' |
||||
import { ref, watchEffect } from 'vue'; |
||||
|
||||
export default { |
||||
name: "CommentView", |
||||
props: { |
||||
article: { |
||||
type: Object, |
||||
required: true, |
||||
} |
||||
}, |
||||
setup(props) { |
||||
// 所有评论 |
||||
let comments = ref([]); |
||||
// 评论控件绑定的文本和占位符 |
||||
let message = ref(''); |
||||
let placeholder = ref('说点啥吧...'); |
||||
// 评论的评论 |
||||
let parentId = ref(null); |
||||
// 发表评论 |
||||
const submit = () => { |
||||
authorization().then((response) => { |
||||
if (response[0]) { |
||||
$.ajax({ |
||||
url: 'http://127.0.0.1:6789/api/comment/', |
||||
type: 'POST', |
||||
data: { |
||||
content: message.value, |
||||
article_id: props.article.id, |
||||
parent_id: parentId.value, |
||||
|
||||
}, |
||||
headers: { |
||||
authorization: "Bearer " + localStorage.getItem('access_blog') |
||||
}, |
||||
success(resp) { |
||||
// 将新评论添加到顶部 |
||||
comments.value.unshift(resp); |
||||
message.value = ''; |
||||
window.alert("留言成功"); |
||||
} |
||||
}) |
||||
}else { |
||||
window.alert("请登录后再评论!"); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
const replyTo = (comment) => { |
||||
console.log(comment); |
||||
parentId.value = comment.id; |
||||
placeholder.value = '对' + comment.author.username + '说'; |
||||
}; |
||||
|
||||
const formatted_time = (iso_date_string) => { |
||||
const date = new Date(iso_date_string); |
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); |
||||
}; |
||||
|
||||
watchEffect(() => { |
||||
comments.value = props.article !== null ? props.article.comments : []; |
||||
}); |
||||
|
||||
return { |
||||
comments, |
||||
message, |
||||
placeholder, |
||||
parentId, |
||||
submit, |
||||
replyTo, |
||||
formatted_time, |
||||
} |
||||
}, |
||||
}; |
||||
</script> |
||||
|
||||
<style scoped> |
||||
button { |
||||
cursor: pointer; |
||||
border: none; |
||||
outline: none; |
||||
color: whitesmoke; |
||||
border-radius: 5px; |
||||
} |
||||
.submitBtn { |
||||
height: 35px; |
||||
background: steelblue; |
||||
width: 60px; |
||||
} |
||||
.commentBtn { |
||||
height: 25px; |
||||
background: lightslategray; |
||||
width: 40px; |
||||
margin-bottom: 40px; |
||||
} |
||||
.comments { |
||||
padding-top: 10px; |
||||
} |
||||
.username { |
||||
font-weight: bold; |
||||
color: darkorange; |
||||
} |
||||
.created { |
||||
font-weight: bold; |
||||
color: darkblue; |
||||
} |
||||
.parent { |
||||
font-weight: bold; |
||||
color: orangered; |
||||
} |
||||
.content { |
||||
font-size: large; |
||||
padding: 15px; |
||||
} |
||||
</style> |
@ -1,95 +1,114 @@ |
||||
<template> |
||||
<BlogHeader /> |
||||
<div v-if="article !== null" class="grid-container"> |
||||
<div> |
||||
<h1 id='title'>{{article.title}}</h1> |
||||
<p id="subtitle"> |
||||
本文由{{article.author.username}} 发布于 {{formatted_time(article.created)}} |
||||
</p> |
||||
<div v-html='article.body_html' class="article-body"> |
||||
</div> |
||||
</div> |
||||
<div v-if="article !== null" class="grid-container"> |
||||
<div> |
||||
<h1 id="title">{{ article.title }}</h1> |
||||
<p id="subtitle"> |
||||
本文由{{ article.author.username }} 发布于 |
||||
{{ formatted_time(article.created) }} |
||||
<span v-if="isSuperUser"> |
||||
<router-link |
||||
:to="{ |
||||
name: 'ArticleEdit', |
||||
params: { id: article.id }, |
||||
}" |
||||
> |
||||
更新与删除 |
||||
</router-link> |
||||
</span> |
||||
</p> |
||||
<div v-html="article.body_html" class="article-body"></div> |
||||
</div> |
||||
|
||||
<div> |
||||
<h3>目录</h3> |
||||
<div v-html="article.toc_html" class="toc"></div> |
||||
</div> |
||||
</div> |
||||
|
||||
<CommentView :article="article" /> |
||||
|
||||
<div> |
||||
<h3>目录</h3> |
||||
<div v-html='article.toc_html' class="toc"></div> |
||||
</div> |
||||
</div> |
||||
<BlogFooter /> |
||||
</template> |
||||
|
||||
<script> |
||||
import BlogHeader from "@/components/BlogHeader.vue"; |
||||
import BlogFooter from "@/components/BlogFooter.vue"; |
||||
import $ from 'jquery'; |
||||
import { ref } from 'vue'; |
||||
import { useRoute } from 'vue-router'; |
||||
import CommentView from "@/components/CommentView.vue"; |
||||
import $ from "jquery"; |
||||
import { ref } from "vue"; |
||||
import { useRoute } from "vue-router"; |
||||
|
||||
export default { |
||||
name: "ArticleDetail", |
||||
components: { |
||||
BlogHeader, |
||||
BlogFooter, |
||||
CommentView, |
||||
}, |
||||
setup() { |
||||
let article = ref(null); |
||||
const route = useRoute(); // 用来解析路由动态参数:id |
||||
$.ajax({ |
||||
url: "http://127.0.0.1:6789/api/article/" + route.params.id, |
||||
type: 'GET', |
||||
success(resp){ |
||||
article.value=resp; |
||||
}, |
||||
error(resp) { |
||||
console.log(resp) |
||||
} |
||||
}); |
||||
let article = ref( ); |
||||
const route = useRoute(); // 用来解析路由动态参数:id |
||||
$.ajax({ |
||||
url: "http://127.0.0.1:6789/api/article/" + route.params.id, |
||||
type: "GET", |
||||
success(resp) { |
||||
article.value = resp; |
||||
}, |
||||
error(resp) { |
||||
console.log(resp); |
||||
}, |
||||
}); |
||||
|
||||
const formatted_time = (iso_date_string) => { |
||||
const date = new Date(iso_date_string); |
||||
return date.toLocaleDateString(); |
||||
} |
||||
const formatted_time = (iso_date_string) => { |
||||
const date = new Date(iso_date_string); |
||||
return date.toLocaleDateString(); |
||||
}; |
||||
|
||||
return { |
||||
article, |
||||
formatted_time, |
||||
} |
||||
}, |
||||
return { |
||||
article, |
||||
formatted_time, |
||||
}; |
||||
}, |
||||
computed: { |
||||
isSuperUser() { |
||||
return localStorage.getItem("is_superuser_blog") === "true"; |
||||
}, |
||||
}, |
||||
}; |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.grid-container { |
||||
display: grid; |
||||
grid-template-columns: 3fr 1fr; |
||||
display: grid; |
||||
grid-template-columns: 3fr 1fr; |
||||
} |
||||
|
||||
#title { |
||||
text-align: center; |
||||
font-size: x-large; |
||||
text-align: center; |
||||
font-size: x-large; |
||||
} |
||||
|
||||
#subtitle { |
||||
text-align: center; |
||||
color: gray; |
||||
font-size: small; |
||||
text-align: center; |
||||
color: gray; |
||||
font-size: small; |
||||
} |
||||
</style> |
||||
|
||||
<style> |
||||
.article-body p img { |
||||
max-width: 100%; |
||||
border-radius: 50px; |
||||
box-shadow: gray 0 0 20px; |
||||
|
||||
max-width: 100%; |
||||
border-radius: 50px; |
||||
box-shadow: gray 0 0 20px; |
||||
} |
||||
|
||||
.toc ul { |
||||
list-style-type: disc; |
||||
padding-inline-start: 15px; |
||||
list-style-type: disc; |
||||
padding-inline-start: 15px; |
||||
} |
||||
|
||||
.toc a { |
||||
color: gray; |
||||
color: gray; |
||||
} |
||||
</style> |
||||
|
@ -0,0 +1,275 @@ |
||||
<template> |
||||
<BlogHeader /> |
||||
<div id="article-create"> |
||||
<h3>更新文章</h3> |
||||
<form> |
||||
<div class="form-elem"> |
||||
<span>标题:</span> |
||||
<input v-model="title" type="text" placeholder="输入标题" /> |
||||
</div> |
||||
|
||||
<div class="form-elem"> |
||||
<span>分类:</span> |
||||
<span v-for="category in categories" :key="category.id"> |
||||
<!--样式也可以通过 :style 绑定--> |
||||
<button |
||||
class="category-btn" |
||||
:style="categoryStyle(category)" |
||||
@click.prevent="chooseCategory(category)" |
||||
> |
||||
{{ category.title }} |
||||
</button> |
||||
</span> |
||||
</div> |
||||
|
||||
<div class="form-elem"> |
||||
<span>标签:</span> |
||||
<input |
||||
v-model="tags" |
||||
type="text" |
||||
placeholder="输入标签,用逗号分隔" |
||||
/> |
||||
</div> |
||||
|
||||
<div class="form-elem"> |
||||
<div style="margin-bottom: 10px">正文:</div> |
||||
<textarea |
||||
v-model="body" |
||||
placeholder="输入正文" |
||||
rows="20" |
||||
cols="100" |
||||
></textarea> |
||||
</div> |
||||
|
||||
<div class="form-elem"> |
||||
<button v-on:click.prevent="submit">提交</button> |
||||
<button |
||||
v-on:click.prevent="deleteArticle" |
||||
style="background-color: darkred; margin-left: 30px" |
||||
> |
||||
删除 |
||||
</button> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
<BlogFooter /> |
||||
</template> |
||||
|
||||
<script> |
||||
import BlogHeader from "@/components/BlogHeader.vue"; |
||||
import BlogFooter from "@/components/BlogFooter.vue"; |
||||
import authorization from "@/utils/authorization"; |
||||
import $ from "jquery"; |
||||
import { onMounted, ref } from "vue"; |
||||
import { useRoute, useRouter } from "vue-router"; |
||||
|
||||
export default { |
||||
name: "ArticleEdit", |
||||
components: { BlogHeader, BlogFooter }, |
||||
setup() { |
||||
// 文章标题 |
||||
let title = ref(""); |
||||
// 文章内容 |
||||
let body = ref(""); |
||||
// 文章的分类 |
||||
let selectedCategory = ref(null); |
||||
// 标签 |
||||
let tags = ref(""); |
||||
// 文章id |
||||
let articleID = ref(null); |
||||
// 所有分类 |
||||
let categories = ref([]); |
||||
|
||||
const route = useRoute(); |
||||
const router = useRouter(); |
||||
|
||||
// 页面初始化时获取所有分类 |
||||
|
||||
onMounted(() => { |
||||
$.ajax({ |
||||
url: "http://127.0.0.1:6789/api/category", |
||||
type: "GET", |
||||
success(resp) { |
||||
categories.value = resp; |
||||
}, |
||||
}); |
||||
|
||||
$.ajax({ |
||||
url: |
||||
"http://127.0.0.1:6789/api/article/" + |
||||
route.params.id + |
||||
"/", |
||||
type: "GET", |
||||
success(resp) { |
||||
const data = resp; |
||||
title.value = data.title; |
||||
body.value = data.body; |
||||
selectedCategory.value = data.category; |
||||
tags.value = data.tags.join(","); |
||||
articleID.value = data.id; |
||||
console.log(tags.value); |
||||
}, |
||||
}); |
||||
}); |
||||
|
||||
// 根据分类是否被选中,按钮的颜色发生变化 |
||||
const categoryStyle = (category) => { |
||||
if ( |
||||
selectedCategory.value !== null && |
||||
category.id === selectedCategory.value.id |
||||
) { |
||||
return { |
||||
backgroundColor: "black", |
||||
}; |
||||
} |
||||
return { |
||||
backgroundColor: "lightgrey", |
||||
color: "black", |
||||
}; |
||||
}; |
||||
|
||||
// 选取分类 |
||||
const chooseCategory = (category) => { |
||||
// 如果点击已选取的分类,则将 selectedCategory 置空 |
||||
if ( |
||||
selectedCategory.value !== null && |
||||
selectedCategory.value.id === category.id |
||||
) { |
||||
selectedCategory.value = null; |
||||
} else { |
||||
selectedCategory.value = category; |
||||
} |
||||
}; |
||||
|
||||
// 点击提交按钮 |
||||
const submit = () => { |
||||
authorization().then((response) => { |
||||
if (response[0]) { |
||||
let data = { |
||||
title: title.value, |
||||
body: body.value, |
||||
}; |
||||
|
||||
data.category_id = selectedCategory.value |
||||
? selectedCategory.value.id |
||||
: null; |
||||
|
||||
// 标签预处理 |
||||
data.tags = tags.value |
||||
// 用逗号分隔标签 |
||||
.split(/[,,]/) |
||||
// 剔除标签首尾空格 |
||||
.map((x) => x.trim()) |
||||
// 剔除长度为零的无效标签 |
||||
.filter((x) => x.charAt(0) !== ""); |
||||
|
||||
const token = localStorage.getItem("access_blog"); |
||||
$.ajax({ |
||||
url: |
||||
"http://127.0.0.1:6789/api/article/" + |
||||
articleID.value + |
||||
"/", |
||||
type: "PUT", |
||||
dataType: "json", // 传入json数据,需要写这三行 |
||||
contentType:"application/json", |
||||
data: JSON.stringify(data), |
||||
headers: { |
||||
Authorization: "Bearer " + token, |
||||
}, |
||||
success(resp) { |
||||
router.push({ |
||||
name: "detail", |
||||
params: { id: resp.id }, |
||||
}); |
||||
}, |
||||
error() { |
||||
window.alert("令牌过期,请重新登录!"); |
||||
}, |
||||
}); |
||||
} else { |
||||
window.alert("令牌过期,请重新登录!"); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
const deleteArticle = () => { |
||||
authorization().then((response) => { |
||||
const token = localStorage.getItem("access_blog"); |
||||
if (response[0]) { |
||||
$.ajax({ |
||||
url: |
||||
"http://127.0.0.1:6789/api/article/" + |
||||
articleID.value + |
||||
"/", |
||||
type: "DELETE", |
||||
headers: { |
||||
Authorization: "Bearer " + token, |
||||
}, |
||||
success() { |
||||
router.push({ |
||||
name: "home", |
||||
}); |
||||
}, |
||||
error() { |
||||
window.alert("令牌过期,请重新登录!"); |
||||
}, |
||||
}); |
||||
} else { |
||||
alert("令牌过期,请重新登录。"); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
return { |
||||
categories, |
||||
title, |
||||
body, |
||||
tags, |
||||
selectedCategory, |
||||
categoryStyle, |
||||
articleID, |
||||
submit, |
||||
chooseCategory, |
||||
deleteArticle, |
||||
}; |
||||
}, |
||||
}; |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.category-btn { |
||||
margin-right: 10px; |
||||
} |
||||
#article-create { |
||||
text-align: center; |
||||
font-size: large; |
||||
} |
||||
form { |
||||
text-align: left; |
||||
padding-left: 100px; |
||||
padding-right: 10px; |
||||
padding-bottom: 50px; |
||||
} |
||||
.form-elem { |
||||
padding: 10px; |
||||
} |
||||
.form-elem-1 { |
||||
display: inline-block; |
||||
padding: 10px; |
||||
} |
||||
input { |
||||
height: 25px; |
||||
padding-left: 10px; |
||||
width: 50%; |
||||
} |
||||
button { |
||||
height: 35px; |
||||
cursor: pointer; |
||||
border: none; |
||||
outline: none; |
||||
background: steelblue; |
||||
color: whitesmoke; |
||||
border-radius: 5px; |
||||
width: 60px; |
||||
} |
||||
</style> |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 943 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1006 B |
After Width: | Height: | Size: 1.5 KiB |