Ошибка интеграции Alpine.js 3.x и Chart.js: «Необработанная ошибка типа: невозможно прочитать свойства со значением null (чтение «сохранить»)»

Я работаю над проектом Laravel, где использую Alpine.js для интерактивности и Chart.js для визуализации данных. Моя установка отлично работает с Alpine.js 2.8.2, но при обновлении до Alpine.js 3.x возникает следующая ошибка:

chart.js:19 Uncaught TypeError: Cannot read properties of null (reading 'save')
    at Ie (chart.js:19:18331)
    at An._drawDataset (chart.js:19:98188)
    at An._drawDatasets (chart.js:19:97819)
    at An.draw (chart.js:19:97350)
    at chart.js:13:6948
    at Map.forEach (<anonymous>)
    at xt._update (chart.js:13:6727)
    at chart.js:13:6620

Соответствующий код:

Вот соответствующая часть моего шаблона Blade с инициализацией JavaScript:

@extends('admin.layouts.app')

@section('vendor-styles')
    <link rel = "stylesheet" href = "{{ asset('plugins/flatpickr/flatpickr.min.css') }}">
@endsection

@section('breadcrumbs')
    <li>
        <a href = "#" class = "text-gray-500 hover:text-gray-700">Dashboard</a>
    </li>
@endsection

