initial version
This commit is contained in:
3
Dockerfile
Normal file
3
Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
FROM nginx
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY ./interface.html /usr/share/nginx/html/index.html
|
||||||
4
compose.yml
Normal file
4
compose.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
build: .
|
||||||
|
image: git.danilkolesnikov.ru/danilko09/converter_nginx
|
||||||
924
interface.html
Normal file
924
interface.html
Normal file
@@ -0,0 +1,924 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Поиск товаров/услуг и конвертер ЕИ</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--blue-950: oklch(0.22 0.08 255);
|
||||||
|
--blue-900: oklch(0.28 0.10 255);
|
||||||
|
--blue-800: oklch(0.35 0.12 252);
|
||||||
|
--blue-700: oklch(0.42 0.14 250);
|
||||||
|
--blue-600: oklch(0.50 0.15 248);
|
||||||
|
--blue-500: oklch(0.58 0.14 248);
|
||||||
|
--blue-400: oklch(0.66 0.12 248);
|
||||||
|
--blue-300: oklch(0.75 0.08 250);
|
||||||
|
--blue-200: oklch(0.84 0.05 250);
|
||||||
|
--blue-100: oklch(0.92 0.025 250);
|
||||||
|
--blue-50: oklch(0.97 0.01 250);
|
||||||
|
|
||||||
|
--slate-900: oklch(0.25 0.02 255);
|
||||||
|
--slate-700: oklch(0.38 0.02 255);
|
||||||
|
--slate-600: oklch(0.48 0.02 255);
|
||||||
|
--slate-500: oklch(0.55 0.015 255);
|
||||||
|
--slate-400: oklch(0.65 0.01 255);
|
||||||
|
--slate-300: oklch(0.78 0.008 255);
|
||||||
|
--slate-200: oklch(0.88 0.005 255);
|
||||||
|
--slate-100: oklch(0.94 0.003 255);
|
||||||
|
--slate-50: oklch(0.98 0.002 255);
|
||||||
|
|
||||||
|
--green-600: oklch(0.55 0.16 155);
|
||||||
|
--green-100: oklch(0.93 0.04 155);
|
||||||
|
--amber-600: oklch(0.55 0.14 75);
|
||||||
|
--amber-100: oklch(0.93 0.04 75);
|
||||||
|
--red-600: oklch(0.55 0.18 25);
|
||||||
|
--red-100: oklch(0.93 0.05 25);
|
||||||
|
|
||||||
|
--surface: oklch(0.985 0.003 250);
|
||||||
|
--surface-raised: oklch(1 0 0);
|
||||||
|
--surface-sunken: oklch(0.96 0.006 250);
|
||||||
|
|
||||||
|
--text-primary: var(--slate-900);
|
||||||
|
--text-secondary: var(--slate-600);
|
||||||
|
--text-tertiary: var(--slate-500);
|
||||||
|
--text-on-accent: oklch(0.99 0.005 250);
|
||||||
|
|
||||||
|
--accent: var(--blue-600);
|
||||||
|
--accent-hover: var(--blue-700);
|
||||||
|
--accent-subtle: var(--blue-100);
|
||||||
|
--accent-border: var(--blue-200);
|
||||||
|
|
||||||
|
--border: var(--slate-200);
|
||||||
|
--border-focus: var(--blue-400);
|
||||||
|
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--radius-md: 10px;
|
||||||
|
--radius-lg: 14px;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px oklch(0.22 0.08 255 / 0.04);
|
||||||
|
--shadow-md: 0 2px 8px oklch(0.22 0.08 255 / 0.06), 0 1px 3px oklch(0.22 0.08 255 / 0.04);
|
||||||
|
--shadow-lg: 0 8px 32px oklch(0.22 0.08 255 / 0.08), 0 2px 8px oklch(0.22 0.08 255 / 0.04);
|
||||||
|
--shadow-focus: 0 0 0 3px oklch(0.58 0.14 248 / 0.18);
|
||||||
|
|
||||||
|
--font-body: 'Commissioner', system-ui, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--surface);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
body { min-height: 100vh; display: flex; flex-direction: column; }
|
||||||
|
|
||||||
|
/* ── Header ── */
|
||||||
|
.app-header {
|
||||||
|
background: var(--blue-950);
|
||||||
|
color: var(--text-on-accent);
|
||||||
|
padding: 0 clamp(1.25rem, 4vw, 3rem);
|
||||||
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.app-header svg { width: 22px; height: 22px; opacity: 0.7; flex-shrink: 0; }
|
||||||
|
.app-header h1 { font-size: 0.9375rem; font-weight: 500; letter-spacing: 0.01em; opacity: 0.92; }
|
||||||
|
|
||||||
|
/* ── Tabs ── */
|
||||||
|
.tabs-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
background: var(--surface-raised);
|
||||||
|
border-bottom: 1.5px solid var(--border);
|
||||||
|
padding: 0 clamp(1.25rem, 4vw, 3rem);
|
||||||
|
position: sticky;
|
||||||
|
top: 56px;
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 14px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: color 0.15s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tab-btn svg { width: 16px; height: 16px; }
|
||||||
|
.tab-btn:hover { color: var(--text-primary); }
|
||||||
|
.tab-btn.active { color: var(--accent); }
|
||||||
|
.tab-btn.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1.5px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main ── */
|
||||||
|
.app-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: clamp(1.5rem, 4vw, 2.5rem) clamp(1.25rem, 4vw, 3rem);
|
||||||
|
max-width: 1180px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-panel { display: none; }
|
||||||
|
.tab-panel.active { display: block; animation: fadeUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) both; }
|
||||||
|
|
||||||
|
@keyframes fadeUp {
|
||||||
|
from { opacity: 0; transform: translateY(8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Form Elements ── */
|
||||||
|
.section-label {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--blue-600);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background: var(--surface-raised);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: clamp(1.25rem, 3vw, 2rem);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.field-label .req { color: var(--blue-500); font-size: 0.75rem; }
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--surface-sunken);
|
||||||
|
border: 1.5px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 10px 14px;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input::placeholder, textarea::placeholder { color: var(--slate-400); font-weight: 300; }
|
||||||
|
input:hover, select:hover, textarea:hover { border-color: var(--slate-300); background: var(--surface-raised); }
|
||||||
|
input:focus, select:focus, textarea:focus { border-color: var(--border-focus); background: var(--surface-raised); box-shadow: var(--shadow-focus); }
|
||||||
|
|
||||||
|
select {
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5l5 5 5-5' stroke='%23788599' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 12px center;
|
||||||
|
padding-right: 36px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Buttons ── */
|
||||||
|
.btn {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.btn:focus-visible { box-shadow: var(--shadow-focus); }
|
||||||
|
.btn svg { width: 16px; height: 16px; flex-shrink: 0; }
|
||||||
|
|
||||||
|
.btn-primary { background: var(--accent); color: var(--text-on-accent); }
|
||||||
|
.btn-primary:hover { background: var(--accent-hover); }
|
||||||
|
.btn-primary:active { transform: scale(0.98); }
|
||||||
|
.btn-primary:disabled { background: var(--slate-300); color: var(--slate-500); cursor: not-allowed; transform: none; }
|
||||||
|
|
||||||
|
.btn-secondary { background: transparent; color: var(--accent); border: 1.5px solid var(--accent-border); }
|
||||||
|
.btn-secondary:hover { background: var(--accent-subtle); border-color: var(--blue-300); }
|
||||||
|
|
||||||
|
.btn-ghost { background: transparent; color: var(--text-secondary); padding: 8px 12px; }
|
||||||
|
.btn-ghost:hover { color: var(--accent); background: var(--accent-subtle); }
|
||||||
|
|
||||||
|
.btn-sm { font-size: 0.8125rem; padding: 7px 14px; }
|
||||||
|
|
||||||
|
/* ── Grid helpers ── */
|
||||||
|
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||||
|
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; }
|
||||||
|
.grid-search { display: grid; grid-template-columns: 1fr 180px; gap: 1rem; align-items: end; }
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.grid-2, .grid-3, .grid-search { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Advanced toggle ── */
|
||||||
|
.advanced-toggle {
|
||||||
|
font-size: 0.8125rem; font-weight: 500; color: var(--text-secondary);
|
||||||
|
background: none; border: none; cursor: pointer;
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
padding: 8px 0; font-family: var(--font-body); transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.advanced-toggle:hover { color: var(--accent); }
|
||||||
|
.advanced-toggle svg { width: 14px; height: 14px; transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||||
|
.advanced-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
|
||||||
|
|
||||||
|
.collapse-panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.35s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||||
|
.collapse-panel[data-open="true"] { grid-template-rows: 1fr; }
|
||||||
|
.collapse-inner { overflow: hidden; }
|
||||||
|
|
||||||
|
.advanced-fields {
|
||||||
|
padding-top: 1.25rem; margin-top: 1.25rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions { display: flex; align-items: center; gap: 12px; margin-top: 1.25rem; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
/* ── Filters bar ── */
|
||||||
|
.filters-bar {
|
||||||
|
background: var(--surface-raised);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: clamp(1rem, 2vw, 1.5rem);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.filters-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0.875rem; align-items: end; }
|
||||||
|
.filters-actions { display: flex; gap: 8px; align-items: end; padding-top: 6px; }
|
||||||
|
|
||||||
|
.date-chips { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
|
.chip {
|
||||||
|
font-family: var(--font-body); font-size: 0.75rem; font-weight: 500;
|
||||||
|
padding: 5px 12px; border-radius: 100px;
|
||||||
|
border: 1.5px solid var(--border); background: var(--surface-raised);
|
||||||
|
color: var(--text-secondary); cursor: pointer; transition: all 0.15s; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.chip:hover { border-color: var(--blue-300); color: var(--accent); }
|
||||||
|
.chip.active { background: var(--accent-subtle); border-color: var(--blue-300); color: var(--blue-700); }
|
||||||
|
|
||||||
|
/* ── Table ── */
|
||||||
|
.table-wrap {
|
||||||
|
background: var(--surface-raised);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.table-scroll { overflow-x: auto; }
|
||||||
|
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
||||||
|
thead { position: sticky; top: 0; z-index: 2; }
|
||||||
|
th {
|
||||||
|
background: var(--blue-50); font-size: 0.75rem; font-weight: 600;
|
||||||
|
letter-spacing: 0.03em; color: var(--text-secondary);
|
||||||
|
text-align: left; padding: 12px 16px;
|
||||||
|
border-bottom: 1.5px solid var(--blue-200); white-space: nowrap;
|
||||||
|
}
|
||||||
|
th:first-child, td:first-child { padding-left: 20px; }
|
||||||
|
td { padding: 12px 16px; border-bottom: 1px solid var(--slate-100); vertical-align: middle; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
tr:hover td { background: oklch(0.97 0.008 250); }
|
||||||
|
|
||||||
|
.cell-name { font-weight: 500; max-width: 360px; }
|
||||||
|
.cell-num { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--text-secondary); text-align: center; }
|
||||||
|
.cell-region { color: var(--text-secondary); }
|
||||||
|
.cell-unit { font-size: 0.8125rem; }
|
||||||
|
.cell-qty { font-family: var(--font-mono); font-size: 0.8125rem; text-align: right; }
|
||||||
|
.cell-date { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--text-secondary); white-space: nowrap; }
|
||||||
|
|
||||||
|
.table-footer {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 12px 20px; border-top: 1px solid var(--border);
|
||||||
|
font-size: 0.8125rem; color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Result card (converter) ── */
|
||||||
|
.result-card {
|
||||||
|
background: var(--surface-raised);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: clamp(1.5rem, 3vw, 2rem);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coeff-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coeff-value {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: clamp(2rem, 5vw, 3rem);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--blue-700);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coeff-label {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coeff-formula {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--surface-sunken);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Logs panel ── */
|
||||||
|
.logs-section { margin-top: 1.5rem; }
|
||||||
|
|
||||||
|
.logs-toggle {
|
||||||
|
font-family: var(--font-body); font-size: 0.8125rem; font-weight: 500;
|
||||||
|
color: var(--text-secondary); background: none; border: none;
|
||||||
|
cursor: pointer; display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 8px 0; transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.logs-toggle:hover { color: var(--accent); }
|
||||||
|
.logs-toggle svg { width: 14px; height: 14px; transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||||
|
.logs-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
|
||||||
|
|
||||||
|
.logs-badge {
|
||||||
|
font-size: 0.6875rem; font-weight: 600;
|
||||||
|
background: var(--slate-100); color: var(--text-tertiary);
|
||||||
|
padding: 1px 7px; border-radius: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-container {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
background: var(--slate-900);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid oklch(0.35 0.02 255);
|
||||||
|
}
|
||||||
|
.logs-header span {
|
||||||
|
font-size: 0.75rem; font-weight: 600;
|
||||||
|
letter-spacing: 0.04em; text-transform: uppercase;
|
||||||
|
color: var(--slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-body {
|
||||||
|
padding: 12px 16px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--slate-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-body::-webkit-scrollbar { width: 6px; }
|
||||||
|
.logs-body::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
.logs-body::-webkit-scrollbar-thumb { background: oklch(0.4 0.02 255); border-radius: 3px; }
|
||||||
|
|
||||||
|
.log-line { display: flex; gap: 10px; }
|
||||||
|
.log-time { color: var(--slate-500); flex-shrink: 0; }
|
||||||
|
.log-level { font-weight: 500; flex-shrink: 0; min-width: 44px; }
|
||||||
|
.log-level.info { color: var(--blue-400); }
|
||||||
|
.log-level.ok { color: var(--green-600); }
|
||||||
|
.log-level.warn { color: var(--amber-600); }
|
||||||
|
.log-level.err { color: var(--red-600); }
|
||||||
|
.log-msg { color: oklch(0.82 0.01 255); }
|
||||||
|
|
||||||
|
/* ── Empty state ── */
|
||||||
|
.empty-state { text-align: center; padding: 4rem 2rem; }
|
||||||
|
.empty-state svg { width: 48px; height: 48px; color: var(--slate-300); margin-bottom: 1rem; }
|
||||||
|
.empty-state p { color: var(--text-tertiary); font-size: 0.9375rem; max-width: 380px; margin: 0 auto; line-height: 1.6; }
|
||||||
|
.empty-state p strong { color: var(--text-secondary); font-weight: 500; }
|
||||||
|
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
/* ── Responsive ── */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filters-grid { grid-template-columns: 1fr 1fr; }
|
||||||
|
.form-actions { flex-direction: column; align-items: stretch; }
|
||||||
|
.form-actions .btn { width: 100%; }
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.filters-grid { grid-template-columns: 1fr; }
|
||||||
|
.tabs-nav { gap: 0; overflow-x: auto; }
|
||||||
|
.tab-btn { padding: 12px 14px; font-size: 0.8125rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ═══ Header ═══ -->
|
||||||
|
<header class="app-header">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
|
||||||
|
</svg>
|
||||||
|
<h1>Поиск товаров/услуг и конвертер ЕИ</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- ═══ Tabs ═══ -->
|
||||||
|
<nav class="tabs-nav">
|
||||||
|
<button class="tab-btn active" data-tab="search">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
|
||||||
|
</svg>
|
||||||
|
Поиск товаров/услуг
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-tab="convert">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/>
|
||||||
|
<polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>
|
||||||
|
</svg>
|
||||||
|
Конвертер ЕИ
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- ═══ Main ═══ -->
|
||||||
|
<main class="app-main">
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════ -->
|
||||||
|
<!-- TAB 1: ПОИСК ТОВАРОВ/УСЛУГ -->
|
||||||
|
<!-- ════════════════════════════════════ -->
|
||||||
|
<div class="tab-panel active" id="panel-search">
|
||||||
|
|
||||||
|
<div class="section-label">Параметры поиска</div>
|
||||||
|
<form class="form-card" id="searchForm" autocomplete="off">
|
||||||
|
<div class="grid-search">
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="s_product">Наименование товара/услуги <span class="req">*</span></label>
|
||||||
|
<input type="text" id="s_product" placeholder="Введите наименование товара или услуги" required>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="s_count">Кол-во записей <span class="req">*</span></label>
|
||||||
|
<input type="number" id="s_count" placeholder="10" min="1" max="500" value="20">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced -->
|
||||||
|
<div class="collapse-panel" id="s_advPanel" data-open="false">
|
||||||
|
<div class="collapse-inner">
|
||||||
|
<div class="advanced-fields">
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="s_region">Регион</label>
|
||||||
|
<select id="s_region">
|
||||||
|
<option value="">Все регионы</option>
|
||||||
|
<option>Москва</option><option>Санкт-Петербург</option>
|
||||||
|
<option>Новосибирская область</option><option>Свердловская область</option>
|
||||||
|
<option>Краснодарский край</option><option>Республика Татарстан</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="s_unit">Единица измерения</label>
|
||||||
|
<select id="s_unit">
|
||||||
|
<option value="">Любая</option>
|
||||||
|
<option>шт</option><option>кг</option><option>л</option>
|
||||||
|
<option>м</option><option>упак</option><option>компл</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="s_qty">Количество</label>
|
||||||
|
<input type="number" id="s_qty" placeholder="Например, 100" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" id="searchBtn" disabled>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
|
||||||
|
</svg>
|
||||||
|
Найти похожие товары/услуги
|
||||||
|
</button>
|
||||||
|
<button type="button" class="advanced-toggle" id="s_advToggle" aria-expanded="false">
|
||||||
|
<span>Дополнительные параметры</span>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="6 9 12 15 18 9"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Search results -->
|
||||||
|
<div id="s_empty" class="empty-state" style="margin-top:2rem">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M11 8v6"/><path d="M8 11h6"/>
|
||||||
|
</svg>
|
||||||
|
<p><strong>Введите наименование товара или услуги</strong> и нажмите «Найти» — система подберёт похожие позиции из реестра закупок</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden" id="s_results" style="margin-top:1.5rem">
|
||||||
|
<div style="display:flex;align-items:baseline;gap:12px;margin-bottom:1.25rem;flex-wrap:wrap">
|
||||||
|
<div class="section-label" style="margin-bottom:0">Результаты</div>
|
||||||
|
<span style="font-size:0.8125rem;color:var(--text-tertiary)">Найдено <strong id="s_totalCount" style="font-family:var(--font-mono);font-size:0.8rem;color:var(--text-primary);font-weight:600">0</strong> записей</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="filters-bar">
|
||||||
|
<div class="filters-grid">
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label">Дата закупки</label>
|
||||||
|
<div class="date-chips">
|
||||||
|
<button type="button" class="chip" data-days="7">Неделя</button>
|
||||||
|
<button type="button" class="chip" data-days="30">Месяц</button>
|
||||||
|
<button type="button" class="chip active" data-days="90">3 месяца</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="f_region">Регион</label>
|
||||||
|
<select id="f_region"><option value="">Все</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="f_unit">Ед. измерения</label>
|
||||||
|
<select id="f_unit"><option value="">Все</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="f_qtyMin">Кол-во от</label>
|
||||||
|
<input type="number" id="f_qtyMin" placeholder="мин">
|
||||||
|
</div>
|
||||||
|
<div class="filters-actions">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" id="applyFilters">Применить</button>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" id="resetFilters">Сбросить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="table-wrap">
|
||||||
|
<div class="table-scroll">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:52px">#</th>
|
||||||
|
<th>Наименование товара/услуги</th>
|
||||||
|
<th>Регион</th>
|
||||||
|
<th>Ед. изм.</th>
|
||||||
|
<th style="text-align:right">Кол-во</th>
|
||||||
|
<th>Дата закупки</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="s_tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="table-footer">
|
||||||
|
<span id="s_shownInfo">Показано 0 из 0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════ -->
|
||||||
|
<!-- TAB 2: КОНВЕРТЕР ЕИ -->
|
||||||
|
<!-- ════════════════════════════════════ -->
|
||||||
|
<div class="tab-panel" id="panel-convert">
|
||||||
|
|
||||||
|
<div class="section-label">Конвертер единиц измерения</div>
|
||||||
|
<form class="form-card" id="convertForm" autocomplete="off">
|
||||||
|
<div class="field" style="margin-bottom:1rem">
|
||||||
|
<label class="field-label" for="c_product">Наименование товара/услуги <span class="req">*</span></label>
|
||||||
|
<input type="text" id="c_product" placeholder="Введите наименование товара или услуги" required>
|
||||||
|
</div>
|
||||||
|
<div class="grid-3">
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="c_count">Кол-во записей</label>
|
||||||
|
<input type="number" id="c_count" placeholder="10" min="1" max="500" value="20">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="c_sourceUnit">Исходная ЕИ <span class="req">*</span></label>
|
||||||
|
<select id="c_sourceUnit" required>
|
||||||
|
<option value="">Выберите...</option>
|
||||||
|
<option>шт</option><option>кг</option><option>г</option>
|
||||||
|
<option>л</option><option>мл</option><option>м</option>
|
||||||
|
<option>см</option><option>упак</option><option>компл</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="c_targetUnit">Целевая ЕИ <span class="req">*</span></label>
|
||||||
|
<select id="c_targetUnit" required>
|
||||||
|
<option value="">Выберите...</option>
|
||||||
|
<option>шт</option><option>кг</option><option>г</option>
|
||||||
|
<option>л</option><option>мл</option><option>м</option>
|
||||||
|
<option>см</option><option>упак</option><option>компл</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" id="convertBtn" disabled>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/>
|
||||||
|
<polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>
|
||||||
|
</svg>
|
||||||
|
Конвертировать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Converter empty state -->
|
||||||
|
<div id="c_empty" class="empty-state" style="margin-top:2rem">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/>
|
||||||
|
<polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>
|
||||||
|
</svg>
|
||||||
|
<p><strong>Укажите товар/услугу и единицы измерения</strong> — система рассчитает коэффициент пересчёта на основе данных из реестра закупок</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Converter result -->
|
||||||
|
<div class="hidden" id="c_result">
|
||||||
|
|
||||||
|
<!-- Coefficient -->
|
||||||
|
<div class="result-card">
|
||||||
|
<div style="font-size:0.75rem;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--text-tertiary);margin-bottom:1rem">Коэффициент пересчёта</div>
|
||||||
|
<div class="coeff-display">
|
||||||
|
<span class="coeff-value" id="c_coeffValue">—</span>
|
||||||
|
<span class="coeff-label" id="c_coeffLabel"></span>
|
||||||
|
</div>
|
||||||
|
<div class="coeff-formula" id="c_formula"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logs -->
|
||||||
|
<div class="logs-section">
|
||||||
|
<button type="button" class="logs-toggle" id="logsToggle" aria-expanded="false">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||||
|
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z"/>
|
||||||
|
<path d="M9 17h6"/><path d="M9 13h6"/>
|
||||||
|
</svg>
|
||||||
|
<span>Логи бэкенда</span>
|
||||||
|
<span class="logs-badge" id="logsBadge">0</span>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="6 9 12 15 18 9"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse-panel" id="logsPanel" data-open="false">
|
||||||
|
<div class="collapse-inner">
|
||||||
|
<div class="logs-container">
|
||||||
|
<div class="logs-header">
|
||||||
|
<span>Backend logs</span>
|
||||||
|
<button class="btn btn-ghost btn-sm" style="color:var(--slate-400);font-size:0.75rem;padding:4px 8px" id="copyLogs">Копировать</button>
|
||||||
|
</div>
|
||||||
|
<div class="logs-body" id="logsBody"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ═══ Tabs ═══
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══ TAB 1: Search ═══
|
||||||
|
const DEMO_DATA = [
|
||||||
|
{ name: 'Бумага офисная А4, 80 г/м², 500 листов', region: 'Москва', unit: 'упак', qty: 500, date: '2026-03-15' },
|
||||||
|
{ name: 'Бумага писчая А4 «Снегурочка»', region: 'Санкт-Петербург', unit: 'упак', qty: 200, date: '2026-03-10' },
|
||||||
|
{ name: 'Бумага для принтера А4, класс B', region: 'Новосибирская область', unit: 'упак', qty: 1000, date: '2026-02-28' },
|
||||||
|
{ name: 'Бумага офисная А3, 80 г/м²', region: 'Краснодарский край', unit: 'упак', qty: 150, date: '2026-02-20' },
|
||||||
|
{ name: 'Бумага для копировальных аппаратов А4', region: 'Москва', unit: 'шт', qty: 3000, date: '2026-02-14' },
|
||||||
|
{ name: 'Бумага SvetoCopy А4 500 л.', region: 'Свердловская область', unit: 'упак', qty: 800, date: '2026-01-30' },
|
||||||
|
{ name: 'Бумага мелованная А4 для лазерной печати', region: 'Республика Татарстан', unit: 'упак', qty: 50, date: '2026-01-22' },
|
||||||
|
{ name: 'Бумага офисная А4 «Ballet Classic»', region: 'Москва', unit: 'упак', qty: 2400, date: '2026-01-15' },
|
||||||
|
{ name: 'Бумага цветная А4, 10 цветов', region: 'Санкт-Петербург', unit: 'упак', qty: 120, date: '2026-01-08' },
|
||||||
|
{ name: 'Бумага для заметок 76×76 мм, жёлтая', region: 'Краснодарский край', unit: 'шт', qty: 5000, date: '2025-12-29' },
|
||||||
|
{ name: 'Бумага А4 для факсимильных аппаратов', region: 'Новосибирская область', unit: 'упак', qty: 300, date: '2025-12-20' },
|
||||||
|
{ name: 'Бумага крафт упаковочная 84 см', region: 'Москва', unit: 'м', qty: 1500, date: '2025-12-15' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let allResults = [], filteredResults = [], activeDateFilter = 90;
|
||||||
|
|
||||||
|
const searchForm = document.getElementById('searchForm');
|
||||||
|
const searchBtn = document.getElementById('searchBtn');
|
||||||
|
const s_product = document.getElementById('s_product');
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
function validateSearch() { searchBtn.disabled = !s_product.value.trim(); }
|
||||||
|
s_product.addEventListener('input', validateSearch);
|
||||||
|
|
||||||
|
// Advanced toggle
|
||||||
|
document.getElementById('s_advToggle').addEventListener('click', function() {
|
||||||
|
const panel = document.getElementById('s_advPanel');
|
||||||
|
const open = panel.dataset.open === 'true';
|
||||||
|
panel.dataset.open = String(!open);
|
||||||
|
this.setAttribute('aria-expanded', String(!open));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit search
|
||||||
|
searchForm.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const count = parseInt(document.getElementById('s_count').value) || 20;
|
||||||
|
allResults = DEMO_DATA.slice(0, Math.min(count, DEMO_DATA.length));
|
||||||
|
|
||||||
|
const regions = [...new Set(allResults.map(r => r.region))];
|
||||||
|
const units = [...new Set(allResults.map(r => r.unit))];
|
||||||
|
document.getElementById('f_region').innerHTML = '<option value="">Все</option>' + regions.map(r => `<option>${r}</option>`).join('');
|
||||||
|
document.getElementById('f_unit').innerHTML = '<option value="">Все</option>' + units.map(u => `<option>${u}</option>`).join('');
|
||||||
|
|
||||||
|
applySearchFilters();
|
||||||
|
document.getElementById('s_empty').classList.add('hidden');
|
||||||
|
document.getElementById('s_results').classList.remove('hidden');
|
||||||
|
document.getElementById('s_results').style.animation = 'none';
|
||||||
|
requestAnimationFrame(() => { document.getElementById('s_results').style.animation = 'fadeUp 0.3s cubic-bezier(0.16,1,0.3,1) both'; });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Date chips
|
||||||
|
document.querySelectorAll('.chip[data-days]').forEach(chip => {
|
||||||
|
chip.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
|
||||||
|
chip.classList.add('active');
|
||||||
|
activeDateFilter = parseInt(chip.dataset.days);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function applySearchFilters() {
|
||||||
|
const rVal = document.getElementById('f_region').value;
|
||||||
|
const uVal = document.getElementById('f_unit').value;
|
||||||
|
const qMin = parseFloat(document.getElementById('f_qtyMin').value) || 0;
|
||||||
|
const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - activeDateFilter);
|
||||||
|
|
||||||
|
filteredResults = allResults.filter(r => {
|
||||||
|
if (rVal && r.region !== rVal) return false;
|
||||||
|
if (uVal && r.unit !== uVal) return false;
|
||||||
|
if (qMin && r.qty < qMin) return false;
|
||||||
|
if (new Date(r.date) < cutoff) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
renderSearchTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('applyFilters').addEventListener('click', applySearchFilters);
|
||||||
|
document.getElementById('resetFilters').addEventListener('click', () => {
|
||||||
|
document.getElementById('f_region').value = '';
|
||||||
|
document.getElementById('f_unit').value = '';
|
||||||
|
document.getElementById('f_qtyMin').value = '';
|
||||||
|
activeDateFilter = 90;
|
||||||
|
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
|
||||||
|
document.querySelector('.chip[data-days="90"]').classList.add('active');
|
||||||
|
applySearchFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderSearchTable() {
|
||||||
|
document.getElementById('s_totalCount').textContent = filteredResults.length;
|
||||||
|
document.getElementById('s_shownInfo').textContent = `Показано ${filteredResults.length} из ${allResults.length}`;
|
||||||
|
const tbody = document.getElementById('s_tbody');
|
||||||
|
if (!filteredResults.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:2.5rem;color:var(--text-tertiary)">Нет записей</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = filteredResults.map((r, i) => `
|
||||||
|
<tr>
|
||||||
|
<td class="cell-num">${i + 1}</td>
|
||||||
|
<td class="cell-name">${r.name}</td>
|
||||||
|
<td class="cell-region">${r.region}</td>
|
||||||
|
<td class="cell-unit">${r.unit}</td>
|
||||||
|
<td class="cell-qty">${r.qty.toLocaleString('ru-RU')}</td>
|
||||||
|
<td class="cell-date">${new Date(r.date).toLocaleDateString('ru-RU', {day:'2-digit',month:'2-digit',year:'numeric'})}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ TAB 2: Converter ═══
|
||||||
|
const convertForm = document.getElementById('convertForm');
|
||||||
|
const convertBtn = document.getElementById('convertBtn');
|
||||||
|
const c_product = document.getElementById('c_product');
|
||||||
|
const c_src = document.getElementById('c_sourceUnit');
|
||||||
|
const c_tgt = document.getElementById('c_targetUnit');
|
||||||
|
|
||||||
|
function validateConvert() {
|
||||||
|
convertBtn.disabled = !(c_product.value.trim() && c_src.value && c_tgt.value);
|
||||||
|
}
|
||||||
|
[c_product, c_src, c_tgt].forEach(el => el.addEventListener('input', validateConvert));
|
||||||
|
[c_src, c_tgt].forEach(el => el.addEventListener('change', validateConvert));
|
||||||
|
|
||||||
|
// Demo logs
|
||||||
|
function generateLogs(product, src, tgt) {
|
||||||
|
const now = new Date();
|
||||||
|
const t = (offset) => {
|
||||||
|
const d = new Date(now.getTime() - offset);
|
||||||
|
return d.toLocaleTimeString('ru-RU', {hour:'2-digit',minute:'2-digit',second:'2-digit'}) + '.' + String(d.getMilliseconds()).padStart(3,'0');
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
{ time: t(2400), level: 'info', msg: `Получен запрос на конвертацию: "${product}"` },
|
||||||
|
{ time: t(2100), level: 'info', msg: `Параметры: source_unit=${src}, target_unit=${tgt}` },
|
||||||
|
{ time: t(1800), level: 'info', msg: 'Отправка запроса в Elasticsearch...' },
|
||||||
|
{ time: t(1200), level: 'ok', msg: `Elasticsearch: найдено 847 записей за 0.124s` },
|
||||||
|
{ time: t(1000), level: 'info', msg: 'Фильтрация записей с совпадающими ЕИ...' },
|
||||||
|
{ time: t(800), level: 'info', msg: `Отобрано 312 записей с ЕИ "${src}" и 89 с ЕИ "${tgt}"` },
|
||||||
|
{ time: t(600), level: 'info', msg: 'Расчёт медианного коэффициента пересчёта...' },
|
||||||
|
{ time: t(400), level: 'warn', msg: '3 выброса исключены из расчёта (отклонение > 3σ)' },
|
||||||
|
{ time: t(200), level: 'ok', msg: `Коэффициент рассчитан: 1 ${src} = 0.45 ${tgt}` },
|
||||||
|
{ time: t(50), level: 'ok', msg: 'Запрос выполнен успешно (2.35s)' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
convertForm.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const product = c_product.value.trim();
|
||||||
|
const src = c_src.value;
|
||||||
|
const tgt = c_tgt.value;
|
||||||
|
|
||||||
|
// Show result
|
||||||
|
document.getElementById('c_empty').classList.add('hidden');
|
||||||
|
const resultEl = document.getElementById('c_result');
|
||||||
|
resultEl.classList.remove('hidden');
|
||||||
|
resultEl.style.animation = 'none';
|
||||||
|
requestAnimationFrame(() => { resultEl.style.animation = 'fadeUp 0.3s cubic-bezier(0.16,1,0.3,1) both'; });
|
||||||
|
|
||||||
|
// Demo coefficient
|
||||||
|
const coeff = 0.45;
|
||||||
|
document.getElementById('c_coeffValue').textContent = coeff;
|
||||||
|
document.getElementById('c_coeffLabel').innerHTML = `${src} → ${tgt}`;
|
||||||
|
document.getElementById('c_formula').textContent = `1 ${src} = ${coeff} ${tgt} • 1 ${tgt} = ${(1/coeff).toFixed(4)} ${src}`;
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
const logs = generateLogs(product, src, tgt);
|
||||||
|
document.getElementById('logsBadge').textContent = logs.length;
|
||||||
|
document.getElementById('logsBody').innerHTML = logs.map(l =>
|
||||||
|
`<div class="log-line"><span class="log-time">${l.time}</span><span class="log-level ${l.level}">${l.level.toUpperCase().padEnd(4)}</span><span class="log-msg">${l.msg}</span></div>`
|
||||||
|
).join('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logs toggle
|
||||||
|
document.getElementById('logsToggle').addEventListener('click', function() {
|
||||||
|
const panel = document.getElementById('logsPanel');
|
||||||
|
const open = panel.dataset.open === 'true';
|
||||||
|
panel.dataset.open = String(!open);
|
||||||
|
this.setAttribute('aria-expanded', String(!open));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy logs
|
||||||
|
document.getElementById('copyLogs').addEventListener('click', () => {
|
||||||
|
const text = [...document.querySelectorAll('#logsBody .log-line')].map(l => l.textContent).join('\n');
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
const btn = document.getElementById('copyLogs');
|
||||||
|
btn.textContent = 'Скопировано';
|
||||||
|
setTimeout(() => btn.textContent = 'Копировать', 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
nginx.conf
Normal file
17
nginx.conf
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /uc/ {
|
||||||
|
proxy_pass http://10.100.10.70:9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /app/ {
|
||||||
|
proxy_pass http://10.100.11.188:8901;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user