用户可新增分类,前端全部用组合式api重构

master
barney 2 years ago
parent c2c6beba70
commit f2d3b6632b
  1. 20
      README.md
  2. BIN
      db.sqlite3
  3. 2
      drf_vue_blog/settings.py
  4. 90
      frontend/src/components/ArticleList.vue
  5. 26
      frontend/src/components/BlogHeader.vue
  6. 2
      frontend/src/components/CommentView.vue
  7. 2
      frontend/src/utils/authorization.js
  8. 28
      frontend/src/views/ArticleDetailView.vue
  9. 59
      frontend/src/views/ArticleEdit.vue
  10. 63
      frontend/src/views/LoginView.vue
  11. 87
      frontend/src/views/UserCenterView.vue
  12. BIN
      media/avatar/20221002/20210703222015.png
  13. BIN
      media/avatar/20221002/Blog.jpg

@ -1 +1,21 @@
### 基于django3和vue3的博客项目
#### 1、博客首页展示
![image-20221003002818644](C:\Users\15270\AppData\Roaming\Typora\typora-user-images\image-20221003002818644.png)
#### 2.发布文章界面
![image-20221003002550725](C:\Users\15270\AppData\Roaming\Typora\typora-user-images\image-20221003002550725.png)
#### 3.登录、注册界面
![image-20221003002956157](C:\Users\15270\AppData\Roaming\Typora\typora-user-images\image-20221003002956157.png)
#### 4.用户管理界面
![image-20221003002845601](C:\Users\15270\AppData\Roaming\Typora\typora-user-images\image-20221003002845601.png)
#### 5.更新文章界面
![image-20221003003047933](C:\Users\15270\AppData\Roaming\Typora\typora-user-images\image-20221003003047933.png)

Binary file not shown.

