Реализация онлайн-калькулятора на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация онлайн-калькулятора на сайте
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация онлайн-калькулятора на сайте

Онлайн-калькулятор — один из наиболее конвертирующих инструментов на сайте. Ипотечный калькулятор, расчёт стоимости услуг, калькулятор ROI — все они строятся по одной архитектуре, но дьявол в деталях: формулы, валидация, форматирование чисел, анимация результата.

Архитектура: формула как конфиг

Неправильный подход — зашить формулы прямо в обработчики событий. Правильный — отделить логику расчёта от UI:

// types.ts
interface CalculatorField {
  id: string
  label: string
  type: 'number' | 'range' | 'select' | 'radio'
  min?: number
  max?: number
  step?: number
  defaultValue: number
  unit?: string
  options?: { label: string; value: number }[]
  format?: 'currency' | 'percent' | 'number'
}

interface CalculatorConfig {
  id: string
  fields: CalculatorField[]
  formula: (inputs: Record<string, number>) => CalculatorResult
  resultFields: ResultField[]
}

interface CalculatorResult {
  [key: string]: number
}

Ипотечный калькулятор: реализация

// mortgage-calculator.ts
export const mortgageCalculator: CalculatorConfig = {
  id: 'mortgage',
  fields: [
    { id: 'price', label: 'Стоимость недвижимости', type: 'number',
      min: 500_000, max: 100_000_000, step: 100_000, defaultValue: 5_000_000,
      format: 'currency' },
    { id: 'downPayment', label: 'Первоначальный взнос', type: 'range',
      min: 10, max: 90, step: 1, defaultValue: 20, unit: '%', format: 'percent' },
    { id: 'rate', label: 'Процентная ставка', type: 'number',
      min: 0.1, max: 30, step: 0.1, defaultValue: 11.5, unit: '% годовых', format: 'percent' },
    { id: 'term', label: 'Срок кредита', type: 'select',
      defaultValue: 20,
      options: [5, 10, 15, 20, 25, 30].map(y => ({ label: `${y} лет`, value: y })) },
  ],
  formula: ({ price, downPayment, rate, term }) => {
    const principal = price * (1 - downPayment / 100)
    const monthlyRate = rate / 100 / 12
    const months = term * 12

    // Формула аннуитетного платежа
    const payment = monthlyRate === 0
      ? principal / months
      : principal * (monthlyRate * Math.pow(1 + monthlyRate, months))
          / (Math.pow(1 + monthlyRate, months) - 1)

    const totalPayment = payment * months
    const overpayment = totalPayment - principal

    return { payment, totalPayment, overpayment, principal }
  },
  resultFields: [
    { id: 'payment', label: 'Ежемесячный платёж', format: 'currency', highlight: true },
    { id: 'totalPayment', label: 'Общая сумма выплат', format: 'currency' },
    { id: 'overpayment', label: 'Переплата', format: 'currency' },
    { id: 'principal', label: 'Сумма кредита', format: 'currency' },
  ],
}

React-компонент калькулятора

import { useState, useCallback, useMemo } from 'react'

function formatValue(value: number, format?: string): string {
  switch (format) {
    case 'currency':
      return new Intl.NumberFormat('ru-RU', {
        style: 'currency',
        currency: 'RUB',
        maximumFractionDigits: 0,
      }).format(value)
    case 'percent':
      return `${value.toFixed(1)}%`
    default:
      return new Intl.NumberFormat('ru-RU').format(value)
  }
}

