Ai编程从零开始全栈开发一个后台管理系统之用户登录、权限控制、用户管理-前端部分(十二)
云风网
云风笔记
云风知识库
一、创建前端部分
1、vite初始化项目
npm create vite@latest admin-frontend – --template vue-ts
2、安装必要的依赖
npm install vue-router pinia axios element-plus @element-plus/icons-vue
安装完成后package.json如下:
{
"name": "admin-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.7",
"element-plus": "^2.8.7",
"pinia": "^2.2.6",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vue-tsc": "^2.1.8"
}
}
3、创建必要的文件结构
├── src/
│ ├── assets/
│ ├── api/
│ ├── components/
│ ├── layouts/
│ ├── router/
│ ├── store/
│ ├── types/
│ ├── utils/
│ └── views/
4、在main.ts引入router 、pinia、elementPlus
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import { createPinia } from 'pinia'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
5、在vite.config.ts进行别名alias和base地址配置
import { defineConfig } from 'vite'
import path from 'path'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue()
],
base: '/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
这里如果采用的是ts的写法,配置完可能不会生效,解决方案可以参考
ts兼容别名处理
6、新建router/index.ts管理项目路由
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/layouts/BasicLayout.vue'
const routes = [
{
path: '/login',
component: () => import('@/views/Login.vue'),
meta: { requiresAuth: false }
},
{
path: '/',
component: Layout,
meta: { requiresAuth: true },
children: [
{
path: '',
redirect: '/dashboard'
},
{
path: 'dashboard',
component: () => import('@/views/Dashboard.vue')
},
{
path: 'users',
component: () => import('@/views/user/UserList.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next('/login')
} else {
next()
}
})
export default router
7、新建公共组件layouts/BasicLayout.vue
<template>
<div class="basic-layout">
<header class="header">
<h1 class="logo">My Application</h1>
<nav class="nav">
<router-link
to="/dashboard"
class="nav-item"
:class="{ active: isActive('/dashboard') }"
@click="setActive('/dashboard')"
>Dashboard</router-link>
<router-link
to="/users"
class="nav-item"
:class="{ active: isActive('/users') }"
@click="setActive('/users')"
>Users</router-link>
</nav>
</header>
<main class="main">
<router-view />
</main>
<footer class="footer">
<p>© 2024 My Application</p>
</footer>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
const activeMenu = ref('')
// 获取当前路由
const route = useRoute()
// 设置选中的菜单项
const setActive = (menu: string) => {
activeMenu.value = menu
}
// 检查当前菜单项是否为选中状态
const isActive = (menu: string) => {
return activeMenu.value === menu || route.path === menu
}
// 初始化选中的菜单项
setActive(route.path)
</script>
<style scoped>
.basic-layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.header {
background-color: #282c34;
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.main {
flex: 1; /* 使主内容区域占据剩余空间 */
background-color: #f4f4f4; /* 浅色背景 */
padding: 20px; /* 内边距 */
}
.footer {
background-color: #282c34; /* 深色背景 */
color: white; /* 白色文本 */
text-align: center; /* 中心对齐 */
padding: 10px; /* 内边距 */
}
.logo {
font-size: 24px;
font-weight: bold;
}
.nav {
display: flex;
}
.nav-item {
color: white;
text-decoration: none;
padding: 10px 15px;
transition: background-color 0.3s;
}
.nav-item:hover {
background-color: #3a3f47;
border-radius: 5px;
}
.nav-item.active {
background-color: #409eff; /* 选中状态的背景色 */
border-radius: 5px; /* 圆角 */
}
</style>
这里可能会报错ref找不到相应的模块,这是可以采用自动导入插件unplugin-auto-import
可以参考 unplugin-auto-import自动导入
8、改写app.vue
<template>
<div id="app">
<router-view />
</div>
</template>
<script setup lang="ts">
</script>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
background-color: #f4f4f4;
}
#app {
min-height: 100vh;
width: 100%;
}
</style>
9、新建views/Login.vue
<template>
<div class="login-container">
<el-card class="login-card" shadow="hover">
<h2 class="login-title">系统登录</h2>
<el-form :model="loginForm" @submit.prevent="handleLogin">
<el-form-item>
<el-input
v-model="loginForm.username"
placeholder="用户名"
:prefix-icon="User"
class="input-field"
/>
</el-form-item>
<el-form-item>
<el-input
v-model="loginForm.password"
type="password"
placeholder="密码"
:prefix-icon="Lock"
class="input-field"
/>
</el-form-item>
<el-form-item class="button-container">
<el-button type="primary" native-type="submit" class="login-button">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '../store/user'
import type { LoginForm } from '../types/user'
import { User, Lock } from '@element-plus/icons-vue'
const router = useRouter()
const userStore = useUserStore()
const loginForm = ref<LoginForm>({
username: '',
password: ''
})
const handleLogin = async () => {
try {
await userStore.login(loginForm.value)
router.push('/')
} catch (error) {
console.error(error)
}
}
</script>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden; /* Prevent scrollbars */
}
.login-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #e9ecef; /* Light background color */
}
.login-card {
width: 400px; /* Increased width for better aesthetics */
padding: 20px; /* Padding for the card */
border-radius: 10px; /* Rounded corners */
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* Soft shadow */
background-color: #ffffff; /* White background for the card */
}
.login-title {
text-align: center;
margin-bottom: 20px; /* Space below the title */
font-size: 24px; /* Larger font size */
color: #333; /* Darker text color */
}
.input-field {
margin-bottom: 15px; /* Space between input fields */
}
.button-container {
display: flex; /* Use flexbox for centering */
justify-content: center; /* Center the button horizontally */
}
.login-button {
background-color: #409eff; /* Primary color */
color: #fff; /* Text color */
border: none; /* Remove border */
border-radius: 5px; /* Rounded corners */
padding: 10px 20px; /* Padding for the button */
font-size: 16px; /* Button font size */
transition: background-color 0.3s, transform 0.2s; /* Transition effects */
width: 100%; /* Make button full width */
}
.login-button:hover {
background-color: #66b1ff; /* Lighter color on hover */
transform: translateY(-2px); /* Slight lift effect */
}
.login-button:active {
background-color: #007bff; /* Darker color on click */
transform: translateY(0); /* Reset lift effect */
}
</style>
10、新建store/user.ts进行全局变量状态管理
import { defineStore } from 'pinia'
import { login } from '@/api/user'
import type { LoginForm, UserInfo } from '@/types/user'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
userInfo: null as UserInfo | null
}),
actions: {
async login(loginForm: LoginForm) {
const { token, userInfo } = await login(loginForm)
this.token = token
this.userInfo = userInfo
localStorage.setItem('token', token)
},
logout() {
this.token = ''
this.userInfo = null
localStorage.removeItem('token')
}
}
})
11、新建api/user.ts定义接口请求方法
import axios from 'axios';
import type { LoginForm, UserInfo } from '@/types/user';
export async function login(loginForm: LoginForm): Promise<{ token: string; userInfo: UserInfo }> {
try {
const response = await axios.post('/api/login', loginForm);
return response.data;
} catch (error) {
console.error('Login failed:', error);
throw new Error('Login failed');
}
}
12、新建types/user.ts定义接口interface
export interface LoginForm {
username: string
password: string
}
export interface UserInfo {
id: string
username: string
}
13、新建utils/request.ts定义请求配置以及拦截器
import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: '/',
timeout: 6000
})
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
request.interceptors.response.use(
response => response.data,
error => {
ElMessage.error(error.response?.data?.message || '请求失败')
return Promise.reject(error)
}
)
export default request
14、项目安装mock进行接口请求模拟数据处理
1、安装依赖
npm i mockjs vite-plugin-mock --save-dev
2、新建src/mock/index.ts定义模拟返回数据
// mock/index.js
export default [{
type: 'get',
url: '/api/login',
response: () => {
return {
token: '1234567890',
userInfo: {
name: 'admin',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'
}
}
}
}]
3、main.ts引入
import './mock/index.ts'
4、vite.config.ts进行配置引入
import { defineConfig } from 'vite'
import path from 'path'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite';
import { viteMockServe } from 'vite-plugin-mock'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports:['vue','vue-router'],
dts: 'src/auto-imports.d.ts'
}),
viteMockServe({
mockPath: 'src/mock/',
enable: true
})
],
base: '/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
}
})
有可能引用不生效可以参考mock数据处理
15、项目首页新建view/Dashboard.vue
<template>
<div class="dashboard">
<h2>Dashboard</h2>
<p>Welcome to the dashboard! Here you can find an overview of your application.</p>
<div class="stats">
<div class="stat-card">
<h3>Total Users</h3>
<p>{{ totalUsers }}</p>
</div>
<div class="stat-card">
<h3>Total Posts</h3>
<p>{{ totalPosts }}</p>
</div>
<div class="stat-card">
<h3>Total Comments</h3>
<p>{{ totalComments }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 定义响应式数据
const totalUsers = ref(100) // 示例数据
const totalPosts = ref(50) // 示例数据
const totalComments = ref(200) // 示例数据
</script>
<style scoped>
.dashboard {
padding: 1rem;
}
.stats {
display: flex;
justify-content: space-around;
margin-top: 2rem;
}
.stat-card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 1rem;
width: 30%;
text-align: center;
}
</style>
15、项目用户管理新建view/user/UserList.vue
<template>
<div class="user-list">
<h2>User List</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>
<button @click="editUser(user.id)">Edit</button>
<button @click="deleteUser(user.id)">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 示例用户数据
const users = ref([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
])
// 编辑用户的函数
const editUser = (id: number) => {
console.log(`Edit user with ID: ${id}`)
// 在这里添加编辑用户的逻辑
}
// 删除用户的函数
const deleteUser = (id: number) => {
console.log(`Delete user with ID: ${id}`)
// 在这里添加删除用户的逻辑
}
</script>
<style scoped>
.user-list {
padding: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ccc;
padding: 0.5rem;
text-align: left;
}
button {
margin-right: 0.5rem;
}
</style>
至此用户相关前端部分基础完成,运行项目可以看到效果
附加 package.json:
{
"name": "admin-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.7",
"element-plus": "^2.8.7",
"pinia": "^2.2.6",
"vite-plugin-mock": "^3.0.2",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@types/mockjs": "^1.0.10",
"@types/node": "^22.9.0",
"@vitejs/plugin-vue": "^5.1.4",
"mockjs": "^1.1.0",
"typescript": "~5.6.2",
"unplugin-auto-import": "^0.18.4",
"vite": "^5.4.10",
"vue-tsc": "^2.1.8"
}
}