@section('content')
    <div x-data = "dashboard()" x-init = "init()">
        <!-- Time Range Filter -->
        <div class = "bg-white p-4 rounded shadow mb-4">
            <div class = "flex space-x-4 mb-4">
                <button @click = "timeRange = 'today'; customRange = false"
                        :class = "{ 'bg-blue-600 text-white': timeRange === 'today', 'bg-gray-200 text-gray-800': timeRange !== 'today' }"
                        class = "p-2 rounded">Hôm nay
                </button>
                <button @click = "timeRange = 'thisWeek'; customRange = false"
                        :class = "{ 'bg-blue-600 text-white': timeRange === 'thisWeek', 'bg-gray-200 text-gray-800': timeRange !== 'thisWeek' }"
                        class = "p-2 rounded">Tuần này
                </button>
                <button @click = "timeRange = 'thisMonth'; customRange = false"
                        :class = "{ 'bg-blue-600 text-white': timeRange === 'thisMonth', 'bg-gray-200 text-gray-800': timeRange !== 'thisMonth' }"
                        class = "p-2 rounded">Tháng này
                </button>
                <button @click = "timeRange = 'thisYear'; customRange = false"
                        :class = "{ 'bg-blue-600 text-white': timeRange === 'thisYear', 'bg-gray-200 text-gray-800': timeRange !== 'thisYear' }"
                        class = "p-2 rounded">Năm này
                </button>
                <button @click = "timeRange = 'custom'; customRange = true"
                        :class = "{ 'bg-blue-600 text-white': timeRange === 'custom', 'bg-gray-200 text-gray-800': timeRange !== 'custom' }"
                        class = "p-2 rounded">Khoảng thời gian
                </button>
            </div>
            <div x-show = "customRange" class = "flex space-x-4">
                <input x-ref = "datepicker" class = "p-2 bg-gray-200 rounded" placeholder = "Chọn khoảng thời gian">
            </div>
        </div>

        <!-- Sales Overview -->
        <div class = "bg-white p-4 rounded shadow mb-4">
            <h2 class = "text-xl font-semibold mb-2">Tổng quan</h2>
            <div class = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
                <div class = "p-6 bg-green-100 rounded-lg shadow-md flex items-center">
                    <div class = "flex-1">
                        <div class = "text-lg font-medium text-green-800 flex items-center">
                            <i class = "fas fa-dollar-sign mr-2"></i>
                            Tổng doanh thu
                        </div>
                        <div class = "text-3xl font-bold text-green-900" x-text = "totalSales.revenue"></div>
                    </div>
                    {{--                    <div class = "text-green-600 flex items-center">--}}
                    {{--                        <i class = "fas fa-arrow-up text-2xl"></i>--}}
                    {{--                        <span class = "ml-1 text-xl font-semibold">15%</span>--}}
                    {{--                    </div>--}}
                </div>

                <div class = "p-6 bg-blue-100 rounded-lg shadow-md flex items-center">
                    <div class = "flex-1">
                        <div class = "text-lg font-medium text-green-800 flex items-center">
                            <i class = "fas fa-dollar-sign mr-2"></i>
                            Tổng chi phí
                        </div>
                        <div class = "text-3xl font-bold text-green-900" x-text = "totalSales.cost"></div>
                    </div>
                    {{--                    <div class = "text-green-600 flex items-center">--}}
                    {{--                        <i class = "fas fa-arrow-up text-2xl"></i>--}}
                    {{--                        <span class = "ml-1 text-xl font-semibold">15%</span>--}}
                    {{--                    </div>--}}
                </div>

                <div class = "p-6 bg-yellow-100 rounded-lg shadow-md flex items-center">
                    <div class = "flex-1">
                        <div class = "text-lg font-medium text-green-800 flex items-center">
                            <i class = "fas fa-dollar-sign mr-2"></i>
                            Tổng lợi nhuận
                        </div>
                        <div class = "text-3xl font-bold text-green-900" x-text = "totalSales.profit"></div>
                    </div>
                    {{--                    <div class = "text-green-600 flex items-center">--}}
                    {{--                        <i class = "fas fa-arrow-up text-2xl"></i>--}}
                    {{--                        <span class = "ml-1 text-xl font-semibold">15%</span>--}}
                    {{--                    </div>--}}
                </div>
            </div>
        </div>

        <!-- Sales Chart -->
        <div class = "bg-white p-4 rounded shadow mb-4">
            <h2 class = "text-xl font-semibold mb-2">Biểu Đồ Kinh Doanh</h2>
            <canvas id = "salesChart" width = "400" height = "200"></canvas>
        </div>

        <div class = "grid grid-cols-1 md:grid-cols-2 gap-4">
            <!-- Sales by Category Chart -->
            <div class = "bg-white p-4 rounded shadow mb-4">
                <h2 class = "text-xl font-semibold mb-2 text-center">Doanh Thu Theo Danh Mục Sản Phẩm</h2>
                <canvas id = "categoryChart" width = "400" height = "200"></canvas>
            </div>

            <!-- Sales by Brand Chart -->
            <div class = "bg-white p-4 rounded shadow mb-4">
                <h2 class = "text-xl font-semibold mb-2 text-center">Doanh Thu Theo Hãng Sản Xuất</h2>
                <canvas id = "brandChart" width = "400" height = "200"></canvas>
            </div>
        </div>

        <!-- Recent Orders -->
        <div class = "bg-white p-4 rounded shadow mb-4">
            <h2 class = "text-xl font-semibold mb-2">Đơn Hàng Gần Đây</h2>
            <table class = "w-full text-left border-collapse">
                <thead>
                <tr>
                    <th class = "p-2 border-b">Mã Đơn Hàng</th>
                    <th class = "p-2 border-b">Khách Hàng</th>
                    <th class = "p-2 border-b">Trạng Thái</th>
                    <th class = "p-2 border-b">Ngày Đặt</th>
                    <th class = "p-2 border-b">Tổng tiền</th>
                </tr>
                </thead>
                <tbody>
                <template x-for = "order in recentOrders" :key = "order.id">
                    <tr>
                        <td class = "p-2 border-b">
                            <a :href = "order?.url" x-text = "order?.order_code" class = "text-blue-600"></a>
                        </td>
                        <td class = "p-2 border-b" x-text = "order?.customer.name"></td>
                        <td class = "p-2 border-b" x-text = "order?.status"></td>
                        <td class = "p-2 border-b" x-text = "order?.formatted_created_at"></td>
                        <td class = "p-2 border-b" x-text = "order?.amount"></td>
                    </tr>
                </template>
                </tbody>
            </table>
        </div>

        <div class = "grid grid-cols-1 md:grid-cols-2 gap-4">
            <!-- Top Selling Products -->
            <div class = "bg-white p-4 rounded shadow mb-4">
                <h2 class = "text-xl font-semibold mb-2">Top Sản Phẩm Bán Chạy</h2>
                <ul>
                    <template x-for = "product in topProducts" :key = "product.id">
                        <li class = "border-b py-2">
                            <span x-text = "product.name"></span> - <span x-text = "product.total_sales"></span>
                        </li>
                    </template>
                </ul>
            </div>

            <!-- Inventory -->
            <div class = "bg-white p-4 rounded shadow mb-4">
                <h2 class = "text-xl font-semibold mb-2">Sản Phẩm Sắp Hết Hàng</h2>
                <table class = "w-full text-left border-collapse">
                    <thead>
                    <tr>
                        <th class = "p-2 border-b">Tên Sản Phẩm</th>
                        <th class = "p-2 border-b">Màu Sắc</th>
                        <th class = "p-2 border-b">Số Lượng Còn Lại</th>
                    </tr>
                    </thead>
                    <tbody>
                    <template x-for = "product in lowStockProducts" :key = "product.id">
                        <tr>
                            <td class = "p-2 border-b">
                                <a :href = "product?.url" x-text = "product?.product_name" class = "text-blue-600"></a>
                            </td>
                            <td class = "p-2 border-b" x-text = "product?.color"></td>
                            <td class = "p-2 border-b" x-text = "product?.quantity"></td>
                        </tr>
                    </template>
                    </tbody>
                </table>
            </div>
        </div>

        <div class = "grid grid-cols-1 md:grid-cols-2 gap-4">
            <div class = "bg-white p-4 rounded shadow mb-4">
                <h2 class = "text-xl font-semibold mb-2">Trạng thái đơn hàng</h2>
                <table class = "w-full text-left border-collapse">
                    <thead>
                    <tr>
                        <th class = "p-2 border-b">Trạng Thái</th>
                        <th class = "p-2 border-b">Số Đơn</th>
                        <th class = "p-2 border-b">Tổng Doanh Thu</th>
                    </tr>
                    </thead>
                    <tbody>
                    <template x-for = "(groupStatus, index) in orderByStatus" :key = "index">
                        <tr>
                            <td class = "p-2 border-b" x-text = "groupStatus?.status"></td>
                            <td class = "p-2 border-b" x-text = "groupStatus?.count"></td>
                            <td class = "p-2 border-b" x-text = "groupStatus?.revenue"></td>
                        </tr>
                    </template>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
