feat: 开发中...

This commit is contained in:
2023-04-06 08:52:04 +08:00
parent 2b507f9329
commit b5eb9b468e
15 changed files with 422 additions and 84 deletions

View File

@@ -1,9 +1,12 @@
package cn.hamster3.application.blog.entity; package cn.hamster3.application.blog.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import org.hibernate.validator.constraints.Length;
import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedBy;
@@ -24,12 +27,15 @@ public class BlogEntity {
@Column(name = "id", nullable = false, updatable = false) @Column(name = "id", nullable = false, updatable = false)
private Long id; private Long id;
@Length(max = 128, message = "标题长度不能超过 128 个字符!")
@Column(name = "title", nullable = false, length = 128) @Column(name = "title", nullable = false, length = 128)
private String title; private String title;
@Column(name = "abstracts", nullable = false, length = 512) @Length(max = 1024, message = "摘要长度不能超过 1024 个字符!")
@Column(name = "abstracts", nullable = false, length = 1024)
private String abstracts; private String abstracts;
@NotBlank(message = "博客文章内容不能为空!")
@ToString.Exclude @ToString.Exclude
@Lob @Lob
@Basic(fetch = FetchType.LAZY) @Basic(fetch = FetchType.LAZY)
@@ -42,6 +48,7 @@ public class BlogEntity {
@Column(name = "publish", nullable = false) @Column(name = "publish", nullable = false)
private Boolean publish = false; private Boolean publish = false;
@NotNull(message = "标签不能为 null")
@Setter @Setter
@ElementCollection @ElementCollection
@Column(name = "tag") @Column(name = "tag")

View File

@@ -1,11 +1,9 @@
package cn.hamster3.application.blog.vo.blog; package cn.hamster3.application.blog.vo.blog;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import java.util.Set; import java.util.Set;
@@ -13,20 +11,16 @@ import java.util.Set;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class BlogUpdateRequireVO { public class BlogUpdateRequireVO {
@Length(max = 32, message = "标题长度不能超过 32 个字符!")
@NotBlank(message = "标题不能为空!") @NotBlank(message = "标题不能为空!")
private String title; private String title;
@Length(max = 512, message = "摘要长度不能超过 512 个字符!")
private String abstracts; private String abstracts;
@NotBlank(message = "博客文章内容不能为空!")
private String content; private String content;
private Boolean top; private Boolean top;
private Boolean publish; private Boolean publish;
@NotNull(message = "标签不能为 null")
private Set<String> tags; private Set<String> tags;
} }

View File

@@ -7,7 +7,7 @@ spring:
ddl-auto: update ddl-auto: update
# open-in-view: true # open-in-view: true
show-ddl: true show-ddl: true
show-sql: true # show-sql: true
autoconfigure: autoconfigure:
exclude: exclude:
# - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration # - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

View File

@@ -43,6 +43,9 @@ onMounted(() => {
.app-container { .app-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
max-width: 1280px;
margin: 0 auto;
} }
.app-header { .app-header {

View File

@@ -26,7 +26,7 @@ export const api = {
console.log("current user info: ", vo.content); console.log("current user info: ", vo.content);
} else { } else {
globalStore.currentUserInfo = undefined; globalStore.currentUserInfo = undefined;
console.warn("ckeck current user info failed!"); console.warn("ckeck current user info failed!", vo);
} }
globalStore.save(); globalStore.save();
}); });

View File

