Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m54s
196 lines
5.6 KiB
React
196 lines
5.6 KiB
React
import { IconCheck, IconX } from '@tabler/icons-react'
|
|
|
|
/**
|
|
* ProgressBar - компонент прогресс бара с estimated time
|
|
* @param {number} progress - прогресс от 0 до 100
|
|
* @param {string} status - статус операции
|
|
* @param {number} estimatedTime - оставшееся время в секундах
|
|
* @param {string} variant - цвет: 'primary', 'success', 'danger', 'warning'
|
|
*/
|
|
function ProgressBar({
|
|
progress = 0,
|
|
status = '',
|
|
estimatedTime = null,
|
|
variant = 'primary',
|
|
showPercentage = true,
|
|
striped = false,
|
|
animated = false,
|
|
size = 'default' // 'sm', 'default', 'lg'
|
|
}) {
|
|
const sizeClass = size === 'sm' ? 'progress-sm' : size === 'lg' ? 'progress-lg' : ''
|
|
const stripedClass = striped ? 'progress-bar-striped' : ''
|
|
const animatedClass = animated ? 'progress-bar-animated' : ''
|
|
|
|
const formatTime = (seconds) => {
|
|
if (seconds < 60) {
|
|
return `${Math.round(seconds)} сек`
|
|
} else if (seconds < 3600) {
|
|
const minutes = Math.floor(seconds / 60)
|
|
const secs = Math.round(seconds % 60)
|
|
return `${minutes} мин ${secs} сек`
|
|
} else {
|
|
const hours = Math.floor(seconds / 3600)
|
|
const minutes = Math.floor((seconds % 3600) / 60)
|
|
return `${hours} ч ${minutes} мин`
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="progress-container">
|
|
<div className="d-flex justify-content-between align-items-center mb-2">
|
|
{status && (
|
|
<div className="text-muted small">{status}</div>
|
|
)}
|
|
<div className="d-flex align-items-center gap-2">
|
|
{estimatedTime !== null && estimatedTime > 0 && (
|
|
<span className="text-muted small">
|
|
Осталось ~{formatTime(estimatedTime)}
|
|
</span>
|
|
)}
|
|
{showPercentage && (
|
|
<span className="text-muted small fw-bold">
|
|
{Math.round(progress)}%
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className={`progress ${sizeClass}`}>
|
|
<div
|
|
className={`progress-bar bg-${variant} ${stripedClass} ${animatedClass}`}
|
|
role="progressbar"
|
|
style={{ width: `${progress}%` }}
|
|
aria-valuenow={progress}
|
|
aria-valuemin="0"
|
|
aria-valuemax="100"
|
|
>
|
|
{size === 'lg' && showPercentage && (
|
|
<span className="progress-bar-label">{Math.round(progress)}%</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<style jsx>{`
|
|
.progress-container {
|
|
width: 100%;
|
|
}
|
|
|
|
.progress-sm {
|
|
height: 0.5rem;
|
|
}
|
|
|
|
.progress-lg {
|
|
height: 2rem;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.progress-bar-label {
|
|
line-height: 2rem;
|
|
color: white;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* MultiStepProgress - компонент для отображения многошагового прогресса
|
|
*/
|
|
function MultiStepProgress({ steps, currentStep, variant = 'primary' }) {
|
|
const progress = ((currentStep) / steps.length) * 100
|
|
|
|
return (
|
|
<div className="multi-step-progress">
|
|
{/* Индикаторы шагов */}
|
|
<div className="steps-indicator mb-3">
|
|
{steps.map((step, index) => {
|
|
const isPast = index < currentStep
|
|
const isCurrent = index === currentStep
|
|
const status = isPast ? 'completed' : isCurrent ? 'current' : 'pending'
|
|
|
|
return (
|
|
<div key={index} className={`step-item step-${status}`}>
|
|
<div className={`step-circle bg-${isPast ? 'success' : isCurrent ? variant : 'secondary'}`}>
|
|
{isPast ? (
|
|
<IconCheck size={16} className="text-white" />
|
|
) : (
|
|
<span className="text-white">{index + 1}</span>
|
|
)}
|
|
</div>
|
|
<div className="step-label text-muted small mt-1">{step}</div>
|
|
{index < steps.length - 1 && (
|
|
<div className={`step-line ${isPast ? 'completed' : ''}`}></div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Прогресс бар */}
|
|
<ProgressBar
|
|
progress={progress}
|
|
variant={variant}
|
|
showPercentage={false}
|
|
animated
|
|
/>
|
|
|
|
<style jsx>{`
|
|
.steps-indicator {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
position: relative;
|
|
}
|
|
|
|
.step-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
flex: 1;
|
|
position: relative;
|
|
}
|
|
|
|
.step-circle {
|
|
width: 2rem;
|
|
height: 2rem;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
z-index: 2;
|
|
position: relative;
|
|
}
|
|
|
|
.step-line {
|
|
position: absolute;
|
|
top: 1rem;
|
|
left: 50%;
|
|
right: -50%;
|
|
height: 2px;
|
|
background: var(--tblr-border-color, #e9ecef);
|
|
z-index: 1;
|
|
}
|
|
|
|
.step-line.completed {
|
|
background: var(--tblr-success, #2fb344);
|
|
}
|
|
|
|
.step-label {
|
|
text-align: center;
|
|
max-width: 100px;
|
|
}
|
|
|
|
.step-current .step-label {
|
|
font-weight: 600;
|
|
color: var(--tblr-body-color) !important;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export { ProgressBar, MultiStepProgress }
|
|
export default ProgressBar
|
|
|