185 lines
10 KiB
Svelte
185 lines
10 KiB
Svelte
|
|
<!-- lib/pages/print/PrintUI.svelte -->
|
|||
|
|
<script>
|
|||
|
|
import { QUALITIES, COLOR_MODES, FORMATS } from '$lib/common/utils/pricing.util.js';
|
|||
|
|
|
|||
|
|
/** @type {'button' | 'icon-button' | 'counter' | 'toggle-group' | 'range-slider' | 'file-card' | 'upload-zone' | 'footer'} */
|
|||
|
|
let { as, ...props } = $props();
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
{#if as === 'button'}
|
|||
|
|
<button
|
|||
|
|
class="action-btn {props.variant ?? 'primary'}"
|
|||
|
|
disabled={props.disabled}
|
|||
|
|
onclick={props.onclick}
|
|||
|
|
type={props.type ?? 'button'}
|
|||
|
|
>
|
|||
|
|
<slot />
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
{:else if as === 'icon-button'}
|
|||
|
|
<button
|
|||
|
|
class="icon-btn"
|
|||
|
|
class:danger={props.variant === 'danger'}
|
|||
|
|
onclick={props.onclick}
|
|||
|
|
title={props.title}
|
|||
|
|
type="button"
|
|||
|
|
><slot /></button>
|
|||
|
|
|
|||
|
|
{:else if as === 'counter'}
|
|||
|
|
<div class="mini-counter">
|
|||
|
|
<button class="circle-btn small" onclick={props.onDecrement} disabled={props.value <= (props.min ?? 1)} type="button">−</button>
|
|||
|
|
<span class="copies-value">{props.value} шт.</span>
|
|||
|
|
<button class="circle-btn small" onclick={props.onIncrement} type="button">+</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{:else if as === 'toggle-group'}
|
|||
|
|
<div class="option-sub">
|
|||
|
|
{#if props.label}<label class="option-title">{props.label}</label>{/if}
|
|||
|
|
<div class="toggle-group horizontal">
|
|||
|
|
{#each props.options as opt}
|
|||
|
|
{@const val = props.valueKey ? opt[props.valueKey] : opt}
|
|||
|
|
{@const lbl = props.labelKey ? opt[props.labelKey] : opt}
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
class="toggle-btn"
|
|||
|
|
class:active={props.selected === val}
|
|||
|
|
onclick={() => props.onSelect?.(val)}
|
|||
|
|
>{lbl}</button>
|
|||
|
|
{/each}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{:else if as === 'range-slider'}
|
|||
|
|
<div class="option-sub quality-sub">
|
|||
|
|
<label class="option-title">{props.label} – {QUALITIES[props.value]?.label ?? ''}</label>
|
|||
|
|
<input
|
|||
|
|
type="range"
|
|||
|
|
min={0}
|
|||
|
|
max={props.options.length - 1}
|
|||
|
|
step={1}
|
|||
|
|
value={props.value}
|
|||
|
|
oninput={(e) => props.onChange?.(parseInt(e.target.value))}
|
|||
|
|
class="quality-slider"
|
|||
|
|
style="--fill: {Math.round((props.value / (props.options.length - 1)) * 100)}%"
|
|||
|
|
/>
|
|||
|
|
<div class="quality-marks-inline">
|
|||
|
|
{#each props.options as q}<span class="quality-mark">{q.label}</span>{/each}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{:else if as === 'file-card'}
|
|||
|
|
{@const file = props.file}
|
|||
|
|
<section class="file-card" class:expanded={file.expanded}>
|
|||
|
|
<div class="file-row" onclick={() => props.onToggle?.(file.id)}>
|
|||
|
|
<span class="chevron">{file.expanded ? '▼' : '►'}</span>
|
|||
|
|
<span class="file-name" title={file.file.name}>{file.file.name}</span>
|
|||
|
|
<span class="file-row-actions" onclick={(e) => e.stopPropagation()}>
|
|||
|
|
{#if file.previewUrl}
|
|||
|
|
<svelte:self as="icon-button" onclick={() => props.onPreview?.(file)} title="Предпросмотр">🔍</svelte:self>
|
|||
|
|
{/if}
|
|||
|
|
<svelte:self as="icon-button" variant="danger" onclick={() => props.onRemove?.(file.id)} title="Удалить">✕</svelte:self>
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
{#if file.expanded}
|
|||
|
|
<div class="file-options">
|
|||
|
|
<div class="print-params-row">
|
|||
|
|
<div class="pages-control">
|
|||
|
|
<label class="option-title compact">Страницы:</label>
|
|||
|
|
<button class="pages-btn" onclick={() => props.onPageSelect?.(file)}>
|
|||
|
|
{props.pagesLabel?.(file) ?? '...'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="copies-control-inline">
|
|||
|
|
<label class="option-title compact">Кол-во:</label>
|
|||
|
|
<svelte:self
|
|||
|
|
as="counter"
|
|||
|
|
value={file.copies}
|
|||
|
|
min={1}
|
|||
|
|
onIncrement={() => props.onUpdate?.(file.id, { copies: file.copies + 1 })}
|
|||
|
|
onDecrement={() => props.onUpdate?.(file.id, { copies: Math.max(1, file.copies - 1) })}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<svelte:self as="toggle-group" label="Формат" options={FORMATS} selected={file.format} onSelect={(v) => props.onUpdate?.(file.id, { format: v })} />
|
|||
|
|
<svelte:self as="toggle-group" label="Цветность" options={COLOR_MODES} selected={file.colorMode} valueKey="id" labelKey="label" onSelect={(v) => props.onUpdate?.(file.id, { colorMode: v })} />
|
|||
|
|
<svelte:self as="range-slider" label="Качество" options={QUALITIES} value={file.qualityIndex} onChange={(v) => props.onUpdate?.(file.id, { qualityIndex: v })} />
|
|||
|
|
</div>
|
|||
|
|
{/if}
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
{:else if as === 'upload-zone'}
|
|||
|
|
<div class="empty-upload">
|
|||
|
|
<p class="empty-hint">Загрузите файлы для печати</p>
|
|||
|
|
<svelte:self as="button" variant="primary" onclick={props.onSelect}>Загрузить файл(ы)</svelte:self>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{:else if as === 'footer'}
|
|||
|
|
<footer class="actions">
|
|||
|
|
{#if props.filesCount > 0}
|
|||
|
|
<p class="file-count">
|
|||
|
|
{props.filesCount} {props.plFiles(props.filesCount)}
|
|||
|
|
{#if props.extraCopies > 0}, {props.extraCopies} {props.plCopies(props.extraCopies)}{/if}
|
|||
|
|
</p>
|
|||
|
|
{/if}
|
|||
|
|
<div class="pricing-info">
|
|||
|
|
<span class="price-tier" class:active={props.hasTierSmall}>10р/л от 1</span>
|
|||
|
|
<span class="price-tier" class:active={props.hasTierMedium}>9р/л от 25</span>
|
|||
|
|
<span class="price-tier" class:active={props.hasTierLarge}>4р/л от 1000</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="price-summary">
|
|||
|
|
<span class="summary-itogo">Итого</span>
|
|||
|
|
<span class="summary-price">{props.totalPrice}₽</span>
|
|||
|
|
</div>
|
|||
|
|
<svelte:self as="button" variant="secondary" disabled={props.isPrinting || props.filesCount === 0} onclick={props.onSubmit}>
|
|||
|
|
{props.isPrinting ? 'Отправка...' : 'Далее'}
|
|||
|
|
</svelte:self>
|
|||
|
|
</footer>
|
|||
|
|
{/if}
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
.action-btn { width: 100%; padding: 16px 24px; border-radius: 16px; font-size: 16px; font-weight: 700; cursor: pointer; border: none; color: #fff; transition: opacity 0.2s; }
|
|||
|
|
.action-btn:disabled { opacity: 0.6; cursor: wait; }
|
|||
|
|
.action-btn.primary { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); box-shadow: 0 8px 24px -6px rgba(99,102,241,0.5); }
|
|||
|
|
.action-btn.secondary { background: linear-gradient(135deg, #5b8cff 0%, #38c6ff 100%); box-shadow: 0 8px 24px -6px rgba(72,150,255,0.4); }
|
|||
|
|
.icon-btn { width: 34px; height: 34px; border-radius: 10px; border: none; background: rgba(255,255,255,0.08); color: #f5f7fa; font-size: 14px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
|
|||
|
|
.icon-btn.danger { color: #ff6b6b; }
|
|||
|
|
.mini-counter { display: flex; align-items: center; gap: 8px; }
|
|||
|
|
.circle-btn { width: 32px; height: 32px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.05); color: #f5f7fa; font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; padding: 0; line-height: 1; }
|
|||
|
|
.circle-btn.small { width: 28px; height: 28px; font-size: 14px; }
|
|||
|
|
.circle-btn:hover:not(:disabled) { background: rgba(99,102,241,0.2); border-color: #6366f1; }
|
|||
|
|
.circle-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
|||
|
|
.copies-value { font-size: 14px; font-weight: 600; color: #fff; min-width: 36px; text-align: center; font-variant-numeric: tabular-nums; }
|
|||
|
|
.option-sub { display: flex; flex-direction: column; gap: 8px; }
|
|||
|
|
.option-title { font-size: 13px; font-weight: 600; color: #8b93a1; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; display: block; }
|
|||
|
|
.option-title.compact { margin-bottom: 0; font-size: 12px; white-space: nowrap; margin-right: 8px; }
|
|||
|
|
.toggle-group { display: flex; gap: 10px; }
|
|||
|
|
.toggle-btn { flex: 1; padding: 12px 8px; border-radius: 14px; border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.05); color: #f5f7fa; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.18s ease; }
|
|||
|
|
.toggle-btn.active { background: rgba(99,102,241,0.25); border-color: #6366f1; color: #fff; }
|
|||
|
|
.quality-sub { gap: 8px; }
|
|||
|
|
.quality-slider { -webkit-appearance: none; width: 100%; height: 6px; border-radius: 4px; background: rgba(255,255,255,0.12); outline: none; }
|
|||
|
|
.quality-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: #6366f1; cursor: pointer; border: 2px solid #fff; }
|
|||
|
|
.quality-marks-inline { display: flex; justify-content: space-between; font-size: 11px; color: #8b93a1; margin-top: 4px; }
|
|||
|
|
.file-card { border-radius: 16px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); overflow: hidden; padding-bottom: 6px; }
|
|||
|
|
.file-row { width: 100%; box-sizing: border-box; display: flex; align-items: center; gap: 12px; padding: 14px; background: none; border: none; color: #f5f7fa; cursor: pointer; user-select: none; text-align: left; }
|
|||
|
|
.file-row:hover { background: rgba(255,255,255,0.03); }
|
|||
|
|
.chevron { color: #6366f1; font-size: 12px; transition: transform 0.15s; }
|
|||
|
|
.file-name { flex: 1; font-size: 15px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|||
|
|
.file-row-actions { display: flex; gap: 6px; }
|
|||
|
|
.file-options { padding: 8px 14px 14px; display: flex; flex-direction: column; gap: 18px; }
|
|||
|
|
.print-params-row { flex-direction: row; align-items: center; justify-content: space-between; gap: 12px; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.06); margin-bottom: 4px; }
|
|||
|
|
.pages-control { display: flex; align-items: center; flex: 1; min-width: 0; }
|
|||
|
|
.pages-btn { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.1); color: #fff; border-radius: 8px; padding: 6px 16px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; }
|
|||
|
|
.pages-btn:hover { background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.2); }
|
|||
|
|
.copies-control-inline { display: flex; align-items: center; flex-shrink: 0; }
|
|||
|
|
.empty-upload { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; text-align: center; }
|
|||
|
|
.empty-hint { color: #8b93a1; font-size: 15px; margin: 0; }
|
|||
|
|
.actions { padding: 16px 20px 32px; display: flex; flex-direction: column; gap: 12px; }
|
|||
|
|
.file-count { color: #8b93a1; font-size: 13px; margin: 0; text-align: center; line-height: 1.4; }
|
|||
|
|
.pricing-info { display: flex; justify-content: center; gap: 12px; font-size: 10px; line-height: 1.2; color: rgba(255,255,255,0.25); margin-bottom: 4px; user-select: none; flex-wrap: wrap; }
|
|||
|
|
.price-tier { transition: color 0.2s ease; }
|
|||
|
|
.price-tier.active { color: rgba(255,255,255,0.6); }
|
|||
|
|
.price-summary { display: flex; justify-content: space-between; align-items: baseline; width: 100%; }
|
|||
|
|
.summary-itogo { font-size: 30px; font-weight: 700; color: #8b93a1; }
|
|||
|
|
.summary-price { font-size: 38px; font-weight: 800; color: #fff; }
|
|||
|
|
</style>
|