使用Vite创建项目

  1. npm create vue@latest vue-antd-scss-project
  2. cd vue-antd-scss-project
  3. npm install

安装核心依赖

  1. npm install ant-design-vue@4x sass axios @vueuse/core
  2. npm install axios vite-plugin-mock mockjs -D

代码块

  • App.vue
<script setup>

</script>

<template>
  <router-view />
</template>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>
  • main.js
import { createApp } from 'vue'
import App from './App.vue'
import './mock'
import router from './router'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'

const app = createApp(App)

app.use(router)
app.use(Antd)
app.mount('#app')
  • vite.config.js 
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { viteMockServe } from 'vite-plugin-mock'
import vueDevTools from 'vite-plugin-vue-devtools'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    viteMockServe({
      mockPath: 'mock',
      localEnabled: true,
      logger: true,
      supportTs: false
    }),
    // vueDevTools(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    },
  },
})
  • src/views/login.vue
<template>
  <div class="login-container">
    <a-card title="用户登录" class="login-card">
      <a-form
        :model="formState"
        @finish="handleSubmit"
      >
        <a-form-item
          name="username"
          :rules="[{ required: true, message: '请输入用户名' }]"
        >
          <a-input v-model:value="formState.username" placeholder="用户名">
            <template #prefix>
              <UserOutlined />
            </template>
          </a-input>
        </a-form-item>

        <a-form-item
          name="password"
          :rules="[{ required: true, message: '请输入密码' }]"
        >
          <a-input-password v-model:value="formState.password" placeholder="密码">
            <template #prefix>
              <LockOutlined />
            </template>
          </a-input-password>
        </a-form-item>

        <a-form-item>
          <a-button
            type="primary"
            html-type="submit"
            :loading="loading"
            block
          >
            登录
          </a-button>
        </a-form-item>
      </a-form>
    </a-card>
  </div>
</template>

<script setup>
import { reactive, ref } from 'vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { login } from '@/api/auth'

const router = useRouter()
const loading = ref(false)
const formState = reactive({
  username: 'admin',
  password: 'admin321'
})

const handleSubmit = async () => {
  try {
    const res = await login(formState)
    localStorage.setItem('token', res.data.token)
    localStorage.setItem('userInfo', JSON.stringify(res.data.userInfo))
    router.push('/home')
  } catch (err) {
    message.error(err.response?.data?.message || '账号或密码错误')
  }
}
</script>

<style lang="scss" scoped>
.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background: #f0f2f5;

  .login-card {
    width: 400px;
    border-radius: 8px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  }
}
</style>
  •  src/views/home.vue
<template>
  <div class="home-container">
    <a-layout>
      <a-layout-header class="header">
        <div class="logo">管理系统</div>
        <a-button type="link" @click="handleLogout">
          <LogoutOutlined /> 退出登录
        </a-button>
      </a-layout-header>
      <a-layout-content class="content">
        <p></p>
        <div class="app-container">
          <UserCard :userData="userData" />
        </div>
      </a-layout-content>
    </a-layout>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { LogoutOutlined } from '@ant-design/icons-vue'
import { logout } from '@/api/auth'
import UserCard from '@/components/UserCard'

const router = useRouter()
const username = computed(() => localStorage.getItem('username') || '未知用户')
const userInfo = localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')) : {}
const userData = {
  avatar: userInfo.avatar || 'https://picsum.photos/200',
  name: userInfo.username || '未知用户',
  title: '前端工程师',
  description: '专注于Vue开发'
}

const handleLogout = async () => {
  await logout()
  router.push('/login')
}
</script>



<style lang="scss" scoped>
.home-container {
  height: 90vh;

  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: #fff;
    padding: 0 24px;
    box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);

    .logo {
      font-size: 20px;
      font-weight: bold;
    }
  }

  .content {
    padding: 24px;
    background: #f0f2f5;
    min-height: calc(100vh - 90px);
  }
  .app-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
  }
}
</style>
  • src/views/components/UserCard/UserCard.vue