@endsection

@section('vendor-scripts')
    <script src = "{{ asset('plugins/chartjs/chart.js') }}"></script>
    <script src = "{{ asset('plugins/flatpickr/flatpickr.min.js') }}"></script>
    <script src = "{{ asset('plugins/flatpickr/lang/vn.js') }}"></script>
@endsection

@section('custom-scripts')
    <script>
        function formatTooltip(tooltipItem) {
            let value = tooltipItem.raw;
            let formattedValue = new Intl.NumberFormat('vi-VN', {
                style: 'currency',
                currency: 'VND'
            }).format(value);
            return `${tooltipItem.dataset.label}: ${formattedValue}`;
        }

        function dashboard() {
            return {
                customRange: false,
                startDate: '',
                endDate: '',
                selectedDate: '',
                totalSales: {
                    today: '',
                    week: '',
                    month: '',
                    year: '',
                },
                recentOrders: [],
                topProducts: [],
                lowStockProducts: [],
                salesChartData: {
                    labels: [],
                    revenue: [],
                    cost: [],
                    profit: []
                },
                categoryChartData: {
                    labels: [],
                    data: []
                },
                brandChartData: {
                    labels: [],
                    data: []
                },
                salesChart: null,
                categoryChart: null,
                brandChart: null,
                timeRange: 'today',
                timeRangePicker: null,
                orderByStatus: [],
                init() {
                    this.fetchDashboardData();
                    this.initFlatpickr();
                    this.$watch('timeRange', (value) => {
                        if (value !== 'custom') {
                            this.fetchDashboardData();
                        }
                    });
                    this.$watch('startDate', () => {
                        if (this.customRange && this.startDate && this.endDate) {
                            this.fetchDashboardData();
                        }
                    });
                    this.$watch('endDate', () => {
                        if (this.customRange && this.startDate && this.endDate) {
                            this.fetchDashboardData();
                        }
                    });
                    this.$watch('customRange', (value) => {
                        if (!value) {
                            this.startDate = '';
                            this.endDate = '';
                            this.timeRangePicker?.clear();
                        }
                    });
                },
                fetchDashboardData() {
                    const url = `{{ route('admin.dashboardData') }}?timeRange=${this.timeRange}&startDate=${this.startDate}&endDate=${this.endDate}`;
                    fetch(url, {headers: {'Accept': 'application/json'}})
                        .then(response => response.json())
                        .then(data => {
                            this.totalSales.revenue = `${data.totalSales.revenue}`;
                            this.totalSales.cost = `${data.totalSales.cost}`;
                            this.totalSales.profit = `${data.totalSales.profit}`;

                            this.recentOrders = data.recentOrders;
                            this.topProducts = data.topProducts;
                            this.lowStockProducts = data.lowStockProducts;
                            this.orderByStatus = data.orderByStatus;

                            this.salesChartData.labels = data.salesChartData.labels;
                            this.salesChartData.revenue = data.salesChartData.revenue;
                            this.salesChartData.cost = data.salesChartData.cost;
                            this.salesChartData.profit = data.salesChartData.profit;

                            this.categoryChartData.labels = data.salesByCategory.labels;
                            this.categoryChartData.data = data.salesByCategory.data;

                            this.brandChartData.labels = data.salesByBrand.labels;
                            this.brandChartData.data = data.salesByBrand.data;

                            this.updateSalesChart();
                            this.updateCategoryChart();
                            this.updateBrandChart();
                        });
                },
                updateSalesChart() {
                    const ctx = document.getElementById('salesChart').getContext('2d');

                    if (this.salesChart) {
                        this.salesChart.destroy();
                    }

                    this.salesChart = new Chart(ctx, {
                        type: 'bar',
                        data: {
                            labels: this.salesChartData.labels,
                            datasets: [
                                {
                                    label: 'Doanh thu',
                                    data: this.salesChartData.revenue,
                                    backgroundColor: 'rgba(75, 192, 192, 0.2)',
                                    borderColor: 'rgba(75, 192, 192, 1)',
                                    borderWidth: 1
                                },
                                {
                                    label: 'Chi phí',
                                    data: this.salesChartData.cost,
                                    backgroundColor: 'rgba(255, 99, 132, 0.2)',
                                    borderColor: 'rgba(255, 99, 132, 1)',
                                    borderWidth: 1
                                },
                                {
                                    label: 'Lợi nhuận',
                                    data: this.salesChartData.profit,
                                    backgroundColor: 'rgba(54, 162, 235, 0.2)',
                                    borderColor: 'rgba(54, 162, 235, 1)',
                                    borderWidth: 1,
                                    type: 'line'
                                }
                            ]
                        },
                        options: {
                            scales: {
                                y: {
                                    beginAtZero: true
                                }
                            },
                            plugins: {
                                tooltip: {
                                    callbacks: {
                                        label: formatTooltip
                                    }
                                }
                            },
                        }
                    });
                },
                updateCategoryChart() {
                    const ctx = document.getElementById('categoryChart').getContext('2d');

                    if (this.categoryChart) {
                        this.categoryChart.destroy();
                    }

                    this.categoryChart = new Chart(ctx, {
                        type: 'pie',
                        data: {
                            labels: this.categoryChartData.labels,
                            datasets: [
                                {
                                    label: 'Doanh thu',
                                    data: this.categoryChartData.data,
                                    backgroundColor: this.categoryChartData.labels.map(() => `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.2)`),
                                    borderColor: this.categoryChartData.labels.map(() => `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 1)`),
                                    borderWidth: 1
                                }
                            ]
                        },
                        options: {
                            plugins: {
                                tooltip: {
                                    callbacks: {
                                        label: formatTooltip
                                    }
                                }
                            },
                        }
                    });
                },
                updateBrandChart() {
                    const ctx = document.getElementById('brandChart').getContext('2d');

                    if (this.brandChart) {
                        this.brandChart.destroy();
                    }

                    this.brandChart = new Chart(ctx, {
                        type: 'pie',
                        data: {
                            labels: this.brandChartData.labels,
                            datasets: [
                                {
                                    label: 'Doanh thu',
                                    data: this.brandChartData.data,
                                    backgroundColor: this.brandChartData.labels.map(() => `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.2)`),
                                    borderColor: this.brandChartData.labels.map(() => `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 1)`),
                                    borderWidth: 1
                                }
                            ]
                        },
                        options: {
                            plugins: {
                                tooltip: {
                                    callbacks: {
                                        label: formatTooltip
                                    }
                                }
                            },
                        }
                    });
                },
                initFlatpickr() {
                    this.timeRangePicker = flatpickr(this.$refs.datepicker, {
                        onChange: ([startDate, endDate]) => {
                            if (startDate && endDate) {
                                this.startDate = flatpickr.formatDate(startDate, 'Y-m-d');
                                this.endDate = flatpickr.formatDate(endDate, 'Y-m-d');
                            }
                        },
                        locale: "vn",
                        mode: "range",
                        altInput: true,
                        conjunction: " - ",
                        maxDate: "today",
                        altFormat: "d/m/Y",
                        dateFormat: "Y-m-d",
                    });
                },
            }
        }
    </script>
