feat(前端): 实现用户登录、注册和搜索功能

- 新增登录和注册页面组件
- 实现用户登录、注册和登出逻辑
- 添加笔记搜索功能
- 更新主页组件,支持用户状态显示和搜索
- 引入 Pinia 状态管理库
This commit is contained in:
2025-07-31 09:45:49 +08:00
parent ab4891d8db
commit 2f9e68c636
13 changed files with 523 additions and 6 deletions

View File

@@ -50,6 +50,33 @@ export const deleteMarkdown = (id) => axiosApi.post(`/api/markdown/delete?id=${i
// 根据分组ID获取Markdown文件列表
export const markdownList = (groupingId) => axiosApi.get(`/api/markdown/grouping/${groupingId}`);
// 登录
export const login = (data) => {
const formData = new FormData()
formData.append('username', data.username)
formData.append('password', data.password)
return axiosApi.post('/api/user/login', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 搜索
export const searchMarkdown = (keyword) => axiosApi.get(`/api/markdown/search?keyword=${keyword}`);
// 注册
export const register = (data) => {
const formData = new FormData()
formData.append('username', data.username)
formData.append('password', data.password)
return axiosApi.post('/api/user/register', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}

View File

@@ -49,10 +49,10 @@
<h2>{{ selectedFile.title }}</h2>
<div class="actions">
<el-button v-if="!showEditor" type="primary" @click="selectedFile = null">清空</el-button>
<el-button v-if="!showEditor" type="primary" @click="editNote(selectedFile); isCollapsed = true">编辑</el-button>
<el-button v-if="!showEditor" type="danger" @click="deleteNote(selectedFile)">删除</el-button>
<el-button v-if="!showEditor && userStore.isLoggedIn" type="primary" @click="editNote(selectedFile); isCollapsed = true">编辑</el-button>
<el-button v-if="!showEditor && userStore.isLoggedIn" type="danger" @click="deleteNote(selectedFile)">删除</el-button>
<el-button v-if="showEditor" type="primary" @click="showEditor = !showEditor; previewFile(editData)">返回</el-button>
<el-button v-if="showEditor" type="success" @click="handleSave(vditor.getValue())">保存</el-button>
<el-button v-if="showEditor && userStore.isLoggedIn" type="success" @click="handleSave(vditor.getValue())">保存</el-button>
</div>
</el-header>
<div v-if="!showEditor" v-html="previewHtml" class="markdown-preview"></div>
@@ -64,8 +64,29 @@
<el-header class="header">
<h1>我的笔记</h1>
<div class="actions">
<el-button type="primary" @click="showCreateNoteDialog = true">新建笔记</el-button>
<el-input
v-model="searchKeyword"
placeholder="搜索笔记标题"
class="search-input"
@keyup.enter="handleSearch"
>
<template #append>
<el-button @click="handleSearch">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
<div v-if="userStore.isLoggedIn">
<span>欢迎, {{ userStore.userInfo?.username }}</span>
<el-button type="danger" @click="handleLogout">退出</el-button>
</div>
<div v-else>
<el-button type="primary" @click="goToLogin">登录</el-button>
<el-button @click="goToRegister">注册</el-button>
</div>
<el-button v-if="userStore.isLoggedIn" type="primary" @click="showCreateNoteDialog = true">新建笔记</el-button>
<el-upload
v-if="userStore.isLoggedIn"
class="upload-btn"
action=""
:show-file-list="false"
@@ -163,9 +184,16 @@ import {
groupingId,
markdownAll, markdownList,
Preview,
updateMarkdown, uploadImage
updateMarkdown, uploadImage,
searchMarkdown
} from '@/api/CommonApi.js'
import { DArrowRight, Plus, Fold, Expand, Folder, Document } from "@element-plus/icons-vue";
import { DArrowRight, Plus, Fold, Expand, Folder, Document, Search } from "@element-plus/icons-vue";
import { useUserStore } from '../stores/user';
import { useRouter } from 'vue-router';
const userStore = useUserStore();
const router = useRouter();
const searchKeyword = ref('');
const isGroup1=ref(true)
// 创建新文件中大分类的信息
@@ -542,6 +570,32 @@ const chushihua = async () => {
await fetchMarkdownFiles();
await fetchGroupings();
}
const goToLogin = () => {
router.push('/login');
};
const goToRegister = () => {
router.push('/register');
};
const handleLogout = () => {
userStore.logout();
router.push('/login');
};
const handleSearch = async () => {
if (!searchKeyword.value) {
await fetchMarkdownFiles();
return;
}
try {
const response = await searchMarkdown(searchKeyword.value);
groupMarkdownFiles.value = response.data;
} catch (error) {
ElMessage.error('搜索失败: ' + error.message);
}
};
</script>
<style scoped>

View File

@@ -0,0 +1,80 @@
<template>
<div class="login-container">
<el-card class="login-card">
<template #header>
<div class="card-header">
<span>登录</span>
</div>
</template>
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
<el-button @click="goToRegister">注册</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '../stores/user';
import { ElMessage } from 'element-plus';
const router = useRouter();
const userStore = useUserStore();
const loginFormRef = ref(null);
const loginForm = ref({
username: '',
password: '',
});
const loginRules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
};
const handleLogin = async () => {
const valid = await loginFormRef.value.validate();
if (valid) {
const success = await userStore.login(loginForm.value.username, loginForm.value.password);
if (success) {
ElMessage.success('登录成功');
router.push('/home');
} else {
ElMessage.error('用户名或密码错误');
}
}
};
const goToRegister = () => {
router.push('/register');
};
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f5f7fa;
}
.login-card {
width: 400px;
}
.card-header {
text-align: center;
font-size: 20px;
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<div class="register-container">
<el-card class="register-card">
<template #header>
<div class="card-header">
<span>注册</span>
</div>
</template>
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input v-model="registerForm.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="registerForm.password" type="password" placeholder="请输入密码" show-password></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="registerForm.confirmPassword" type="password" placeholder="请再次输入密码" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleRegister">注册</el-button>
<el-button @click="goToLogin">返回登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { register } from '../api/CommonApi';
import { ElMessage } from 'element-plus';
const router = useRouter();
const registerFormRef = ref(null);
const registerForm = ref({
username: '',
password: '',
confirmPassword: '',
});
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== registerForm.value.password) {
callback(new Error("两次输入的密码不一致!"));
} else {
callback();
}
};
const registerRules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
confirmPassword: [{ validator: validatePass, trigger: 'blur' }],
};
const handleRegister = async () => {
const valid = await registerFormRef.value.validate();
if (valid) {
try {
await register({ username: registerForm.value.username, password: registerForm.value.password });
ElMessage.success('注册成功');
router.push('/login');
} catch (error) {
ElMessage.error('注册失败,请稍后再试');
}
}
};
const goToLogin = () => {
router.push('/login');
};
</script>
<style scoped>
.register-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f5f7fa;
}
.register-card {
width: 400px;
}
.card-header {
text-align: center;
font-size: 20px;
}
</style>

View File

@@ -3,6 +3,8 @@ import App from './App.vue'
import router from './router/'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// 1. 导入编辑器和预览组件
import VMdEditor from '@kangc/v-md-editor'
@@ -24,6 +26,8 @@ import '@kangc/v-md-editor/lib/plugins/copy-code/copy-code.css'
import hljs from 'highlight.js'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// 6. 配置编辑器
VMdEditor.use(githubTheme, {
@@ -46,5 +50,6 @@ app.use(VMdPreview)
// 10. 使用Element Plus和路由
app.use(ElementPlus)
app.use(router)
app.use(pinia)
app.mount('#app')

View File

@@ -1,6 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router';
import HomePage from '../components/HomePage.vue';
import MarkdownEditor from '../components/MarkdownEditor.vue';
import LoginPage from '../components/LoginPage.vue';
import RegisterPage from '../components/RegisterPage.vue';
const routes = [
{
@@ -22,6 +24,16 @@ const routes = [
name: 'EditMarkdown',
component: MarkdownEditor,
props: true
},
{
path: '/login',
name: 'Login',
component: LoginPage
},
{
path: '/register',
name: 'Register',
component: RegisterPage
}
];

View File

@@ -0,0 +1,35 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { login as loginApi } from '../api/CommonApi'; // 假设你的API调用函数是这样组织的
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: null,
}),
actions: {
async login(username, password) {
try {
const response = await loginApi({ username, password });
if (response.data && response.data.token) {
this.token = response.data.token;
// 你可能还需要一个接口来获取用户信息
// this.userInfo = await getUserInfo();
return true;
}
return false;
} catch (error) {
console.error('Login failed:', error);
return false;
}
},
logout() {
this.token = '';
this.userInfo = null;
},
},
getters: {
isLoggedIn: (state) => !!state.token,
},
persist: true,
});

View File

@@ -1,4 +1,5 @@
import axios from 'axios'
import { useUserStore } from '../stores/user'
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
@@ -12,6 +13,10 @@ const instance = axios.create({
// 请求拦截器
instance.interceptors.request.use(
config => {
const userStore = useUserStore()
if (userStore.token) {
config.headers['Authorization'] = `Bearer ${userStore.token}`
}
return config
},
error => {