文章详情,翻页与监听,搜索文章

master
barney 2 years ago
parent 7f08beda6d
commit dfff087598
  1. 3
      article/serializers.py
  2. BIN
      db.sqlite3
  3. 99
      frontend/package-lock.json
  4. 6
      frontend/package.json
  5. 2
      frontend/public/index.html
  6. 28
      frontend/src/App.vue
  7. BIN
      frontend/src/assets/images/search.png
  8. 229
      frontend/src/components/ArticleList.vue
  9. 29
      frontend/src/components/BlogFooter.vue
  10. 109
      frontend/src/components/BlogHeader.vue
  11. 11
      frontend/src/main.js
  12. 23
      frontend/src/router/index.js
  13. 14
      frontend/src/store/index.js
  14. 5
      frontend/src/views/AboutView.vue
  15. 95
      frontend/src/views/ArticleDetailView.vue
  16. 22
      frontend/src/views/HomePageView.vue

@ -3,6 +3,7 @@ from article.models import Article, Category, Tag, Avatar
from user_info.serializers import UserDescSerializer
from comment.serializers import CommentSerializer
class CategorySerializer(serializers.ModelSerializer):
"""所有分类的序列化器"""
# 将路由间的表示转换为超链接
@ -26,6 +27,8 @@ class AvatarSerializer(serializers.ModelSerializer):
class ArticleBaseSerializer(serializers.HyperlinkedModelSerializer):
"""将原来的ArticleSerializer抽象出一个父类"""
# 添加文章id
id = serializers.IntegerField(read_only=True)
author = UserDescSerializer(read_only=True)
# 希望文章接口不仅仅只返回分类的id而已,所以需要显式指定category,将其变成一个嵌套数据,
# 分类的嵌套序列化字段

Binary file not shown.

