2024年12月16日20:54:25

main
LeJingS 2 months ago
parent adc2c45b9d
commit b3f42d1247

@ -13,7 +13,8 @@
"marked": "^15.0.3",
"pinia": "^2.2.6",
"vue": "^3.5.13",
"vue-router": "^4.0.13"
"vue-router": "^4.0.13",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.0",
@ -3425,6 +3426,15 @@
"vue": "^3.2.0"
}
},
"node_modules/vue-toastification": {
"version": "2.0.0-rc.5",
"resolved": "https://registry.npmmirror.com/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz",
"integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.2"
}
},
"node_modules/vue-tsc": {
"version": "2.1.10",
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.1.10.tgz",

@ -16,7 +16,8 @@
"marked": "^15.0.3",
"pinia": "^2.2.6",
"vue": "^3.5.13",
"vue-router": "^4.0.13"
"vue-router": "^4.0.13",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.0",

@ -12,8 +12,8 @@
<div>
<!-- 跳转路由 跳转到登录 或者个人页面-->
<RouterLink :to="loginRoute">
<img src="./assets/img/deLogin.png" alt="点击登录">
<RouterLink to="/personalSpace">
<img :src="imgUrl" alt="点击登录">
</RouterLink>
</div>
@ -30,18 +30,29 @@
<script lang="ts" setup name="App">
import { RouterView,RouterLink } from 'vue-router';
import { ref } from 'vue';
import { useLoginStore } from './stores/login';
import { onMounted, ref } from 'vue';
import { useLoginStore } from './stores/Login';
//
import deLogin from './assets/img/deLogin.png'
import Login from './assets/img/defaultAvatar.png'
//Personal space & login
const loginStore = useLoginStore();
const loginRoute = ref({path: '/login'});
const loginStore = useLoginStore();
const imgUrl = ref(deLogin)
//
console.log('App.vue')
console.log(loginStore.userInfo.avatarurl);
onMounted(() => {
if(loginStore.userInfo.token!=null&&loginStore.userInfo.token!=""){
if(loginStore.isLogin){
loginRoute.value = {path: '/personalSpace'}
imgUrl.value = Login
console.log('已经登录',imgUrl.value)
}
console.log('App.vue')
console.log(loginStore.userInfo.avatarurl);
else{
console.log('未登录')
}
});
</script>

@ -1,55 +1,243 @@
<template>
<div class="user-profile">
<h2>个人空间</h2>
<div class="profile-info">
<img src="" alt="Avatar" class="avatar" />
<p><strong>用户名:</strong> Le</p>
<p><strong>ID:</strong> 001</p>
<img src="../../assets/img/defaultAvatar.png" alt="Avatar" class="avatar" />
<p><strong>用户名:</strong> {{ loginStore.userInfo.username }}</p>
</div>
<button @click="">登出</button>
<button @click="logout"></button>
<button @click="showEditDialog = true">修改信息</button>
</div>
<!-- 文章展示区域 -->
<div class="articles">
<div class="articles-header">
<b class="articles-title">您的全部文章</b>
<select v-model="selectedOption" class="dropdown-select">
<option value="byLike">最受欢迎</option>
<option value="byData">发布日期</option>
</select>
排序
<button class="confirm-button" @click="">确认</button>
</div>
<div class="article">
<!-- 文章内容 -->
<div v-for="(p,index) in postOverviewList" :key="p.post_id">
<Article :post_id="p.post_id" :user_id="p.user_id" :username="p.username" :title="p.title" :updated_at="p.updated_at" :Likes="p.Likes" :showButtons="true"
@destroy="handleDestroy(index)"/>
</div>
</div>
<!-- 更多文章可以继续添加 -->
</div>
<div >
<el-dialog v-model="showEditDialog" title="修改信息">
<el-form :model="form" label-width="80px">
<el-form-item label="用户名">
<el-input v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input
v-model="form.password"
:type="passwordType"
placeholder="请输入新密码"
>
<template #suffix>
<i
class="el-input__icon el-icon-view"
@click="togglePasswordVisibility"
></i>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showEditDialog = false">取消</el-button>
<el-button type="primary" @click="handleSubmit"></el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { defineComponent, ref, reactive, onMounted } from 'vue';
import Article from '@/components/utils/Article.vue'
import { useLoginStore } from '@/stores/Login';
import { useRouter } from 'vue-router';
import { ElDialog, ElForm, ElFormItem, ElInput, ElButton, ElMessageBox } from 'element-plus';
import axios from 'axios';
//
import { useToast } from 'vue-toastification'
const toast = useToast();
const router = useRouter();
const loginStore = useLoginStore();
//
interface postOverview{
post_id: number;
user_id: number;
Likes: number;
username: string;
title: string;
updated_at: string;
}
let postOverviewList = reactive<postOverview[]>([]);
const showEditDialog = ref(false);
const passwordType = ref('password');
//
const selectedOption = ref('byData'); // 1
// articles
const articles = ref<postOverview[]>([]);
const form = reactive({
username: loginStore.userInfo.username,
password: '',
});
const togglePasswordVisibility = () => {
passwordType.value = passwordType.value === 'password' ? 'text' : 'password';
};
function handleDestroy(index: number) {
articles.value.splice(index, 1); //
postOverviewList.splice(index, 1);
}
function getAll(by: string) {
axios.get('http://localhost:8080/post/overview', {
params: { by: by },
headers: { token: loginStore.userInfo.token }
})
.then(res => {
// 使
postOverviewList.splice(0, postOverviewList.length)
postOverviewList.push(...res.data.data)
console.log("获取文章概览数据成功", res.data.data);
})
.catch(error => console.error('There was an error fetchingthe data!', error))
}
<script lang="ts" setup>
import { useLoginStore } from '@/stores/login';
import { useRouter } from 'vue-router';
const handleSubmit = () => {
//
console.log(form);
showEditDialog.value = false;
//axios
console.log("-------------",passwordType.value,'-------------',loginStore.userInfo.token)
axios.put('http://localhost:8080/user/revise',{
username: form.username,
password: form.password,
token: loginStore.userInfo.token
})
.then(response => {
//
console.log('Response:', response.data);
toast.success("修改成功,请重新登陆");
//
logout();
})
.catch(error => {
//
console.error('Error:', error);
})
toast.success("信息修改成功");
};
</script>
<style scoped>
.user-profile {
const logout = () => {
// useLoginStore
loginStore.userInfo = { id: '',
//
avatarurl: '../../assets/img/deLogin.png',
username: '',
token: ''
};
toast.success("退出登录成功");
//
router.push('/');
};
onMounted(() => {
// getAll
getAll(selectedOption.value);
});
</script>
<style scoped>
.user-profile {
text-align: center;
padding: 20px;
}
padding: 40px;
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
max-width: 400px;
margin: 0 auto;
}
.profile-info {
.profile-info {
margin-bottom: 20px;
}
}
.avatar {
width: 100px;
height: 100px;
.avatar {
width: 150px;
height: 150px;
border-radius: 50%;
object-fit: cover;
}
.login-prompt {
text-align: center;
padding: 20px;
}
margin-bottom: 20px;
}
button {
padding: 10px 20px;
button {
padding: 12px 24px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
border-radius: 6px;
cursor: pointer;
}
font-size: 16px;
transition: background-color 0.3s ease;
margin-right: 10px; /* 添加间距 */
}
button:hover {
button:hover {
background-color: #0056b3;
}
</style>
}
.articles {
flex: 1; /* 占据剩余空间 */
padding: 20px;
overflow-y: auto; /* 滚动条 */
display: flex;
flex-direction: column;
align-items: center; /* 水平居中 */
justify-content: flex-start; /* 垂直方向从顶部开始 */
}
/* 单篇文章样式 */
.article {
margin-bottom: 20px;
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
width: 100%;
}
/* 文章标题样式 */
.articles-title {
font-size: 24px;
color: #333;
font-weight: bold;
margin-bottom: 10px;
}
/* 下拉选择框样式 */
.dropdown-select {
margin-left: 10px;
padding: 5px;
border: 1px solid #ccc;
border-radius: 4px;
}
</style>

@ -14,6 +14,9 @@
<label for="password">密码</label>
<input type="password" id="password" v-model="password" required />
</div>
<div>
您提交之后管理员会进行审核审核通过后我们会向您发送邮件告知
</div>
<button type="submit">注册</button>
</form>
</div>
@ -22,7 +25,9 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
import { useToast } from 'vue-toastification'
const toast = useToast()
const router = useRouter();
const username = ref('');
const email = ref('');
@ -33,6 +38,27 @@
console.log('用户名:', username.value);
console.log('邮箱:', email.value);
console.log('密码:', password.value);
axios.post('http://localhost:8080/user/registered', {
username: username.value,
email: email.value,
password: password.value
})
.then(response => {
if(response.data.code==1)
{
//
console.log('Response:', response.data);
toast.success("注册成功,请等待管理员审核");
}
else{
toast.success("注册失败,此邮箱已被注册");
}
})
.catch(error => {
//
console.error('Error:', error);
});
//
router.push('/login');

@ -6,6 +6,7 @@
<label for="email">邮箱</label>
<input type="email" id="email" v-model="email" required />
</div>
<div>请输入您的邮箱稍后我们会将您的密码发给您</div>
<button type="submit">发送重置链接</button>
</form>
</div>
@ -14,16 +15,34 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
import { useToast } from 'vue-toastification'
const toast = useToast()
const router = useRouter();
const email = ref('');
const handleSubmit = () => {
// API
console.log('邮箱:', email.value);
//
// 使 Axios POST
axios.post(`http://127.0.0.1:8080/user/findpassword/${encodeURIComponent(email.value)}`)
.then(function (response) {
if(response.data.code==1)
{
//
console.log('Response:', response.data);
toast.success("密码已经发送到您的邮箱,请注意查收");
router.push('/login');
}
else{
toast.error("邮箱不存在,请重新输入");
}
})
.catch(function (error) {
//
console.error('Error:', error);
});
};
</script>

@ -1,14 +1,18 @@
<!-- 文章概览 -->
<template>
<div class="article-overview" @click="handleClick">
<div class="article-overview" @click="handleClick(post_id)">
<div class="article-header">
<div class="article-title-author">
<div class="article-title">文章名称</div>
<div class="article-author">作者</div>
<div class="article-title">{{title}}</div>
<div class="article-author">{{ username }}</div>
</div>
<div class="article-details">
<div class="article-likes">点赞数: 1000</div>
<div class="article-published-date">发布时间: 2024年12月12日16:22:05</div>
<div class="article-buttons" v-if="showButtons">
<button class="edit-button" @click.stop="handleEdit">修改</button>
<button class="delete-button" @click.stop="handleDelete">删除</button>
</div>
<div class="article-likes">点赞数: {{ Likes }}</div>
<div class="article-published-date">发布时间: {{ updated_at }}</div>
</div>
</div>
</div>
@ -16,15 +20,48 @@
<script lang="ts" setup name="Article">
import { useRouter } from 'vue-router';
import { defineProps } from 'vue';
import { defineProps, defineEmits ,ref, reactive, onMounted } from 'vue';
import { ElMessageBox } from 'element-plus'; // ElMessageBox
import axios from 'axios';
import { useToast } from 'vue-toastification';
const toast = useToast()
const router = useRouter();
function handleClick() {
// pinia
const emit = defineEmits(['destroy']);
const props = defineProps(['post_id','user_id','username','title','updated_at','Likes','showButtons'])
router.push('/paper');
function handleDelete() {
ElMessageBox.confirm('您确定要删除这篇文章吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
//
axios.delete('http://localhost:8080/post/delete',{
params: {
post_id: props.post_id, // post_id
}
})
toast.success("删除成功");
emit('destroy');
}).catch(() => {
//
console.log('要删除的文章id是',props.post_id)
});
}
function handleEdit() {
}
function handleClick(post_id:number) {
//
router.push({ name: 'Paper', params: { id: post_id } });
}
</script>

@ -1,12 +1,12 @@
<template>
<div class="paper-container">
<h1>{{ paperStore.paper.title }}</h1>
<p><strong>作者:</strong> {{ paperStore.paper.writer }}</p>
<p><strong>发布时间:</strong> {{ paperStore.paper.publishedDate }}</p>
<h1>{{ paper.title }}</h1>
<p><strong>作者:</strong> {{ paper.writer }}</p>
<p><strong>发布时间:</strong> {{ paper.updated_at }}</p>
<div class="interaction-buttons">
<button @click="toggleLike" :class="{ active: isLiked }">
<img :src="likeIconSrc" alt="Like" class="icon">
点赞数
点赞数{{ paper.Likes }}
</button>
<button @click="toggleDislike" :class="{ active: isDisliked }">
<img :src="dislikeIconSrc" alt="Dislike" class="icon">
@ -22,18 +22,36 @@
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, onMounted, ref,reactive } from 'vue';
import { marked } from 'marked';
import { usePaperStore } from '@/stores/paper';
import axios from 'axios';
import { useRoute } from 'vue-router';
import likeIcon from '@/assets/img/like.svg';
import delikeIcon from '@/assets/img/delike.svg';
import abhorIcon from '@/assets/img/abhor.svg';
import deabhorIcon from '@/assets/img/deabhor.svg';
const paperStore = usePaperStore();
//
import { useToast } from 'vue-toastification'
const toast = useToast();
console.log()
const route = useRoute();
// post_id
const post_id = route.params.id;
const paper = reactive({
id: '',
user_id: '',
title: '',
writer: '',
updated_at: '',
content: '',
Likes:'0'
});
const compiledMarkdown = computed(() => {
return marked(paperStore.paper.text);
return marked(paper.content);
});
const isLiked = ref(false);
@ -42,6 +60,22 @@ const isDisliked = ref(false);
const likeIconSrc = computed(() => isLiked.value ? likeIcon : delikeIcon);
const dislikeIconSrc = computed(() => isDisliked.value ? abhorIcon : deabhorIcon);
function getpaper(){
axios.get('/api/post/essay',{ params: {post_id:post_id}})
.then(res => {
console.log("获取文章详情数据成功", res.data.data);
console.log("传入对象");
paper.id = res.data.data.post_id
paper.title = res.data.data.title
paper.content = res.data.data.content
paper.writer = res.data.data.username
paper.updated_at = res.data.data.updated_at
})
}
const toggleLike = () => {
isLiked.value = !isLiked.value;
isDisliked.value = false;
@ -55,8 +89,20 @@ const toggleDislike = () => {
};
const handleShare = () => {
const currentUrl = window.location.href;
navigator.clipboard.writeText(currentUrl)
.then(() => {
toast.success("链接已复制到剪贴板");
})
.catch(err => {
console.error('无法复制到剪贴板', err);
toast.error("复制失败,请手动复制链接");
});
//
};
onMounted(() => {
getpaper();
});
</script>
<style scoped>

@ -1,100 +1,203 @@
<!-- writing.vue 写作页面-->
<template>
<div class="writing-container">
<!-- 标题输入框 -->
<input v-model="title" class="title-input" placeholder="请输入文章标题" />
<!-- 编辑器与预览容器 -->
<div class="content-container">
<div class="editor-container">
<textarea v-model="markdownContent" class="markdown-editor" placeholder="输入 Markdown 内容..."></textarea>
<textarea
id="markdown-editor"
v-model="markdownContent"
class="markdown-editor"
placeholder="输入 Markdown 内容..."
@input="resizeTextarea"
></textarea>
</div>
<div class="preview-container">
<h2>实时预览</h2>
<div class="preview-content" v-html="compiledMarkdown"></div>
</div>
</div>
<!-- 保存并发布按钮 -->
<button class="scroll-to-top" @click="scrollToTop"></button>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, onMounted } from 'vue';
import { marked } from 'marked';
import axios from 'axios';
import { useLoginStore } from '@/stores/Login';
//
import { useToast } from 'vue-toastification'
// Markdown
const markdownContent = ref('');
const title = ref('');
const loginStore = useLoginStore();
const toast = useToast()
// 使 marked Markdown HTML
const compiledMarkdown = computed(() => {
return marked(markdownContent.value);
return marked.parse(markdownContent.value, { breaks: true });
});
// DOM
onMounted(() => {
const editor = document.getElementById('markdown-editor');
if (editor) {
//
resizeTextarea();
}
});
// textarea
const resizeTextarea = () => {
const editor = document.getElementById('markdown-editor') as HTMLTextAreaElement;
if (editor) {
editor.style.height = 'auto'; //
editor.style.height = `${editor.scrollHeight}px`; //
}
};
const scrollToTop = () => {
//
console.log("正在发布中")
//
console.log(title.value,'id',loginStore.userInfo.id)
axios.post('http://localhost:8080/post/new', {
token: loginStore.userInfo.token,
title: title.value,
content: markdownContent.value
})
.then(response => {
//
console.log(response.data);
toast.success("发布成功");
})
.catch(error => {
//
console.error(error);
});
};
</script>
<style scoped>
/* 全局样式 */
html, body {
height: 100%;
margin: 0;
padding: 0;
font-family: 'Arial', sans-serif; /* 设置默认字体 */
}
body {
display: flex;
justify-content: center;
align-items: center;
align-items: stretch; /* 让子元素拉伸以填充父容器 */
min-height: 100vh;
background-color: #f0f0f0; /* 可以根据需要调整背景颜色 */
background-color: #f9f9f9; /* 更柔和的背景颜色 */
}
/* 内容容器 */
.content-container {
display: flex;
flex: 1; /* 占据剩余的空间 */
margin-top: 70px; /* 避开标题输入框 */
width: 100%;
height: 100%; /* 确保内容容器占据整个高度 */
}
.editor-container,
.preview-container {
width: 50%; /* 平分左右空间 */
height: 100%;
box-sizing: border-box;
padding: 20px;
border-radius: 8px;
}
.writing-container {
display: flex;
flex-direction: row;
justify-content: space-between;
flex-direction: column; /* 改为列布局 */
width: 100%; /* 留出一点边缘空间 */
height: 100%; /* 占据整个高度 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #fff;
border-radius: 8px;
position: relative;
overflow: auto; /* 使用浏览器自带的滚动条 */
padding: 20px;
width: 100%;
height: 100vh;
box-sizing: border-box;
position: relative; /* 添加相对定位,以便子元素绝对定位 */
}
/* 标题输入框 */
.title-input {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 60%; /* 调整宽度 */
padding: 10px;
font-size: 18px; /* 减小字体大小 */
margin-bottom: 20px;
border: none;
border-bottom: 2px solid #ccc;
outline: none;
text-align: center;
transition: border-color 0.3s ease-in-out;
z-index: 10;
}
.title-input:focus {
border-color: #007bff;
}
/* 编辑器容器 */
.editor-container {
width: 48%;
background-color: #f7f7f7;
height: 100%;
box-sizing: border-box;
padding: 20px;
overflow: hidden; /* 确保内容不会溢出 */
}
/* 预览容器 */
.preview-container {
width: 48%;
height: 100%;
background-color: #fff;
border-left: 1px solid #ccc;
padding-left: 20px;
height: 100%;
box-sizing: border-box;
padding: 20px;
overflow: auto; /* 使用内部滚动条 */
}
/* Markdown 编辑器 */
.markdown-editor {
width: 100%;
height: calc(100% - 40px); /* 减去 padding 的高度 */
padding: 10px;
box-sizing: border-box;
font-family: monospace;
border: 1px solid #ccc;
border-radius: 5px;
resize: vertical; /* 允许垂直调整大小 */
height: 100%;
border: none;
outline: none;
resize: none; /* 取消内部滚动条 */
font-size: 16px;
font-family: 'Arial', sans-serif;
background-color: transparent;
color: #333;
overflow: hidden; /* 隐藏滚动条 */
}
/* 预览内容 */
.preview-content {
height: calc(100% - 60px); /* 减去 h2 的高度和 padding */
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
padding: 10px;
box-sizing: border-box;
height: 100%;
overflow: auto; /* 使用内部滚动条 */
font-size: 16px;
line-height: 1.6;
color: #333;
}
/* 按钮样式 */
/* 保存并发布按钮 */
.scroll-to-top {
position: absolute; /* 使用绝对定位 */
bottom: 100px;
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 20px;
background-color: #007bff;
@ -106,7 +209,6 @@ body {
z-index: 1000; /* 确保按钮在最上层 */
}
/* 按钮悬停效果 */
.scroll-to-top:hover {
background-color: #0056b3;
}

@ -3,13 +3,32 @@ import App from './App.vue';
import { createPinia } from 'pinia';
import ElementPlus from 'element-plus';
import Toast from "vue-toastification";
import "vue-toastification/dist/index.css";
import 'element-plus/dist/index.css';
// 引入路由器
import router from './router/index'
const app = createApp(App);
const options = {
transition: 'Vue-Toastification__bounce',
maxToasts: 3,
newestOnTop: true,
position: 'top-left',
timeout: 2000,
closeOnClick: true,
pauseOnFocusLoss: true,
pauseOnHover: false,
draggable: true,
draggablePercent: 0.7,
showCloseButtonOnHover: false,
hideProgressBar: true,
closeButton: 'button',
icon: true,
rtl: false
};
//使用路由
app.use(Toast, options);
app.use(router)
app.use(createPinia());
app.use(ElementPlus);

@ -2,29 +2,45 @@
<!-- Announcement.vue -->
<template>
<div class="announcement-container">
<div v-for="(announcement, index) in announcements" :key="index" class="announcement-item">
<h3>{{ announcement.title }}</h3>
<p class="announcement-time">{{ announcement.time }}</p>
<div v-html="announcement.content" class="announcement-content"></div>
<div v-for="a in announcements" :key="a.announcement_id" class="announcement-item">
<h3>{{ a.title }}</h3>
<p class="announcement-time">{{ a.updated_at }}</p>
<div v-html="a.content" class="announcement-content"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, reactive,onMounted } from 'vue';
import axios from 'axios';
const announcements = ref([
{
title: '公告一',
time: '2023-10-01 12:00',
content: `<p>这是第一条公告的内容。</p>`
},
{
title: '公告二',
time: '2023-10-02 14:30',
content: `<p>这是第二条公告的内容,支持 <strong>HTML</strong> 格式。</p>`
interface Announcement {
announcement_id: number;
title: string;
updated_at: string;
content: string;
}
]);
let announcements = reactive<Announcement[]>([]);
//axios
// api
function getAll() {
axios.get('/api/announcements')
.then(res => {
// 使
console.log("开始存放",res.data)
announcements.splice(0, announcements.length)
announcements.push(...res.data.data)
console.log("获取公告数据成功", res.data.data);
})
.catch(error => console.error('There was an error fetching the data!', error))
}
onMounted(() => {
console.log("组件已挂载,开始请求公告数据");
getAll();
});
</script>
<style scoped>

@ -28,7 +28,9 @@
<div class="article">
<!-- 文章内容 -->
<Article v-for="(item,index) in Array(20)"></Article>
<div v-for="p in postOverviewList" :key="p.post_id">
<Article :post_id="p.post_id" :user_id="p.user_id" :username="p.username" :title="p.title" :updated_at="p.updated_at" :Likes="p.Likes" :showButtons="false" />
</div>
</div>
<!-- 更多文章可以继续添加 -->
</div>
@ -41,7 +43,8 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref,reactive,onMounted } from 'vue';
import axios from 'axios';
//
import Publicity from '../components/home/Publicity.vue';
//
@ -51,9 +54,35 @@ import Announcement from './Announcement.vue';
//
import StartWriting from '@/components/utils/StartWriting.vue';
console.log('home');
// -----------------------------------------
interface postOverview{
post_id: number;
user_id: number;
Likes: number;
username: string;
title: string;
updated_at: string;
}
let postOverviewList = reactive<postOverview[]>([]);
//
function getAll(by: string) {
axios.get('/api/post/overview', {
params: { by: by },
headers: { token: '' }
})
.then(res => {
// 使
postOverviewList.splice(0, postOverviewList.length)
postOverviewList.push(...res.data.data)
console.log("获取文章概览数据成功", res.data.data);
})
.catch(error => console.error('There was an error fetchingthe data!', error))
}
//---------------------------------------
//
const selectedOption = ref('byLike'); // 1
const selectedOption = ref('byData'); // 1
//
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
@ -61,8 +90,12 @@ const scrollToTop = () => {
//
const handleConfirm = () => {
console.log('选择的选项是:', selectedOption.value);
getAll(selectedOption.value)
//
};
onMounted(()=>{
getAll(selectedOption.value)
})
</script>

@ -20,24 +20,54 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { h, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useLoginStore } from '@/stores/Login';
import axios from 'axios';
const router = useRouter();
const username = ref('');
const password = ref('');
const handleLogin = () => {
const store = useLoginStore();
const handleLogin = async () => {
// API
console.log('用户名:', username.value);
console.log('密码:', password.value);
//
try {
//
const response = await axios.post('http://localhost:8080/user/login', {
username: username.value,
password: password.value
}, {
headers: {
'Content-Type': 'application/json',
}
});
//
console.log(response.data.code);
if (response.data.code === 1) {
//
console.log('登录成功');
store.userInfo.token = response.data.data;
store.userInfo.username = username.value;
router.push('/home');
};
console
return;
} else {
//
alert('用户名或密码错误');
}
} catch (error) {
console.error('登录请求失败:', error);
//
}
//
// then
// router.push('/home');
};
const handleForgotPassword = () => {
//
@ -52,9 +82,7 @@ const handleRegister = () => {
//
router.push('/registered');
};
</script>
<style scoped>
html, body {
height: 100%;

@ -7,12 +7,15 @@ import Home from '@/page/Home.vue'
// 引入登录页
import Login from '@/page/Login.vue'
// 引入文章详情页
import Paper from '@/components/utils/paper.vue'
import Paper from '@/components/utils/Paper.vue'
// 引入书写页面
import Writing from '@/components/writeBlog/Writing.vue'
// 引入找回密码和注册
import RetrievePassword from '@/components/login/RetrievePassword.vue'
import Registered from '@/components/login/Registered.vue'
import PersonalSpace from '@/components/login/PersonalSpace.vue'
// 引入全局数据Login
import { useLoginStore } from '@/stores/Login'
// 创建路由
const router = createRouter({
@ -31,7 +34,8 @@ const router = createRouter({
},
// 文章详情页
{
path:'/paper',
path:'/paper/:id',
name: 'Paper',
component:Paper
},
// 书写页面
@ -53,7 +57,7 @@ const router = createRouter({
//个人空间
{
path:'/personalSpace',
component:()=>import('@/components/login/PersonalSpace.vue')
component: PersonalSpace
},
@ -64,16 +68,26 @@ const router = createRouter({
}
]
})
});
// 添加全局前置守卫
router.beforeEach((to, from, next) => {
const loginStore = useLoginStore(); // 获取 login store 实例
const isAuthenticated = !!loginStore.userInfo.token; // 检查 token 是否存在
// 只拦截 /writing 路径
if ((to.path === '/writing'||to.path === '/personalSpace') && !isAuthenticated) {
next('/login'); // 如果未认证,重定向到登录页面
// window.alert('请先登录');
}
else {
next(); // 否则继续导航
}
});
// 暴露出去

@ -1,106 +0,0 @@
import { defineStore } from 'pinia';
export const usePaperStore = defineStore('paper', {
state() {
return {
paper: {
id: '000',
title: '示例标题',
writer: 'LeJingS',
text: `## 标题
Markdown使`+"#"+`
#
##
###
####
#####
######
##
** **
**** ****
****** ******
##
###
- 使
- 使
- 使
###
1.
2.
3.
1.
-
-
2.
##
[](https://example.com/) 是Markdown中创建超链接的方式。
##
![](https://via.placeholder.com/150)
##
使\`var example = "code";\`
使
\`\`\`javascript
function helloWorld() {
console.log("Hello, world!");
}
\`\`\`
##
| | |
| ------ | ------ |
| | |
| | |
| | | |
| :----- | :------: | -----: |
| | | |
| | | |
## 线
使线线
------
------
------
##
>
>
> > `
} as Record<string, any>
};
}
});

@ -11,8 +11,15 @@ export default defineConfig({
vueDevTools(),
],
server:{
port:80, // 设置为你想要的端口号
port:82, // 设置为你想要的端口号
host: '0.0.0.0', // 监听所有网络接口
proxy: {
'/api': {
target: 'http://localhost:8080', // 将此替换为你的后端API服务器地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
resolve: {
alias: {

@ -13,5 +13,19 @@
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="blog@localhost" uuid="4094ca86-b2a8-4044-a5f6-01018a901176">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/src/main/resources/application.properties</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/blog</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

@ -48,9 +48,10 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version> <!-- 确保使用最新版本 -->
<version>1.18.8</version> <!-- 确保使用最新版本 -->
<scope>provided</scope> <!-- 如果是用于编译期处理,可以使用 provided -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@ -62,6 +63,27 @@
<version>3.0.4</version>
<scope>test</scope>
</dependency>
<!-- JWP令牌依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.1</version>
</dependency>
<!-- 邮件发送 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
</dependencies>
<build>

@ -2,6 +2,9 @@ package top.lejings.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootApplication
public class DemoApplication {
@ -10,4 +13,16 @@ public class DemoApplication {
SpringApplication.run(DemoApplication.class, args);
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:82")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
}

@ -3,6 +3,7 @@ package top.lejings.demo.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import top.lejings.demo.pojo.Announcements;

@ -18,27 +18,37 @@ public class PostsController {
//概览 Overview
@GetMapping("/overview")
public Result PostsOverview() {
public Result PostsOverview(@RequestParam(defaultValue = "byLike") String by,@RequestHeader String token) {
log.info("查询文章所有概览");
List<Posts> postsOverviewList = postsService.postsOverview();
List<Posts> postsOverviewList = postsService.postsOverview(by,token);
System.out.println(postsOverviewList);
return Result.success(postsOverviewList);
}
//具体文章,接收参数 文章id essay
//请求url:127.0.0.1:8080/post/essay/4
@GetMapping("/essay/{post_id}")
public Result PostsEssay(@PathVariable Integer post_id){
@GetMapping("/essay")
public Result PostsEssay(Integer post_id){
log.info("查询id为{}的文章",post_id);
Posts posts = postsService.postsEssay(post_id);
return Result.success(posts);
}
//新建文章,前端传来user_id,title,content
//新建文章,前端传来token,title,content。修改和创建时间由数据库完成
@PostMapping("/new")
@CrossOrigin(origins = "http://localhost:82")
public Result PostsNew(@RequestBody Posts posts){
log.info("新建文章");
postsService.postsNew(posts);
return Result.success();
}
// 删除文章,接收参数 文章id post_id
// 请求url:127.0.0.1:8080/post/delete?post_id=4
@DeleteMapping("/delete")
public Result deletePost(@RequestParam Integer post_id) {
log.info("删除id为{}的文章", post_id);
postsService.deletePost(post_id);
return Result.success();
}
}

@ -0,0 +1,88 @@
package top.lejings.demo.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import top.lejings.demo.pojo.Result;
import top.lejings.demo.pojo.Users;
import top.lejings.demo.service.UsersService;
import top.lejings.demo.utils.JwtUtils;
import java.util.HashMap;
import java.util.Map;
/*
*
*
* */
@Slf4j
@RestController
@RequestMapping("/user")
public class UsersController {
@Autowired UsersService usersService;
/*
*
* */
@PostMapping("/login")
@CrossOrigin(origins = "http://localhost:82")
public Result login(@RequestBody Users users){
log.info("正在进行登录。用户名是:{}",users.getUsername());
Users u = usersService.login(users);
//登录成功逻辑
if(u != null){
log.info("登录成功下发jwt令牌");
Map<String,Object> claims = new HashMap<>();
claims.put("user_id",u.getUser_id());
claims.put("username",u.getUsername());
claims.put("email",u.getEmail());
String jwt = JwtUtils.generateJwt(claims);//jwt包含了当前员工信息
return Result.success(jwt);
}
//登录失败,返回错误信息
return Result.error("用户名或密码错误");
}
//注册后自动登录
@PostMapping("/registered")
@CrossOrigin(origins = "http://localhost:82")
public Result register(@RequestBody Users users){
log.info("正在进行注册操作。用户名为{},邮箱为{}",users.getUsername(),users.getEmail());
boolean f = usersService.registered(users);
if(f){
return Result.success("注册成功!");
}
else{
//注册失败,用户名或者邮箱已被注册
return Result.error("此邮箱已被注册");
}
}
@PostMapping("/findpassword/{email}")
@CrossOrigin(origins = "http://localhost:82")
public Result findPassword(@PathVariable String email){
log.info("邮箱为{}",email);
boolean f = usersService.findPassword(email);
if(f){
return Result.success("密码已经发送");
}
else{
return Result.error("这个邮箱尚未被注册");
}
}
//用户修改个人信息根据用户id修改用户名密码
@PutMapping("/revise")
public Result update(@RequestBody Users users){
usersService.revise(users);
return Result.success();
}
}

@ -0,0 +1,9 @@
package top.lejings.demo.filter;
/*
*
* */
public class LoginCheckFilter {
}

@ -1,5 +1,6 @@
package top.lejings.demo.mapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import top.lejings.demo.pojo.Posts;
@ -8,8 +9,15 @@ import java.util.List;
@Mapper
public interface PostsMapper {
List<Posts> postsOverview();
List<Posts> postsOverview(String by);
Posts postsEssay(Integer post_id);
void postsNew(Posts posts);
@Select("SELECT p.post_id,p.user_id,COUNT(CASE WHEN r.type = 'like' THEN 1 END) AS Likes,u.username,p.title,p.updated_at FROM posts p JOIN users u ON p.user_id = u.user_id LEFT JOIN reactions r ON p.post_id = r.post_id AND r.type = 'like' WHERE u.user_id = ${user_id} GROUP BY p.post_id, p.user_id, u.username, p.title, p.updated_at, p.priority ORDER BY ${by} DESC;")
List<Posts> postsOverviewAndId(String by, Integer user_id);
@Delete("DELETE FROM posts WHERE post_id = #{post_id}")
void deletePost(Integer post_id);
}

@ -0,0 +1,25 @@
package top.lejings.demo.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import top.lejings.demo.pojo.Users;
@Mapper
public interface UsersMapper {
@Select("select * from Users where username=#{username} and password=#{password}")
Users getByUsernameAndPassword(Users users) ;
@Select("select username,password from users where email=#{email}")
Users findPassword(String email);
@Select("select * from users where email=#{email}")
Users findById(Users users);
@Insert("INSERT INTO users (username, email, password) VALUES (#{username}, #{email}, #{password})")
void registered(Users users);
@Update("UPDATE users SET username = #{username},password = #{password} where user_id = #{user_id}")
void revise(Users users);
}

@ -12,9 +12,13 @@ import java.time.LocalDateTime;
public class Posts {
private Integer post_id;
private Integer user_id;
// 点赞数
private Integer Likes;
private String username;
private String title;
private String content;
private String token;
private LocalDateTime updated_at; //修改时间
private Integer priority;//优先级
// private Integer priority;//优先级
}

@ -14,6 +14,7 @@ public class Users {
private String username;
private String email;
private String password;
private String token;
private String status;//账号状态:
private LocalDateTime created_at;
private LocalDateTime updated_at; //修改时间

@ -5,7 +5,11 @@ import top.lejings.demo.pojo.Posts;
import java.util.List;
public interface PostsService {
List<Posts> postsOverview();
List<Posts> postsOverview(String by,String token);
Posts postsEssay(Integer post_id);
void postsNew(Posts posts);
void deletePost(Integer postId);
}

@ -0,0 +1,13 @@
package top.lejings.demo.service;
import top.lejings.demo.pojo.Users;
public interface UsersService {
Users login(Users users) ;
boolean registered(Users users);
boolean findPassword(String email);
void revise(Users users);
}

@ -1,12 +1,16 @@
package top.lejings.demo.service.impl;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import top.lejings.demo.mapper.PostsMapper;
import top.lejings.demo.pojo.Posts;
import top.lejings.demo.service.PostsService;
import top.lejings.demo.utils.JwtUtils;
import java.util.List;
@Slf4j
@Service
public class PostsServiceImpl implements PostsService {
@ -14,8 +18,44 @@ public class PostsServiceImpl implements PostsService {
private PostsMapper postsMapper;
@Override
public List<Posts> postsOverview() {return postsMapper.postsOverview();}
public List<Posts> postsOverview(String by,String token) {
if(by.equals("byLike"))by="p.priority";
else by="p.updated_at";
log.info("根据{}查询",by);
if(token == null || token.equals("")){
return postsMapper.postsOverview(by);
}
else{
//解析令牌返回user_id
Claims claims = JwtUtils.parseJWT(token);
Integer user_id = claims.get("user_id", Integer.class); // 假设 user_id 是字符串类型
log.info("id为{}",user_id);
return postsMapper.postsOverviewAndId(by,user_id);
}
}
@Override
public Posts postsEssay(Integer post_id) {return postsMapper.postsEssay(post_id);}
//新建文章在这里解析token得到写作者的id
@Override
public void postsNew(Posts posts) {
// 解析 token 获取 user_id
String token = posts.getToken();
if (token != null) {
Claims claims = JwtUtils.parseJWT(token);
Integer userId = (Integer) claims.get("user_id");
posts.setUser_id(userId);
} else {
throw new IllegalArgumentException("Token cannot be null");
}
postsMapper.postsNew(posts);
}
@Override
public void deletePost(Integer postId) {
postsMapper.deletePost(postId);
}
}

@ -0,0 +1,67 @@
package top.lejings.demo.service.impl;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import top.lejings.demo.mapper.UsersMapper;
import top.lejings.demo.pojo.Users;
import top.lejings.demo.service.UsersService;
import top.lejings.demo.utils.Email;
import top.lejings.demo.utils.JwtUtils;
@Slf4j
@Service
public class UsersServiceImpl implements UsersService {
@Autowired UsersMapper usersMapper;
@Autowired
private JavaMailSender javaMailSender;
public Users login(Users users){return usersMapper.getByUsernameAndPassword(users) ;}
@Override
public boolean registered(Users users) {
if(usersMapper.findById(users)==null){
Email email = new Email(javaMailSender);
email.SendSimpleMail(users.getEmail(),users.getUsername()+",您好。","恭喜您,注册成功");
usersMapper.registered(users);
return true;
}
else{
return false;
}
}
@Override
public boolean findPassword(String email) {
Users users = usersMapper.findPassword(email);
if(users == null)
{
return false;
}
else{
Email sendEmail = new Email(javaMailSender);
sendEmail.SendSimpleMail(email,users.getUsername()+"您好!请尽快查看您的密码","您的密码为:"+users.getPassword());
return true;
}
}
@Override
public void revise(Users users) {
//根据token解析出id原用户名原密码来
Claims claims = JwtUtils.parseJWT(users.getToken());
Integer user_id = claims.get("user_id", Integer.class); // 假设 user_id 是字符串类型
log.info("id为{}",user_id);
String username = claims.get("username", String.class);
users.setUser_id(user_id);
log.info("用户修改信息id为{},将用户名{}修改为{},修改密码为{}",user_id,username,users.getUsername(),users.getPassword());
usersMapper.revise(users);
}
}

@ -0,0 +1,26 @@
package top.lejings.demo.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;
@Component
public class Email {
private final JavaMailSender sender;
@Autowired
public Email(JavaMailSender sender) {
this.sender = sender;
}
public void SendSimpleMail(String to, String subject, String text) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("bloglejingstop@126.com");
message.setTo(to);
message.setSubject(subject);
message.setText(text);
sender.send(message);
}
}

@ -0,0 +1,41 @@
package top.lejings.demo.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
//签名秘钥 和 过期时间
private static String signKey = "lejings";
private static Long expire = 43200000L;
/**
* JWT
* @param claims JWT payload
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
/**
* JWT
* @param jwt JWT
* @return JWT payload
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}

@ -1,3 +1,4 @@
spring.application.name=blog
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
@ -10,5 +11,17 @@ spring.datasource.password=123456
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis.configuration.map-underscore-to-camel-case=true
//????
# ???????
spring.mail.host=smtp.126.com
spring.mail.username=bloglejingstop@126.com
#GHVrH3BhNv9h82wy ???
spring.mail.password=GHVrH3BhNv9h82wy
spring.mvc.cors.allowed-origins=http://localhost:8080
spring.mvc.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
spring.mvc.cors.allowed-headers=*
spring.mvc.cors.allow-credentials=true

@ -7,35 +7,36 @@
<select id="postsOverview" resultType="top.lejings.demo.pojo.Posts">
SELECT
p.post_id,
p.user_id,
COUNT(CASE WHEN r.type = 'like' THEN 1 END) AS Likes,
u.username,
p.title,
u.username AS author,
(
SELECT COUNT(*)
FROM reactions r
WHERE r.post_id = p.post_id AND r.type = 'like'
) AS like_count,
p.priority,
p.updated_at AS last_updated
p.updated_at
FROM
posts p
JOIN
users u ON p.user_id = u.user_id
LEFT JOIN
reactions r ON p.post_id = r.post_id AND r.type = 'like'
GROUP BY
p.post_id, p.user_id, u.username, p.title, p.content, p.updated_at, p.priority
ORDER BY
p.updated_at DESC;
${by} DESC;
</select>
<select id="postsEssay" resultType="top.lejings.demo.pojo.Posts">
SELECT
p.post_id,
p.user_id AS author_id,
p.user_id,
u.username,
p.title,
p.content,
p.updated_at AS last_updated,
r.like_count,
GROUP_CONCAT(r.user_id) AS like_user_ids
p.updated_at,
COALESCE(r.like_count, 0) AS like_count, -- 使用 COALESCE 处理 NULL 值
GROUP_CONCAT(DISTINCT r.user_id) AS like_user_ids -- 确保 user_id 不重复
FROM
posts p
JOIN
users u ON p.user_id = u.user_id -- 加入 users 表以获取用户名
LEFT JOIN
(SELECT
post_id,
@ -49,8 +50,13 @@
post_id) r ON p.post_id = r.post_id
WHERE
p.post_id = #{post_id}
AND p.user_id IN (SELECT user_id FROM users WHERE status IN ('normal', 'admin'))
AND u.status IN ('normal', 'admin') -- 修改这里的条件以直接使用 u.status
GROUP BY
p.post_id, p.user_id, p.title, p.content, p.updated_at, r.like_count;
p.post_id, p.user_id, u.username, p.title, p.content, p.updated_at, r.like_count;
</select>
<insert id="postsNew">
INSERT INTO posts (user_id, title, content, priority)
VALUES (#{user_id}, #{title}, #{content}, 0);
</insert>
</mapper>

@ -3,11 +3,13 @@ package top.lejings.demo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DemoApplicationTests {
@Test
void contextLoads() {
}
}

@ -1,3 +1,4 @@
spring.application.name=blog
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
@ -10,5 +11,17 @@ spring.datasource.password=123456
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis.configuration.map-underscore-to-camel-case=true
//????
# ???????
spring.mail.host=smtp.126.com
spring.mail.username=bloglejingstop@126.com
#GHVrH3BhNv9h82wy ???
spring.mail.password=GHVrH3BhNv9h82wy
spring.mvc.cors.allowed-origins=http://localhost:8080
spring.mvc.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
spring.mvc.cors.allowed-headers=*
spring.mvc.cors.allow-credentials=true

@ -7,35 +7,36 @@
<select id="postsOverview" resultType="top.lejings.demo.pojo.Posts">
SELECT
p.post_id,
p.user_id,
COUNT(CASE WHEN r.type = 'like' THEN 1 END) AS Likes,
u.username,
p.title,
u.username AS author,
(
SELECT COUNT(*)
FROM reactions r
WHERE r.post_id = p.post_id AND r.type = 'like'
) AS like_count,
p.priority,
p.updated_at AS last_updated
p.updated_at
FROM
posts p
JOIN
users u ON p.user_id = u.user_id
LEFT JOIN
reactions r ON p.post_id = r.post_id AND r.type = 'like'
GROUP BY
p.post_id, p.user_id, u.username, p.title, p.content, p.updated_at, p.priority
ORDER BY
p.updated_at DESC;
${by} DESC;
</select>
<select id="postsEssay" resultType="top.lejings.demo.pojo.Posts">
SELECT
p.post_id,
p.user_id AS author_id,
p.user_id,
u.username,
p.title,
p.content,
p.updated_at AS last_updated,
r.like_count,
GROUP_CONCAT(r.user_id) AS like_user_ids
p.updated_at,
COALESCE(r.like_count, 0) AS like_count, -- 使用 COALESCE 处理 NULL 值
GROUP_CONCAT(DISTINCT r.user_id) AS like_user_ids -- 确保 user_id 不重复
FROM
posts p
JOIN
users u ON p.user_id = u.user_id -- 加入 users 表以获取用户名
LEFT JOIN
(SELECT
post_id,
@ -49,8 +50,13 @@
post_id) r ON p.post_id = r.post_id
WHERE
p.post_id = #{post_id}
AND p.user_id IN (SELECT user_id FROM users WHERE status IN ('normal', 'admin'))
AND u.status IN ('normal', 'admin') -- 修改这里的条件以直接使用 u.status
GROUP BY
p.post_id, p.user_id, p.title, p.content, p.updated_at, r.like_count;
p.post_id, p.user_id, u.username, p.title, p.content, p.updated_at, r.like_count;
</select>
<insert id="postsNew">
INSERT INTO posts (user_id, title, content, priority)
VALUES (#{user_id}, #{title}, #{content}, 0);
</insert>
</mapper>

@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
Loading…
Cancel
Save