@endsection

Это admin.layouts.app

<!DOCTYPE html>
<html lang = "en">
<head>
    <meta charset = "UTF-8">
    <meta name = "viewport" content = "width=device-width, initial-scale=1.0">
    <meta http-equiv = "Content-Security-Policy" content = "upgrade-insecure-requests">
    <title>Admin Dashboard - Product Management</title>
    {{--    <link rel = "stylesheet" href = "{{ asset('AdminLTE/dist/css/AdminLTE.min.css') }}">--}}
    <script src = "{{ asset('plugins/tailwindcss/tailwindcss.min.css') }}"></script>
    <link rel = "stylesheet" href = "{{ asset('plugins/fontawesome/css/all.min.css') }}"/>
    <script src = "{{ asset('plugins/alpinejs/alpine.min.js') }}" defer></script>

    @yield('vendor-styles')

    @yield('custom-styles')
</head>
<body class = "bg-gray-100">

<!-- Sidebar -->
@include('admin.layouts.includes.sidebar')

<!-- Main Content -->
<div class = "flex-1 flex flex-col">
    <!-- Navbar -->
    @include('admin.layouts.includes.header')

    <!-- Breadcrumbs -->
    <nav class = "bg-gray border-b border-gray-200">
        <div class = "max-w-7xl mx-auto py-3 px-4 sm:px-6 lg:px-8">
            <ol class = "flex items-center space-x-4">
                @yield('breadcrumbs')
            </ol>
        </div>
    </nav>

    <!-- Main Section -->
    <main class = "flex-1 bg-gray-100">
        <div class = "max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
            @yield('content')
        </div>
    </main>

    <!-- Footer -->
    @include('admin.layouts.includes.footer')
