Vue3 搭配 Tailwind CSS 是构建现代后台管理系统的绝佳组合。Vue3 提供了高效的响应式框架,而 Tailwind CSS 则让样式编写变得快速且灵活。下面我将分步骤教你如何创建一个功能完整的后台管理系统。
第 1 步:创建项目
首先,我们需要使用 Vite 创建一个 Vue3 项目,并安装 Tailwind CSS、路由、 Font Awesome:
html
npm init vite@latest admin-system -- --template vue
cd admin-system
npm install
npm install -D tailwindcss@3.4.1 postcss autoprefixer
npx tailwindcss init -p
npm install vue-router@4
npm install font-awesome
第 2 步:配置 Tailwind CSS
创建 tailwind.config.js
文件并配置:
javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: '#165DFF',
secondary: '#36CFC9',
success: '#52C41A',
warning: '#FAAD14',
danger: '#F5222D',
info: '#86909C',
light: '#F2F3F5',
dark: '#1D2129',
},
fontFamily: {
inter: ['Inter', 'sans-serif'],
},
},
},
plugins: [],
}
在 src/index.css
中引入 Tailwind CSS:
javascript
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.content-auto {
content-visibility: auto;
}
.sidebar-item-active {
@apply bg-primary/10 text-primary border-l-4 border-primary;
}
}
第 4 步:创建布局组件
html
<template>
<div class="min-h-screen flex flex-col bg-gray-50">
<!-- 顶部导航栏 -->
<header class="bg-white shadow-sm">
<div class="flex items-center justify-between px-4 py-3">
<div class="flex items-center">
<button @click="toggleSidebar" class="md:hidden text-gray-500 focus:outline-none">
<i class="fa fa-bars text-xl"></i>
</button>
<div class="ml-4 text-xl font-bold text-primary">管理系统</div>
</div>
<div class="flex items-center">
<div class="relative mr-4">
<input type="text" placeholder="搜索..." class="pl-8 pr-4 py-2 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-primary/50">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<div class="relative ml-4">
<button class="relative text-gray-500 focus:outline-none">
<i class="fa fa-bell text-xl"></i>
<span class="absolute top-0 right-0 h-4 w-4 bg-red-500 rounded-full flex items-center justify-center text-white text-xs">3</span>
</button>
</div>
<div class="relative ml-6">
<button @click="toggleDropdown" class="flex items-center focus:outline-none">
<img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-8 h-8 rounded-full object-cover">
<span class="ml-2 text-sm font-medium">管理员</span>
<i class="fa fa-angle-down ml-1 text-gray-500"></i>
</button>
<div v-show="dropdownVisible" class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-10">
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fa fa-user mr-2"></i>个人信息
</a>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fa fa-cog mr-2"></i>设置
</a>
<div class="border-t border-gray-100 my-1"></div>
<a href="#" @click="logout" class="block px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
<i class="fa fa-sign-out mr-2"></i>退出登录
</a>
</div>
</div>
</div>
</div>
</header>
<div class="flex flex-1 overflow-hidden">
<!-- 侧边栏导航 -->
<aside :class="sidebarVisible ? 'translate-x-0' : '-translate-x-full'" class="fixed md:relative z-20 w-64 bg-white shadow-lg h-full transition-transform duration-300 ease-in-out">
<nav class="py-4">
<div class="px-4 mb-6">
<div class="flex items-center">
<img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-10 h-10 rounded-full object-cover">
<div class="ml-3">
<div class="text-sm font-medium text-gray-900">管理员</div>
<div class="text-xs text-gray-500">系统管理员</div>
</div>
</div>
</div>
<div class="px-2 space-y-1">
<a href="/dashboard" class="flex items-center px-4 py-3 text-gray-600 rounded-lg sidebar-item-active">
<i class="fa fa-tachometer mr-3"></i>
<span>仪表盘</span>
</a>
<a href="/users" class="flex items-center px-4 py-3 text-gray-600 rounded-lg hover:bg-gray-100 transition-colors duration-200">
<i class="fa fa-users mr-3"></i>
<span>用户管理</span>
</a>
<a href="/products" class="flex items-center px-4 py-3 text-gray-600 rounded-lg hover:bg-gray-100 transition-colors duration-200">
<i class="fa fa-shopping-bag mr-3"></i>
<span>产品管理</span>
</a>
<a href="#" class="flex items-center px-4 py-3 text-gray-600 rounded-lg hover:bg-gray-100 transition-colors duration-200">
<i class="fa fa-bar-chart mr-3"></i>
<span>数据分析</span>
</a>
<a href="#" class="flex items-center px-4 py-3 text-gray-600 rounded-lg hover:bg-gray-100 transition-colors duration-200">
<i class="fa fa-cog mr-3"></i>
<span>系统设置</span>
</a>
</div>
<div class="px-4 py-4 mt-6 border-t border-gray-100">
<div class="text-xs font-medium text-gray-500 uppercase tracking-wider">帮助</div>
<a href="#" class="flex items-center px-4 py-2 mt-2 text-gray-600 rounded-lg hover:bg-gray-100 transition-colors duration-200">
<i class="fa fa-question-circle mr-3"></i>
<span>帮助中心</span>
</a>
<a href="#" class="flex items-center px-4 py-2 text-gray-600 rounded-lg hover:bg-gray-100 transition-colors duration-200">
<i class="fa fa-life-ring mr-3"></i>
<span>联系支持</span>
</a>
</div>
</nav>
</aside>
<!-- 主内容区 -->
<main class="flex-1 overflow-y-auto p-6 bg-gray-50">
<div class="mb-6">
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-gray-900">仪表盘</h1>
<p class="mt-1 text-gray-500">欢迎使用管理系统</p>
</div>
<div class="mb-6 flex flex-col md:flex-row md:space-x-4 space-y-4 md:space-y-0">
<div class="bg-white rounded-xl shadow-sm p-6 flex-1">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">用户总数</p>
<h3 class="text-3xl font-bold text-gray-900 mt-1">1,284</h3>
<p class="text-xs text-green-500 mt-2">
<i class="fa fa-arrow-up"></i> 12% 较上月
</p>
</div>
<div class="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center">
<i class="fa fa-users text-primary text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-6 flex-1">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">产品总数</p>
<h3 class="text-3xl font-bold text-gray-900 mt-1">528</h3>
<p class="text-xs text-green-500 mt-2">
<i class="fa fa-arrow-up"></i> 8% 较上月
</p>
</div>
<div class="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center">
<i class="fa fa-shopping-bag text-green-600 text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-6 flex-1">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">订单总数</p>
<h3 class="text-3xl font-bold text-gray-900 mt-1">2,451</h3>
<p class="text-xs text-green-500 mt-2">
<i class="fa fa-arrow-up"></i> 16% 较上月
</p>
</div>
<div class="w-12 h-12 rounded-full bg-purple-100 flex items-center justify-center">
<i class="fa fa-file-text text-purple-600 text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-6 flex-1">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">销售额</p>
<h3 class="text-3xl font-bold text-gray-900 mt-1">¥156,284</h3>
<p class="text-xs text-green-500 mt-2">
<i class="fa fa-arrow-up"></i> 23% 较上月
</p>
</div>
<div class="w-12 h-12 rounded-full bg-yellow-100 flex items-center justify-center">
<i class="fa fa-line-chart text-yellow-600 text-xl"></i>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold text-gray-900">销售趋势</h2>
<div class="flex space-x-2">
<button class="px-3 py-1 text-xs rounded-full bg-gray-100 text-gray-600">日</button>
<button class="px-3 py-1 text-xs rounded-full bg-primary text-white">周</button>
<button class="px-3 py-1 text-xs rounded-full bg-gray-100 text-gray-600">月</button>
</div>
</div>
<div class="h-80">
<!-- 这里可以放置图表组件 -->
<div class="w-full h-full flex items-center justify-center">
<p class="text-gray-400">销售趋势图表将显示在这里</p>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold text-gray-900">销售分布</h2>
<button class="text-primary text-sm">查看全部</button>
</div>
<div class="h-80">
<!-- 这里可以放置图表组件 -->
<div class="w-full h-full flex items-center justify-center">
<p class="text-gray-400">销售分布图表将显示在这里</p>
</div>
</div>
</div>
</div>
<div class="mt-6 bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold text-gray-900">最近订单</h2>
<button class="text-primary text-sm">查看全部</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">订单ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">客户</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">产品</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">#ORD-12345</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img class="h-10 w-10 rounded-full" src="https://picsum.photos/id/1001/40/40" alt="用户头像">
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">张三</div>
<div class="text-sm text-gray-500">zhangsan@example.com</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">电子产品</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">¥1,299.00</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">已完成</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-primary">
<button>查看详情</button>
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">#ORD-12346</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img class="h-10 w-10 rounded-full" src="https://picsum.photos/id/1002/40/40" alt="用户头像">
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">李四</div>
<div class="text-sm text-gray-500">lisi@example.com</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">家居用品</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">¥499.00</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">处理中</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-primary">
<button>查看详情</button>
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">#ORD-12347</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img class="h-10 w-10 rounded-full" src="https://picsum.photos/id/1003/40/40" alt="用户头像">
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">王五</div>
<div class="text-sm text-gray-500">wangwu@example.com</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">图书音像</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">¥299.00</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">已发货</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-primary">
<button>查看详情</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const sidebarVisible = ref(true)
const dropdownVisible = ref(false)
const toggleSidebar = () => {
sidebarVisible.value = !sidebarVisible.value
}
const toggleDropdown = () => {
dropdownVisible.value = !dropdownVisible.value
}
const logout = () => {
localStorage.removeItem('token')
router.push('/login')
}
onMounted(() => {
// 检查登录状态
if (!localStorage.getItem('token')) {
router.push('/login')
}
})
</script>
<style scoped>
/* 移动端适配 */
@media (max-width: 768px) {
.sidebar {
position: fixed;
z-index: 100;
transform: translateX(-100%);
transition: transform 0.3s ease-in-out;
}
.sidebar-visible {
transform: translateX(0);
}
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 99;
display: none;
}
.sidebar-visible + .overlay {
display: block;
}
}
</style>
第 5 步:创建登录页面
创建登录页面 src/views/Login.vue
:
html
<template>
<div class="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div class="max-w-md w-full bg-white rounded-xl shadow-lg overflow-hidden">
<div class="p-6 sm:p-8">
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-gray-900">管理系统登录</h1>
<p class="mt-1 text-sm text-gray-500">请输入账号密码登录</p>
</div>
<form @submit.prevent="handleLogin">
<div class="space-y-4">
<div>
<label for="email" class="block text-sm font-medium text-gray-700">邮箱</label>
<div class="mt-1 relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fa fa-envelope text-gray-400"></i>
</div>
<input type="email" id="email" v-model="form.email" class="pl-10 block w-full rounded-md border-gray-300 shadow-sm focus:ring-primary focus:border-primary" placeholder="your@email.com">
</div>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">密码</label>
<div class="mt-1 relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fa fa-lock text-gray-400"></i>
</div>
<input type="password" id="password" v-model="form.password" class="pl-10 block w-full rounded-md border-gray-300 shadow-sm focus:ring-primary focus:border-primary" placeholder="••••••••">
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<input id="remember-me" name="remember-me" type="checkbox" class="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded">
<label for="remember-me" class="ml-2 block text-sm text-gray-900">记住我</label>
</div>
<div class="text-sm">
<a href="#" class="font-medium text-primary hover:text-primary/80">忘记密码?</a>
</div>
</div>
<div>
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
登录
</button>
</div>
</div>
</form>
<div class="mt-6 relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500">其他登录方式</span>
</div>
</div>
<div class="mt-6 grid grid-cols-3 gap-3">
<button type="button" class="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fa fa-weixin text-green-600"></i>
</button>
<button type="button" class="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fa fa-qq text-blue-500"></i>
</button>
<button type="button" class="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fa fa-github text-gray-800"></i>
</button>
</div>
</div>
<div class="bg-gray-50 px-6 py-4 text-center">
<p class="text-sm text-gray-600">
还没有账号? <a href="#" class="font-medium text-primary hover:text-primary/80">注册</a>
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const form = ref({
email: '',
password: ''
})
const handleLogin = () => {
// 模拟登录验证
if (form.value.email && form.value.password) {
localStorage.setItem('token', 'fake_token')
router.push('/dashboard')
} else {
alert('请输入邮箱和密码')
}
}
</script>
第 6 步:创建用户管理页面
创建用户管理页面 src/views/Users.vue
:
html
<template>
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
<div>
<h2 class="text-xl font-bold text-gray-900">用户管理</h2>
<p class="mt-1 text-sm text-gray-500">管理系统用户信息</p>
</div>
<div class="mt-4 md:mt-0 flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3">
<div class="relative">
<input type="text" v-model="searchQuery" placeholder="搜索用户..." class="pl-10 pr-4 py-2 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-primary/50 w-full sm:w-64">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<button @click="handleCreateUser" class="inline-flex items-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
<i class="fa fa-plus mr-2"></i>
创建用户
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户信息</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">角色</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">创建时间</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="user in users" :key="user.id">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ user.id }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img class="h-10 w-10 rounded-full" :src="user.avatar" alt="用户头像">
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ user.name }}</div>
<div class="text-sm text-gray-500">{{ user.email }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
{{ user.role }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="user.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full">
{{ user.status === 'active' ? '活跃' : '禁用' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ user.createdAt }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button class="text-indigo-600 hover:text-indigo-900 mr-3" @click="handleEditUser(user)">编辑</button>
<button class="text-red-600 hover:text-red-900" @click="handleDeleteUser(user)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-6 flex items-center justify-between">
<div class="flex-1 flex justify-between sm:hidden">
<button class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
上一页
</button>
<button class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
下一页
</button>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
显示第 <span class="font-medium">{{ (currentPage - 1) * pageSize + 1 }}</span> 至 <span class="font-medium">{{ Math.min(currentPage * pageSize, totalUsers) }}</span> 条,共 <span class="font-medium">{{ totalUsers }}</span> 条记录
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<button :disabled="currentPage === 1" @click="currentPage--" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span class="sr-only">上一页</span>
<i class="fa fa-chevron-left h-5 w-5"></i>
</button>
<button v-for="page in totalPages" :key="page" :class="page === currentPage ? 'z-10 bg-primary text-white' : 'bg-white text-gray-700'" :aria-current="page === currentPage ? 'page' : undefined" @click="currentPage = page" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium">
{{ page }}
</button>
<button :disabled="currentPage === totalPages" @click="currentPage++" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span class="sr-only">下一页</span>
<i class="fa fa-chevron-right h-5 w-5"></i>
</button>
</nav>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const searchQuery = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const totalUsers = ref(124)
const totalPages = computed(() => {
return Math.ceil(totalUsers.value / pageSize.value)
})
const users = ref([
{
id: 1,
name: '张三',
email: 'zhangsan@example.com',
role: '管理员',
status: 'active',
avatar: 'https://picsum.photos/id/1001/40/40',
createdAt: '2023-01-15'
},
{
id: 2,
name: '李四',
email: 'lisi@example.com',
role: '编辑',
status: 'active',
avatar: 'https://picsum.photos/id/1002/40/40',
createdAt: '2023-02-20'
},
{
id: 3,
name: '王五',
email: 'wangwu@example.com',
role: '普通用户',
status: 'disabled',
avatar: 'https://picsum.photos/id/1003/40/40',
createdAt: '2023-03-10'
},
{
id: 4,
name: '赵六',
email: 'zhaoliu@example.com',
role: '普通用户',
status: 'active',
avatar: 'https://picsum.photos/id/1004/40/40',
createdAt: '2023-04-05'
},
{
id: 5,
name: '钱七',
email: 'qianqi@example.com',
role: '编辑',
status: 'active',
avatar: 'https://picsum.photos/id/1005/40/40',
createdAt: '2023-05-12'
}
])
const handleCreateUser = () => {
console.log('创建用户')
// 打开创建用户模态框
}
const handleEditUser = (user) => {
console.log('编辑用户', user)
// 打开编辑用户模态框
}
const handleDeleteUser = (user) => {
if (confirm(`确定要删除用户 ${user.name} 吗?`)) {
console.log('删除用户', user)
// 调用API删除用户
}
}
onMounted(() => {
// 加载用户数据
})
</script>
第 7 步:创建产品管理页面
创建产品管理页面 src/views/Products.vue
:
html
<template>
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
<div>
<h2 class="text-xl font-bold text-gray-900">产品管理</h2>
<p class="mt-1 text-sm text-gray-500">管理系统产品信息</p>
</div>
<div class="mt-4 md:mt-0 flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3">
<div class="relative">
<input type="text" v-model="searchQuery" placeholder="搜索产品..." class="pl-10 pr-4 py-2 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-primary/50 w-full sm:w-64">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<div class="relative">
<select v-model="categoryFilter" class="pl-4 pr-10 py-2 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-primary/50 appearance-none bg-white">
<option value="">全部分类</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
<option value="home">家居用品</option>
<option value="books">图书音像</option>
</select>
<i class="fa fa-chevron-down absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none"></i>
</div>
<button @click="handleCreateProduct" class="inline-flex items-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
<i class="fa fa-plus mr-2"></i>
创建产品
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">产品信息</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">分类</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">价格</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">库存</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="product in products" :key="product.id">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ product.id }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-12 w-12">
<img class="h-12 w-12 rounded-md object-cover" :src="product.image" alt="产品图片">
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ product.name }}</div>
<div class="text-sm text-gray-500">{{ product.description }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-purple-100 text-purple-800">
{{ product.category }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">¥{{ product.price.toFixed(2) }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="product.stock < 10 ? 'text-red-600' : 'text-gray-900'" class="text-sm font-medium">
{{ product.stock }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="product.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full">
{{ product.status === 'active' ? '上架' : '下架' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button class="text-indigo-600 hover:text-indigo-900 mr-3" @click="handleEditProduct(product)">编辑</button>
<button class="text-red-600 hover:text-red-900" @click="handleDeleteProduct(product)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-6 flex items-center justify-between">
<div class="flex-1 flex justify-between sm:hidden">
<button class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
上一页
</button>
<button class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
下一页
</button>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
显示第 <span class="font-medium">{{ (currentPage - 1) * pageSize + 1 }}</span> 至 <span class="font-medium">{{ Math.min(currentPage * pageSize, totalProducts) }}</span> 条,共 <span class="font-medium">{{ totalProducts }}</span> 条记录
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<button :disabled="currentPage === 1" @click="currentPage--" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span class="sr-only">上一页</span>
<i class="fa fa-chevron-left h-5 w-5"></i>
</button>
<button v-for="page in totalPages" :key="page" :class="page === currentPage ? 'z-10 bg-primary text-white' : 'bg-white text-gray-700'" :aria-current="page === currentPage ? 'page' : undefined" @click="currentPage = page" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium">
{{ page }}
</button>
<button :disabled="currentPage === totalPages" @click="currentPage++" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span class="sr-only">下一页</span>
<i class="fa fa-chevron-right h-5 w-5"></i>
</button>
</nav>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const searchQuery = ref('')
const categoryFilter = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const totalProducts = ref(86)
const totalPages = computed(() => {
return Math.ceil(totalProducts.value / pageSize.value)
})
const products = ref([
{
id: 1,
name: '智能手表',
description: '多功能智能手表,支持心率监测、睡眠分析等功能',
category: '电子产品',
price: 1299.99,
stock: 56,
status: 'active',
image: 'https://picsum.photos/id/1/80/80'
},
{
id: 2,
name: '无线耳机',
description: '主动降噪无线耳机,提供沉浸式音乐体验',
category: '电子产品',
price: 899.99,
stock: 32,
status: 'active',
image: 'https://picsum.photos/id/2/80/80'
},
{
id: 3,
name: '纯棉T恤',
description: '舒适纯棉T恤,多种颜色可选',
category: '服装',
price: 99.99,
stock: 87,
status: 'active',
image: 'https://picsum.photos/id/3/80/80'
},
{
id: 4,
name: '家用咖啡机',
description: '全自动家用咖啡机,一键制作美味咖啡',
category: '家居用品',
price: 1999.99,
stock: 12,
status: 'active',
image: 'https://picsum.photos/id/4/80/80'
},
{
id: 5,
name: '前端开发实战',
description: '全面讲解前端开发技术,从入门到精通',
category: '图书音像',
price: 89.99,
stock: 5,
status: 'active',
image: 'https://picsum.photos/id/5/80/80'
}
])
const handleCreateProduct = () => {
console.log('创建产品')
// 打开创建产品模态框
}
const handleEditProduct = (product) => {
console.log('编辑产品', product)
// 打开编辑产品模态框
}
const handleDeleteProduct = (product) => {
if (confirm(`确定要删除产品 ${product.name} 吗?`)) {
console.log('删除产品', product)
// 调用API删除产品
}
}
onMounted(() => {
// 加载产品数据
})
</script>
第 8 步:配置主应用
修改 src/main.js
来配置主应用:
html
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './index.css'
import 'font-awesome/css/font-awesome.min.css'
const app = createApp(App)
app.use(router)
app.mount('#app')
第 9 步:创建主应用组件
修改 src/App.vue
:
html
<template>
<router-view />
</template>
<script setup>
// 主应用逻辑
</script>
<style>
/* 全局样式 */
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
</style>