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
Requires
- php: ^8.1
- illuminate/database: ^10.0|^11.0
- illuminate/http: ^10.0|^11.0
- illuminate/routing: ^10.0|^11.0
- illuminate/support: ^10.0|^11.0
- illuminate/view: ^10.0|^11.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
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- подробная установка через ComposerCOMPOSER_QUICK_START.md- быстрый старт с ComposerVUE3_INTEGRATION.md- интеграция Vue 3 (базовое руководство)LARAVEL_PASSPORT.md- настройка Laravel PassportASSETS_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 в репозитории проекта.