@ -11,13 +11,17 @@
"axios": "^0.27.2",
"core-js": "^3.8.3",
"jquery": "^3.6.1",
"vue": "^3.2.13"
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
@ -2856,6 +2860,11 @@
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
"dev": true
},
"node_modules/@vue/devtools-api": {
"version": "6.3.0",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.3.0.tgz",
"integrity": "sha512-OfjtreoF3LtHmte3TrWSoZcyL4XWBL5+dTnCARuJZzTCYuaaO29PGMKCKdmXi4CZ0SiN0Exz1IGSo2S5BgDwEQ=="
},
"node_modules/@vue/reactivity": {
"version": "3.2.39",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.39.tgz",
@ -10171,6 +10180,17 @@
"node": ">=8"
}
},
"node_modules/vue-router": {
"version": "4.1.5",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.5.tgz",
"integrity": "sha512-IsvoF5D2GQ/EGTs/Th4NQms9gd2NSqV+yylxIyp/OYp8xOwxmU8Kj/74E9DTSYAyH5LX7idVUngN3JSj1X4xcQ==",
"dependencies": {
"@vue/devtools-api": "^6.1.4"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/vue-style-loader": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
@ -10193,6 +10213,17 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"node_modules/vuex": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/vuex/-/vuex-4.0.2.tgz",
"integrity": "sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==",
"dependencies": {
"@vue/devtools-api": "^6.0.0-beta.11"
},
"peerDependencies": {
"vue": "^3.0.2"
}
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.0.tgz",
@ -12766,8 +12797,7 @@
"version": "5.0.8",
"resolved": "https://registry.npmmirror.com/@vue/cli-plugin-vuex/-/cli-plugin-vuex-5.0.8.tgz",
"integrity": "sha512-HSYWPqrunRE5ZZs8kVwiY6oWcn95qf/OQabwLfprhdpFWAGtLStShjsGED2aDpSSeGAskQETrtR/5h7VqgIlBA==",
"dev": true,
"requires": {}
"dev": true
},
"@vue/cli-service": {
"version": "5.0.8",
@ -13015,6 +13045,11 @@
}
}
},
"@vue/devtools-api": {
"version": "6.3.0",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.3.0.tgz",
"integrity": "sha512-OfjtreoF3LtHmte3TrWSoZcyL4XWBL5+dTnCARuJZzTCYuaaO29PGMKCKdmXi4CZ0SiN0Exz1IGSo2S5BgDwEQ=="
},
"@vue/reactivity": {
"version": "3.2.39",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.39.tgz",
@ -13273,15 +13308,13 @@
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
"integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
"dev": true,
"requires": {}
"dev": true
},
"acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"requires": {}
"dev": true
},
"acorn-walk": {
"version": "8.2.0",
@ -13340,8 +13373,7 @@
"version": "3.5.2",
"resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"requires": {}
"dev": true
},
"ansi-colors": {
"version": "4.1.3",
@ -14165,8 +14197,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmmirror.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz",
"integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==",
"dev": true,
"requires": {}
"dev": true
},
"css-loader": {
"version": "6.7.1",
@ -14337,8 +14368,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz",
"integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==",
"dev": true,
"requires": {}
"dev": true
},
"csso": {
"version": "4.2.0",
@ -15808,8 +15838,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmmirror.com/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"dev": true,
"requires": {}
"dev": true
},
"ieee754": {
"version": "1.2.1",
@ -17179,29 +17208,25 @@
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz",
"integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-discard-duplicates": {
"version": "5.1.0",
"resolved": "https://registry.npmmirror.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz",
"integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-discard-empty": {
"version": "5.1.1",
"resolved": "https://registry.npmmirror.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz",
"integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-discard-overridden": {
"version": "5.1.0",
"resolved": "https://registry.npmmirror.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz",
"integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-loader": {
"version": "6.2.1",
@ -17291,8 +17316,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-modules-local-by-default": {
"version": "4.0.0",
@ -17327,8 +17351,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmmirror.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz",
"integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-normalize-display-values": {
"version": "5.1.0",
@ -18822,6 +18845,14 @@
}
}
},
"vue-router": {
"version": "4.1.5",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.5.tgz",
"integrity": "sha512-IsvoF5D2GQ/EGTs/Th4NQms9gd2NSqV+yylxIyp/OYp8xOwxmU8Kj/74E9DTSYAyH5LX7idVUngN3JSj1X4xcQ==",
"requires": {
"@vue/devtools-api": "^6.1.4"
}
},
"vue-style-loader": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
@ -18846,6 +18877,14 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"vuex": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/vuex/-/vuex-4.0.2.tgz",
"integrity": "sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==",
"requires": {
"@vue/devtools-api": "^6.0.0-beta.11"
}
},
"watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.0.tgz",
@ -19149,8 +19188,7 @@
"version": "8.9.0",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.9.0.tgz",
"integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==",
"dev": true,
"requires": {}
"dev": true
}
}
},
@ -19277,8 +19315,7 @@
"version": "7.5.9",
"resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"dev": true,
"requires": {}
"dev": true
},
"y18n": {
"version": "5.0.8",

@ -11,13 +11,17 @@
"axios": "^0.27.2",
"core-js": "^3.8.3",
"jquery": "^3.6.1",
"vue": "^3.2.13"
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"

@ -7,7 +7,7 @@
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<body style="margin: 0;">
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>

@ -1,29 +1,17 @@
<template>
<BlogHeader />
<ArticleList />
<BlogFooter />
<router-view />
</template>
<script>
import ArticleList from '@/components/ArticleList.vue'
import BlogHeader from '@/components/BlogHeader.vue'
import BlogFooter from '@/components/BlogFooter.vue'
export default {
name: 'App',
components: {
ArticleList,
BlogHeader,
BlogFooter,
}
}
export default {
name: "App",
};
</script>
<style>
#app {
font-family: Georgia, Arial, sans-serif;
margin-left: 40px;
margin-right: 40px;
font-family: Georgia, Arial, sans-serif;
margin-left: 40px;
margin-right: 40px;
}
</style>
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -1,73 +1,198 @@
<template>
<div v-for="article in articles" :key="article.url" id="articles">
<span
v-for="tag in article.tags"
v-bind:key="tag"
class="tag"
>
{{ tag }}
</span>
<div class="article-title">
{{ article.title }}
</div>
{{ formatted_time(article.created) }}
</div>
<div v-for="article in articles" :key="article.url" id="articles">
<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 } }">
<div class="article-title">{{ article.title }}</div>
</router-link>
<div>
{{ formatted_time(article.created) }}
</div>
</div>
<!-- 实现分页 -->
<div id="paginator">
<span v-if="is_page_exists('previous')">
<router-link
:to="get_path('previous')"
>
Prev
</router-link>
</span>
<span class="current-page">
{{ get_page_param("current") }}
</span>
<span v-if="is_page_exists('next')">
<router-link
:to="get_path('next')"
>
Next
</router-link>
</span>
</div>
</template>
<script>
import $ from 'jquery'
import { ref } from 'vue'
import $ from "jquery";
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
export default {
name: "ArticleList",
setup() {
name: "ArticleList",
setup() {
onMounted(()=>{
get_article_data();
})
let articles = ref([]); //
let previous = ref(null); // url
let next = ref(null); // url
let articles = ref([]);
$.ajax({
url: "http://127.0.0.1:6789/api/article/",
type: "GET",
success: resp => {
articles.value = resp.results
}
});
let router = useRoute();
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();
};
//
const is_page_exists = (direction) => {
if (direction === "next") {
return next.value !== null;
}
return previous.value !== null;
};
//
const get_page_param = (direction) => {
try {
let url_string;
switch (direction) {
case "next":
url_string = next.value;
break;
case "previous":
url_string = previous.value;
if (
url_string === "http://127.0.0.1:6789/api/article/"
) {
url_string =
"http://127.0.0.1:6789/api/article/?page=1";
}
break;
default:
//
if (!('page' in router.query)) return 1;
if (router.query.page === null) return 1;
return router.query.page;
}
const url = new URL(url_string);
return url.searchParams.get("page");
} catch (error) {
return;
}
};
return {
articles,
formatted_time,
}
}
}
</script>
const get_article_data = () => {
let article_url = "http://127.0.0.1:6789/api/article"; // BaseUrl
let params = new URLSearchParams();
// appendIfExists
// append
// url
params.appendIfExists('page', router.query.page);
params.appendIfExists('search', router.query.search);
//
const paramsString = params.toString();
if (paramsString.charAt(0) !== '') {
article_url += '/?' + paramsString;
}
$.ajax({
url: article_url,
type: "GET",
success: (resp) => {
articles.value = resp.results; //
previous.value = resp.previous; //
next.value = resp.next; //
},
});
};
// pagesearch
const get_path = (direction) => {
let url = "";
try {
switch(direction) {
case 'next':
if (next.value !== undefined) {
url += (new URL(next.value)).search
}
break;
case 'previous':
if (previous.value !== undefined) {
url += (new URL(previous.value)).search
}
break;
}
}
catch { return url}
return url;
}
<style scoped>
return {
articles,
formatted_time,
is_page_exists,
get_page_param,
get_article_data,
get_path,
};
},
watch: {
//
$route() {
this.get_article_data();
},
},
};
</script>
<style scoped>
#articles {
padding: 10px;
padding: 1rem;
}
.article-title {
font-size: large;
font-weight: bolder;
color: black;
text-decoration: none;
padding: 5px 0 5px 0;
font-size: 1.2rem;
font-weight: bolder;
text-decoration: none;
color: blue;
padding: 5px 0 5px 0;
}
.tag {
padding: 2px 5px 2px 5px;
margin: 5px 5px 5px 0;
font-family: Georgia, Arial, sans-serif;
font-size: small;
background-color: #4e4e4e;
color: whitesmoke;
border-radius: 5px;
padding: 2px 5px 2px 5px;
margin: 5px 5px 5px 0;
font-family: Georgia, Arial, sans-serif;
font-size: small;
background-color: #4e4e4e;
color: whitesmoke;
border-radius: 5px;
}
#paginator {
text-align: center;
padding-top: 2rem;
}
</style>
#paginator a {
color: black;
}
.current-page {
font-size: x-large;
font-weight: bold;
padding-left: 10px;
padding-right: 10px;
}
</style>