export function Calculator({ config }: { config: CalculatorConfig }) {
  const [values, setValues] = useState<Record<string, number>>(
    Object.fromEntries(config.fields.map(f => [f.id, f.defaultValue]))
  )

  const result = useMemo(() => {
    try {
      return config.formula(values)
    } catch {
      return null
    }
  }, [values, config])

  const handleChange = useCallback((id: string, value: number) => {
    setValues(prev => ({ ...prev, [id]: value }))
  }, [])

  return (
    <div className="calculator">
      <div className="calculator__inputs">
        {config.fields.map(field => (
          <CalculatorField
            key={field.id}
            field={field}
            value={values[field.id]}
            onChange={val => handleChange(field.id, val)}
          />
        ))}
      </div>
      {result && (
        <div className="calculator__results">
          {config.resultFields.map(rf => (
            <div key={rf.id} className={`result-item ${rf.highlight ? 'result-item--highlight' : ''}`}>
              <span className="result-item__label">{rf.label}</span>
              <AnimatedNumber
                value={result[rf.id]}
                format={rf.format}
              />
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

Анимация изменения числа

import { useEffect, useRef, useState } from 'react'

function AnimatedNumber({ value, format }: { value: number; format?: string }) {
  const [displayValue, setDisplayValue] = useState(value)
  const animationRef = useRef<number>()
  const startRef = useRef(value)
  const startTimeRef = useRef<number>()

  useEffect(() => {
    const startValue = displayValue
    startRef.current = startValue
    startTimeRef.current = undefined

    const duration = 400  // ms

    const animate = (timestamp: number) => {
      if (!startTimeRef.current) startTimeRef.current = timestamp
      const elapsed = timestamp - startTimeRef.current
      const progress = Math.min(elapsed / duration, 1)

      // Easing: ease-out
      const eased = 1 - Math.pow(1 - progress, 3)
      const current = startValue + (value - startValue) * eased

      setDisplayValue(current)

      if (progress < 1) {
        animationRef.current = requestAnimationFrame(animate)
      }
    }

    animationRef.current = requestAnimationFrame(animate)
    return () => { if (animationRef.current) cancelAnimationFrame(animationRef.current) }
  }, [value])

  return <span className="animated-number">{formatValue(displayValue, format)}</span>
}

Валидация ввода

function CalculatorField({ field, value, onChange }: FieldProps) {
  const [rawValue, setRawValue] = useState(String(value))
  const [error, setError] = useState('')

  function handleInput(e: React.ChangeEvent<HTMLInputElement>) {
    const raw = e.target.value
    setRawValue(raw)

    const num = parseFloat(raw.replace(/\s/g, '').replace(',', '.'))

    if (isNaN(num)) {
      setError('Введите число')
      return
    }

    if (field.min !== undefined && num < field.min) {
      setError(`Минимум: ${formatValue(field.min, field.format)}`)
      return
    }

    if (field.max !== undefined && num > field.max) {
      setError(`Максимум: ${formatValue(field.max, field.format)}`)
      return
    }

    setError('')
    onChange(num)
  }

  // Синхронизируем при внешнем изменении (например, от range slider)
  useEffect(() => {
    setRawValue(String(value))
    setError('')
  }, [value])

  return (
    <div className={`field ${error ? 'field--error' : ''}`}>
      <label htmlFor={field.id}>{field.label}</label>
      <input
        id={field.id}
        type="text"
        inputMode="decimal"
        value={rawValue}
        onChange={handleInput}
      />
      {field.unit && <span className="field__unit">{field.unit}</span>}
      {error && <span className="field__error">{error}</span>}
    </div>
  )
}

Сохранение результата и поделиться

// Кодируем параметры в URL для шаринга
function encodeCalcState(values: Record<string, number>): string {
  const params = new URLSearchParams(
    Object.entries(values).map(([k, v]) => [k, String(v)])
  )
  return params.toString()
}

function decodeCalcState(search: string): Record<string, number> {
  const params = new URLSearchParams(search)
  const result: Record<string, number> = {}
  params.forEach((v, k) => { result[k] = parseFloat(v) })
  return result
}

// При изменении значений обновляем URL без перезагрузки
function syncToUrl(values: Record<string, number>) {
  const url = new URL(window.location.href)
  url.search = encodeCalcState(values)
  window.history.replaceState({}, '', url.toString())
}

Диаграмма распределения платежей

// Простая pie chart через SVG без библиотек
function PaymentPieChart({ principal, overpayment }: { principal: number; overpayment: number }) {
  const total = principal + overpayment
  const principalPct = principal / total
  const angle = principalPct * 360

  // SVG arc path
  const r = 80
  const cx = 100, cy = 100
  const rad = (deg: number) => (deg - 90) * Math.PI / 180
  const x = (deg: number) => cx + r * Math.cos(rad(deg))
  const y = (deg: number) => cy + r * Math.sin(rad(deg))

  const largeArc = angle > 180 ? 1 : 0
  const path1 = `M ${cx} ${cy} L ${x(0)} ${y(0)} A ${r} ${r} 0 ${largeArc} 1 ${x(angle)} ${y(angle)} Z`
  const path2 = `M ${cx} ${cy} L ${x(angle)} ${y(angle)} A ${r} ${r} 0 ${1 - largeArc} 1 ${x(360)} ${y(360)} Z`

  return (
    <svg viewBox="0 0 200 200" width="200" height="200">
      <path d={path1} fill="#6366f1" />
      <path d={path2} fill="#f43f5e" />
    </svg>
  )
}

Сроки

Простой калькулятор с 3–5 полями и одним результатом — 1 день. Ипотечный с анимацией, диаграммой, URL-шарингом и адаптивной вёрсткой — 2–3 дня. Конфигурируемая система с несколькими калькуляторами и управлением через CMS — 1 неделя.