Подключение кнопки Печать к бэкенду + временный прокси через vite.config.js
This commit is contained in:
@ -1,105 +1,118 @@
|
||||
<script>
|
||||
import { onDestroy, tick } from 'svelte';
|
||||
import SelectionPage from './preview/SelectionPage.svelte';
|
||||
import PreviewPage from './preview/PreviewPage.svelte';
|
||||
import { onDestroy, tick } from 'svelte';
|
||||
import SelectionPage from './preview/SelectionPage.svelte';
|
||||
import PreviewPage from './preview/PreviewPage.svelte';
|
||||
|
||||
let { onBack = () => {}, onPrint = () => {} } = $props();
|
||||
let { onBack = () => {} } = $props();
|
||||
|
||||
// ── Модель данных ──
|
||||
/** @typedef {{ id: number, file: File, previewUrl: string | null, fileType: 'image' | 'pdf' | 'other', expanded: boolean, format: 'A4' | 'A5', colorMode: 'bw' | 'color', qualityIndex: number, copies: number, pageRange: string, totalPages: number, selectedPages: Set<number>, pdfDoc: any }} FileEntry */
|
||||
/** @type {FileEntry[]} */
|
||||
let files = $state([]);
|
||||
// ── Модель данных ──
|
||||
/** @typedef {{ id: number, file: File, previewUrl: string | null, fileType: 'image' | 'pdf' | 'other', expanded: boolean, format: 'A4' | 'A5', colorMode: 'bw' | 'color', qualityIndex: number, copies: number, pageRange: string, totalPages: number, selectedPages: Set<number>, pdfDoc: any }} FileEntry */
|
||||
/** @type {FileEntry[]} */
|
||||
let files = $state([]);
|
||||
|
||||
// ── Общие состояния оверлея ──
|
||||
/** @type {FileEntry | null} */
|
||||
let activeFile = $state(null);
|
||||
/** Режим просмотра: 'gallery' (выбор страниц) или 'preview' (чтение) */
|
||||
let viewMode = $state(/** @type {'gallery' | 'preview'} */ ('gallery'));
|
||||
// ── Общие состояния оверлея ──
|
||||
/** @type {FileEntry | null} */
|
||||
let activeFile = $state(null);
|
||||
/** Режим просмотра: 'gallery' (выбор страниц) или 'preview' (чтение) */
|
||||
let viewMode = $state(/** @type {'gallery' | 'preview'} */ ('gallery'));
|
||||
|
||||
// ── Данные для галереи (выбор страниц) ──
|
||||
let galleryThumbnails = $state([]);
|
||||
let galleryLoading = $state(false);
|
||||
// ── Данные для галереи (выбор страниц) ──
|
||||
let galleryThumbnails = $state([]);
|
||||
let galleryLoading = $state(false);
|
||||
|
||||
// ── Данные для превью (чтение) ──
|
||||
/** Кэшированные URL больших страниц для превью */
|
||||
let previewPagesCache = $state({});
|
||||
let isPreviewRendering = $state(false);
|
||||
// ── Данные для превью (чтение) ──
|
||||
/** Кэшированные URL больших страниц для превью */
|
||||
let previewPagesCache = $state({});
|
||||
let isPreviewRendering = $state(false);
|
||||
|
||||
let uid = 0;
|
||||
// ── Состояние кнопки печати ──
|
||||
let isPrinting = $state(false);
|
||||
|
||||
const qualities = [
|
||||
let uid = 0;
|
||||
const PRICE_PER_PAGE = 9;
|
||||
|
||||
const qualities = [
|
||||
{ id: 'low', label: 'Эконом' },
|
||||
{ id: 'medium', label: 'Стандарт' },
|
||||
{ id: 'high', label: 'Максимум' }
|
||||
];
|
||||
const formats = ['A4', 'A5'];
|
||||
const colorModes = [
|
||||
];
|
||||
const formats = ['A4', 'A5'];
|
||||
const colorModes = [
|
||||
{ id: 'bw', label: 'Чб' },
|
||||
{ id: 'color', label: 'Цвет' }
|
||||
];
|
||||
];
|
||||
|
||||
// ── PDF.js Core ──
|
||||
async function loadPdfDocument(entry) {
|
||||
// ── Вспомогательная функция для реактивного обновления ──
|
||||
function updateFile(id, changes) {
|
||||
files = files.map(f => f.id === id ? { ...f, ...changes } : f);
|
||||
if (activeFile && activeFile.id === id) {
|
||||
activeFile = files.find(f => f.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
// ── PDF.js Core ──
|
||||
async function loadPdfDocument(entry) {
|
||||
if (entry.pdfDoc) return entry.pdfDoc;
|
||||
try {
|
||||
const pdfjsLib = await import('https://mozilla.github.io/pdf.js/build/pdf.mjs');
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://mozilla.github.io/pdf.js/build/pdf.worker.mjs';
|
||||
|
||||
const arrayBuffer = await entry.file.arrayBuffer();
|
||||
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
entry.pdfDoc = pdf;
|
||||
entry.totalPages = pdf.numPages;
|
||||
// Инициализируем выбранные страницы только если они еще не заданы
|
||||
if (!entry.selectedPages || entry.selectedPages.size === 0) {
|
||||
entry.selectedPages = new Set(Array.from({ length: pdf.numPages }, (_, i) => i + 1));
|
||||
}
|
||||
|
||||
const numPages = pdf.numPages;
|
||||
const allPages = new Set(Array.from({ length: numPages }, (_, i) => i + 1));
|
||||
|
||||
updateFile(entry.id, {
|
||||
pdfDoc: pdf,
|
||||
totalPages: numPages,
|
||||
selectedPages: allPages
|
||||
});
|
||||
|
||||
return pdf;
|
||||
} catch (err) {
|
||||
console.error('PDF load error:', err);
|
||||
entry.totalPages = 0;
|
||||
updateFile(entry.id, { totalPages: 0 });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPageToImage(pdfDoc, pageNum, scale = 1.0) {
|
||||
async function renderPageToImage(pdfDoc, pageNum, scale = 1.0) {
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
const outputScale = window.devicePixelRatio || 1;
|
||||
|
||||
canvas.width = Math.floor(viewport.width * outputScale);
|
||||
canvas.height = Math.floor(viewport.height * outputScale);
|
||||
canvas.style.width = `${Math.floor(viewport.width)}px`;
|
||||
canvas.style.height = `${Math.floor(viewport.height)}px`;
|
||||
|
||||
const transform = outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : undefined;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport,
|
||||
transform
|
||||
}).promise;
|
||||
|
||||
return canvas.toDataURL('image/jpeg', 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Логика Галереи ──
|
||||
async function openPageGallery(entry) {
|
||||
// ── Логика Галереи ──
|
||||
async function openPageGallery(entry) {
|
||||
activeFile = entry;
|
||||
viewMode = 'gallery';
|
||||
galleryThumbnails = [];
|
||||
galleryLoading = true;
|
||||
previewPagesCache = {};
|
||||
|
||||
await tick();
|
||||
|
||||
// Используем уже загруженный документ или загружаем при необходимости
|
||||
if (!entry.pdfDoc) await loadPdfDocument(entry);
|
||||
let pdfDoc = entry.pdfDoc;
|
||||
if (!pdfDoc) {
|
||||
pdfDoc = await loadPdfDocument(entry);
|
||||
}
|
||||
|
||||
if (!entry.pdfDoc || entry.totalPages === 0) {
|
||||
if (!pdfDoc || entry.totalPages === 0) {
|
||||
galleryLoading = false;
|
||||
return;
|
||||
}
|
||||
@ -107,7 +120,7 @@ async function openPageGallery(entry) {
|
||||
const thumbs = [];
|
||||
for (let i = 1; i <= entry.totalPages; i++) {
|
||||
try {
|
||||
const url = await renderPageToImage(entry.pdfDoc, i, 0.4);
|
||||
const url = await renderPageToImage(pdfDoc, i, 0.4);
|
||||
thumbs.push(url);
|
||||
galleryThumbnails = [...thumbs];
|
||||
} catch (err) {
|
||||
@ -116,31 +129,30 @@ async function openPageGallery(entry) {
|
||||
}
|
||||
}
|
||||
galleryLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Логика Превью ──
|
||||
async function openPreview(entry) {
|
||||
// ── Логика Превью ──
|
||||
async function openPreview(entry) {
|
||||
activeFile = entry;
|
||||
viewMode = 'preview';
|
||||
previewPagesCache = {};
|
||||
galleryThumbnails = [];
|
||||
|
||||
await tick();
|
||||
|
||||
if (entry.fileType === 'pdf') {
|
||||
// Документ должен быть уже загружен при добавлении файла,
|
||||
// но на всякий случай проверяем
|
||||
if (!entry.pdfDoc) await loadPdfDocument(entry);
|
||||
let pdfDoc = entry.pdfDoc;
|
||||
if (!pdfDoc) pdfDoc = await loadPdfDocument(entry);
|
||||
|
||||
// Берем totalPages из entry, которое было получено при добавлении файла
|
||||
if (entry.totalPages > 0) {
|
||||
preloadPreviewPages(entry, 1, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function preloadPreviewPages(entry, startPage, count, direction = 'forward') {
|
||||
if (!entry.pdfDoc || isPreviewRendering) return;
|
||||
async function preloadPreviewPages(entry, startPage, count, direction = 'forward') {
|
||||
const currentFile = files.find(f => f.id === entry.id);
|
||||
const pdfDoc = currentFile ? currentFile.pdfDoc : entry.pdfDoc;
|
||||
|
||||
if (!pdfDoc || isPreviewRendering) return;
|
||||
isPreviewRendering = true;
|
||||
|
||||
let pagesToLoad = [];
|
||||
@ -160,7 +172,7 @@ async function preloadPreviewPages(entry, startPage, count, direction = 'forward
|
||||
const newCacheEntries = {};
|
||||
for (const pageNum of pagesToLoad) {
|
||||
try {
|
||||
const url = await renderPageToImage(entry.pdfDoc, pageNum, 1.5);
|
||||
const url = await renderPageToImage(pdfDoc, pageNum, 1.5);
|
||||
newCacheEntries[pageNum] = url;
|
||||
} catch (e) {
|
||||
console.error(`Failed to render preview page ${pageNum}`, e);
|
||||
@ -171,21 +183,21 @@ async function preloadPreviewPages(entry, startPage, count, direction = 'forward
|
||||
}
|
||||
}
|
||||
isPreviewRendering = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePreviewRequestPages(start, count, direction) {
|
||||
function handlePreviewRequestPages(start, count, direction) {
|
||||
if (activeFile) preloadPreviewPages(activeFile, start, count, direction);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Общие функции закрытия ──
|
||||
function closeOverlay() {
|
||||
// ── Общие функции закрытия ──
|
||||
function closeOverlay() {
|
||||
activeFile = null;
|
||||
galleryThumbnails = [];
|
||||
previewPagesCache = {};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Управление выбором страниц ──
|
||||
function togglePageSelection(pageNum) {
|
||||
// ── Управление выбором страниц ──
|
||||
function togglePageSelection(pageNum) {
|
||||
if (!activeFile?.selectedPages) return;
|
||||
const newSet = new Set(activeFile.selectedPages);
|
||||
if (newSet.has(pageNum)) {
|
||||
@ -193,42 +205,44 @@ function togglePageSelection(pageNum) {
|
||||
} else {
|
||||
newSet.add(pageNum);
|
||||
}
|
||||
activeFile.selectedPages = newSet;
|
||||
}
|
||||
updateFile(activeFile.id, { selectedPages: newSet });
|
||||
}
|
||||
|
||||
function selectAllPages() {
|
||||
function selectAllPages() {
|
||||
if (!activeFile?.totalPages) return;
|
||||
activeFile.selectedPages = new Set(Array.from({ length: activeFile.totalPages }, (_, i) => i + 1));
|
||||
}
|
||||
const allPages = new Set(Array.from({ length: activeFile.totalPages }, (_, i) => i + 1));
|
||||
updateFile(activeFile.id, { selectedPages: allPages });
|
||||
}
|
||||
|
||||
function deselectAllPages() {
|
||||
function deselectAllPages() {
|
||||
if (!activeFile) return;
|
||||
activeFile.selectedPages = new Set([1]);
|
||||
}
|
||||
updateFile(activeFile.id, { selectedPages: new Set([1]) });
|
||||
}
|
||||
|
||||
function pagesLabel(entry) {
|
||||
if (!entry.totalPages || entry.totalPages === 0) return 'Все';
|
||||
if (!entry.selectedPages || entry.selectedPages.size === 0) return 'Все';
|
||||
function pagesLabel(entry) {
|
||||
if (!entry.totalPages || entry.totalPages === 0) return '...';
|
||||
if (!entry.selectedPages || entry.selectedPages.size === 0) return 'Нет';
|
||||
if (entry.selectedPages.size === entry.totalPages) return 'Все';
|
||||
if (entry.selectedPages.size === 1) return `стр. ${[...entry.selectedPages][0]}`;
|
||||
return `${entry.selectedPages.size} из ${entry.totalPages}`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Файловые операции ──
|
||||
let fileInput;
|
||||
// ── Файловые операции ──
|
||||
let fileInput;
|
||||
|
||||
function detectFileType(file) {
|
||||
function detectFileType(file) {
|
||||
if (!file) return 'other';
|
||||
if (file.type.startsWith('image/')) return 'image';
|
||||
if (file.type === 'application/pdf') return 'pdf';
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileSelect(event) {
|
||||
async function handleFileSelect(event) {
|
||||
const input = /** @type {HTMLInputElement} */(event.target);
|
||||
const chosen = input.files;
|
||||
if (!chosen || chosen.length === 0) return;
|
||||
|
||||
const newFiles = [];
|
||||
for (let i = 0; i < chosen.length; i++) {
|
||||
const fType = detectFileType(chosen[i]);
|
||||
const entry = {
|
||||
@ -246,58 +260,113 @@ async function handleFileSelect(event) {
|
||||
selectedPages: fType === 'image' ? new Set([1]) : new Set(),
|
||||
pdfDoc: null,
|
||||
};
|
||||
newFiles.push(entry);
|
||||
}
|
||||
|
||||
files.push(entry);
|
||||
files = [...files, ...newFiles];
|
||||
|
||||
// ВАЖНО: Загружаем PDF сразу при добавлении, чтобы сохранить totalPages
|
||||
if (fType === 'pdf') {
|
||||
for (const entry of newFiles) {
|
||||
if (entry.fileType === 'pdf') {
|
||||
loadPdfDocument(entry);
|
||||
}
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function triggerFileInput(event) {
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function triggerFileInput(event) {
|
||||
event.preventDefault();
|
||||
fileInput.click();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExpanded(file) { file.expanded = !file.expanded; }
|
||||
function toggleExpanded(file) {
|
||||
updateFile(file.id, { expanded: !file.expanded });
|
||||
}
|
||||
|
||||
function removeFile(file) {
|
||||
function removeFile(file) {
|
||||
if (file.previewUrl) URL.revokeObjectURL(file.previewUrl);
|
||||
files = files.filter((f) => f.id !== file.id);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Опции печати ──
|
||||
function toggleFormat(file, f) { file.format = f; }
|
||||
function toggleColor(file, c) { file.colorMode = c.id; }
|
||||
function incrementCopies(file) { file.copies += 1; }
|
||||
function decrementCopies(file) { if (file.copies > 1) file.copies -= 1; }
|
||||
function qualityFillFor(file) { return Math.round((file.qualityIndex / (qualities.length - 1)) * 100); }
|
||||
function pageStubFor() { return 0; }
|
||||
// ── Опции печати ──
|
||||
function toggleFormat(file, f) { updateFile(file.id, { format: f }); }
|
||||
function toggleColor(file, c) { updateFile(file.id, { colorMode: c.id }); }
|
||||
function incrementCopies(file) { updateFile(file.id, { copies: file.copies + 1 }); }
|
||||
function decrementCopies(file) { if (file.copies > 1) updateFile(file.id, { copies: file.copies - 1 }); }
|
||||
function qualityFillFor(file) { return Math.round((file.qualityIndex / (qualities.length - 1)) * 100); }
|
||||
function updateQuality(file, val) { updateFile(file.id, { qualityIndex: val }); }
|
||||
|
||||
const totalPrice = $derived(`${files.reduce((sum, f) => sum + pageStubFor(f), 0)}₽`);
|
||||
const extraCopies = $derived(files.reduce((sum, f) => sum + (f.copies - 1), 0));
|
||||
// ── Расчет цены ──
|
||||
const totalPrice = $derived(() => {
|
||||
let sum = 0;
|
||||
for (const f of files) {
|
||||
const pagesCount = f.selectedPages ? f.selectedPages.size : 0;
|
||||
sum += pagesCount * f.copies;
|
||||
}
|
||||
return sum * PRICE_PER_PAGE;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
const extraCopies = $derived(files.reduce((sum, f) => sum + (f.copies - 1), 0));
|
||||
|
||||
onDestroy(() => {
|
||||
files.forEach((f) => { if (f.previewUrl) URL.revokeObjectURL(f.previewUrl); });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Плюрализация ──
|
||||
function pluralize(n, forms) {
|
||||
// ── Плюрализация ──
|
||||
function pluralize(n, forms) {
|
||||
const mod10 = n % 10, mod100 = n % 100;
|
||||
if (mod10 === 1 && mod100 !== 11) return forms[0];
|
||||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 > 14)) return forms[1];
|
||||
return forms[2];
|
||||
}
|
||||
const plFiles = (n) => pluralize(n, ['файл', 'файла', 'файлов']);
|
||||
const plCopies = (n) => pluralize(n, ['копия', 'копии', 'копий']);
|
||||
}
|
||||
const plFiles = (n) => pluralize(n, ['файл', 'файла', 'файлов']);
|
||||
const plCopies = (n) => pluralize(n, ['копия', 'копии', 'копий']);
|
||||
|
||||
function handlePagesClick(file) {
|
||||
function handlePagesClick(file) {
|
||||
if (file.fileType === 'pdf') openPageGallery(file);
|
||||
else openPreview(file);
|
||||
}
|
||||
}
|
||||
|
||||
// ── НОВАЯ ФУНКЦИЯ: Отправка на сервер ──
|
||||
async function submitPrint() {
|
||||
if (files.length === 0 || isPrinting) return;
|
||||
|
||||
isPrinting = true;
|
||||
const formData = new FormData();
|
||||
|
||||
for (const f of files) {
|
||||
formData.append('files', f.file, f.file.name);
|
||||
|
||||
const settings = {
|
||||
filename: f.file.name,
|
||||
pages: f.selectedPages ? Array.from(f.selectedPages).sort((a, b) => a - b).join(',') : 'all',
|
||||
copies: f.copies,
|
||||
colorMode: f.colorMode,
|
||||
format: f.format
|
||||
};
|
||||
formData.append('settings', JSON.stringify(settings));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/print', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
if (response.ok) {
|
||||
alert(`✅ Успешно отправлено в печать!\n${text}`);
|
||||
} else {
|
||||
alert(`❌ Ошибка печати (${response.status}):\n${text}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Print submission error:', error);
|
||||
alert(`⚠️ Не удалось связаться с сервером печати.`);
|
||||
} finally {
|
||||
isPrinting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !activeFile}
|
||||
@ -366,7 +435,16 @@ function handlePagesClick(file) {
|
||||
|
||||
<div class="option-sub quality-sub">
|
||||
<label class="option-title">Качество – {qualities[file.qualityIndex].label}</label>
|
||||
<input type="range" min={0} max={qualities.length - 1} step={1} bind:value={file.qualityIndex} class="quality-slider" style="--fill: {qualityFillFor(file)}%" />
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={qualities.length - 1}
|
||||
step={1}
|
||||
value={file.qualityIndex}
|
||||
oninput={(e) => updateQuality(file, parseInt(e.target.value))}
|
||||
class="quality-slider"
|
||||
style="--fill: {qualityFillFor(file)}%"
|
||||
/>
|
||||
<div class="quality-marks-inline">
|
||||
{#each qualities as q}<span class="quality-mark">{q.label}</span>{/each}
|
||||
</div>
|
||||
@ -375,7 +453,6 @@ function handlePagesClick(file) {
|
||||
{/if}
|
||||
</section>
|
||||
{/each}
|
||||
|
||||
<button type="button" class="add-file-btn" onclick={triggerFileInput}>+ Добавить файлы</button>
|
||||
{/if}
|
||||
</main>
|
||||
@ -389,9 +466,17 @@ function handlePagesClick(file) {
|
||||
{/if}
|
||||
<div class="price-summary">
|
||||
<span class="summary-itogo">Итого</span>
|
||||
<span class="summary-price">{totalPrice}</span>
|
||||
<span class="summary-price">{totalPrice()}₽</span>
|
||||
</div>
|
||||
<button type="button" class="action-btn secondary" onclick={onPrint}>Печать</button>
|
||||
<!-- ИЗМЕНЕННАЯ КНОПКА -->
|
||||
<button
|
||||
type="button"
|
||||
class="action-btn secondary"
|
||||
onclick={submitPrint}
|
||||
disabled={isPrinting || files.length === 0}
|
||||
>
|
||||
{isPrinting ? 'Отправка...' : 'Печать'}
|
||||
</button>
|
||||
<input type="file" bind:this={fileInput} accept="image/*,.pdf" multiple onchange={handleFileSelect} style="display:none" />
|
||||
</footer>
|
||||
</div>
|
||||
@ -466,13 +551,13 @@ function handlePagesClick(file) {
|
||||
.add-file-btn { align-self: center; background: none; border: 1px dashed rgba(255, 255, 255, 0.18); color: #a9b0c0; font-size: 14px; font-weight: 600; padding: 12px 18px; border-radius: 14px; cursor: pointer; margin-top: 8px; }
|
||||
.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; }
|
||||
.action-btn { width: 100%; padding: 16px 24px; border-radius: 16px; font-size: 16px; font-weight: 700; cursor: pointer; border: none; color: #fff; }
|
||||
.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); }
|
||||
.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; }
|
||||
|
||||
.overlay-backdrop {
|
||||
position: fixed; inset: 0; z-index: 1000;
|
||||
background: rgba(10, 12, 18, 0.92); backdrop-filter: blur(12px);
|
||||
|
||||
@ -24,4 +24,13 @@ export default defineConfig({
|
||||
}
|
||||
})
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user