@ -122,7 +122,7 @@ STATIC_URL = '/static/'
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
# 每页5条记录
'PAGE_SIZE': 5,
'PAGE_SIZE': 3,
# 使用django-filter后端过滤引擎
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'DEFAULT_AUTHENTICATION_CLASSES': (

@ -1,28 +1,40 @@
<template>
<div v-for="article in articles" :key="article.url" id="articles">
<div class="grid" :style="gridStyle(article)">
<div class="grid" :style="gridStyle()">
<div class="image-container">
<img :src="imageIfExists(article)" alt="" class="image" />
<img
:src="imageIfExists(article)"
alt=""
class="image"
v-if="imageIfExists(article) !== null"
/>
</div>
<div>
</div>
<div>
<router-link
:to="{ name: 'detail', params: { id: article.id } }"
class="article-info"
>
<span v-if="article.category !== null" class="category">
{{ article.category.title }}
</span>
<span v-for="tag in article.tags" v-bind:key="tag" class="tag">
<span
v-for="tag in article.tags"
v-bind:key="tag"
class="tag"
>
{{ tag }}
</span>
<!-- 路由链接:to的两个参数分别是: 路由名称和传入的id -->
<router-link :to="{ name: 'detail', params: { id: article.id } }">
<router-link
:to="{ name: 'detail', params: { id: article.id } }"
>
<div class="article-title">{{ article.title }}</div>
</router-link>
<div>
{{ formatted_time(article.created) }}
</div>
</div>
</router-link>
</div>
</div>
<!-- 实现分页 -->
@ -41,7 +53,7 @@
<script>
import $ from "jquery";
import { onMounted, ref, watch} from "vue";
import { onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
export default {
@ -144,28 +156,28 @@ export default {
const imageIfExists = (article) => {
if (article.avatar) {
return article.avatar.content;
} else {
//
return "http://127.0.0.1:6789/media/avatar/20221002/Blog.jpg";
}
};
const gridStyle = (article) => {
if(article.avatar) {
const gridStyle = () => {
return {
display: 'grid',
gridTemplateColumns: '1fr 1fr 8fr',
}
}
display: "grid",
gridTemplateColumns: "1fr 10fr",
};
};
onMounted(get_article_data);
// watch(()=>router.fullPath, () => {
// console.log("!");
// get_article_data();
// })
//
watch(router,get_article_data);
watch(router, get_article_data);
return {
articles,
@ -188,38 +200,65 @@ export default {
</script>
<style scoped>
#articles {
padding: 1rem;
padding: 0.4rem;
}
.image {
width: 100px;
width: 100%;
border-radius: 10px;
box-shadow: darkslategrey 0 0 12px;
user-select: none;
}
.image-container {
width: 100px;
width: 100%;
display: grid;
place-items: center;
}
.article-info {
width:90%;
border-radius: 10px;
background-color: lightgray;
display: grid;
place-items: start start;
padding-left: 10px;
user-select: none;
position: relative;
left: 20px;
}
.article-info:hover {
cursor: pointer;
}
.grid {
padding-bottom: 10px;
padding-bottom: 5px;
}
a {
text-decoration: none;
}
.article-title {
font-size: 1.5rem;
font-weight: bolder;
text-decoration: none;
color: blue;
padding: 15px 0 15px 0;
color: blueviolet;
padding: 5px 0 5px 0;
}
.tag {
padding: 2px 5px 2px 5px;
padding: 5px 10px 5px 10px;
margin: 5px 5px 5px 0;
font-family: Georgia, Arial, sans-serif;
font-size: small;
background-color: #4e4e4e;
color: whitesmoke;
border-radius: 5px;
border-radius: 15px;
}
#paginator {
@ -237,7 +276,6 @@ export default {
font-weight: bold;
padding-left: 10px;
padding-right: 10px;
}
.category {

@ -71,7 +71,7 @@ export default {
let haslogin = ref(false);
const quit = () => {
window.alert("已退出登录!");
console.log("登出成功,过期时间: ", new Date().toLocaleString());
// console.log(",: ", new Date().toLocaleString());
router.push({ name: "home" });
storage.clear(); //
haslogin.value = false;
@ -95,25 +95,22 @@ export default {
);
});
const refresh = () => {
username.value = localStorage.getItem("username_blog");
}
return {
username,
haslogin,
refresh,
homePage,
quit,
};
},
methods: {
refresh() {
this.username = localStorage.getItem("username_blog");
},
},
}
};
</script>
<style scoped>
/* 样式来源: https://www.runoob.com/css/css-dropdowns.html* /
/* 下拉按钮样式 */
.dropbtn {
width: 10vw;
background-color: mediumslateblue;
@ -155,11 +152,10 @@ export default {
/* 在鼠标移上去后显示下拉菜单 */
.dropdown:hover .dropdown-content {
display: block;
background-color: lightblue;
z-index: 10;
}
/* 当下拉内容显示后修改下拉按钮的背景颜色 */
.dropdown:hover .dropbtn {
background-color: darkslateblue;
}
</style>
<style scoped>
@ -175,7 +171,7 @@ export default {
.grid-1 {
display: grid;
grid-template-columns: 8fr 2fr;
grid-template-columns: 12fr 2fr;
}
.login-link {

@ -60,7 +60,6 @@ export default {
name: "CommentView",
props: {
article: {
type: Object,
required: true,
}
},
@ -102,7 +101,6 @@ export default {
};
const replyTo = (comment) => {
console.log(comment);
parentId.value = comment.id;
placeholder.value = '对' + comment.author.username + '说';
};

@ -51,7 +51,7 @@ const authorization = async () => {
// console.log('authorization exp');
}
console.log('authorization done');
// console.log('authorization done');
return [haslogin, username];
}

@ -36,7 +36,7 @@ import BlogHeader from "@/components/BlogHeader.vue";
import BlogFooter from "@/components/BlogFooter.vue";
import CommentView from "@/components/CommentView.vue";
import $ from "jquery";
import { ref } from "vue";
import { onMounted, ref, computed } from "vue";
import { useRoute } from "vue-router";
export default {
@ -47,34 +47,40 @@ export default {
CommentView,
},
setup() {
let article = ref( );
let article = ref(null); //
const route = useRoute(); // :id
onMounted(() => {
$.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();
};
// vue3使computed
//
let isSuperUser = computed(()=> {
return localStorage.getItem("is_superuser_blog") === "true";
})
return {
article,
formatted_time,
isSuperUser,
};
},
computed: {
isSuperUser() {
return localStorage.getItem("is_superuser_blog") === "true";
},
},
// computed: {
// isSuperUser() {
// return localStorage.getItem("is_superuser_blog") === "true";
// },
// },
};
</script>

@ -20,6 +20,18 @@
{{ category.title }}
</button>
</span>
<div v-if="selectedCategory !== null" style="margin-top:5px; color:red;">:&nbsp;不选分类时可创建新分类!</div>
<div class="newCategory" v-if="selectedCategory === null">
<div>创建新分类:&nbsp;</div>
<br />
<input
v-model="newCategory"
type="text"
placeholder="请输入一个新分类"
/>
</div>
</div>
<div class="form-elem">
@ -79,6 +91,8 @@ export default {
let articleID = ref(null);
//
let categories = ref([]);
//
let newCategory = ref("");
const route = useRoute();
const router = useRouter();
@ -107,7 +121,6 @@ export default {
selectedCategory.value = data.category;
tags.value = data.tags.join(",");
articleID.value = data.id;
console.log(tags.value);
},
});
});
@ -150,10 +163,6 @@ export default {
body: body.value,
};
data.category_id = selectedCategory.value
? selectedCategory.value.id
: null;
//
data.tags = tags.value
//
@ -163,7 +172,34 @@ export default {
//
.filter((x) => x.charAt(0) !== "");
const token = localStorage.getItem("access_blog");
//
if (
selectedCategory.value === null &&
newCategory.value !== null
) {
$.ajax({
url: "http://127.0.0.1:6789/api/category/",
type: "post",
async: false,
data: {
title: newCategory.value,
},
headers: {
Authorization: "Bearer " + localStorage.getItem("access_blog"),
},
success(resp) {
selectedCategory.value = resp;
},
});
}
data.category_id = selectedCategory.value
? selectedCategory.value.id
: null;
$.ajax({
url:
"http://127.0.0.1:6789/api/article/" +
@ -171,10 +207,10 @@ export default {
"/",
type: "PUT",
dataType: "json", // json
contentType:"application/json",
contentType: "application/json",
data: JSON.stringify(data),
headers: {
Authorization: "Bearer " + token,
Authorization: "Bearer " + localStorage.getItem("access_blog"),
},
success(resp) {
router.push({
@ -231,6 +267,7 @@ export default {
submit,
chooseCategory,
deleteArticle,
newCategory,
};
},
};
@ -272,4 +309,10 @@ button {
border-radius: 5px;
width: 60px;
}
.newCategory {
font-size: large;
margin-top: 5px;
margin-bottom: 5px;
}
</style>

@ -26,6 +26,10 @@
<div class="form-elem">
<button v-on:click.prevent="login">登录</button>
</div>
<div v-if="!haslogin">
<div class="error">{{error_msg}}</div>
</div>
</form>
</div>
</div>
@ -38,7 +42,7 @@ import BlogHeader from "@/components/BlogHeader.vue";
import BlogFooter from "@/components/BlogFooter.vue";
import $ from "jquery";
import router from "@/router/index";
import { useStore } from 'vuex';
import { useStore } from "vuex";
export default {
name: "LoginView",
@ -47,12 +51,11 @@ export default {
BlogFooter,
},
setup() {
let username = ref("");
let password = ref("");
let error_msg = ref("");
let haslogin = ref(false); //
const store = useStore();
const login = () => {
$.ajax({
url: "http://127.0.0.1:6789/api/token/",
@ -64,35 +67,49 @@ export default {
success(resp) {
// tokenlocalStorage
const storage = localStorage;
const current = (new Date()).getTime();
const expiredTime = current + 24*3600*1000;
storage.setItem('access_blog', resp.access);
storage.setItem('refresh_blog', resp.refresh);
storage.setItem('expired_time', expiredTime);
storage.setItem('username_blog', username.value);
const current = new Date().getTime();
const expiredTime = current + 24 * 3600 * 1000;
storage.setItem("access_blog", resp.access);
storage.setItem("refresh_blog", resp.refresh);
storage.setItem("expired_time", expiredTime);
storage.setItem("username_blog", username.value);
$.ajax({
url: 'http://127.0.0.1:6789/api/user/' + username.value + '/',
type: 'GET',
success(resp){
url:
"http://127.0.0.1:6789/api/user/" +
username.value +
"/",
type: "GET",
success(resp) {
//
error_msg.value = "";
//
haslogin.value = true;
//
storage.setItem("is_superuser_blog",resp.is_superuser);
store.commit("updateIsSuperUser",JSON.parse(storage.getItem("is_superuser_blog")));
console.log(store.state);
}
storage.setItem(
"is_superuser_blog",
resp.is_superuser
);
store.commit(
"updateIsSuperUser",
JSON.parse(storage.getItem("is_superuser_blog"))
);
},
});
console.log("登录时间: ",new Date().toLocaleString());
router.push({ name: "home" });
},
error(resp) {
console.log(resp);
},
error_msg.value = resp.responseText;
haslogin.value = false;
}
});
};
return {
username,
password,
error_msg,
haslogin,
login,
};
},
@ -129,4 +146,10 @@ button {
span {
font-weight: bold;
}
.error {
color: red;
font-size: 20px;
font-weight: bold;
}
</style>

@ -28,7 +28,7 @@
<div class="form-elem">
<button
v-on:click.prevent="showingDeleteAlert=true"
v-on:click.prevent="showingDeleteAlert = true"
class="delete-btn"
>
删除用户
@ -53,55 +53,54 @@ 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 { useRouter } from "vue-router";
const storage = localStorage;
export default {
name: "UserCenter",
components: { BlogHeader, BlogFooter },
data: function () {
return {
username: "",
password: "",
token: "",
showingDeleteAlert: false,
};
},
mounted() {
this.username = storage.getItem("username_blog");
},
methods: {
confirmDelete() {
const that = this;
setup() {
let username = ref("");
let password = ref("");
let token = ref("");
let showingDeleteAlert = ref(false);
const router = useRouter();
const header = ref(null); // 使refdom
const confirmDelete = () => {
authorization().then((response) => {
if (response[0]) {
//
that.token = storage.getItem("access_blog");
token.value = storage.getItem("access_blog");
$.ajax({
url: 'http://127.0.0.1:6789/api/user/' + that.username + '/',
type: 'DELETE',
url:
"http://127.0.0.1:6789/api/user/" +
username.value +
"/",
type: "DELETE",
headers: {
Authorization: "Bearer " + that.token
Authorization: "Bearer " + token.value,
},
success(resp) {
console.log(resp);
success() {
storage.clear();
that.$router.push({ name: "home" });
}
})
router.push({ name: "home" });
},
});
}
});
},
changeInfo() {
const that = this;
};
const changeInfo = () => {
authorization().then((response) => {
if (response[0] === false) {
window.alert("登录已过期, 请重新登录!");
storage.clear();
that.$router.push({ name: "login" });
router.push({ name: "login" });
return;
}
// 6
if (that.password.length >= 0 && that.password.length < 6) {
if (password.value.length >= 0 && password.value.length < 6) {
window.alert("密码太短了");
return;
}
@ -110,33 +109,47 @@ export default {
const oldName = storage.getItem("username_blog");
//
let data = {};
if (that.username !== "") {
data.username = that.username;
if (username.value !== "") {
data.username = username.value;
}
data.password = that.password;
that.token = storage.getItem("access_blog");
data.password = password.value;
token.value = storage.getItem("access_blog");
//
$.ajax({
url: "http://127.0.0.1:6789/api/user/" + oldName + "/",
type: "PATCH",
data: data,
headers: {
Authorization: "Bearer " + that.token,
Authorization: "Bearer " + token.value,
},
success(resp) {
window.alert("修改成功");
const name = resp.username;
storage.setItem("username_blog", name);
//
that.$router.push({
router.push({
name: "UserCenter",
params: { username: name },
});
that.$refs.header.refresh();
header.value.refresh();
},
});
});
},
};
onMounted(() => {
username.value = storage.getItem("username_blog");
});
return {
username,
password,
token,
showingDeleteAlert,
confirmDelete,
changeInfo,
header,
};
},
};
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Loading…
Cancel
Save