<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue3 复杂二维布局仪表盘</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #4361ee;
--primary-light: #4895ef;
--secondary: #3f37c9;
--accent: #f72585;
--success: #4cc9f0;
--warning: #f9c74f;
--danger: #f94144;
--text-primary: #2b2d42;
--text-secondary: #8d99ae;
--bg-main: #f8f9fa;
--bg-card: #ffffff;
--bg-header: #ffffff;
--border: #e9ecef;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
--transition: all 0.3s ease;
}
.dark-theme {
--text-primary: #edf2f4;
--text-secondary: #8d99ae;
--bg-main: #121212;
--bg-card: #1e1e1e;
--bg-header: #1e1e1e;
--border: #333333;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background-color: var(--bg-main);
color: var(--text-primary);
transition: var(--transition);
line-height: 1.6;
}
#app {
min-height: 100vh;
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: 60px 1fr 40px;
grid-template-areas:
"sidebar header"
"sidebar main"
"sidebar footer";
transition: var(--transition);
}
/* 头部样式 */
.header {
grid-area: header;
background: var(--bg-header);
box-shadow: var(--shadow);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 100;
border-bottom: 1px solid var(--border);
}
.logo {
display: flex;
align-items: center;
gap: 12px;
font-weight: 700;
font-size: 1.2rem;
}
.logo i {
color: var(--primary);
font-size: 1.8rem;
}
.header-actions {
display: flex;
align-items: center;
gap: 20px;
}
.theme-toggle {
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
color: var(--text-secondary);
}
.user-profile {
display: flex;
align-items: center;
gap: 10px;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), var(--secondary));
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}
/* 侧边栏样式 */
.sidebar {
grid-area: sidebar;
background: var(--bg-card);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
transition: var(--transition);
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid var(--border);
}
.nav-title {
font-size: 0.9rem;
text-transform: uppercase;
color: var(--text-secondary);
padding: 15px 20px 5px;
}
.nav-menu {
flex: 1;
padding: 10px 0;
overflow-y: auto;
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 20px;
color: var(--text-secondary);
text-decoration: none;
transition: var(--transition);
cursor: pointer;
}
.nav-item:hover, .nav-item.active {
background: rgba(67, 97, 238, 0.1);
color: var(--primary);
}
.nav-item i {
width: 24px;
margin-right: 12px;
font-size: 1.1rem;
}
/* 主内容区域 - 复杂的二维布局 */
.main-content {
grid-area: main;
padding: 20px;
display: grid;
gap: 20px;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: minmax(100px, auto);
grid-template-areas:
"stats stats stats stats"
"chart chart chart activity"
"chart chart chart activity"
"projects projects tasks tasks"
"projects projects tasks tasks";
}
/* 响应式布局调整 */
@media (max-width: 1200px) {
.main-content {
grid-template-areas:
"stats stats stats stats"
"chart chart chart chart"
"activity activity activity activity"
"projects projects projects projects"
"tasks tasks tasks tasks";
}
}
@media (max-width: 768px) {
#app {
grid-template-columns: 1fr;
grid-template-rows: 60px 1fr 40px;
grid-template-areas:
"header"
"main"
"footer";
}
.sidebar {
position: fixed;
left: -240px;
top: 60px;
height: calc(100vh - 100px);
z-index: 1000;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar.active {
left: 0;
}
}
/* 卡片通用样式 */
.card {
background: var(--bg-card);
border-radius: 12px;
box-shadow: var(--shadow);
overflow: hidden;
transition: var(--transition);
border: 1px solid var(--border);
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.card-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 1.1rem;
font-weight: 600;
}
.card-body {
padding: 20px;
}
/* 特定卡片区域 */
.stats-container {
grid-area: stats;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.stat-card {
display: flex;
flex-direction: column;
padding: 20px;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
}
.stat-card:nth-child(1)::before { background: var(--primary); }
.stat-card:nth-child(2)::before { background: var(--success); }
.stat-card:nth-child(3)::before { background: var(--warning); }
.stat-card:nth-child(4)::before { background: var(--accent); }
.stat-value {
font-size: 1.8rem;
font-weight: 700;
margin: 10px 0;
}
.stat-label {
color: var(--text-secondary);
font-size: 0.9rem;
}
.stat-icon {
position: absolute;
top: 20px;
right: 20px;
font-size: 2rem;
opacity: 0.2;
}
.chart-container {
grid-area: chart;
}
.activity-container {
grid-area: activity;
}
.projects-container {
grid-area: projects;
}
.tasks-container {
grid-area: tasks;
}
/* 图表容器 */
.chart-wrapper {
height: 300px;
position: relative;
}
/* 活动列表 */
.activity-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.activity-item {
display: flex;
gap: 15px;
padding: 10px 0;
}
.activity-item:not(:last-child) {
border-bottom: 1px solid var(--border);
}
.activity-icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: rgba(67, 97, 238, 0.1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--primary);
}
.activity-content {
flex: 1;
}
.activity-title {
font-weight: 500;
margin-bottom: 5px;
}
.activity-time {
font-size: 0.85rem;
color: var(--text-secondary);
}
/* 项目表格 */
.projects-table {
width: 100%;
border-collapse: collapse;
}
.projects-table th {
text-align: left;
padding: 12px 0;
color: var(--text-secondary);
font-weight: 500;
border-bottom: 1px solid var(--border);
}
.projects-table td {
padding: 12px 0;
border-bottom: 1px solid var(--border);
}
.project-name {
font-weight: 500;
}
.project-team {
display: flex;
}
.team-member {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--primary-light);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.7rem;
margin-right: -8px;
border: 2px solid var(--bg-card);
}
.status-badge {
padding: 4px 10px;
border-radius: 20px;
font-size: 0.8rem;
}
.status-active {
background: rgba(76, 201, 240, 0.15);
color: var(--success);
}
.status-pending {
background: rgba(249, 199, 79, 0.15);
color: var(--warning);
}
.status-completed {
background: rgba(67, 97, 238, 0.15);
color: var(--primary);
}
/* 任务列表 */
.task-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--border);
}
.task-item:last-child {
border-bottom: none;
}
.task-checkbox {
margin-right: 15px;
}
.task-content {
flex: 1;
}
.task-title {
margin-bottom: 5px;
}
.task-progress {
height: 6px;
background: var(--border);
border-radius: 3px;
margin-top: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 3px;
}
/* 页脚样式 */
.footer {
grid-area: footer;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
background: var(--bg-header);
border-top: 1px solid var(--border);
color: var(--text-secondary);
font-size: 0.9rem;
}
/* 移动端菜单按钮 */
.menu-toggle {
display: none;
background: none;
border: none;
font-size: 1.4rem;
cursor: pointer;
color: var(--text-primary);
}
@media (max-width: 768px) {
.menu-toggle {
display: block;
}
}
</style>
</head>
<body>
<div id="app">
<!-- 头部区域 -->
<header class="header">
<div class="logo">
<button class="menu-toggle" @click="toggleSidebar">
<i class="fas fa-bars"></i>
</button>
<i class="fas fa-chart-pie"></i>
<span>数据仪表盘</span>
</div>
<div class="header-actions">
<button class="theme-toggle" @click="toggleTheme">
<i :class="darkMode ? 'fas fa-sun' : 'fas fa-moon'"></i>
</button>
<div class="user-profile">
<div class="avatar">A</div>
<span>管理员</span>
</div>
</div>
</header>
<!-- 侧边导航 -->
<aside class="sidebar" :class="{ active: sidebarActive }">
<div class="sidebar-header">
<h2>导航菜单</h2>
</div>
<div class="nav-menu">
<div class="nav-title">主要功能</div>
<div
v-for="item in menuItems"
:key="item.id"
class="nav-item"
:class="{ active: activeMenu === item.id }"
@click="activeMenu = item.id"
>
<i :class="item.icon"></i>
<span>{{ item.title }}</span>
</div>
</div>
</aside>
<!-- 主内容区域 - 复杂二维布局 -->
<main class="main-content">
<!-- 统计卡片区域 -->
<div class="stats-container">
<div class="card stat-card">
<div class="stat-label">总访问量</div>
<div class="stat-value">24,568</div>
<i class="fas fa-eye stat-icon"></i>
</div>
<div class="card stat-card">
<div class="stat-label">用户注册</div>
<div class="stat-value">1,254</div>
<i class="fas fa-users stat-icon"></i>
</div>
<div class="card stat-card">
<div class="stat-label">总收入</div>
<div class="stat-value">¥56,780</div>
<i class="fas fa-yen-sign stat-icon"></i>
</div>
<div class="card stat-card">
<div class="stat-label">任务完成率</div>
<div class="stat-value">78%</div>
<i class="fas fa-tasks stat-icon"></i>
</div>
</div>
<!-- 图表区域 -->
<div class="card chart-container">
<div class="card-header">
<h3 class="card-title">月度数据分析</h3>
<div class="card-actions">
<select v-model="selectedChart">
<option value="sales">销售额</option>
<option value="users">用户增长</option>
<option value="conversion">转化率</option>
</select>
</div>
</div>
<div class="card-body">
<div class="chart-wrapper">
<canvas ref="chart"></canvas>
</div>
</div>
</div>
<!-- 活动区域 -->
<div class="card activity-container">
<div class="card-header">
<h3 class="card-title">最近活动</h3>
<button class="btn-icon">
<i class="fas fa-ellipsis-h"></i>
</button>
</div>
<div class="card-body">
<div class="activity-list">
<div class="activity-item" v-for="activity in activities" :key="activity.id">
<div class="activity-icon">
<i :class="activity.icon"></i>
</div>
<div class="activity-content">
<div class="activity-title">{{ activity.title }}</div>
<div class="activity-desc">{{ activity.description }}</div>
<div class="activity-time">{{ activity.time }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 项目区域 -->
<div class="card projects-container">
<div class="card-header">
<h3 class="card-title">项目进度</h3>
<button class="btn-icon">
<i class="fas fa-plus"></i>
</button>
</div>
<div class="card-body">
<table class="projects-table">
<thead>
<tr>
<th>项目名称</th>
<th>团队</th>
<th>截止日期</th>
<th>状态</th>
<th>进度</th>
</tr>
</thead>
<tbody>
<tr v-for="project in projects" :key="project.id">
<td>
<div class="project-name">{{ project.name }}</div>
<div class="project-client">{{ project.client }}</div>
</td>
<td>
<div class="project-team">
<div class="team-member" v-for="(member, index) in project.team" :key="index">
{{ member.charAt(0) }}
</div>
</div>
</td>
<td>{{ project.deadline }}</td>
<td>
<span class="status-badge" :class="'status-' + project.status">
{{ getStatusText(project.status) }}
</span>
</td>
<td>
<div class="progress">
<div class="task-progress">
<div class="progress-bar" :style="{ width: project.progress + '%', 'background-color': getStatusColor(project.status) }"></div>
</div>
<div class="progress-value">{{ project.progress }}%</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 任务区域 -->
<div class="card tasks-container">
<div class="card-header">
<h3 class="card-title">任务列表</h3>
<button class="btn-icon">
<i class="fas fa-sync"></i>
</button>
</div>
<div class="card-body">
<div class="task-list">
<div class="task-item" v-for="task in tasks" :key="task.id">
<div class="task-checkbox">
<input type="checkbox" v-model="task.completed">
</div>
<div class="task-content">
<div class="task-title" :class="{ completed: task.completed }">{{ task.title }}</div>
<div class="task-desc">{{ task.description }}</div>
<div class="task-progress">
<div class="progress-bar" :style="{ width: task.progress + '%', 'background-color': task.completed ? '#4cc9f0' : '#4361ee' }"></div>
</div>
</div>
<div class="task-date">{{ task.date }}</div>
</div>
</div>
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="footer">
<p>© 2023 数据仪表盘 | Vue3 二维布局示例</p>
</footer>
</div>
<script>
const { createApp, ref, reactive, onMounted, watch, nextTick } = Vue;
createApp({
setup() {
// 状态管理
const darkMode = ref(false);
const sidebarActive = ref(false);
const activeMenu = ref('dashboard');
const selectedChart = ref('sales');
// 图表引用
const chart = ref(null);
let chartInstance = null;
// 菜单项
const menuItems = ref([
{ id: 'dashboard', title: '仪表盘', icon: 'fas fa-tachometer-alt' },
{ id: 'analytics', title: '数据分析', icon: 'fas fa-chart-line' },
{ id: 'projects', title: '项目管理', icon: 'fas fa-tasks' },
{ id: 'users', title: '用户管理', icon: 'fas fa-users' },
{ id: 'settings', title: '系统设置', icon: 'fas fa-cog' },
{ id: 'messages', title: '消息中心', icon: 'fas fa-envelope' },
{ id: 'calendar', title: '日程安排', icon: 'fas fa-calendar' },
{ id: 'documents', title: '文档中心', icon: 'fas fa-file-alt' }
]);
// 活动数据
const activities = ref([
{ id: 1, icon: 'fas fa-user-plus', title: '新用户注册', description: '用户 "张三" 注册了账户', time: '10分钟前' },
{ id: 2, icon: 'fas fa-shopping-cart', title: '新订单', description: '订单 #ORD-20230925001 已创建', time: '25分钟前' },
{ id: 3, icon: 'fas fa-file-invoice', title: '发票生成', description: '发票 #INV-20230924005 已生成', time: '1小时前' },
{ id: 4, icon: 'fas fa-comment', title: '新评论', description: '用户 "李四" 在商品页发表了评论', time: '2小时前' },
{ id: 5, icon: 'fas fa-server', title: '系统更新', description: '系统已完成版本更新至 v2.5.0', time: '3小时前' }
]);
// 项目数据
const projects = ref([
{ id: 1, name: '官网改版', client: 'ABC公司', team: ['张三', '李四', '王五'], deadline: '2023-10-15', status: 'active', progress: 65 },
{ id: 2, name: '移动端应用', client: 'XYZ科技', team: ['赵六', '钱七'], deadline: '2023-11-30', status: 'pending', progress: 25 },
{ id: 3, name: '数据平台', client: '内部项目', team: ['孙八', '周九', '吴十'], deadline: '2023-09-30', status: 'completed', progress: 100 },
{ id: 4, name: '营销活动', client: '市场部', team: ['郑十一', '王十二'], deadline: '2023-10-10', status: 'active', progress: 85 }
]);
// 任务数据
const tasks = ref([
{ id: 1, title: '完成设计稿', description: '首页及产品页面设计', progress: 75, date: '今天', completed: false },
{ id: 2, title: '开发用户模块', description: '用户注册登录功能', progress: 50, date: '明天', completed: false },
{ id: 3, title: '编写测试用例', description: '核心功能测试用例', progress: 30, date: '09/28', completed: false },
{ id: 4, title: '项目会议', description: '与客户讨论需求变更', progress: 100, date: '已完成', completed: true }
]);
// 切换主题
const toggleTheme = () => {
darkMode.value = !darkMode.value;
document.documentElement.classList.toggle('dark-theme', darkMode.value);
};
// 切换侧边栏(移动端)
const toggleSidebar = () => {
sidebarActive.value = !sidebarActive.value;
};
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
active: '进行中',
pending: '待启动',
completed: '已完成'
};
return statusMap[status] || status;
};
// 获取状态颜色
const getStatusColor = (status) => {
const colorMap = {
active: '#4cc9f0',
pending: '#f9c74f',
completed: '#4361ee'
};
return colorMap[status] || '#4361ee';
};
// 初始化图表
const initChart = () => {
if (chartInstance) {
chartInstance.destroy();
}
const ctx = chart.value.getContext('2d');
// 根据选择的图表类型显示不同数据
let data, options;
if (selectedChart.value === 'sales') {
data = {
labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月'],
datasets: [{
label: '销售额 (万元)',
data: [65, 59, 80, 81, 56, 55, 40, 75, 92],
backgroundColor: 'rgba(67, 97, 238, 0.2)',
borderColor: 'rgba(67, 97, 238, 1)',
borderWidth: 2,
tension: 0.4,
fill: true
}]
};
options = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
};
} else if (selectedChart.value === 'users') {
data = {
labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月'],
datasets: [{
label: '新增用户',
data: [120, 190, 300, 500, 200, 300, 450, 600, 750],
backgroundColor: 'rgba(76, 201, 240, 0.2)',
borderColor: 'rgba(76, 201, 240, 1)',
borderWidth: 2,
tension: 0.4,
fill: true
}]
};
options = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
};
} else {
data = {
labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月'],
datasets: [{
label: '转化率 (%)',
data: [2.5, 3.1, 2.8, 4.2, 3.5, 5.0, 4.2, 5.5, 6.8],
backgroundColor: 'rgba(247, 37, 133, 0.2)',
borderColor: 'rgba(247, 37, 133, 1)',
borderWidth: 2,
tension: 0.4,
fill: true
}]
};
options = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
}
};
}
chartInstance = new Chart(ctx, {
type: 'line',
data: data,
options: options
});
};
// 监听图表类型变化
watch(selectedChart, () => {
initChart();
});
// 组件挂载后初始化图表
onMounted(() => {
nextTick(() => {
initChart();
});
});
return {
darkMode,
sidebarActive,
activeMenu,
selectedChart,
chart,
menuItems,
activities,
projects,
tasks,
toggleTheme,
toggleSidebar,
getStatusText,
getStatusColor
};
}
}).mount('#app');
</script>
</body>
</html>