@ -1,27 +1,26 @@
<template>
<div id="footer">
<p>&copy; bnblogs.cc</p>
</div>
<div id="footer">
<p>&copy; bnblogs.cc</p>
</div>
</template>
<script>
export default {
name: "BlogFooter",
}
name: "BlogFooter",
};
</script>
<style scoped>
#footer {
position: fixed;
left: 0;
bottom: 0;
height: 50px;
width: 100%;
background: whitesmoke;
text-align: center;
font-weight: bold;
position: fixed;
left: 0;
bottom: 0;
height: 50px;
width: 100%;
background: whitesmoke;
text-align: center;
font-weight: bold;
}
</style>

@ -1,22 +1,113 @@
<template>
<div id="header">
<h1>My Drf-Vue Blog</h1>
<hr>
</div>
<div id="header">
<div class="grid">
<div></div>
<h1>My Drf-Vue Blog</h1>
<div class="search">
<form>
<input v-model="searchText" type="text" placeholder="输入搜索内容..." />
<button v-on:click.prevent="searchArticles">
</button>
</form>
</div>
<hr>
</div>
</div>
</template>
<script>
import { ref } from "vue"
import router from '@/router/index';
export default {
name: "BlogHeader",
}
name: "BlogHeader",
setup() {
let searchText = ref('');
const searchArticles = () => {
const text = searchText.value.trim();
if (text.charAt(0) !== '') {
router.push({name:'home', query: {search: text }})
}
}
return {
searchText,
searchArticles,
}
}
};
</script>
<style scoped>
#header {
text-align: center;
margin-top: 20px;
text-align: center;
margin-top: 20px;
}
.grid {
display: grid;
grid-template-columns: 1fr 4fr 1fr;
}
.search {
padding-top: 22px;
}
/* 搜索框样式 */
* {
box-sizing: border-box;
}
form {
position: relative;
width: 20vw;
margin: 0 auto;
}
input,
button {
outline: none;
}
input {
border-color: gray;
width: 100%;
height: 30px;
padding-left: 13px;
padding-right: 46px;
overflow:hidden;
text-overflow:ellipsis;
}
button {
border: none;
height: 30px;
width: 30px;
cursor: pointer;
position: absolute;
}
.search input {
border: 2px solid gray;
border-radius: 5px;
background: transparent;
top: 0;
right: 0;
}
.search button {
background: gray;
border-radius: 0 5px 5px 0;
width: 45px;
top: 0;
right: 0;
}
.search button:before {
content: "搜索";
font-size: 13px;
color: white;
}
</style>