</div>
<script src = "{{ asset('plugins/htmx/htmx.js') }}"></script>

@yield('vendor-scripts')

@yield('custom-scripts')
</body>
</html>

Проблема:

Диаграммы корректно отображаются в Alpine.js 2.8.2, но не работают в Alpine.js 3.x, показывая упомянутую выше ошибку. Что может быть причиной этой проблемы в Alpine.js 3.x и как ее устранить?

Среда:

  • Версия Ларавел: 9.x
  • Версия Alpine.js: 3.14.1
  • Версия Chart.js: 4.4.3

Будем очень признательны за любые идеи или решения.

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
0
61
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В AlpineJs v3 метод init() объекта данных, если он присутствует, выполняется автоматически при запуске.
В вашем основном <div> вы указываете x-init="init()" (вероятно, потому, что код был разработан для AlpineJs v2, который не поддерживает эту функцию), поэтому метод init() выполняется два раза, и это приводит к ошибка.
Вы можете проверить эту ситуацию, добавив console.info в init():

init() {
   console.info ("init executed", Date.now());

   .....

Чтобы решить проблему, удалите x-init="init()" из основного <div>.

РЕДАКТИРОВАТЬ

Если проблема возникает снова, когда вы выбираете диапазон дат, это связано с тем, что вы установили $watch на обоих концах диапазона, и это снова приводит к двум быстрым последовательным вызовам fetchDashboardData().

Чтобы решить эту проблему простым способом, возможное решение состоит в том, чтобы сгруппировать ограничения дат в один объект, определить один $watch для этого объекта, а затем выполнить кумулятивное присвоение в обратном вызове datepicker:


function dashboard() {

    return {

        customRange: false,
        // startDate: '',   <~~~ removed
        // endDate: '',     <~~~ removed
        dateRange: {startDate: '', endDate: ''}, //   <~~~ added
    
        .....

        init() {

           .....

           //this.$watch('startDate', () => {  <~~~ removed
           //   
           //   if (this.customRange && this.startDate && this.endDate) {
           //       this.fetchDashboardData();
           //   }
           //});
           //
           //this.$watch('endDate', () => { <~~~ removed
           //   
           //   if (this.customRange && this.startDate && this.endDate) {
           //       this.fetchDashboardData();
           //   }
           //});


           this.$watch('dateRange', () => {   //   <~~~ added

               if (this.customRange && this.dateRange.startDate && this.dateRange.endDate) {
                   this.fetchDashboardData();
               }
           });

           .....
        },

        .....

        initFlatpickr() {

            this.timeRangePicker = flatpickr(this.$refs.datepicker, {

                onChange: ([startDate, endDate]) => {

                    if (startDate && endDate) {

                        // this.startDate = flatpickr.formatDate(startDate, 'Y-m-d'); <~~~ removed
                        // this.endDate = flatpickr.formatDate(endDate, 'Y-m-d'); <~~~ removed

                        this.dateRange = {  // <~~~ added
                            startDate: flatpickr.formatDate(startDate, 'Y-m-d'),
                            endDate: flatpickr.formatDate(endDate, 'Y-m-d')
                         }
                    }
                },

                ..... 

Теперь каждую ссылку на this.startDate и this.endDate необходимо заменить на this.dateRange.startDate и this.dateRange.endDate.

РЕДАКТИРОВАТЬ 2

Все возникающие проблемы возникают из-за того, что графики быстро уничтожаются и перерисовываются, когда они еще не закончены. Поскольку по умолчанию анимация активна, построение диаграмм может занять больше времени, чем ожидалось.

В предыдущих случаях перерисовка диаграмм происходила из-за ошибок проектирования кода, а теперь проблема возникает из-за взаимодействия с пользователем, которым необходимо управлять.

Самое простое решение — отключить анимацию: добавьте ключ анимации в разделе «Параметры» объекта конфигурации каждого графика и присвойте ему значение false:

.....

options: {

    animation: false,

    .....

Более сложное решение — запретить ввод данных во время рисования диаграмм. Для достижения этой цели мы можем:

  • добавьте в объект AlpineJs свойство, отслеживающее состояние графиков:
.....
lowStockProducts: [],
animating: {sales: false, categs: false, brand: false}, // <~~~ added
.....
  • добавьте в объект AlpineJs функцию, возвращающую текущее совокупное состояние:
.....
init() {
    .....
},

animationInProgress() { // <~~~ added
    return this.animating.sales  ||  this.animating.categs  ||  this.animating.brand;
},

.....    
  • в каждой функции, которая рисует диаграмму, инициализируйте связанную переменную значением true, затем в параметрах объекта конфигурации добавьте обратный вызов для сброса значения false в переменную при завершении анимации. Например:
.....

updateSalesChart() {
    .....

    this.animating.sales = true; //   <~~~ added

    this.salesChart = new Chart(ctx, {

        .....

        options: {

            animation: {             //   <~~~ added
                onComplete: () => {
                    this.animating.sales = false;
                }
            },
    
        .....
  • наконец, в каждую кнопку и в каждый элемент управления, который необходимо заблокировать при отрисовке графиков, добавьте атрибут :disabled, например:
.....

<button @click = "...."
        :class = "....."
        .....
        :disabled = "animationInProgress()"  {{-- <~~~ added --}}
>
    Hôm nay
</button>
.....

Чтобы быть придирчивыми, вы также можете отключить средство выбора даты, добавив $watch() в метод init():

this.$watch('animating', () => {
    this.timeRangePicker.set("clickOpens", !this.animationInProgress());
});

Большое спасибо. Я следовал вашему методу, и он сработал, но когда я нажимаю кнопку фильтра по диапазону времени, он иногда выдает ошибку, как и раньше. Я использую $watch для прослушивания изменений в фильтре временного диапазона для получения данных и обновления диаграммы.

DragonVN 08.07.2024 17:06

Это связанная, но другая проблема, следует открыть новый запрос ;-) Смотрите обновление моего ответа.

TUPKAP 08.07.2024 19:11

Он также сталкивается с вышеуказанными ошибками, когда я выбираю диапазоны времени, такие как «сегодня», «этот месяц», «этот год» и т. д. Эти значения хранятся в состоянии «timeRange». Первые несколько раз при замене фильтра работает, но потом вылетает, или вылетает сразу, если я меняю фильтр слишком быстро. Я не сталкивался с этой проблемой в версии 2x; в версии 2x я мог переключать фильтры очень быстро, и все равно все работало нормально. Я не являюсь фронтенд-разработчиком и впервые использую Alpine.js. Я прочитал их документацию об изменениях в версии 3x, но решения так и не нашел.

DragonVN 08.07.2024 21:20

Добавление вопросов к одному и тому же запросу не является хорошей практикой. Если вам все еще нужна дополнительная помощь, вы должны открыть еще один запрос. Имейте в виду, что я также преимущественно бэкэнд-разработчик :-)

TUPKAP 08.07.2024 23:41

Большое спасибо, это сработало, когда я добавил анимацию: false. Мне интересно, почему эта ошибка не возникла в Alpine.js 2.x. Есть ли разница в жизненном цикле или обработке DOM?

DragonVN 09.07.2024 01:06

Есть некоторые различия, однако я использовал версию 2 очень мало. Возможно, если вы тоже обновили Chart.js, это может зависеть от этого.

TUPKAP 09.07.2024 03:06

Другие вопросы по теме