Vue3 + antd + Sass 搭建项目
·
使用Vite创建项目
- npm create vue@latest vue-antd-scss-project
- cd vue-antd-scss-project
- npm install
安装核心依赖
- npm install ant-design-vue@4x sass axios @vueuse/core
- 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)),
},
}),
)
更多推荐


所有评论(0)