前言
前面章节没看过的朋友请先从第一章开始看 第一章。这章主要写登录相关功能。我的构思是这样的。单独账号admin,不需要注册和权限管理,只实现最简单的登录。
后端
数据库
编辑.env文件
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog-api
DB_USERNAME=root
DB_PASSWORD=root
如果laravel本身自带user表就看下面,如果没有执行生成命令
php artisan make:migration create_users_table
我是自带的,就在原来的上面修改了。
编辑迁移文件database/migrations/2014_10_12_000000_create_users_table.php
// 只修改原来的up方法
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id(); // 主键,自增ID
$table->string('name'); // 用户名,用于登录
$table->string('nickname')->nullable(); // 昵称,用于显示 允许为null
$table->string('email')->unique(); // 邮箱,唯一,用于登录或通知
$table->string('password'); // 密码,使用 bcrypt 加密存储
$table->string('avatar')->nullable(); // 头像路径
$table->timestamps(); // 创建时间和更新时间
});
}
运行迁移
php artisan migrate
新建种子文件
php artisan make:seeder UserSeeder
编辑database/seeders/UserSeeder.php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
public function run()
{
// 插入管理员账号
DB::table('users')->insert([
'name' => 'admin',
'nickname' => '管理员',
'email' => 'admin@example.com',
'password' => Hash::make('123456'), // 使用 bcrypt 加密密码
'avatar' => 'default-avatar.jpg', // 默认头像路径,根据实际情况设置
'created_at' => now(),
'updated_at' => now(),
]);
}
编辑文件database/seeders/DatabaseSeeder
public function run()
{
$this->call([
UserSeeder::class,
]);
}
运行
php artisan db:seed
到这一步的时候去查看数据库里user表是否生成数据,有的话继续,没有的话自己检查一下
API实现
创建模型
php artisan make:model User
编辑app/Models/User.php
/**
* 可填充字段,允许批量赋值。
*/
protected $fillable = ['name', 'nickname', 'email', 'password', 'avatar'];
/**
* 隐藏字段,在序列化时不会包含这些字段。
*/
protected $hidden = [
'password',
'remember_token',
];
创建控制器
php artisan make:controller AuthController
编辑.env文件
APP_URL=http://127.0.0.1:8000
编辑控制器app/Http/Controllers/AuthController.php
/**
* 用户登录并生成认证令牌
*
* @param Request $request
* @return JsonResponse
* @throws ValidationException
*/
public function login(Request $request): JsonResponse
{
// 验证请求数据
$credentials = $request->validate([
'name' => 'required|string',
'password' => 'required|string',
]);
// 尝试使用用户名查找用户
$user = User::where('name', $credentials['name'])->first();
// 验证用户存在
if(!$user) {
return response()->json([
'message' => '用户名不存在'
], 401);
}
// 密码正确
if(!Hash::check($credentials['password'], $user->password)){
return response()->json([
'message' => '密码错误'
],401);
}
// 删除旧令牌并创建新令牌
$user->tokens()->delete();
$token = $user->createToken('blog-token', ['*'])->plainTextToken;
// 返回响应
return response()->json([
'data' => [
'token' => $token,
'user' => [
'id' => $user->id,
'name' => $user->name,
'nickname' => $user->nickname,
'email' => $user->email,
'avatar' => $user->avatar ? asset($user->avatar) : null,
],
],
'message' => '登录成功',
], 200);
}
返回信息,注意我的url是完整路径,这里童鞋可以自己改
{
"data": {
"token": "5|DvBZBXuNBsRLlBxpGvyAodgoRUpJTFNW3TxYlrER",
"user": {
"id": 1,
"name": "admin",
"nickname": "四季豆",
"email": "506717715@qq.com",
"avatar": "http://localhost:8000/default-avatar.jpg"
}
},
"message": "登录成功"
}
编辑routes/api.php
use App\Http\Controllers\AuthController;
Route::post('/login', [AuthController::class, 'login']);
测试token接口
/*
|--------------------------------------------------------------------------
| 受保护接口(需要认证)
|--------------------------------------------------------------------------
*/
Route::middleware('auth:sanctum')->group(function () {
// 测试token接口
Route::post('/test', function () {
return response()->json([
'message' => 'token is valid',
'data' => [
'name' => '四季豆',
'url' => 'https://blog.youkuaiyun.com/php_ACDE?type=blog'
]
], 200);
});
});
返回结果
{
"message": "token is valid",
"data": {
"name": "四季豆",
"url": "https://blog.youkuaiyun.com/php_ACDE?type=blog"
}
}
到这这章后端结束,如果不能正确返回就检查错误或者私我
前端
修改src/views/Home.vue
<template>
<Header />
<h1>Home</h1>
<Footer />
</template>
<script setup>
import Header from '@/components/layout/Header.vue'
import Footer from '@/components/layout/Footer.vue'
</script>
<style scoped>
</style>
创建文件src/components/layout/Header.vue
<template>
<div class="header">
<div class="container">
<div class="logo">
<router-link to="/">Green Beans</router-link>
</div>
<div class="nav" :class="{ active: isMenuOpen}">
<router-link to="/">首页</router-link>
<router-link target="_blank" to="/login">登录</router-link>
</div>
<div class="nav-icon" @click="toggleMenu">
<span>☰</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isMenuOpen = ref(false);
const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value;
};
</script>
<style scoped lang="scss">
.header {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
position: sticky;
top: 0;
z-index: 100;
.container {
display: flex;
justify-content: space-between;
align-items: center;
height: 64px;
}
}
.logo a {
font-size: 24px;
font-weight: 700;
color: #222;
}
.nav {
display: flex;
gap: 20px;
a {
color: #5c6b77;
font-size: 16px;
transition: color 0.3s;
font-weight: 500;
}
}
.nav a:hover,
.nav a.router-link-exact-active {
color: #2563eb;
}
.nav-icon {
display: none;
cursor: pointer;
font-size: 24px;
color: #334155;
transition: color 0.2s ease;
}
.nav-icon:hover {
color: #2563eb;
}
@media (max-width: 768px) {
.header .container {
position: relative;
}
.nav {
display: none;
flex-direction: column;
position: absolute;
top: 60px;
right: 0;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
width: 100%;
padding: 10px;
border-radius: 0 0 16px 16px;
}
.nav-icon {
display: block;
}
.nav.active {
display: flex;
}
}
</style>
创建文件src/components/layout/Footer.vue
<template>
<div class="footer">
<div class="container">
<p>
© {{ currentYear }} Green Beans. All rights reserved.
</p>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
const currentYear = computed(() => new Date().getFullYear());
</script>
<style scoped>
.footer {
background-color: #fff;
color: #888;
text-align: center;
padding: 18px 0 12px 0;
width: 100%;
font-size: 13px;
z-index: 999;
border-top: 1px solid #f0f1f3;
}
.footer p {
margin: 0;
line-height: 1.5;
}
</style>
编辑src/assets/base.scss
.container {
width:100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
修改scr/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
},
// 后台
{
path: '/admin',
name: 'AdminLayout',
component: () => import('@/components/layouts/AdminLayout.vue'),
}
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
创建src/views/Login.vue
<template>
<h1>login</h1>
</template>
<script setup>
</script>
<style scoped>
</style>
到这里,你该去点击下顶部的登录,如果能正常跳转到login页,那么之前算是成功,否则就回去检查错误。
OK,继续!
创建src/api/admin.js
import apiClient from "@/utils/request";
// 认证相关api
export const authApi = {
login(credentials){
return apiClient.post('/login', credentials);
},
logout(){
return apiClient.delete('/logout');
},
user(){
return apiClient.get('/user')
},
nickname() {
return apiClient.patch('nickname')
}
}
创建src/stores/auth.js
import { defineStore } from 'pinia'
import { authApi } from '@/api/admin'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: JSON.parse(localStorage.getItem('user')) || null,
token: localStorage.getItem('token') || null,
loading: false
}),
getters: {
isAuthenticated: (state) => !!state.token && !!state.user,
userRole: (state) => state.user?.role || 'user'
},
actions: {
async login(credentials) {
this.loading = true
try {
const response = await authApi.login(credentials)
const { token, user } = response.data.data
this.token = token
this.user = user
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(user))
return response
} catch (error) {
throw error
} finally {
this.loading = false
}
},
async logout() {
try {
await authApi.logout()
} catch (error) {
console.error('登出API调用失败:', error)
} finally {
this.token = null
this.user = null
localStorage.removeItem('token')
localStorage.removeItem('user')
}
},
async fetchUserInfo() {
if (!this.token) return
try {
// 这里可以添加获取用户信息的API调用
// const response = await authApi.getUserInfo()
// this.user = response.data
// localStorage.setItem('user', JSON.stringify(response.data))
} catch (error) {
console.error('获取用户信息失败:', error)
this.logout()
}
},
updateUserInfo(userInfo) {
this.user = { ...this.user, ...userInfo }
localStorage.setItem('user', JSON.stringify(this.user))
}
}
})
编辑src/views/Login.vue
<template>
<div class="login-container">
<div class="login-card">
<div class="widget-title">登陆</div>
<el-form
class="login-form"
:model="form"
ref="loginForm"
:rules="rules"
@submit.prevent="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="form.username"
placeholder="请输入用户名"
prefix-icon="User"
clearable
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="请输入密码"
prefix-icon="Lock"
show-password
clearable
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
native-type="submit"
:loading="loading"
class="login-button"
>
登录
</el-button>
</el-form-item>
<el-alert
v-if="error"
type="error"
:title="error"
show-icon
:closable="false"
class="error-alert"
/>
</el-form>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
import { ElMessage } from 'element-plus';
const form = ref({
username: '',
password: ''
})
const loading = ref(false);
const error = ref('');
const loginForm = ref(null);
const rules = ref({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
})
const router = useRouter();
const authStore = useAuthStore();
const handleLogin = async () => {
try {
await loginForm.value.validate();
loading.value = true;
error.value = '';
await authStore.login({
name: form.value.username,
password: form.value.password
})
ElMessage.success('登陆成功');
router.push('/admin');
} catch (err){
console.error('登陆错误', err);
if (err.response?.data?.message) {
error.value = err.response.data.message;
} else {
error.value = '登录失败,请检查用户名或密码';
}
ElMessage.error(error.value);
} finally {
loading.value = false;
}
};
</script>
<style lang="scss" scoped>
.login-container {
background: #fafbfc;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 24px 16px;
}
.login-card {
max-width: 360px;
width: 100%;
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 32px 28px;
}
.widget-title {
font-size: 24px;
font-weight: 700;
color: #222;
margin-bottom: 24px;
text-align: center;
}
.login-form {
display: flex;
flex-direction: column;
gap: 18;
}
/* 输入框 */
:deep(.el-input__wrapper) {
border-radius: 6px;
border: 1px solid #e2e8f0;
background: #fff;
transition: border-color 0.2s ease;
}
:deep(.el-input__inner) {
font-size: 15px; /* 主页字体 */
color: #1e293b; /* 主页文字色 */
}
:deep(.el-input__wrapper.is-focus) {
border-color: #2563eb; /* 主页主题色 */
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2); /* 聚焦光晕 */
}
/* 按钮 */
:deep(.el-button) {
background: #2563eb;
border-radius: 6px;
color: #fff;
font-weight: 600;
border: none;
transition: background 0.2s;
}
:deep(.el-button):hover {
background: #1d4ed8;
}
/* 错误提示 */
.error-alert {
margin-top: 16px; /* 与主页 .error-alert 一致 */
}
/* 响应式 */
@media (max-width: 768px) {
.login-container {
padding: 16px; /* 主页移动端 */
}
.login-card {
max-width: 90%; /* 适配小屏幕 */
padding: 16px;
}
}
</style>
创建src/components/layouts/AdminLayout.vue
<template>
<div class="admin-layout">
<h2 class="logo">博客后台</h2>
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
</style>
进入登陆页面,账号admin密码123456 登陆成功进入博客后台页面就算成功
4498

被折叠的 条评论
为什么被折叠?



