Расчет стоимости
Калькулятор грузоперевозок
Укажите точки загрузки и выгрузки — маршрут построится автоматически по Яндекс Картам.
.apcalc {
--ap-bg: #f6f1e8;
--ap-surface: rgba(255, 255, 255, 0.86);
--ap-surface-strong: #ffffff;
--ap-text: #1f1d1a;
--ap-muted: #6f685f;
--ap-line: #e4d9c9;
--ap-accent: #b4874d;
--ap-accent-dark: #946a37;
--ap-accent-soft: #f4eadc;
--ap-info: #eef4ff;
--ap-info-text: #2a5ab8;
--ap-success: #edf9f0;
--ap-success-text: #17804b;
--ap-error: #fff1f2;
--ap-error-text: #b2263b;
--ap-shadow: 0 18px 44px rgba(31, 29, 26, 0.08);
--ap-radius-xl: 22px;
--ap-radius-lg: 18px;
--ap-radius-md: 14px;
--ap-radius-sm: 12px;
--ap-transition: 0.2s ease;
padding: 24px 0;
}
.apcalc,
.apcalc * {
box-sizing: border-box;
}
.apcalc__inner {
max-width: 1120px;
margin: 0 auto;
padding: 34px 24px;
border-radius: var(--ap-radius-xl);
background:
radial-gradient(circle at top right, rgba(180, 135, 77, 0.08), transparent 25%),
linear-gradient(135deg, #faf7f2 0%, var(--ap-bg) 100%);
box-shadow: var(--ap-shadow);
font-family: inherit;
color: var(--ap-text);
}
.apcalc__head {
margin-bottom: 24px;
}
.apcalc__eyebrow {
margin-bottom: 8px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ap-accent-dark);
}
.apcalc__title {
margin: 0 0 8px;
font-size: 34px;
line-height: 1.15;
font-weight: 800;
color: var(--ap-text);
}
.apcalc__subtitle {
max-width: 720px;
margin: 0;
font-size: 15px;
line-height: 1.6;
color: var(--ap-muted);
}
.apcalc__form {
display: flex;
flex-direction: column;
gap: 18px;
}
.apcalc__card {
padding: 22px;
border: 1px solid var(--ap-line);
border-radius: var(--ap-radius-lg);
background: var(--ap-surface);
backdrop-filter: blur(6px);
}
.apcalc__card-title {
margin-bottom: 16px;
font-size: 18px;
font-weight: 700;
color: var(--ap-text);
}
.apcalc__grid {
display: grid;
gap: 14px;
}
.apcalc__grid--route {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.apcalc__grid--params {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.apcalc__field {
display: flex;
flex-direction: column;
}
.apcalc__field--wide {
grid-column: span 3;
}
.apcalc__field label {
margin-bottom: 7px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #504a42;
}
.apcalc__field input,
.apcalc__field select {
height: 50px;
width: 100%;
padding: 0 14px;
border: 1.5px solid var(--ap-line);
border-radius: var(--ap-radius-sm);
background: var(--ap-surface-strong);
font-size: 15px;
color: var(--ap-text);
transition: border-color var(--ap-transition), box-shadow var(--ap-transition), background var(--ap-transition);
}
.apcalc__field input::placeholder {
color: #9a9186;
}
.apcalc__field input:focus,
.apcalc__field select:focus {
outline: none;
border-color: var(--ap-accent);
box-shadow: 0 0 0 4px rgba(180, 135, 77, 0.14);
}
.apcalc__field input[readonly] {
background: #fbfaf7;
color: #5d564d;
}
.apcalc__hint {
margin-top: 8px;
font-size: 13px;
line-height: 1.45;
color: var(--ap-muted);
}
.apcalc__routebar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-top: 16px;
margin-bottom: 16px;
}
.apcalc__ghost-btn {
height: 46px;
padding: 0 18px;
border: 1.5px solid var(--ap-accent);
border-radius: var(--ap-radius-sm);
background: transparent;
color: var(--ap-accent-dark);
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: all var(--ap-transition);
}
.apcalc__ghost-btn:hover {
background: var(--ap-accent-soft);
border-color: var(--ap-accent-dark);
}
.apcalc__status {
display: inline-flex;
align-items: center;
min-height: 46px;
padding: 10px 14px;
border-radius: var(--ap-radius-sm);
font-size: 14px;
font-weight: 600;
}
.apcalc__status--info {
background: var(--ap-info);
color: var(--ap-info-text);
}
.apcalc__status--loading {
background: #fff7e8;
color: #c68216;
}
.apcalc__status--success {
background: var(--ap-success);
color: var(--ap-success-text);
}
.apcalc__status--error {
background: var(--ap-error);
color: var(--ap-error-text);
}
.apcalc__map {
width: 100%;
height: 370px;
border: 1px solid var(--ap-line);
border-radius: 18px;
overflow: hidden;
background: #e7ddd0;
}
.apcalc__checks {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-top: 18px;
}
.apcalc__check {
display: flex;
align-items: center;
gap: 10px;
min-height: 52px;
padding: 12px 14px;
border: 1.5px solid var(--ap-line);
border-radius: var(--ap-radius-sm);
background: var(--ap-surface-strong);
cursor: pointer;
transition: border-color var(--ap-transition), background var(--ap-transition);
font-size: 14px;
font-weight: 600;
color: #453f38;
}
.apcalc__check:hover {
border-color: #d1bc9d;
background: #fffdf9;
}
.apcalc__check input {
width: 18px;
height: 18px;
accent-color: var(--ap-accent);
flex: 0 0 auto;
}
.apcalc__main-btn {
height: 56px;
border: none;
border-radius: 14px;
background: linear-gradient(135deg, var(--ap-accent) 0%, var(--ap-accent-dark) 100%);
color: #fff;
font-size: 16px;
font-weight: 800;
letter-spacing: 0.01em;
cursor: pointer;
box-shadow: 0 12px 24px rgba(148, 106, 55, 0.24);
transition: transform var(--ap-transition), box-shadow var(--ap-transition), opacity var(--ap-transition);
}
.apcalc__main-btn:hover {
transform: translateY(-1px);
box-shadow: 0 14px 26px rgba(148, 106, 55, 0.3);
}
.apcalc__result {
display: none;
margin-top: 22px;
padding: 24px;
border: 1px solid var(--ap-line);
border-radius: var(--ap-radius-lg);
background: #fff;
}
.apcalc__result.is-visible {
display: block;
}
.apcalc__result.is-error {
border-color: #f1c1c8;
background: #fff7f8;
color: var(--ap-error-text);
}
.apcalc__result-top {
margin-bottom: 12px;
}
.apcalc__result-tag {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #7f776d;
}
.apcalc__result-route {
margin-top: 5px;
font-size: 20px;
line-height: 1.35;
font-weight: 700;
color: var(--ap-text);
}
.apcalc__result-meta {
margin-top: 8px;
font-size: 14px;
color: var(--ap-muted);
}
.apcalc__result-price {
margin: 18px 0 16px;
font-size: 44px;
line-height: 1;
font-weight: 800;
color: var(--ap-accent-dark);
}
.apcalc__result-list {
margin: 0;
padding-left: 18px;
font-size: 14px;
line-height: 1.85;
color: #4f4a43;
}
.apcalc__result-note {
margin-top: 16px;
padding: 12px 14px;
border-radius: 12px;
background: #fbf6ee;
font-size: 13px;
line-height: 1.55;
color: var(--ap-muted);
}
@media (max-width: 980px) {
.apcalc__grid--route,
.apcalc__grid--params {
grid-template-columns: 1fr;
}
.apcalc__field--wide {
grid-column: span 1;
}
.apcalc__checks {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.apcalc__inner {
padding: 20px 14px;
}
.apcalc__title {
font-size: 26px;
}
.apcalc__map {
height: 290px;
}
.apcalc__result-price {
font-size: 34px;
}
}(function () {
"use strict";
const YANDEX_API_KEY = f0576e4c-a2f5-4574-9921-44c19657539e;
const VEHICLES = {
gazelle: {
name: "Газель 1.5 т",
rate: 32,
minPrice: 9000,
maxWeight: 1.5,
maxVolume: 9,
loading: 1000,
unloading: 1000
},
truck3: {
name: "3-тонник",
rate: 40,
minPrice: 13000,
maxWeight: 3,
maxVolume: 18,
loading: 1400,
unloading: 1400
},
truck5: {
name: "5-тонник",
rate: 48,
minPrice: 17000,
maxWeight: 5,
maxVolume: 36,
loading: 1800,
unloading: 1800
},
truck10: {
name: "10-тонник",
rate: 58,
minPrice: 24000,
maxWeight: 10,
maxVolume: 45,
loading: 2200,
unloading: 2200
},
truck20: {
name: "Фура 20 т",
rate: 68,
minPrice: 32000,
maxWeight: 20,
maxVolume: 82,
loading: 2800,
unloading: 2800
},
reefer20: {
name: "Рефрижератор 20 т",
rate: 85,
minPrice: 42000,
maxWeight: 20,
maxVolume: 82,
loading: 3200,
unloading: 3200
}
};
const dom = {
form: document.getElementById("apcalcForm"),
from: document.getElementById("apFrom"),
to: document.getElementById("apTo"),
buildRoute: document.getElementById("apBuildRoute"),
routeStatus: document.getElementById("apRouteStatus"),
distance: document.getElementById("apDistance"),
time: document.getElementById("apTime"),
manualDistance: document.getElementById("apManualDistance"),
map: document.getElementById("apMap"),
vehicle: document.getElementById("apVehicle"),
vehicleHint: document.getElementById("apVehicleHint"),
weight: document.getElementById("apWeight"),
volume: document.getElementById("apVolume"),
loading: document.getElementById("apLoading"),
unloading: document.getElementById("apUnloading"),
urgent: document.getElementById("apUrgent"),
insurance: document.getElementById("apInsurance"),
result: document.getElementById("apResult")
};
const state = {
map: null,
route: null,
ymapsReady: false,
routeDistanceKm: 0,
routeDurationMin: 0,
routeRequestId: 0,
inputTimer: null
};
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function money(value) {
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
maximumFractionDigits: 0
}).format(value);
}
function durationText(totalMinutes) {
const h = Math.floor(totalMinutes / 60);
const m = totalMinutes % 60;
if (h > 0 && m > 0) return h + " ч " + m + " мин";
if (h > 0) return h + " ч";
return m + " мин";
}
function setStatus(text, type) {
dom.routeStatus.className = "apcalc__status apcalc__status--" + type;
dom.routeStatus.textContent = text;
}
function clearRouteInfo() {
state.routeDistanceKm = 0;
state.routeDurationMin = 0;
dom.distance.value = "";
dom.time.value = "";
}
function getVehicle() {
return VEHICLES[dom.vehicle.value];
}
function updateVehicleHint() {
const v = getVehicle();
dom.vehicleHint.textContent =
"Грузоподъемность: " +
v.maxWeight +
" т • Объем: " +
v.maxVolume +
" м³ • Минимальный заказ: " +
money(v.minPrice);
}
function getEffectiveDistance() {
const manual = Number(dom.manualDistance.value);
if (manual > 0) return manual;
return state.routeDistanceKm;
}
function loadYandexMaps() {
return new Promise(function (resolve, reject) {
if (window.ymaps) {
window.ymaps.ready(resolve);
return;
}
const script = document.createElement("script");
script.src =
"https://api-maps.yandex.ru/2.1/?apikey=" +
encodeURIComponent(YANDEX_API_KEY) +
"&lang=ru_RU";
script.async = true;
script.onload = function () {
if (!window.ymaps) {
reject(new Error("ymaps not found"));
return;
}
window.ymaps.ready(resolve);
};
script.onerror = function () {
reject(new Error("maps load error"));
};
document.head.appendChild(script);
});
}
function initMap() {
state.map = new window.ymaps.Map("apMap", {
center: [59.9386, 30.3141],
zoom: 9,
controls: ["zoomControl", "fullscreenControl"]
});
state.ymapsReady = true;
setStatus("Карта загружена. Можно строить маршрут.", "success");
}
function removeRoute() {
if (state.map && state.route) {
state.map.geoObjects.remove(state.route);
state.route = null;
}
}
async function buildRoute() {
const from = dom.from.value.trim();
const to = dom.to.value.trim();
if (!from || !to) {
clearRouteInfo();
removeRoute();
setStatus("Укажите точки загрузки и выгрузки.", "error");
return;
}
if (!state.ymapsReady || !window.ymaps) {
setStatus("Яндекс Карты еще загружаются.", "loading");
return;
}
const currentRequestId = ++state.routeRequestId;
setStatus("Строим маршрут...", "loading");
try {
const route = await window.ymaps.route([from, to], {
routingMode: "auto",
mapStateAutoApply: false
});
if (currentRequestId !== state.routeRequestId) return;
removeRoute();
state.route = route;
state.map.geoObjects.add(route);
const bounds = route.getBounds();
if (bounds) {
state.map.setBounds(bounds, {
checkZoomRange: true,
zoomMargin: 30
});
}
state.routeDistanceKm = Math.max(1, Math.round(route.getLength() / 1000));
state.routeDurationMin = Math.max(1, Math.round(route.getTime() / 60));
dom.distance.value = state.routeDistanceKm.toLocaleString("ru-RU") + " км";
dom.time.value = durationText(state.routeDurationMin);
setStatus(
"Маршрут построен: " + state.routeDistanceKm.toLocaleString("ru-RU") + " км",
"success"
);
} catch (error) {
if (currentRequestId !== state.routeRequestId) return;
clearRouteInfo();
removeRoute();
setStatus(
"Не удалось построить маршрут. Проверьте адреса или используйте расстояние вручную.",
"error"
);
}
}
function debounceBuildRoute() {
clearTimeout(state.inputTimer);
clearRouteInfo();
if (!dom.from.value.trim() || !dom.to.value.trim()) {
removeRoute();
setStatus("Введите точки маршрута.", "info");
return;
}
state.inputTimer = setTimeout(function () {
buildRoute();
}, 900);
}
function showError(message) {
dom.result.className = "apcalc__result is-visible is-error";
dom.result.innerHTML = "
" + escapeHtml(message) + "";
}
function showResult(html) {
dom.result.className = "apcalc__result is-visible";
dom.result.innerHTML = html;
}
function calculate(event) {
event.preventDefault();
const from = dom.from.value.trim();
const to = dom.to.value.trim();
const distanceKm = getEffectiveDistance();
const weight = Number(dom.weight.value);
const volume = Number(dom.volume.value);
const vehicle = getVehicle();
if (!from || !to) {
showError("Укажите точки загрузки и выгрузки.");
return;
}
if (!distanceKm || distanceKm <= 0) {
showError("Сначала постройте маршрут или укажите расстояние вручную.");
return;
}
if (!weight || weight <= 0) {
showError("Укажите корректный вес груза.");
return;
}
if (!volume || volume <= 0) {
showError("Укажите корректный объем груза.");
return;
}
if (weight > vehicle.maxWeight) {
showError(
"Вес груза превышает грузоподъемность выбранного транспорта (" +
vehicle.maxWeight +
" т)."
);
return;
}
if (volume > vehicle.maxVolume) {
showError(
"Объем груза превышает допустимый объем выбранного транспорта (" +
vehicle.maxVolume +
" м³)."
);
return;
}
const basePrice = Math.max(distanceKm * vehicle.rate, vehicle.minPrice);
let total = basePrice;
const breakdown = [];
breakdown.push(
"Базовый тариф: " + money(basePrice) + " (" + distanceKm + " км × " + vehicle.rate + " ₽/км)"
);
breakdown.push(
"Транспорт: " + vehicle.name + " • " + vehicle.maxWeight + " т / " + vehicle.maxVolume + " м³"
);
breakdown.push("Вес груза: " + weight + " т");
breakdown.push("Объем груза: " + volume + " м³");
if (dom.loading.checked) {
total += vehicle.loading;
breakdown.push("Погрузка: " + money(vehicle.loading));
}
if (dom.unloading.checked) {
total += vehicle.unloading;
breakdown.push("Разгрузка: " + money(vehicle.unloading));
}
if (dom.insurance.checked) {
const insurancePrice = Math.round(basePrice * 0.01);
total += insurancePrice;
breakdown.push("Страхование: " + money(insurancePrice));
}
if (dom.urgent.checked) {
const urgentPrice = Math.round(total * 0.2);
total += urgentPrice;
breakdown.push("Срочная доставка: " + money(urgentPrice));
}
const usingManualDistance = Number(dom.manualDistance.value) > 0;
const html =
'
' +
'
Маршрут
' +
'
' +
escapeHtml(from) +
" → " +
escapeHtml(to) +
"
" +
'
' +
"Расстояние: " +
distanceKm.toLocaleString("ru-RU") +
" км" +
(state.routeDurationMin && !usingManualDistance
? " • Время в пути: " + escapeHtml(durationText(state.routeDurationMin))
: "") +
(usingManualDistance ? " • Использовано расстояние, введенное вручную" : "") +
"
" +
"
" +
'
' +
money(total) +
"
" +
'
' +
breakdown.map(function (item) {
return "- " + escapeHtml(item) + "
";
}).join("") +
"
" +
'
' +
"Это ориентировочная стоимость. Для точного коммерческого предложения рекомендуем подтвердить детали маршрута, характер груза и условия погрузки/выгрузки." +
"
";
showResult(html);
}
function bindEvents() {
dom.buildRoute.addEventListener("click", buildRoute);
dom.from.addEventListener("input", debounceBuildRoute);
dom.to.addEventListener("input", debounceBuildRoute);
dom.vehicle.addEventListener("change", updateVehicleHint);
dom.form.addEventListener("submit", calculate);
updateVehicleHint();
}
async function start() {
bindEvents();
if (!YANDEX_API_KEY || YANDEX_API_KEY === "ВАШ_API_КЛЮЧ_ЯНДЕКС") {
setStatus("Укажите API-ключ Яндекс Карт в JS-коде.", "error");
return;
}
try {
setStatus("Загружаем Яндекс Карты...", "loading");
await loadYandexMaps();
initMap();
} catch (error) {
setStatus(
"Не удалось загрузить Яндекс Карты. Проверьте API-ключ и настройки доступа.",
"error"
);
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start);
} else {
start();
}
})();