<template>
  <a-card
    class="user-card"
    hoverable
    :body-style="{ padding: '16px', display: 'flex', flexDirection: layout }"
  >
    <div class="avatar-container">
      <a-avatar :size="64" :src="userData.avatar" />
    </div>
    <div class="content-container">
      <h3 class="name">{{ userData.name }}</h3>
      <p class="title">{{ userData.title }}</p>
      <p class="description">{{ userData.description }}</p>
    </div>
  </a-card>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useWindowSize } from '@vueuse/core'

interface UserData {
  avatar: string
  name: string
  title: string
  description: string
}

const props = defineProps<{
  userData: UserData
}>()

const { width } = useWindowSize()
const layout = computed(() => (width.value > 768 ? 'row' : 'column'))

</script>

<style lang="scss">
$primary-color: #1890ff;
$card-padding: 16px;

.user-card {
  transition: all 0.3s ease;
  max-width: 600px;
  margin: 0 auto;

  &:hover {
    border-color: $primary-color;
    box-shadow: 0 2px 8px rgba($primary-color, 0.15);
  }

  .avatar-container {
    margin-right: $card-padding;
    margin-bottom: $card-padding;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .content-container {
    flex: 1;

    .name {
      margin: 0 0 8px 0;
      font-size: 18px;
      font-weight: 500;
      color: rgba(0, 0, 0, 0.85);
    }

    .title {
      margin: 0 0 8px 0;
      color: $primary-color;
      font-size: 14px;
    }

    .description {
      margin: 0;
      color: rgba(0, 0, 0, 0.65);
      font-size: 14px;
    }
  }

  @media (max-width: 768px) {
    .avatar-container {
      margin-right: 0;
      margin-bottom: $card-padding;
    }
  }
}
</style>
  •  src/views/components/UserCard/index.js 
import UserCard from './UserCard.vue'

export default UserCard
  • src/views/router/index.js 
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/login.vue'
import Home from '@/views/home.vue'

const routes = [
  {
    path: '/',
    redirect: '/login'
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/home',
    name: 'Home',
    component: Home,
    meta: { requiresAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
})

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !localStorage.getItem('token')) {
    next('/login')
  } else {
    next()
  }
})

export default router
  •  src/views/api/auth.js
import axios from 'axios'

const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE,
  timeout: 10000
})

// 请求拦截器
service.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截器
service.interceptors.response.use(
  response => response.data,
  error => {
    console.error('API Error:', error)
    return Promise.reject(error)
  }
)

export const login = data => service.post('/api/login', data)
export const logout = () => {
  localStorage.removeItem('token')
  return Promise.resolve()
}
  •  src/views/mock/index.js
import Mock from 'mockjs'
import login from './login' // 显式引入login模块

Mock.setup({
  timeout: '200-600'
})

// 统一注册所有mock模块
const modules = [login]
modules.forEach(module => {
  for (const [path, target] of Object.entries(module)) {
    const [method, url] = path.split('|')
    Mock.mock(new RegExp(url), method.toLowerCase(), target)
  }
})
  • src/views/mock/login.js 
import Mock from "mockjs";

export default {
  'post|/api/login': (options) => {
    const { username, password } = JSON.parse(options.body)
    if (username === 'admin' && password === 'admin321') {
      return Mock.mock({
        code: 200,
        data: {
          'token': '@guid',
          'userId': '@id',
          'roles|1': ['admin', 'editor'],
          userInfo: {
            username: username,
            avatar: 'https://picsum.photos/80/80?user'
          }
        }
      })
    }
    return { code: 401, message: '认证失败' }
  },
    'get|/api/logout': () => ({ code: 200 })
}
  • vitest.config.js 
import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
import viteConfig from './vite.config'

export default mergeConfig(
  viteConfig,
  defineConfig({
    test: {
      environment: 'jsdom',
      exclude: [...configDefaults.exclude, 'e2e/**'],
      root: fileURLToPath(new URL('./', import.meta.url)),
    },
  }),
)

 

 

 

 

 

 

 

 

 

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