parent
7f08beda6d
commit
dfff087598
16 changed files with 545 additions and 130 deletions
Binary file not shown.
@ -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> |
||||
|
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; // 获取后一页的链接 |
||||
}, |
||||
}); |
||||
}; |
||||
// 在翻页后取得包括page和search的正确路径 |
||||
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>© bnblogs.cc</p> |
||||
</div> |
||||
<div id="footer"> |
||||
<p>© 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…
Reference in new issue