letoceiling-coder/ceiling-calculator

Калькулятор натяжных потолков для Laravel с поддержкой Vue 3

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

Language:JavaScript

pkg:composer/letoceiling-coder/ceiling-calculator

v1.0.0 2025-11-15 03:42 UTC

This package is auto-updated.

Last update: 2025-11-15 12:03:00 UTC


README

📦 Профессиональный калькулятор натяжных потолков для Laravel

Независимый Laravel-пакет для расчета стоимости натяжных потолков с интерактивным чертежом, выбором материалов, цветов и автоматическим формированием сметы.

✅ Подтверждение

Да! Пакет устанавливается через Composer и Vue 3 компонент полностью работает с API!

Что работает из коробки:

  • Установка через Composer - composer require letoceiling-coder/ceiling-calculator:dev-main
  • Vue 3 интеграция - Полная поддержка Vue 3 компонентов
  • API endpoints - Автоматически регистрируются после установки
  • Подключение к API - Vue компонент автоматически загружает данные через API
  • Сохранение расчетов - Vue компонент сохраняет расчеты через API
  • Laravel Passport - Поддержка авторизации через Passport (опционально)

Доступные API Endpoints:

После установки автоматически доступны:

  • GET /api/ceiling/rooms - Список помещений
  • GET /api/ceiling/materials - Список материалов (текстуры, производители, цвета)
  • POST /api/ceiling - Сохранить расчет
  • PUT /api/ceiling/{id} - Обновить расчет
  • DELETE /api/ceiling/{id} - Удалить расчет

Vue 3 компонент:

<CeilingCalculator
    api-url="/api/ceiling"
    :use-auth="true"
    @calculation-saved="onCalculationSaved"
/>

См. QUICK_START_VUE3.md для быстрого старта и VUE3_API_INTEGRATION.md для полного руководства

Все работает из коробки! Нет необходимости в дополнительной настройке! 🎉

📖 О проекте

Ceiling Calculator - это полнофункциональный калькулятор для расчета стоимости натяжных потолков, который позволяет:

  • 🎨 Создавать интерактивные чертежи помещений с помощью canvas
  • 📐 Рассчитывать площадь и периметр потолка автоматически
  • 🎨 Выбирать текстуры и производителей из базы данных
  • 🌈 Выбирать цвета потолка
  • 💰 Автоматически формировать смету с детализацией работ и материалов
  • 💾 Сохранять расчеты для последующего использования
  • 📄 Экспортировать сметы в удобном формате

Пакет полностью независим и содержит все необходимые ресурсы (CSS, JS, изображения, иконки), не требует дополнительных зависимостей от основного проекта.

✨ Основной функционал

🎯 Основные возможности

1. Интерактивное создание чертежей

  • Рисование контуров помещений на canvas
  • Автоматический расчет площади и периметра
  • Поддержка сложных форм (многоугольники, кривые)
  • Добавление отверстий и проемов
  • Измерение углов и расстояний

2. Выбор материалов

  • База данных текстур/фактур (матовые, глянцевые, сатиновые и т.д.)
  • Список производителей (Pongs, Clipso, Alkor Draka и др.)
  • Выбор цвета из палитры
  • Выбор ширины полотна

3. Расчет стоимости

  • Автоматический расчет площади потолка
  • Расчет длины швов
  • Учет усадки материала
  • Расчет стоимости материалов
  • Расчет стоимости работ (монтаж, установка светильников и т.д.)
  • Расчет коммуникаций (обход труб, проводов)
  • Формирование итоговой сметы

4. Управление расчетами

  • Сохранение расчетов в базу данных
  • Просмотр истории расчетов
  • Редактирование существующих расчетов
  • Удаление расчетов
  • Экспорт смет

5. Работа с помещениями

  • Создание списка помещений
  • Привязка расчетов к помещениям
  • Группировка расчетов по помещениям

🎨 Интерфейс

  • Адаптивный дизайн - работает на всех устройствах
  • Интуитивное управление - простое и понятное
  • Визуальный редактор - чертежи с помощью мыши/касаний
  • Живой предпросмотр - изменения видны сразу

📦 Установка через Composer

Требования

  • PHP >= 8.1
  • Laravel >= 10.0
  • Composer >= 2.0
  • MySQL/PostgreSQL/SQLite

Шаг 1: Добавить репозиторий

В composer.json вашего Laravel проекта добавьте репозиторий:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/letoceiling-coder/cieling-vue.git"
        }
    ],
    "require": {
        "letoceiling-coder/ceiling-calculator": "dev-main"
    }
}

Для установки из Packagist (после публикации):

