← JavaScript/Проект: Dashboard на Vue#382 из 383← ПредыдущийСледующий →+150 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Капстоун проект: Admin Dashboard на Vue

О проекте

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

    Технологии

  • Vue 3 с Composition API
  • Vue Router для навигации
  • Pinia для state management
  • Chart.js или ApexCharts для графиков
  • Tailwind CSS для стилей
  • Структура состояния (Pinia)

    // 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
        }
      }
    })

    Примеры компонентов

    StatsCard.vue

    <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>

    DataTable.vue

    <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>

    Чек-лист готовности

  • [ ] Sidebar навигация работает
  • [ ] Dashboard показывает статистику
  • [ ] Графики отображаются корректно
  • [ ] Таблицы с сортировкой и пагинацией
  • [ ] Формы создания/редактирования
  • [ ] Тёмная тема
  • [ ] Responsive дизайн
  • [ ] Нет ошибок в консоли
  • Примеры

    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) }}
              </a>
            </nav>
          </aside>
    
          <!-- Main Content -->
          <main class="main">
            <!-- Header -->
            <header class="header">
              <h1>{{ activeTab.charAt(0).toUpperCase() + activeTab.slice(1) }}</h1>
              <button @click="isDark = !isDark" class="theme-toggle">
                {{ isDark ? '☀️' : '🌙' }}
              </button>
            </header>
    
            <!-- Dashboard Content -->
            <div v-if="activeTab === 'dashboard'" class="content">
              <!-- Stats Cards -->
              <div class="stats-grid">
                <div class="stat-card blue">
                  <div class="stat-icon">💰</div>
                  <div class="stat-info">
                    <span class="stat-label">Продажи</span>
                    <span class="stat-value">{{ formatCurrency(stats.totalSales) }}</span>
                    <span class="stat-change positive">+12%</span>
                  </div>
                </div>
                <div class="stat-card green">
                  <div class="stat-icon">👥</div>
                  <div class="stat-info">
                    <span class="stat-label">Пользователи</span>
                    <span class="stat-value">{{ stats.totalUsers.toLocaleString() }}</span>
                    <span class="stat-change positive">+8%</span>
                  </div>
                </div>
                <div class="stat-card orange">
                  <div class="stat-icon">📦</div>
                  <div class="stat-info">
                    <span class="stat-label">Заказы</span>
                    <span class="stat-value">{{ stats.totalOrders }}</span>
                    <span class="stat-change negative">-3%</span>
                  </div>
                </div>
                <div class="stat-card purple">
                  <div class="stat-icon">📈</div>
                  <div class="stat-info">
                    <span class="stat-label">Конверсия</span>
                    <span class="stat-value">{{ stats.conversionRate }}%</span>
                    <span class="stat-change positive">+0.5%</span>
                  </div>
                </div>
              </div>
    
              <!-- Chart -->
              <div class="chart-card">
                <h3>Продажи по месяцам</h3>
                <div class="chart">
                  <div 
                    v-for="item in salesData" 
                    :key="item.month"
                    class="chart-bar"
                  >
                    <div 
                      class="bar" 
                      :style="{ height: (item.value / maxSale * 100) + '%' }"
                    >
                      <span class="bar-value">{{ (item.value / 1000).toFixed(0) }}k</span>
                    </div>
                    <span class="bar-label">{{ item.month }}</span>
                  </div>
                </div>
              </div>
    
              <!-- Recent Orders -->
              <div class="orders-card">
                <h3>Последние заказы</h3>
                <table>
                  <thead>
                    <tr>
                      <th>ID</th>
                      <th>Клиент</th>
                      <th>Сумма</th>
                      <th>Статус</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr v-for="order in recentOrders" :key="order.id">
                      <td>#{{ order.id }}</td>
                      <td>{{ order.customer }}</td>
                      <td>{{ formatCurrency(order.amount) }}</td>
                      <td>
                        <span 
                          class="status-badge" 
                          :style="{ background: statusColors[order.status] }"
                        >
                          {{ order.status === 'completed' ? 'Выполнен' : 'В обработке' }}
                        </span>
                      </td>
                    </tr>
                  </tbody>
                </table>
              </div>
            </div>
    
            <!-- Other tabs placeholder -->
            <div v-else class="content placeholder">
              <p>Страница {{ activeTab }} в разработке</p>
            </div>
          </main>
        </div>
      `
    }).mount('#app')
    </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>

    Капстоун проект: Admin Dashboard на Vue

    О проекте

    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

    Технологии

  • Vue 3 с Composition API
  • Vue Router для навигации
  • Pinia для state management
  • Chart.js или ApexCharts для графиков
  • Tailwind CSS для стилей
  • Структура состояния (Pinia)

    // 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
        }
      }
    })

    Примеры компонентов

    StatsCard.vue

    <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>

    DataTable.vue

    <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>

    Чек-лист готовности

  • [ ] Sidebar навигация работает
  • [ ] Dashboard показывает статистику
  • [ ] Графики отображаются корректно
  • [ ] Таблицы с сортировкой и пагинацией
  • [ ] Формы создания/редактирования
  • [ ] Тёмная тема
  • [ ] Responsive дизайн
  • [ ] Нет ошибок в консоли
  • Примеры

    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) }}
              </a>
            </nav>
          </aside>
    
          <!-- Main Content -->
          <main class="main">
            <!-- Header -->
            <header class="header">
              <h1>{{ activeTab.charAt(0).toUpperCase() + activeTab.slice(1) }}</h1>
              <button @click="isDark = !isDark" class="theme-toggle">
                {{ isDark ? '☀️' : '🌙' }}
              </button>
            </header>
    
            <!-- Dashboard Content -->
            <div v-if="activeTab === 'dashboard'" class="content">
              <!-- Stats Cards -->
              <div class="stats-grid">
                <div class="stat-card blue">
                  <div class="stat-icon">💰</div>
                  <div class="stat-info">
                    <span class="stat-label">Продажи</span>
                    <span class="stat-value">{{ formatCurrency(stats.totalSales) }}</span>
                    <span class="stat-change positive">+12%</span>
                  </div>
                </div>
                <div class="stat-card green">
                  <div class="stat-icon">👥</div>
                  <div class="stat-info">
                    <span class="stat-label">Пользователи</span>
                    <span class="stat-value">{{ stats.totalUsers.toLocaleString() }}</span>
                    <span class="stat-change positive">+8%</span>
                  </div>
                </div>
                <div class="stat-card orange">
                  <div class="stat-icon">📦</div>
                  <div class="stat-info">
                    <span class="stat-label">Заказы</span>
                    <span class="stat-value">{{ stats.totalOrders }}</span>
                    <span class="stat-change negative">-3%</span>
                  </div>
                </div>
                <div class="stat-card purple">
                  <div class="stat-icon">📈</div>
                  <div class="stat-info">
                    <span class="stat-label">Конверсия</span>
                    <span class="stat-value">{{ stats.conversionRate }}%</span>
                    <span class="stat-change positive">+0.5%</span>
                  </div>
                </div>
              </div>
    
              <!-- Chart -->
              <div class="chart-card">
                <h3>Продажи по месяцам</h3>
                <div class="chart">
                  <div 
                    v-for="item in salesData" 
                    :key="item.month"
                    class="chart-bar"
                  >
                    <div 
                      class="bar" 
                      :style="{ height: (item.value / maxSale * 100) + '%' }"
                    >
                      <span class="bar-value">{{ (item.value / 1000).toFixed(0) }}k</span>
                    </div>
                    <span class="bar-label">{{ item.month }}</span>
                  </div>
                </div>
              </div>
    
              <!-- Recent Orders -->
              <div class="orders-card">
                <h3>Последние заказы</h3>
                <table>
                  <thead>
                    <tr>
                      <th>ID</th>
                      <th>Клиент</th>
                      <th>Сумма</th>
                      <th>Статус</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr v-for="order in recentOrders" :key="order.id">
                      <td>#{{ order.id }}</td>
                      <td>{{ order.customer }}</td>
                      <td>{{ formatCurrency(order.amount) }}</td>
                      <td>
                        <span 
                          class="status-badge" 
                          :style="{ background: statusColors[order.status] }"
                        >
                          {{ order.status === 'completed' ? 'Выполнен' : 'В обработке' }}
                        </span>
                      </td>
                    </tr>
                  </tbody>
                </table>
              </div>
            </div>
    
            <!-- Other tabs placeholder -->
            <div v-else class="content placeholder">
              <p>Страница {{ activeTab }} в разработке</p>
            </div>
          </main>
        </div>
      `
    }).mount('#app')
    </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 уже реализован правильно.

    Загружаем среду выполнения...
    Загружаем AI-помощника...