@@ -11,6 +11,10 @@ function showBlog() {
router.push("/blog/" + props.blog.id + "/read/"); router.push("/blog/" + props.blog.id + "/read/");
} }
function showTags(tag: string) {
console.log("show tag blogs: " + tag);
}
function showCreator() { function showCreator() {
console.log("showCreator"); console.log("showCreator");
} }
@@ -24,26 +28,49 @@ function showUpdater() {
<el-card class="blog-card"> <el-card class="blog-card">
<template #header> <template #header>
<div class="blog-card-header"> <div class="blog-card-header">
<p @click="showBlog" class="blog-card-title">{{ blog.title }}</p> <h1 @click="showBlog" class="blog-card-title">{{ blog.title }}</h1>
<div> <div style="flex-grow: 1"></div>
<div @click="showCreator" class="blog-card-creater">
<p> <p>
<el-avatar :size="24" src="/favicon.ico" /> <el-avatar
{{ blog.creator?.nickname }} :size="24"
src="/favicon.ico"
style="vertical-align: middle"
/>
{{ blog.creator.nickname }}
</p> </p>
<p @click="showCreator" class="blog-card-creater"> <p>
{{ " 发表于:" + blog.updateTime?.toLocaleString() }} {{ " 首次发表于:" + blog.updateTime.toLocaleString() }}
</p> </p>
</div> </div>
</div> </div>
</template> </template>
<div class="blog-card-body"> <div class="blog-card-body">
<div class="blog-card-abstract" v-html="blog.abstracts"></div> <div class="blog-card-abstract" v-html="blog.abstracts"></div>
<div class="blog-card-footer"> </div>
<div @click="showUpdater"> <div class="blog-card-footer">
<p> <el-tag
{{ " 最后修改于:" + blog.updateTime?.toLocaleString() }} style="margin-left: 5px"
</p> v-for="tag in blog.tags"
</div> type="info"
size="large"
@click="showTags(tag)"
>
{{ tag }}
</el-tag>
<div style="flex-grow: 1"></div>
<div @click="showUpdater" class="blog-card-updater">
<p>
<el-avatar
:size="24"
src="/favicon.ico"
style="vertical-align: middle"
/>
{{ blog.updater.nickname }}
</p>
<p>
{{ " 最后修改于:" + blog.updateTime?.toLocaleString() }}
</p>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -52,40 +79,51 @@ function showUpdater() {
<style scoped> <style scoped>
.blog-card { .blog-card {
width: 100%; width: 100%;
cursor: pointer;
} }
.blog-card-header { .blog-card-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
} }
.blog-card-header :hover { .blog-card-header :hover {
cursor: pointer;
text-decoration: underline; text-decoration: underline;
} }
.blog-card-title { .blog-card-title {
font-size: x-large; font-size: x-large;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.blog-card-creater { .blog-card-creater {
font-size: medium; text-align: right;
font-size: small;
white-space: nowrap;
} }
.blog-card-abstract { .blog-card-abstract {
font-size: medium; font-size: medium;
margin-bottom: 20px; padding-bottom: 20px;
} }
.blog-card-footer { .blog-card-footer {
display: flex;
align-items: center;
padding-top: 20px; padding-top: 20px;
border: 1px solid #ccc; border-top: 1px solid #ccc;
border-bottom-width: 0;
border-left-width: 0;
border-right-width: 0;
} }
.blog-card-footer :hover { .blog-card-footer :hover {
cursor: pointer;
text-decoration: underline; text-decoration: underline;
} }
.blog-card-updater {
text-align: right;
font-size: small;
white-space: nowrap;
}
</style> </style>

View File

@@ -56,7 +56,7 @@ function onSelectMenu(index: string) {
{{ customMenu.text }} {{ customMenu.text }}
</el-menu-item> </el-menu-item>
<div class="flex-grow" /> <div style="flex-grow: 1" />
<el-menu-item index="/login" v-if="!globalStore.currentUserInfo?.id"> <el-menu-item index="/login" v-if="!globalStore.currentUserInfo?.id">
登录 登录
@@ -78,8 +78,4 @@ function onSelectMenu(index: string) {
</el-menu> </el-menu>
</template> </template>
<style scoped> <style scoped></style>
.flex-grow {
flex-grow: 1;
}
</style>

View File

@@ -0,0 +1,216 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import type { BlogInfoResponseVO } from "@/swagger";
import { api } from "@/api";
import router from "@/router";
import { ElMessage } from "element-plus";
let pageIndex = ref(0);
let pageSize = 2;
let pageCount = 100;
const blogs = ref<Array<BlogInfoResponseVO>>();
onMounted(() => {
changePage(1);
});
function editBlog(blogID: number) {
router.push("/blog/" + blogID + "/edit/");
}
function deleteBlog(blogID: number) {
api.BlogController.removeBlog(blogID).then((resp) => {
const vo = resp.data;
if (vo.code === 200) {
ElMessage({
type: "success",
message: "删除博客成功!",
});
} else {
ElMessage({
type: "error",
message: "删除博客失败:" + vo.msg,
});
}
});
}
function showBlog(blogID: number) {
router.push("/blog/" + blogID + "/read/");
}
function showTags(tag: string) {
console.log("show tag blogs: " + tag);
}
function showCreator() {
console.log("showCreator");
}
function showUpdater() {
console.log("showUpdater");
}
function changePage(newPage: number) {
api.BlogController.getBlogInfoList(newPage - 1, pageSize).then((resp) => {
const vo = resp.data;
const page = vo.content;
for (let item of page?.elements ?? []) {
item.createTime = new Date(item?.createTime ?? 0);
item.updateTime = new Date(item?.updateTime ?? 0);
}
blogs.value = page?.elements;
pageCount = page?.totalPage;
pageIndex.value = newPage;
});
}
</script>
<template>
<div class="blog-container">
<div class="blog-card-container">
<el-card class="blog-card" v-for="blog in blogs">
<template #header>
<div class="blog-card-header">
<p @click="showBlog(blog.id)" class="blog-card-title">
{{ blog.title }}
</p>
<div style="flex-grow: 1"></div>
<div @click="showUpdater" class="blog-card-updater">
<p>
<el-avatar
:size="24"
src="/favicon.ico"
style="vertical-align: middle"
/>
{{ blog.updater.nickname }}
</p>
<p>
{{ " 最后修改于:" + blog.updateTime?.toLocaleString() }}
</p>
</div>
<div @click="showCreator" class="blog-card-creater">
<p>
<el-avatar
:size="24"
src="/favicon.ico"
style="vertical-align: middle"
/>
{{ blog.creator.nickname }}
</p>
<p>
{{ " 首次发表于:" + blog.updateTime.toLocaleString() }}
</p>
</div>
</div>
</template>
<div class="blog-card-body">
<div class="blog-card-abstract" v-html="blog.abstracts"></div>
</div>
<div class="blog-card-footer">
<el-tag
style="margin-left: 5px"
v-for="tag in blog.tags"
type="info"
size="large"
@click="showTags(tag)"
>
{{ tag }}
</el-tag>
<div style="flex-grow: 1"></div>
<div>
<el-button @click="editBlog(blog.id)" type="primary"
>编辑</el-button
>
<el-button @click="deleteBlog(blog.id)" type="danger"
>删除</el-button
>
</div>
</div>
</el-card>
</div>
<div style="display: flex">
<div style="flex-grow: 1"></div>
<el-pagination
class="pagination"
background
layout="prev, pager, next"
:hide-on-single-page="false"
:current-page="pageIndex"
:page-count="pageCount"
@update:current-page="changePage"
/>
<div style="flex-grow: 1"></div>
</div>
</div>
</template>
<style scoped>
.blog-container {
width: 100%;
}
.blog-card-container {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
.blog-card {
margin: 0 10px 10px 10px;
}
.blog-card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.blog-card-header :hover {
cursor: pointer;
text-decoration: underline;
}
.blog-card-title {
font-size: x-large;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.blog-card-creater {
text-align: left;
font-size: small;
white-space: nowrap;
}
.blog-card-updater {
text-align: left;
font-size: small;
white-space: nowrap;
margin-right: 32px;
}
.blog-card-abstract {
font-size: medium;
padding-bottom: 20px;
}
.blog-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 20px;
border-top: 1px solid #ccc;
}
.blog-card-footer :hover {
cursor: pointer;
text-decoration: underline;
}
.pagination {
margin-bottom: 10px;
}
</style>

View File

@@ -35,6 +35,7 @@ function changeCustomCSS(cssText: string) {
<template> <template>
<el-input <el-input
class="imput-line"
v-model="siteSetting.title" v-model="siteSetting.title"
placeholder="网站标题" placeholder="网站标题"
@change="changeSetting(siteSetting.keys.site.title, siteSetting.title)" @change="changeSetting(siteSetting.keys.site.title, siteSetting.title)"
@@ -42,6 +43,7 @@ function changeCustomCSS(cssText: string) {
maxlength="10" maxlength="10"
/> />
<el-input <el-input
class="imput-line"
type="textarea" type="textarea"
show-word-limit show-word-limit
maxlength="1024" maxlength="1024"
@@ -50,6 +52,7 @@ function changeCustomCSS(cssText: string) {
@change="changeFooterHTML(siteSetting.footer)" @change="changeFooterHTML(siteSetting.footer)"
/> />
<el-input <el-input
class="imput-line"
type="textarea" type="textarea"
autosize autosize
v-model="siteSetting.css" v-model="siteSetting.css"
@@ -59,7 +62,7 @@ function changeCustomCSS(cssText: string) {
</template> </template>
<style scoped> <style scoped>
.el-input { .imput-line {
margin-bottom: 5px; margin-bottom: 5px;
} }
</style> </style>

View File

@@ -10,9 +10,10 @@ import type {
IEditorConfig, IEditorConfig,
} from "@wangeditor/editor"; } from "@wangeditor/editor";
import { api, globalStore } from "@/api";
import type { BlogInfoResponseVO, BlogUpdateRequireVO } from "@/swagger";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { api } from "@/api";
import type { BlogUpdateRequireVO } from "@/swagger";
import router from "@/router"; import router from "@/router";
const editorRef = shallowRef(); const editorRef = shallowRef();
@@ -24,7 +25,7 @@ const editBlog = reactive<BlogUpdateRequireVO>({
content: "", content: "",
tags: [], tags: [],
}); });
let tags = ref<string>(); const tagInput = ref<string>("");
const blogID = parseInt(useRoute().params.id.toString()); const blogID = parseInt(useRoute().params.id.toString());
onMounted(() => { onMounted(() => {
@@ -82,6 +83,29 @@ function cancelSave() {
router.push("/blog/" + blogID + "/read/"); router.push("/blog/" + blogID + "/read/");
} }
function addTag() {
if (tagInput.value.length === 0) {
ElMessage({
type: "warning",
message: "请输入标签名称!",
});
return;
}
if (editBlog.tags.includes(tagInput.value)) {
ElMessage({
type: "warning",
message: "该博文已拥有相同的标签名称!",
});
return;
}
editBlog.tags.push(tagInput.value);
tagInput.value = "";
}
function removeTag(index: number) {
editBlog.tags.splice(index, 1);
}
function handleCreated(editor: IDomEditor) { function handleCreated(editor: IDomEditor) {
editorRef.value = editor; // 记录 editor 实例,重要! editorRef.value = editor; // 记录 editor 实例,重要!
} }
@@ -122,11 +146,29 @@ function handleCreated(editor: IDomEditor) {
maxlength="512" maxlength="512"
show-word-limit show-word-limit
type="textarea" type="textarea"
autosize
v-model="editBlog.abstracts" v-model="editBlog.abstracts"
/> />
</el-form-item> </el-form-item>
<el-form-item label="标签"> <el-form-item label="标签" style="display: flex">
<el-input placeholder="用 '|' 隔开" v-model="tags" /> <el-tag
style="margin-left: 5px"
v-for="(tag, index) in editBlog.tags"
:key="index"
closable
size="large"
@close="removeTag(index)"
>
{{ tag }}
</el-tag>
<div style="margin-left: 5px">
<el-input
style="margin: 0 5px 0 0; width: 100px"
v-model="tagInput"
/>
<el-button type="primary" @click="addTag">添加</el-button>
</div>
</el-form-item> </el-form-item>
<el-form-item label="是否置顶"> <el-form-item label="是否置顶">
<el-switch v-model="editBlog.top" /> <el-switch v-model="editBlog.top" />

View File

@@ -29,12 +29,7 @@ function deleteBlog() {
<el-container class="blog-container"> <el-container class="blog-container">
<el-header class="blog-header"> <el-header class="blog-header">
<el-row :gutter="20" style="height: 100%; padding: 0; margin: 0"> <el-row :gutter="20" style="height: 100%; padding: 0; margin: 0">
<el-col :span="4"> <el-col :span="4"> </el-col>
<div style="display: flex; width: 100% height: 100%">
<p>{{ blogInfo?.creator.nickname }}</p>
<div style="flex-grow: 1"></div>
</div>
</el-col>
<el-col :span="16"> <el-col :span="16">
<h1 class="blog-title">{{ blogInfo?.title }}</h1> <h1 class="blog-title">{{ blogInfo?.title }}</h1>
</el-col> </el-col>
@@ -48,8 +43,8 @@ function deleteBlog() {
</el-row> </el-row>
</el-header> </el-header>
<el-main class="blog-main"> <el-main class="blog-main">
<el-scrollbar class="blog-content"> <el-scrollbar class="blog-scrollbar">
<div v-html="blogInfo?.content"></div> <div class="blog-content" v-html="blogInfo?.content"></div>
</el-scrollbar> </el-scrollbar>
</el-main> </el-main>
</el-container> </el-container>
@@ -69,24 +64,32 @@ function deleteBlog() {
margin: 0; margin: 0;
text-align: center; text-align: center;
width: 100%; width: 100%;
height: auto;
} }
.blog-author { .blog-author {
font-size: x-large; font-size: medium;
white-space: nowrap; white-space: nowrap;
} }
.blog-title { .blog-title {
font-size: x-large; font-size: x-large;
} white-space: nowrap;
overflow: hidden;
.blog-content { text-overflow: ellipsis;
height: 100%;
width: 100%;
} }
.blog-main { .blog-main {
width: 100%; width: 100%;
padding: 0; padding: 0;
} }
.blog-scrollbar {
width: 100%;
}
.blog-content {
width: 100%;
overflow-x: hidden;
}
</style> </style>

View File

@@ -53,13 +53,10 @@ function load() {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
/* height: 50px; */
background: var(--el-color-primary-light-9);
margin: 10px; margin: 10px;
color: var(--el-color-primary);
} }
.infinite-list .infinite-list-item + .list-item { .infinite-list .infinite-list-item {
margin-top: 10px; margin-top: 10px;
} }
</style> </style>

View File

@@ -34,31 +34,47 @@ const onSubmit = () => {
</script> </script>
<template> <template>
<el-card class="login-card"> <div class="login-div">
<el-form :model="form" label-width="60px"> <div style="flex-grow: 1"></div>
<el-form-item label="邮箱"> <el-card class="login-card">
<el-input v-model="form.email" /> <el-form :model="form" label-width="60px">
</el-form-item> <h1 class="card-title"> </h1>
<el-form-item label="密码"> <el-form-item label="邮箱">
<el-input v-model="form.password" type="password" show-password /> <el-input v-model="form.email" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item label="密码">
<el-button class="login-button" type="primary" @click="onSubmit" <el-input v-model="form.password" type="password" show-password />
>登录</el-button </el-form-item>
> <el-form-item>
</el-form-item> <el-button class="login-button" type="primary" @click="onSubmit"
</el-form> >登录</el-button
</el-card> >
</el-form-item>
</el-form>
</el-card>
<div style="flex-grow: 1"></div>
</div>
</template> </template>
<style scoped> <style scoped>
.login-div { .login-div {
width: 100%;
height: 100%; height: 100%;
display: flex;
flex-direction: column;
align-items: center;
} }
.login-card { .login-card {
width: 480px; width: 480px;
margin: 0 auto; max-width: 90%;
margin-bottom: 160px;
}
.card-title {
text-align: center;
margin-bottom: 32px;
padding-bottom: 12px;
border-bottom: 1px solid #ccc;
} }
.login-button { .login-button {

View File

@@ -3,14 +3,20 @@ import { onMounted, ref } from "vue";
import { globalStore } from "@/api"; import { globalStore } from "@/api";
import SiteManageComponent from "@/components/manage/SiteManageComponent.vue"; import SiteManageComponent from "@/components/manage/SiteManageComponent.vue";
import BlogManageComponent from "@/components/manage/BlogManageComponent.vue";
const activeName = ref<string>("site"); const activeName = ref<string>("site");
onMounted(() => { onMounted(() => {
if (!globalStore.currentUserInfo?.id) { setTimeout(() => {
location.pathname = "/"; console.log("check current user info");
console.log("not login!"); if (globalStore.currentUserInfo?.role === "ADMIN") {
} console.log("current user is admin!");
} else {
location.pathname = "/";
console.warn("not admin user!");
}
}, 300);
}); });
</script> </script>
@@ -19,7 +25,9 @@ onMounted(() => {
<el-tab-pane label="网站设置" name="site"> <el-tab-pane label="网站设置" name="site">
<SiteManageComponent /> <SiteManageComponent />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="博文设置" name="third">Role</el-tab-pane> <el-tab-pane label="博文设置" name="third">
<BlogManageComponent />
</el-tab-pane>
<el-tab-pane label="用户设置" name="user">Config</el-tab-pane> <el-tab-pane label="用户设置" name="user">Config</el-tab-pane>
<el-tab-pane label="评论设置" name="test">Role</el-tab-pane> <el-tab-pane label="评论设置" name="test">Role</el-tab-pane>
</el-tabs> </el-tabs>

View File

@@ -28,8 +28,10 @@ const onSubmit = () => {
<template> <template>
<div class="register-div"> <div class="register-div">
<el-card> <div style="flex-grow: 1"></div>
<el-card class="register-card">
<el-form :model="form" label-width="60px"> <el-form :model="form" label-width="60px">
<h1 class="card-title"> </h1>
<el-form-item label="邮箱"> <el-form-item label="邮箱">
<el-input v-model="form.email" /> <el-input v-model="form.email" />
</el-form-item> </el-form-item>
@@ -46,17 +48,30 @@ const onSubmit = () => {
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
<div style="flex-grow: 1"></div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.register-div { .register-div {
width: 100%;
height: 100%; height: 100%;
display: flex;
flex-direction: column;
align-items: center;
} }
.el-card { .register-card {
width: 480px; width: 480px;
margin: 0 auto; max-width: 90%;
margin-bottom: 240px;
}
.card-title {
text-align: center;
margin-bottom: 32px;
padding-bottom: 12px;
border-bottom: 1px solid #ccc;
} }
.register-button { .register-button {