{
    "require": {
        "letoceiling-coder/ceiling-calculator": "^1.0"
    }
}

Шаг 2: Установить пакет

composer require letoceiling-coder/ceiling-calculator:dev-main

Шаг 3: Публикация ресурсов

php artisan vendor:publish --tag=ceiling-config
php artisan vendor:publish --tag=ceiling-assets
php artisan vendor:publish --tag=ceiling-migrations

Или используйте команду установки (выполняет все вышеперечисленное):

php artisan ceiling:install

Шаг 4: Запустить миграции

php artisan migrate

Шаг 5: Заполнить базу данных (опционально)

php artisan db:seed --class=Database\\Seeders\\CeilingDatabaseSeeder

Это заполнит базу данных начальными данными:

  • 20 текстур/фактур
  • 11 производителей
  • Примеры цветов

Шаг 6: Проверить установку

Откройте в браузере:

http://your-domain.com/ceilng

🎨 Интеграция Vue 3

Пакет поддерживает опциональную интеграцию Vue 3 для создания интерактивных компонентов.

Шаг 1: Включить Vue 3

В файле .env:

CEILING_VUE_ENABLED=true
CEILING_VUE_MODE=production

Или в config/ceiling.php:

'vue' => [
    'enabled' => true,
    'mode' => 'production', // или 'development'
    'cdn' => 'https://unpkg.com/vue@3/dist/vue.global',
],

Шаг 2: Регистрация Vue компонента

Создайте файл resources/js/ceiling-calculator.js:

import { createApp } from 'vue';
import CeilingCalculator from './components/CeilingCalculator.vue';

// Регистрация компонента глобально
window.createCeilingApp = function(containerId, props = {}) {
    const app = createApp(CeilingCalculator, props);
    app.mount(containerId);
    return app;
};

// Или регистрация через plugin
export default {
    install(app) {
        app.component('CeilingCalculator', CeilingCalculator);
    }
};

Шаг 3: Создание компонента Vue

Создайте файл resources/js/components/CeilingCalculator.vue:

