当前位置: 首页 > article >正文

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>&copy; 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"
  }
}


http://www.kler.cn/a/394580.html

相关文章:

  • 批量从Excel某一列中找到符合要求的值并提取其对应数据
  • ECharts 实现大屏地图功能
  • 修改数据库和表的字符集
  • 云运维基础
  • Servlet入门 Servlet生命周期 Servlet体系结构
  • 马斯克万卡集群AI数据中心引发的科技涟漪:智算数据中心挑战与机遇的全景洞察
  • nacos配置中心入门
  • 【达梦数据库】参数优化脚本主要改什么
  • spark.default.parallelism 在什么时候起作用,与spark.sql.shuffle.partitions有什么异同点?
  • LaTeX中浮动体(图片、表格)的位置及上下间距设置
  • 使用命令强制给ESXI上的硬盘分区
  • Grafana Username password invalid
  • JavaScript的展开运算符在React中的应用
  • 游戏引擎学习第11天
  • 软件测试计划和测试用例详解
  • 鸿蒙学习生态应用开发能力全景图-鸿蒙生态伙伴 SDK 市场(4)
  • 家政服务平台管理系统(源码+文档+部署+讲解)
  • Sql进阶:字段中包含CSV,如何通过Sql解析CSV成多行多列?
  • 【数据结构】顺序表解析及实战运用
  • 【Redis实战篇】利用布隆过滤器解决缓存穿透问题
  • 力扣题目解析--合并两个链表
  • SystemVerilog学习笔记(十一):接口
  • 相机光学(四十)——2x2 Adjacent Pixel Binning
  • 小程序开发者工具的network选项卡中有某域名的接口请求,但是在charles中抓不到该接口
  • Python图像识别详解
  • STL学习-排序算法