Dashboard — продвинутый проект для демонстрации навыков Vue.js. Показывает умение работать с данными, графиками и сложным UI.
Что вы создадите:
1. Dashboard (главная)
- KPI карточки (продажи, пользователи, конверсия)
- График продаж за период
- Топ продуктов
- Последние заказы
2. Users (пользователи)
- Таблица с пагинацией
- Поиск и фильтры
- CRUD операции
- Роли и права
3. Products (товары)
- Каталог с карточками
- Фильтрация по категории
- Управление наличием
4. Settings
- Профиль пользователя
- Тема (светлая/тёмная)
- Уведомления
components/
├── layout/
│ ├── Sidebar.vue
│ ├── Header.vue
│ └── MainContent.vue
├── dashboard/
│ ├── StatsCard.vue
│ ├── SalesChart.vue
│ └── RecentOrders.vue
├── ui/
│ ├── DataTable.vue
│ ├── Modal.vue
│ ├── Dropdown.vue
│ └── Badge.vue
└── forms/
├── UserForm.vue
└── ProductForm.vue// stores/dashboard.js
export const useDashboardStore = defineStore('dashboard', {
state: () => ({
stats: {
totalSales: 0,
totalUsers: 0,
totalOrders: 0,
conversionRate: 0
},
salesData: [],
recentOrders: [],
isLoading: false
}),
actions: {
async fetchDashboardData() {
this.isLoading = true
// API calls...
this.isLoading = false
}
}
})<template>
<div class="stats-card" :class="colorClass">
<div class="icon">
<slot name="icon" />
</div>
<div class="content">
<h3>{{ title }}</h3>
<p class="value">{{ formattedValue }}</p>
<span class="change" :class="{ positive: change > 0 }">
{{ change > 0 ? '+' : '' }}{{ change }}%
</span>
</div>
</div>
</template>
<script setup>
const props = defineProps({
title: String,
value: Number,
change: Number,
color: { type: String, default: 'blue' }
})
const formattedValue = computed(() =>
props.value.toLocaleString()
)
</script><template>
<div class="data-table">
<table>
<thead>
<tr>
<th v-for="col in columns" @click="sort(col.key)">
{{ col.label }}
<SortIcon :direction="sortKey === col.key ? sortDir : null" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in sortedData" :key="row.id">
<td v-for="col in columns">
<slot :name="col.key" :row="row">
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
<Pagination
:total="total"
:page="page"
@change="$emit('page-change', $event)"
/>
</div>
</template>Vue Dashboard: структура и компоненты
<!-- Dashboard Demo -->
<div id="app"></div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref, computed, onMounted } = Vue
// Симуляция данных
const mockData = {
stats: {
totalSales: 125430,
totalUsers: 8420,
totalOrders: 1250,
conversionRate: 3.2
},
salesData: [
{ month: 'Янв', value: 12000 },
{ month: 'Фев', value: 19000 },
{ month: 'Мар', value: 15000 },
{ month: 'Апр', value: 22000 },
{ month: 'Май', value: 28000 },
{ month: 'Июн', value: 25000 },
],
recentOrders: [
{ id: 1, customer: 'Алексей М.', amount: 2500, status: 'completed' },
{ id: 2, customer: 'Мария К.', amount: 1800, status: 'pending' },
{ id: 3, customer: 'Иван П.', amount: 3200, status: 'completed' },
]
}
createApp({
setup() {
const activeTab = ref('dashboard')
const isDark = ref(false)
const stats = ref(mockData.stats)
const salesData = ref(mockData.salesData)
const recentOrders = ref(mockData.recentOrders)
const maxSale = computed(() =>
Math.max(...salesData.value.map(d => d.value))
)
const formatCurrency = (value) =>
value.toLocaleString('ru-RU') + ' ₽'
const statusColors = {
completed: '#4caf50',
pending: '#ff9800',
cancelled: '#e53935'
}
return {
activeTab,
isDark,
stats,
salesData,
recentOrders,
maxSale,
formatCurrency,
statusColors
}
},
template: `
<div :class="['dashboard', { dark: isDark }]">
<!-- Sidebar -->
<aside class="sidebar">
<div class="logo">📊 Admin</div>
<nav>
<a
v-for="tab in ['dashboard', 'users', 'products', 'settings']"
:key="tab"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
>
{{ tab === 'dashboard' ? '📈' : tab === 'users' ? '👥' : tab === 'products' ? '📦' : '⚙️' }}
{{ tab.charAt(0).toUpperCase() + tab.slice(1) }}{{ activeTab.charAt(0).toUpperCase() + activeTab.slice(1) }}{{ isDark ? '☀️' : '🌙' }}{{ formatCurrency(stats.totalSales) }}{{ stats.totalUsers.toLocaleString() }}{{ stats.totalOrders }}{{ stats.conversionRate }}{{ (item.value / 1000).toFixed(0) }}{{ item.month }}{{ order.id }}{{ order.customer }}{{ formatCurrency(order.amount) }}{{ order.status === 'completed' ? 'Выполнен' : 'В обработке' }}{{ activeTab }}</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.dashboard {
display: flex;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
transition: background 0.3s;
}
.dashboard.dark {
background: #1a1a2e;
color: #eee;
}
.sidebar {
width: 200px;
background: #2d3748;
color: white;
padding: 20px 0;
}
.dark .sidebar {
background: #16213e;
}
.logo {
font-size: 20px;
font-weight: bold;
padding: 0 20px 20px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
nav a {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
color: rgba(255,255,255,0.7);
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
}
nav a:hover, nav a.active {
background: rgba(255,255,255,0.1);
color: white;
}
.main {
flex: 1;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.theme-toggle {
padding: 8px 16px;
background: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
}
.dark .theme-toggle {
background: #2d3748;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
display: flex;
gap: 16px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.dark .stat-card {
background: #2d3748;
}
.stat-card.blue { border-left: 4px solid #2196f3; }
.stat-card.green { border-left: 4px solid #4caf50; }
.stat-card.orange { border-left: 4px solid #ff9800; }
.stat-card.purple { border-left: 4px solid #9c27b0; }
.stat-icon {
font-size: 32px;
}
.stat-info {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 12px;
color: #999;
text-transform: uppercase;
}
.stat-value {
font-size: 24px;
font-weight: bold;
}
.stat-change {
font-size: 12px;
}
.stat-change.positive { color: #4caf50; }
.stat-change.negative { color: #e53935; }
.chart-card, .orders-card {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
}
.dark .chart-card, .dark .orders-card {
background: #2d3748;
}
.chart {
display: flex;
align-items: flex-end;
gap: 16px;
height: 200px;
padding-top: 20px;
}
.chart-bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
}
.bar {
width: 100%;
background: linear-gradient(180deg, #2196f3, #1976d2);
border-radius: 4px 4px 0 0;
position: relative;
min-height: 20px;
transition: height 0.3s;
}
.bar-value {
position: absolute;
top: -20px;
font-size: 11px;
font-weight: bold;
}
.bar-label {
margin-top: 8px;
font-size: 12px;
color: #666;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.dark th, .dark td {
border-bottom-color: #444;
}
.status-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
color: white;
}
.placeholder {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
color: #999;
}
</style>Dashboard — продвинутый проект для демонстрации навыков Vue.js. Показывает умение работать с данными, графиками и сложным UI.
Что вы создадите:
1. Dashboard (главная)
- KPI карточки (продажи, пользователи, конверсия)
- График продаж за период
- Топ продуктов
- Последние заказы
2. Users (пользователи)
- Таблица с пагинацией
- Поиск и фильтры
- CRUD операции
- Роли и права
3. Products (товары)
- Каталог с карточками
- Фильтрация по категории
- Управление наличием
4. Settings
- Профиль пользователя
- Тема (светлая/тёмная)
- Уведомления
components/
├── layout/
│ ├── Sidebar.vue
│ ├── Header.vue
│ └── MainContent.vue
├── dashboard/
│ ├── StatsCard.vue
│ ├── SalesChart.vue
│ └── RecentOrders.vue
├── ui/
│ ├── DataTable.vue
│ ├── Modal.vue
│ ├── Dropdown.vue
│ └── Badge.vue
└── forms/
├── UserForm.vue
└── ProductForm.vue// stores/dashboard.js
export const useDashboardStore = defineStore('dashboard', {
state: () => ({
stats: {
totalSales: 0,
totalUsers: 0,
totalOrders: 0,
conversionRate: 0
},
salesData: [],
recentOrders: [],
isLoading: false
}),
actions: {
async fetchDashboardData() {
this.isLoading = true
// API calls...
this.isLoading = false
}
}
})<template>
<div class="stats-card" :class="colorClass">
<div class="icon">
<slot name="icon" />
</div>
<div class="content">
<h3>{{ title }}</h3>
<p class="value">{{ formattedValue }}</p>
<span class="change" :class="{ positive: change > 0 }">
{{ change > 0 ? '+' : '' }}{{ change }}%
</span>
</div>
</div>
</template>
<script setup>
const props = defineProps({
title: String,
value: Number,
change: Number,
color: { type: String, default: 'blue' }
})
const formattedValue = computed(() =>
props.value.toLocaleString()
)
</script><template>
<div class="data-table">
<table>
<thead>
<tr>
<th v-for="col in columns" @click="sort(col.key)">
{{ col.label }}
<SortIcon :direction="sortKey === col.key ? sortDir : null" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in sortedData" :key="row.id">
<td v-for="col in columns">
<slot :name="col.key" :row="row">
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
<Pagination
:total="total"
:page="page"
@change="$emit('page-change', $event)"
/>
</div>
</template>Vue Dashboard: структура и компоненты
<!-- Dashboard Demo -->
<div id="app"></div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref, computed, onMounted } = Vue
// Симуляция данных
const mockData = {
stats: {
totalSales: 125430,
totalUsers: 8420,
totalOrders: 1250,
conversionRate: 3.2
},
salesData: [
{ month: 'Янв', value: 12000 },
{ month: 'Фев', value: 19000 },
{ month: 'Мар', value: 15000 },
{ month: 'Апр', value: 22000 },
{ month: 'Май', value: 28000 },
{ month: 'Июн', value: 25000 },
],
recentOrders: [
{ id: 1, customer: 'Алексей М.', amount: 2500, status: 'completed' },
{ id: 2, customer: 'Мария К.', amount: 1800, status: 'pending' },
{ id: 3, customer: 'Иван П.', amount: 3200, status: 'completed' },
]
}
createApp({
setup() {
const activeTab = ref('dashboard')
const isDark = ref(false)
const stats = ref(mockData.stats)
const salesData = ref(mockData.salesData)
const recentOrders = ref(mockData.recentOrders)
const maxSale = computed(() =>
Math.max(...salesData.value.map(d => d.value))
)
const formatCurrency = (value) =>
value.toLocaleString('ru-RU') + ' ₽'
const statusColors = {
completed: '#4caf50',
pending: '#ff9800',
cancelled: '#e53935'
}
return {
activeTab,
isDark,
stats,
salesData,
recentOrders,
maxSale,
formatCurrency,
statusColors
}
},
template: `
<div :class="['dashboard', { dark: isDark }]">
<!-- Sidebar -->
<aside class="sidebar">
<div class="logo">📊 Admin</div>
<nav>
<a
v-for="tab in ['dashboard', 'users', 'products', 'settings']"
:key="tab"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
>
{{ tab === 'dashboard' ? '📈' : tab === 'users' ? '👥' : tab === 'products' ? '📦' : '⚙️' }}
{{ tab.charAt(0).toUpperCase() + tab.slice(1) }}{{ activeTab.charAt(0).toUpperCase() + activeTab.slice(1) }}{{ isDark ? '☀️' : '🌙' }}{{ formatCurrency(stats.totalSales) }}{{ stats.totalUsers.toLocaleString() }}{{ stats.totalOrders }}{{ stats.conversionRate }}{{ (item.value / 1000).toFixed(0) }}{{ item.month }}{{ order.id }}{{ order.customer }}{{ formatCurrency(order.amount) }}{{ order.status === 'completed' ? 'Выполнен' : 'В обработке' }}{{ activeTab }}</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.dashboard {
display: flex;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
transition: background 0.3s;
}
.dashboard.dark {
background: #1a1a2e;
color: #eee;
}
.sidebar {
width: 200px;
background: #2d3748;
color: white;
padding: 20px 0;
}
.dark .sidebar {
background: #16213e;
}
.logo {
font-size: 20px;
font-weight: bold;
padding: 0 20px 20px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
nav a {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
color: rgba(255,255,255,0.7);
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
}
nav a:hover, nav a.active {
background: rgba(255,255,255,0.1);
color: white;
}
.main {
flex: 1;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.theme-toggle {
padding: 8px 16px;
background: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
}
.dark .theme-toggle {
background: #2d3748;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
display: flex;
gap: 16px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.dark .stat-card {
background: #2d3748;
}
.stat-card.blue { border-left: 4px solid #2196f3; }
.stat-card.green { border-left: 4px solid #4caf50; }
.stat-card.orange { border-left: 4px solid #ff9800; }
.stat-card.purple { border-left: 4px solid #9c27b0; }
.stat-icon {
font-size: 32px;
}
.stat-info {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 12px;
color: #999;
text-transform: uppercase;
}
.stat-value {
font-size: 24px;
font-weight: bold;
}
.stat-change {
font-size: 12px;
}
.stat-change.positive { color: #4caf50; }
.stat-change.negative { color: #e53935; }
.chart-card, .orders-card {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
}
.dark .chart-card, .dark .orders-card {
background: #2d3748;
}
.chart {
display: flex;
align-items: flex-end;
gap: 16px;
height: 200px;
padding-top: 20px;
}
.chart-bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
}
.bar {
width: 100%;
background: linear-gradient(180deg, #2196f3, #1976d2);
border-radius: 4px 4px 0 0;
position: relative;
min-height: 20px;
transition: height 0.3s;
}
.bar-value {
position: absolute;
top: -20px;
font-size: 11px;
font-weight: bold;
}
.bar-label {
margin-top: 8px;
font-size: 12px;
color: #666;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.dark th, .dark td {
border-bottom-color: #444;
}
.status-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
color: white;
}
.placeholder {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
color: #999;
}
</style>Создай мини-dashboard на Vue с карточками статистики, простым bar-графиком и таблицей заказов. Добавь переключение тёмной темы и навигацию по разделам.
maxChartValue: d.value. isDarkMode: !isDarkMode (toggle). getBarHeight уже реализован правильно.