# 前后端联调登录
## 1. 创建认证 Store
### 1.1 创建 stores 目录
**执行命令**:
```bash
mkdir resources\js\admin\stores
```
**说明**:Windows 下需要使用反斜杠 `\`,正斜杠 `/` 会报错「命令语法不正确」。
### 1.2 创建 auth.ts
**文件路径**:`resources\js\admin\stores\auth.ts`
**完整代码**:
```typescript
import { defineStore } from 'pinia';
import axios from 'axios';
interface User {
id: number;
name: string;
email: string;
}
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null as User | null,
token: localStorage.getItem('token'),
}),
getters: {
isAuthenticated: (state) => !!state.token,
},
actions: {
async login(email: string, password: string) {
try {
const response = await axios.post('/api/admin/login', {
email,
password,
});
this.token = response.data.token;
this.user = response.data.user;
localStorage.setItem('token', this.token);
axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`;
return true;
} catch (error) {
return false;
}
},
async logout() {
this.token = null;
this.user = null;
localStorage.removeItem('token');
delete axios.defaults.headers.common['Authorization'];
},
async checkAuth() {
if (!this.token) return;
axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`;
try {
const response = await axios.get('/api/admin/user');
this.user = response.data;
} catch {
this.logout();
}
},
},
});
```
### 1.3 代码说明
| 代码 | 作用 |
|------|------|
| `interface User` | 定义用户数据类型(id、name、email) |
| `state: () => ({...})` | 定义状态:用户信息、token |
| `isAuthenticated` | 计算属性,判断是否已登录 |
| `login()` | 发送登录请求,保存 token 到 localStorage |
| `logout()` | 清除 token 和用户信息 |
| `checkAuth()` | 页面刷新时验证 token 有效性 |
---
## 2. 修改 Login.vue 对接 Store
### 2.1 文件路径
`resources\js\admin\views\Login.vue`
### 2.2 完整代码
```vue
<template>
<div>
<el-card>
<template #header>
<div>
<h2>好站站后台管理</h2>
<p>登录开始建站</p>
</div>
</template>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item prop="email">
<el-input
v-model="form.email"
placeholder="邮箱"
:prefix-icon="User"
size="large"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="密码"
:prefix-icon="Lock"
size="large"
show-password
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
@click="handleLogin"
style="width: 100%"
>
登录
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { User, Lock } from '@element-plus/icons-vue';
import { useAuthStore } from '../stores/auth';
const router = useRouter();
const authStore = useAuthStore();
const formRef = ref();
const loading = ref(false);
const form = reactive({
email: '',
password: '',
});
const rules = {
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email' as const, message: '邮箱格式不正确', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少6位', trigger: 'blur' },
],
};
const handleLogin = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return;
loading.value = true;
const success = await authStore.login(form.email, form.password);
loading.value = false;
if (success) {
ElMessage.success('登录成功');
router.push('/admin');
} else {
ElMessage.error('邮箱或密码错误');
}
});
};
</script>
<style scoped>
.login-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
width: 400px;
}
.login-header {
text-align: center;
}
</style>
```
### 2.3 代码说明
| 代码 | 作用 |
|------|------|
| `import { useAuthStore } from '../stores/auth'` | 导入认证 Store |
| `const authStore = useAuthStore()` | 实例化 Store |
| `await authStore.login(...)` | 调用登录方法 |
| `router.push('/admin')` | 登录成功后跳转仪表盘 |
| `ElMessage.success()` | 成功提示 |
| `ElMessage.error()` | 错误提示 |
### 2.4 登录流程
```
用户填写邮箱和密码
↓
点击登录按钮
↓
前端表单验证(邮箱格式、密码长度)
↓ 验证通过
调用 authStore.login(email, password)
↓
发送 POST 请求到 /api/admin/login
↓ 成功
保存 token 到 localStorage
设置 axios 默认 Authorization Header
↓
显示「登录成功」提示
↓
跳转到 /admin 仪表盘
↓ 失败
显示「邮箱或密码错误」提示
```
---
## 3. 在 main.ts 中引入并使用 Pinia
### 3.1 文件路径
`resources\js\admin\main.ts`
### 3.2 完整代码
```typescript
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
// 注册所有 Element Plus 图标组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.use(createPinia());
app.use(ElementPlus);
app.use(router);
app.mount('#app');
```
### 3.3 修改说明
| 新增代码 | 位置 | 作用 |
|---------|------|------|
| `import { createPinia } from 'pinia'` | 文件顶部 | 引入 Pinia |
| `app.use(createPinia())` | `app.use(router)` 之前 | 安装 Pinia 插件 |
### 3.4 说明
- Pinia 依赖包在阶段1已通过 `npm install pinia` 安装
- 阶段5需要在 `main.ts` 中**引入并使用** Pinia
- 如果不安装 Pinia,`useAuthStore()` 会返回 undefined,导致页面报错
---
## 4. 编译验证
### 4.1 执行命令
```bash
npm run build
```
### 4.2 预期结果
编译成功,无报错。
---
## 5. 测试登录
### 5.1 访问登录页面
浏览器打开:`http://engine-api.test/admin/login`
### 5.2 输入账号密码
| 字段 | 值 |
|------|-----|
| 邮箱 | `admin@haozhanzhan.com` |
| 密码 | `123456` |
### 5.3 点击登录
**预期结果**:
- 显示「登录成功」提示
- 自动跳转到 `/admin`
- 显示仪表盘页面
---
## 6. 遇到的问题及解决
### 6.1 问题:mkdir 命令语法错误
| 项目 | 内容 |
|------|------|
| **现象** | 执行 `mkdir resources/js/admin/stores` 报错「命令语法不正确」 |
| **原因** | Windows 的 `mkdir` 不支持正斜杠 `/` |
| **解决方案** | 使用反斜杠:`mkdir resources\js\admin\stores` |
### 6.2 问题:页面报错 `Cannot read properties of undefined (reading '_s')`
| 项目 | 内容 |
|------|------|
| **现象** | 访问 `/admin/login` 页面空白,控制台报错 |
| **原因** | `main.ts` 中没有引入和使用 Pinia,导致 `useAuthStore()` 返回 undefined |
| **解决方案** | 在 `main.ts` 中添加 `import { createPinia } from 'pinia'` 和 `app.use(createPinia())` |
---
## 7. 验证结果
| 验证项 | 预期结果 | 实际结果 |
|--------|---------|---------|
| 编译 | 无报错 | ✅ 通过 |
| 登录页面访问 | 显示登录表单 | ✅ 通过 |
| 正确账号密码 | 登录成功,跳转仪表盘 | ✅ 通过 |
| 错误密码 | 提示「邮箱或密码错误」 | ✅ 通过 |
| 刷新页面 | 保持登录状态 | ✅ 通过 |