@ -1,4 +1,13 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
createApp(App).mount('#app')
URLSearchParams.prototype.appendIfExists = function (key, value) {
if (value !== null && value !== undefined) {
this.append(key, value)
}
};
createApp(App).use(store).use(router).mount('#app')

@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from '@/views/HomePageView.vue'
import ArticleDetail from '@/views/ArticleDetailView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomePage
},
{
path: '/detail/:id',
name: 'detail',
component: ArticleDetail,
},
]
const router = createRouter({
history: createWebHistory(), // 路径中不再有恶心的#号
routes
})
export default router

@ -0,0 +1,14 @@
import { createStore } from 'vuex'
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})

@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

@ -0,0 +1,95 @@
<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>
<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';
export default {
name: "ArticleDetail",
components: {
BlogHeader,
BlogFooter,
},
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)
}
});
const formatted_time = (iso_date_string) => {
const date = new Date(iso_date_string);
return date.toLocaleDateString();
}
return {
article,
formatted_time,
}
},
};
</script>
<style scoped>
.grid-container {
display: grid;
grid-template-columns: 3fr 1fr;
}
#title {
text-align: center;
font-size: x-large;
}
#subtitle {
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;
}
.toc ul {
list-style-type: disc;
padding-inline-start: 15px;
}
.toc a {
color: gray;
}
</style>

@ -0,0 +1,22 @@
<template>
<BlogHeader />
<ArticleList />
<BlogFooter />
</template>
<script>
import ArticleList from "@/components/ArticleList.vue";
import BlogHeader from "@/components/BlogHeader.vue";
import BlogFooter from "@/components/BlogFooter.vue";
export default {
name: "HomePage",
components: {
ArticleList,
BlogHeader,
BlogFooter,
},
};
</script>
<style></style>
Loading…
Cancel
Save