<template>
    <div class="ceiling-calculator">
        <div class="calculator-header">
            <h2>{{ title }}</h2>
            <button @click="toggleMode" class="btn btn-primary">
                {{ mode === 'draw' ? 'Режим просмотра' : 'Режим рисования' }}
            </button>
        </div>
        
        <div class="calculator-body">
            <!-- Выбор помещения -->
            <div class="room-selector" v-if="showRoomSelector">
                <label>Выберите помещение:</label>
                <select v-model="selectedRoom" @change="onRoomChange">
                    <option v-for="room in rooms" :key="room.id" :value="room.id">
                        {{ room.name }}
                    </option>
                </select>
            </div>
            
            <!-- Canvas для чертежа -->
            <div class="canvas-container">
                <canvas 
                    ref="canvas"
                    @mousedown="onMouseDown"
                    @mousemove="onMouseMove"
                    @mouseup="onMouseUp"
                ></canvas>
            </div>
            
            <!-- Панель параметров -->
            <div class="parameters-panel">
                <div class="parameter-group">
                    <label>Площадь:</label>
                    <span class="value">{{ area }} м²</span>
                </div>
                <div class="parameter-group">
                    <label>Периметр:</label>
                    <span class="value">{{ perimeter }} м</span>
                </div>
                <div class="parameter-group">
                    <label>Выберите текстуру:</label>
                    <select v-model="selectedTexture" @change="onTextureChange">
                        <option v-for="texture in textures" :key="texture.id" :value="texture.id">
                            {{ texture.title }}
                        </option>
                    </select>
                </div>
                <div class="parameter-group">
                    <label>Выберите производителя:</label>
                    <select v-model="selectedManufacturer" @change="onManufacturerChange">
                        <option v-for="manufacturer in manufacturers" :key="manufacturer.id" :value="manufacturer.id">
                            {{ manufacturer.name }}
                        </option>
                    </select>
                </div>
                <div class="parameter-group">
                    <label>Выберите цвет:</label>
                    <div class="color-picker">
                        <div 
                            v-for="color in colors" 
                            :key="color.id"
                            class="color-item"
                            :class="{ active: selectedColor === color.id }"
                            :style="{ backgroundColor: color.hex_color }"
                            @click="selectColor(color.id)"
                        ></div>
                    </div>
                </div>
            </div>
            
            <!-- Расчет стоимости -->
            <div class="cost-calculation">
                <h3>Расчет стоимости</h3>
                <div class="cost-item">
                    <span>Материалы:</span>
                    <span class="cost">{{ materialCost }} ₽</span>
                </div>
                <div class="cost-item">
                    <span>Работы:</span>
                    <span class="cost">{{ workCost }} ₽</span>
                </div>
                <div class="cost-item total">
                    <span>Итого:</span>
                    <span class="cost">{{ totalCost }} ₽</span>
                </div>
                <button @click="saveCalculation" class="btn btn-success">
                    Сохранить расчет
                </button>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    name: 'CeilingCalculator',
    props: {
        // Основные props
        title: {
            type: String,
            default: 'Калькулятор натяжных потолков'
        },
        initialData: {
            type: Object,
            default: () => ({})
        },
        apiUrl: {
            type: String,
            default: '/api/ceiling'
        },
        // Конфигурация
        showRoomSelector: {
            type: Boolean,
            default: true
        },
        editable: {
            type: Boolean,
            default: true
        },
        // Данные
        rooms: {
            type: Array,
            default: () => []
        },
        textures: {
            type: Array,
            default: () => []
        },
        manufacturers: {
            type: Array,
            default: () => []
        },
        colors: {
            type: Array,
            default: () => []
        },
        // Состояние
        selectedRoom: {
            type: [Number, String],
            default: null
        },
        selectedTexture: {
            type: [Number, String],
            default: null
        },
        selectedManufacturer: {
            type: [Number, String],
            default: null
        },
        selectedColor: {
            type: [Number, String],
            default: null
        }
    },
    data() {
        return {
            mode: 'draw', // 'draw' или 'view'
            area: 0,
            perimeter: 0,
            materialCost: 0,
            workCost: 0,
            canvas: null,
            ctx: null,
            drawing: false,
            currentPath: [],
            paths: []
        };
    },
    computed: {
        totalCost() {
            return this.materialCost + this.workCost;
        }
    },
    mounted() {
        this.initCanvas();
        this.loadInitialData();
    },
    methods: {
        initCanvas() {
            this.canvas = this.$refs.canvas;
            this.ctx = this.canvas.getContext('2d');
            
            // Установка размеров canvas
            const container = this.canvas.parentElement;
            this.canvas.width = container.clientWidth;
            this.canvas.height = container.clientHeight;
            
            // Инициализация canvas
            this.ctx.strokeStyle = '#000';
            this.ctx.lineWidth = 2;
        },
        loadInitialData() {
            if (this.initialData) {
                if (this.initialData.rooms) {
                    this.rooms = this.initialData.rooms;
                }
                if (this.initialData.textures) {
                    this.textures = this.initialData.textures;
                }
                if (this.initialData.manufacturers) {
                    this.manufacturers = this.initialData.manufacturers;
                }
                if (this.initialData.colors) {
                    this.colors = this.initialData.colors;
                }
            }
        },
        onMouseDown(e) {
            if (!this.editable || this.mode !== 'draw') return;
            
            this.drawing = true;
            const rect = this.canvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            
            this.currentPath = [{ x, y }];
            this.ctx.beginPath();
            this.ctx.moveTo(x, y);
        },
        onMouseMove(e) {
            if (!this.drawing) return;
            
            const rect = this.canvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            
            this.currentPath.push({ x, y });
            this.ctx.lineTo(x, y);
            this.ctx.stroke();
        },
        onMouseUp() {
            if (!this.drawing) return;
            
            this.drawing = false;
            if (this.currentPath.length > 2) {
                this.paths.push([...this.currentPath]);
                this.calculateArea();
            }
        },
        calculateArea() {
            // Расчет площади и периметра на основе путей
            // Это упрощенный пример, реальная реализация будет более сложной
            let totalArea = 0;
            let totalPerimeter = 0;
            
            this.paths.forEach(path => {
                // Расчет площади по формуле Гаусса (Shoelace formula)
                let area = 0;
                for (let i = 0; i < path.length; i++) {
                    const j = (i + 1) % path.length;
                    area += path[i].x * path[j].y;
                    area -= path[j].x * path[i].y;
                }
                totalArea += Math.abs(area) / 2;
                
                // Расчет периметра
                for (let i = 0; i < path.length; i++) {
                    const j = (i + 1) % path.length;
                    const dx = path[j].x - path[i].x;
                    const dy = path[j].y - path[i].y;
                    totalPerimeter += Math.sqrt(dx * dx + dy * dy);
                }
            });
            
            // Конвертация пикселей в метры (пример: 1px = 0.01м)
            const scale = 0.01;
            this.area = (totalArea * scale * scale).toFixed(2);
            this.perimeter = (totalPerimeter * scale).toFixed(2);
            
            this.recalculateCosts();
        },
        recalculateCosts() {
            // Расчет стоимости на основе выбранных параметров
            // Это упрощенный пример
            const pricePerSquareMeter = 500; // Цена за м²
            this.materialCost = (this.area * pricePerSquareMeter).toFixed(2);
            this.workCost = (this.area * 300).toFixed(2); // Работы за м²
        },
        toggleMode() {
            this.mode = this.mode === 'draw' ? 'view' : 'draw';
        },
        onRoomChange() {
            this.$emit('room-changed', this.selectedRoom);
        },
        onTextureChange() {
            this.$emit('texture-changed', this.selectedTexture);
            this.recalculateCosts();
        },
        onManufacturerChange() {
            this.$emit('manufacturer-changed', this.selectedManufacturer);
            this.recalculateCosts();
        },
        selectColor(colorId) {
            this.selectedColor = colorId;
            this.$emit('color-changed', colorId);
            this.recalculateCosts();
        },
        async saveCalculation() {
            try {
                const response = await fetch(this.apiUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${this.getAuthToken()}`
                    },
                    body: JSON.stringify({
                        room_id: this.selectedRoom,
                        texture_id: this.selectedTexture,
                        manufacturer_id: this.selectedManufacturer,
                        color_id: this.selectedColor,
                        area: this.area,
                        perimeter: this.perimeter,
                        material_cost: this.materialCost,
                        work_cost: this.workCost,
                        total_cost: this.totalCost,
                        drawing_data: this.paths
                    })
                });
                
                if (response.ok) {
                    const result = await response.json();
                    this.$emit('calculation-saved', result);
                    alert('Расчет успешно сохранен!');
                } else {
                    throw new Error('Ошибка сохранения');
                }
            } catch (error) {
                console.error('Ошибка сохранения расчета:', error);
                alert('Ошибка сохранения расчета');
            }
        },
        getAuthToken() {
            // Получение токена для Laravel Passport
            return localStorage.getItem('access_token') || '';
        }
    }
};
</script>

<style scoped>
.ceiling-calculator {
    width: 100%;
    max-width: 1200px;
    margin: 0 auto;
}

.calculator-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 20px;
    background: #f5f5f5;
    border-radius: 8px;
    margin-bottom: 20px;
}

.calculator-body {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: 20px;
}

.canvas-container {
    border: 2px solid #ddd;
    border-radius: 8px;
    overflow: hidden;
}

canvas {
    width: 100%;
    height: 500px;
    cursor: crosshair;
}

.parameters-panel {
    background: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.parameter-group {
    margin-bottom: 20px;
}

.parameter-group label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
}

.color-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.color-item {
    width: 40px;
    height: 40px;
    border-radius: 4px;
    cursor: pointer;
    border: 2px solid transparent;
}

.color-item.active {
    border-color: #333;
}

.cost-calculation {
    background: #f9f9f9;
    padding: 20px;
    border-radius: 8px;
    margin-top: 20px;
}

.cost-item {
    display: flex;
    justify-content: space-between;
    padding: 10px 0;
    border-bottom: 1px solid #ddd;
}

.cost-item.total {
    font-weight: bold;
    font-size: 1.2em;
    border-bottom: none;
}

.btn {
    padding: 10px 20px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
}

.btn-primary {
    background: #007bff;
    color: white;
}

.btn-success {
    background: #28a745;
    color: white;
    width: 100%;
    margin-top: 10px;
}
</style>

Шаг 4: Использование компонента

Вариант 1: Глобальная регистрация

В вашем Blade шаблоне:

<div id="ceiling-calculator"></div>

@push('scripts')
<script>
    document.addEventListener('DOMContentLoaded', function() {
        const props = {
            title: 'Калькулятор натяжных потолков',
            rooms: @json($data['rooms'] ?? []),
            textures: @json($data['textures'] ?? []),
            manufacturers: @json($data['manufacturers'] ?? []),
            colors: @json($data['colors'] ?? []),
            apiUrl: '{{ config('ceiling.api.prefix') }}',
            showRoomSelector: true,
            editable: true
        };
        
        window.createCeilingApp('#ceiling-calculator', props);
    });
</script>
@endpush

Вариант 2: Локальное использование

<template>
    <div>
        <CeilingCalculator
            :rooms="rooms"
            :textures="textures"
            :manufacturers="manufacturers"
            :colors="colors"
            :api-url="apiUrl"
            @calculation-saved="onCalculationSaved"
        />
    </div>
</template>

<script>
import CeilingCalculator from './components/CeilingCalculator.vue';

export default {
    components: {
        CeilingCalculator
    },
    data() {
        return {
            rooms: [],
            textures: [],
            manufacturers: [],
            colors: [],
            apiUrl: '/api/ceiling'
        };
    },
    methods: {
        onCalculationSaved(result) {
            console.log('Расчет сохранен:', result);
        }
    }
};
</script>

Props компонента

Prop Тип По умолчанию Описание
title String 'Калькулятор натяжных потолков' Заголовок калькулятора
initialData Object {} Начальные данные
apiUrl String '/api/ceiling' URL API для сохранения
showRoomSelector Boolean true Показывать выбор помещения
editable Boolean true Разрешить редактирование
rooms Array [] Список помещений
textures Array [] Список текстур
manufacturers Array [] Список производителей
colors Array [] Список цветов
selectedRoom Number/String null Выбранное помещение
selectedTexture Number/String null Выбранная текстура
selectedManufacturer Number/String null Выбранный производитель
selectedColor Number/String null Выбранный цвет

События (Events)

Событие Параметры Описание
room-changed roomId Выбрано помещение
texture-changed textureId Выбрана текстура
manufacturer-changed manufacturerId Выбран производитель
color-changed colorId Выбран цвет
calculation-saved result Расчет сохранен

🔐 Интеграция с Laravel Passport

Пакет поддерживает аутентификацию через Laravel Passport для защиты API.

Шаг 1: Установка Laravel Passport

composer require laravel/passport
php artisan migrate
php artisan passport:install

Шаг 2: Настройка модели User

В app/Models/User.php:

use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
    // ...
}

Шаг 3: Настройка AuthServiceProvider

В app/Providers/AuthServiceProvider.php:

use Laravel\Passport\Passport;

public function boot()
{
    $this->registerPolicies();
    
    Passport::routes();
    
    // Настройка токенов
    Passport::tokensExpireIn(now()->addDays(15));
    Passport::refreshTokensExpireIn(now()->addDays(30));
}

Шаг 4: Защита API роутов

В config/ceiling.php:

'api' => [
    'prefix' => env('CEILING_API_PREFIX', 'api/ceiling'),
    'middleware' => ['api', 'auth:api'], // Добавлен auth:api
    'rate_limit' => env('CEILING_API_RATE_LIMIT', 60),
],

Шаг 5: Использование токенов в компоненте Vue

Компонент автоматически использует токен из localStorage:

getAuthToken() {
    return localStorage.getItem('access_token') || '';
}

Для сохранения токена после авторизации:

// После успешной авторизации
localStorage.setItem('access_token', response.data.access_token);

Шаг 6: Пример авторизации

// Получение токена
async function login(email, password) {
    const response = await fetch('/api/login', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email, password })
    });
    
    const data = await response.json();
    localStorage.setItem('access_token', data.access_token);
    return data;
}

Использование токенов в запросах

Компонент автоматически добавляет токен в заголовки:

headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${this.getAuthToken()}`
}

📚 Использование

Через контроллер

use LetoCeilingCoder\CeilingCalculator\Http\Controllers\CeilingController;

Route::get('/ceilng', [CeilingController::class, 'index']);

Через сервис

use LetoCeilingCoder\CeilingCalculator\CeilingService;

$service = app(CeilingService::class);
$data = $service->getInitialData($request);

Через фасад

use LetoCeilingCoder\CeilingCalculator\Facades\Ceiling;

$data = Ceiling::getInitialData($request);
$rooms = Ceiling::getRooms();

📖 Дополнительная документация

  • QUICK_START_VUE3.md - Быстрый старт с Vue 3 и API
  • VUE3_API_INTEGRATION.md - Полное руководство по Vue 3 с API
  • COMPOSER_INSTALLATION.md - подробная установка через Composer
  • COMPOSER_QUICK_START.md - быстрый старт с Composer
  • VUE3_INTEGRATION.md - интеграция Vue 3 (базовое руководство)
  • LARAVEL_PASSPORT.md - настройка Laravel Passport
  • ASSETS_CHECKLIST.md - чеклист ресурсов
  • PACKAGE_INDEPENDENCE.md - независимость пакета
  • database/README.md - структура базы данных

🔧 Требования

  • PHP >= 8.1
  • Laravel >= 10.0
  • Composer >= 2.0
  • MySQL/PostgreSQL/SQLite
  • Laravel Passport (опционально, для API аутентификации)

📝 Лицензия

MIT

👥 Авторы

LetoCeiling Coder

🔗 Репозиторий

GitHub: https://github.com/letoceiling-coder/cieling-vue

🆘 Поддержка

Для вопросов и поддержки создайте Issue в репозитории проекта.