制作博客主页及单篇博客界面

This commit is contained in:
Guarp 2025-03-10 12:37:25 +08:00
parent 82380bdc86
commit add84cc1c2
7 changed files with 251 additions and 29 deletions

View File

@ -1,4 +1,6 @@
<script setup>
import {setRandomBGCL} from "../utils/randomBGCL.js";
defineProps({
blog: Object,
});
@ -6,12 +8,17 @@ defineProps({
<template>
<router-link :to="'/blog/'+blog.id">
<div class="blog-box">
<img :src="blog.image" alt="Blog image" class="blog-image" />
<div class="blog-box" :style="{background: setRandomBGCL(blog.title)}">
<div class="image-container" :style="{background: setRandomBGCL(blog.title)}">
<div class="cover-character">{{ blog.title[0] }}</div>
<img v-if="blog.image" :src="blog.image" alt="Blog image" class="blog-image" />
</div>
<div class="blog-details">
<h3>{{ blog.title }}</h3>
<p class="author">By {{ blog.author }}</p>
<p class="time"> {{ blog.creatTime }}</p>
<p class="author">By uid{{ blog.poster }}</p>
<p class="time"> {{ blog.post_date }}</p>
<div class="tags">
<span v-for="tag in blog.tags" :key="tag" class="tag">{{ tag }}</span>
</div>
@ -32,7 +39,19 @@ defineProps({
cursor: pointer;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.image-container {
width: 100%;
height: 100%;
}
.image-container .cover-character {
width: 100%;
height: 100%;
font-size: 200px;;
font-family: 'huangkaihua', Tahoma, Geneva, Verdana, sans-serif;
color: gray;
mix-blend-mode: difference;
}
.blog-image {
width: 100%;
height: 200px;
@ -40,9 +59,14 @@ defineProps({
}
.blog-details {
background: #111111;
width: 100%;
padding: 10px;
text-align: center;
}
.theme-light .blog-details {
background: white;
}
.blog-details h3 {
margin: 5px 0;

View File

@ -1,21 +1,21 @@
<script setup>
import { ref } from 'vue';
import {onMounted, ref} from 'vue';
import BlogBox from "../components/Blog_box.vue";
import {getRandomIMG} from "../utils/randomIMG.js";
import PagingController from "../components/PagingController.vue";
import api from "../utils/axios.js";
import store from "../store/index.js";
const tags = ref([
{ name: '家猪饲养', active: false },
{ name: '猪舍管理', active: false },
]);
const tags = ref([]);
const searchQuery = ref('');
const blogs = ref([
{ title: '如何提升家猪的生长速度', author: '张强', image: null, creatTime: '2025-02-12 08:30', tags: ['家猪饲养', '生长调控'], id: 1}])
const blogs = ref(store.state.sessionStore.blog?.blogs || []);
const amount = ref(store.state.sessionStore.blog?.blogAmount);
const currentPage = ref(store.state.sessionStore.blog?.BlogCurrentPage);
const loadingText = ref('正在加载...');
const toggleTag = (tag) => {
tag.active = !tag.active;
@ -28,23 +28,54 @@ const filterBlogs = () => {
return (matchesTags || tags.value.every(t => !t.active)) && matchesSearch;
});
};
const refreshBlogs = async (page, size, sort) => {
size = size || 20;
sort = sort || 'post_date';
const tempPage = currentPage.value;
currentPage.value = page;
const blogsResponse = await api.get(`/blogs?page=${page}&size=${size}&sort=${sort}`);
if (!blogsResponse.amount) {
currentPage.value = tempPage;
return;
}
amount.value = Math.ceil(blogsResponse.amount / size);
blogs.value = blogsResponse.blogs;
currentPage.value = page;
store.commit('setSessionValue', {key: 'blog', value: {blogs: blogs, blogAmount: amount, blogCurrentPage: currentPage}});
console.log(store.state.sessionStore.blog)
}
onMounted(() => {
refreshBlogs(1, 20, 'post_date');
console.log(blogs.value)
})
</script>
<template>
<div class="container">
<div class="filters">
<div class="tags">
<Transition name="fade">
<div class="filters" v-if="tags">
<div class="tags">
<span v-for="tag in tags" :key="tag.name" :class="{ active: tag.active }" @click="toggleTag(tag)">
{{ tag.name }}
</span>
</div>
<!-- <input type="text" v-model="searchQuery" placeholder="搜索博客..." class="search-bar" />-->
</div>
<input type="text" v-model="searchQuery" placeholder="搜索博客..." class="search-bar" />
</div>
</Transition>
<Transition name="fade">
<div class="blogs" v-if="amount">
<BlogBox v-for="blog in blogs" :key="blog.title" :blog="blog"/>
</div>
<span v-else style="position: absolute">正在加载...</span>
</Transition>
<Transition name="fade">
<PagingController v-if="amount" :current-page="currentPage" :amount="amount"
:go-page-func="refreshBlogs"></PagingController>
</Transition>
<div class="blogs">
<BlogBox v-for="blog in filterBlogs()" :key="blog.title" :blog="blog" />
</div>
<PagingController :current-page="3" :amount="10" :go-page-func="()=>{}"></PagingController>
</div>
</template>
@ -73,7 +104,7 @@ const filterBlogs = () => {
.tags {
display: flex;
flex-wrap: wrap; /* 允许标签换行 */
flex-wrap: wrap; /* 允许标签换行 */
gap: 10px;
justify-content: center; /* 使标签居中 */
max-width: 1200px;
@ -119,4 +150,21 @@ const filterBlogs = () => {
::-webkit-scrollbar {
display: none;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease, transform 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(20px); /* 从下方 20px 进入 */
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
transform: translateY(0);
}
</style>

View File

@ -236,7 +236,7 @@ const submitBlog = async () => {
formData.append("title", title);
formData.append("content", content);
formData.append("allow_comments", allowComments);
formData.append("draft", true);
formData.append("draft", false);
console.log(images);
images.forEach((imgData) => {
@ -252,7 +252,7 @@ const submitBlog = async () => {
},
}).then(response => {
if (response.code === 0) {
swal.window('success', `提交成功, 博客id${response.blogId || '未找到blogId字段'}`, 'ok', '好的');
swal.window('success', `提交成功, 博客id${response.blogId || '未找到blogId字段'}`,'', 'ok', '好的');
return;
}
swal.window('error', '提交失败', `code字段为${response.code}; 错误信息: ${response.message}`, 'ok', '好的');

View File

@ -1,24 +1,157 @@
<script setup>
import {onMounted, ref} from "vue";
import {ref, onMounted, computed} from 'vue'
import {useRoute} from 'vue-router'
import api from '../../utils/axios.js'
import {blogImage, userProfile} from '../../utils/imageResource'
import router from "../../router/index.js";
const blogDisplay = ref(null);
//
const route = useRoute()
const blog = ref(null)
const posterInfo = ref(null)
const id = route.params.id
onMounted(() => {
//
onMounted(async () => {
//
const blogResponse = await api.get(`/blogs/${id}`)
if (blogResponse.code === 1) {
await router.push('/404')
return;
}
if (blogResponse.code === 0) {
blog.value = blogResponse.blog
//
const posterResponse = await api.get(`/userinfo?UID=${blog.value.poster}`)
if (posterResponse.code === 0) {
posterInfo.value = posterResponse.info
} else {
console.error('Failed to fetch poster info:', posterResponse)
}
}
})
// <holder> <img>
const processedContent = computed(() => {
if (!blog.value || !blog.value.content) return ''
let content = blog.value.content
const regex = /<holder image ([\w.]+\.\w+) width=([\d.]+) height=([\d.]+)>/g
return content.replace(regex, (match, filename, width, height) => {
const url = blogImage(filename) // filename "1a2b3c4d.png"
return `<img src="${url}" width="${width}" height="${height}" alt="Blog Image" />`
})
})
</script>
<template>
<div class="container">
<div v-html="blogDisplay"></div>
<div class="blog-container">
<!-- 使用 Transition 包裹博客内容 -->
<Transition name="fade">
<div v-if="blog" class="blog-wrapper">
<h1>{{ blog.title }}</h1>
<div class="blog-meta">
<div class="avatar">
<img
v-if="posterInfo"
:src="userProfile(posterInfo.profile)"
alt="Poster Avatar"
/>
</div>
<span>{{ posterInfo?.username || '' }}</span>
<span>{{ blog.post_date }}</span>
</div>
<div class="blog-content" v-html="processedContent"></div>
<div class="comments-section">
<Transition name="fade">
<p v-if="blog.allow_comments === 1">评论区域待实现</p>
<p v-else>不允许评论</p>
</Transition>
</div>
</div>
<!-- blog 未加载时显示加载提示 -->
<div v-else class="loading">加载中...</div>
</Transition>
</div>
</template>
<style scoped>
.container {
.loading {
height: 0;
text-align: center;
font-size: 1.2em;
}
.blog-wrapper {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
height: calc(100vh - 130px);
}
.blog-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
overflow: auto;
max-width: 1200px;
margin: 0 auto;
padding: 20px 30px 50px;
background-color: #333; /* 默认深色背景 */
color: #fff; /* 默认深色文字 */
}
.blog-meta {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
margin-bottom: 20px;
}
.avatar, .avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
.blog-content {
margin-bottom: 20px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.comments-section {
margin-top: 20px;
}
/* 浅色模式 */
.theme-light .blog-container {
background-color: #ffffff;
color: #333;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease, transform 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(20px); /* 从下方 20px 进入 */
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
transform: translateY(0);
}
</style>

View File

@ -31,6 +31,7 @@ import Editor from "../pages/Editor.vue";
import NotFound from "../pages/errorPages/notFound.vue";
import Test_page from "../pages/Test_page.vue";
import SingleBlog_page from "../pages/blogPages/SingleBlog_page.vue";
const routes = [
{path: '/404',
@ -52,6 +53,11 @@ const routes = [
name: 'Blog',
component: Blog_home,
meta: {title: '博客'}
}, {
path: '/blog/:id',
name: 'Blogs',
component: SingleBlog_page,
meta: {title: '博客'}
}, {
path: '/projects',
name: 'Projects',

View File

@ -12,6 +12,7 @@ const store = createStore({
editStore: {},
editAutoSave: {on: true, interval: 30000},
demosLocal: {},
sessionStore: {},
},
mutations: {
toggleTheme(state) {
@ -41,6 +42,12 @@ const store = createStore({
deleteLocalDemoValue(state, obj) {
delete state.demosLocal[obj.demo][obj.value];
},
setSessionValue(state, obj) {
state.sessionStore[obj.key] = {...state.sessionStore[obj.key], ...obj.value}
},
deleteSessionValue(state, obj) {
delete state.sessionStore[obj.key][obj.value];
},
saveEdit(state, obj) {
state.editStore = {...state.editStore, ...obj};
},

View File

@ -2,4 +2,8 @@ import {getDomain} from "./getDomain.js";
export function blogImage(id) {
return `https://${getDomain()}/data/blog/images/${id}`;
}
export function userProfile(id) {
return `https://${getDomain()}/data/user/profile/${id}`
}