add regional converter
This commit is contained in:
794
www/interface.html
Normal file
794
www/interface.html
Normal file
@@ -0,0 +1,794 @@
|
||||
<!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="field">
|
||||
<label class="field-label" for="s_product">Наименование товара/услуги <span class="req">*</span></label>
|
||||
<input type="text" id="s_product" placeholder="Введите наименование товара или услуги" required>
|
||||
</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" 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="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>Наименование (raw)</th>
|
||||
<th>Норм. наименование</th>
|
||||
<th>Регион</th>
|
||||
<th>Ед. изм.</th>
|
||||
<th>ID контракта</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_sourceUnit">Исходная ЕИ <span class="req">*</span></label>
|
||||
<input type="text" id="c_sourceUnit" placeholder="Например: кг" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="c_targetUnit">Целевая ЕИ <span class="req">*</span></label>
|
||||
<input type="text" id="c_targetUnit" placeholder="Например: шт" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="c_attributes">Характеристики</label>
|
||||
<input type="text" id="c_attributes" placeholder="Например: фракция 0-40, объём 500мл">
|
||||
</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 (будет подключён когда появится API) -->
|
||||
<div class="hidden" id="c_result">
|
||||
<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>
|
||||
</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 ═══
|
||||
let allResults = [], filteredResults = [];
|
||||
|
||||
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', async (e) => {
|
||||
e.preventDefault();
|
||||
const queryText = s_product.value.trim();
|
||||
const topK = 20;
|
||||
|
||||
searchBtn.disabled = true;
|
||||
searchBtn.textContent = 'Поиск…';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/app/api/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query_text: queryText, top_k: topK, use_rerank: false, rerank_strategy: 'lexical' })
|
||||
});
|
||||
if (!resp.ok) throw new Error('Ошибка сервера: ' + resp.status);
|
||||
const data = await resp.json();
|
||||
allResults = data.results || [];
|
||||
} catch (err) {
|
||||
alert('Не удалось выполнить поиск: ' + err.message);
|
||||
searchBtn.disabled = false;
|
||||
searchBtn.innerHTML = '<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> Найти похожие товары/услуги';
|
||||
return;
|
||||
}
|
||||
|
||||
searchBtn.disabled = false;
|
||||
searchBtn.innerHTML = '<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> Найти похожие товары/услуги';
|
||||
|
||||
const regions = [...new Set(allResults.map(r => r.region).filter(Boolean))];
|
||||
const units = [...new Set(allResults.map(r => r.unit).filter(Boolean))];
|
||||
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'; });
|
||||
});
|
||||
|
||||
function applySearchFilters() {
|
||||
const rVal = document.getElementById('f_region').value;
|
||||
const uVal = document.getElementById('f_unit').value;
|
||||
|
||||
filteredResults = allResults.filter(r => {
|
||||
if (rVal && r.region !== rVal) return false;
|
||||
if (uVal && r.unit !== uVal) 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 = '';
|
||||
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.raw_name || ''}</td>
|
||||
<td class="cell-name">${r.norm_name || ''}</td>
|
||||
<td class="cell-region">${r.region || '—'}</td>
|
||||
<td class="cell-unit">${r.unit || ''}</td>
|
||||
<td class="cell-num">${r.contract_id || ''}</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.trim() && c_tgt.value.trim());
|
||||
}
|
||||
[c_product, c_src, c_tgt].forEach(el => el.addEventListener('input', validateConvert));
|
||||
|
||||
convertForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
// TODO: подключить API конвертера, когда будет готов endpoint
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user