[794-8][@ant] feat: verifier-spa отчёт, печать, i18n и дизайн-токены бордо — презентация вердикта в фирменном минималистичном дизайн-языке кооперативной экономики (плоскости, ведущий цвет бордо только chrome)
CI / build (push) Failing after 7s

This commit is contained in:
coopops
2026-06-03 07:31:25 +00:00
parent 4146d1f0dc
commit 98453ccda3
8 changed files with 278 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import UploadDropzone from './components/UploadDropzone.vue';
import DocumentReport from './components/DocumentReport.vue';
import SyncIndicator from './components/SyncIndicator.vue';
import { verifyDocument, type DocumentVerification } from './lib/verify.js';
import type { ParseResult } from './lib/parse.js';
const { t } = useI18n();
const results = ref<DocumentVerification[]>([]);
const error = ref('');
const busy = ref(false);
async function onParsed(parsed: ParseResult) {
error.value = '';
busy.value = true;
try {
results.value = await Promise.all(parsed.documents.map(verifyDocument));
} finally {
busy.value = false;
}
}
function reset() {
results.value = [];
error.value = '';
}
function printReport() {
window.print();
}
</script>
<template>
<div class="mx-auto max-w-3xl px-6 py-10">
<header class="no-print mb-8 flex items-end justify-between">
<div>
<h1 class="text-xl font-semibold" style="color: var(--color-brand-9)">
{{ t('app.title') }}
</h1>
<p class="text-sm" style="color: var(--color-mauve-11)">{{ t('app.subtitle') }}</p>
</div>
<SyncIndicator />
</header>
<main class="flex flex-col gap-6">
<UploadDropzone v-if="results.length === 0" @parsed="onParsed" @error="error = $event" />
<p v-if="error" class="text-sm" style="color: var(--color-verdict-invalid)">{{ error }}</p>
<p v-if="busy" class="text-sm" style="color: var(--color-mauve-11)">Проверка</p>
<template v-if="results.length">
<DocumentReport v-for="(r, i) in results" :key="i" :result="r" />
<div class="no-print flex gap-3">
<button
class="rounded-lg px-4 py-2 text-sm font-medium text-white"
style="background: var(--color-brand-9)"
@click="reset"
>
Новая проверка
</button>
<button
class="rounded-lg border px-4 py-2 text-sm font-medium"
style="border-color: var(--color-mauve-6); color: var(--color-mauve-12)"
@click="printReport"
>
{{ t('print') }}
</button>
</div>
</template>
</main>
</div>
</template>
+25
View File
@@ -0,0 +1,25 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import type { ExtendedCheck } from '../lib/verify.js';
const props = defineProps<{ check: ExtendedCheck }>();
const { t } = useI18n();
const icon = computed(() =>
props.check.status === 'pass' ? '✅' : props.check.status === 'fail' ? '❌' : '⚠',
);
const title = computed(() => t(`checks.${props.check.check}`));
</script>
<template>
<div class="flex items-start gap-3 py-2">
<span class="mt-0.5">{{ icon }}</span>
<div>
<div class="text-sm font-medium" style="color: var(--color-mauve-12)">
{{ check.check }} · {{ title }}
</div>
<div class="text-xs" style="color: var(--color-mauve-11)">{{ check.detail }}</div>
</div>
</div>
</template>
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import type { DocumentVerification } from '../lib/verify.js';
import Plane from './Plane.vue';
import VerdictBadge from './VerdictBadge.vue';
import CheckRow from './CheckRow.vue';
defineProps<{ result: DocumentVerification }>();
const { t } = useI18n();
</script>
<template>
<Plane>
<header class="mb-4 flex items-center justify-between gap-4">
<div>
<h3 class="text-base font-semibold" style="color: var(--color-mauve-12)">
{{ result.name }}
</h3>
<span
v-if="result.legacy"
class="text-xs"
style="color: var(--color-verdict-warn)"
>{{ t('legacy') }}</span
>
</div>
<VerdictBadge :verdict="result.verdict" />
</header>
<div class="divide-y" style="border-color: var(--color-mauve-6)">
<CheckRow v-for="(c, i) in result.checks" :key="i" :check="c" />
</div>
</Plane>
</template>
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
/** Плоскость — базовая подложка с hairline-границей (не material-карточка). */
defineProps<{ padded?: boolean }>();
</script>
<template>
<section class="plane" :class="padded === false ? '' : 'p-6'">
<slot />
</section>
</template>
+30
View File
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import type { Verdict } from '../lib/verify.js';
const props = defineProps<{ verdict: Verdict }>();
const { t } = useI18n();
// Вердикт — ОТДЕЛЬНАЯ семантика, НЕ бордо.
const style = computed(() => {
switch (props.verdict) {
case 'valid':
return { icon: '✅', color: 'var(--color-verdict-valid)', label: t('verdict.valid') };
case 'invalid':
return { icon: '❌', color: 'var(--color-verdict-invalid)', label: t('verdict.invalid') };
case 'warn':
return { icon: '⚠', color: 'var(--color-verdict-warn)', label: t('verdict.warn') };
}
});
</script>
<template>
<div
class="inline-flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium"
:style="{ color: style.color, border: `1px solid ${style.color}` }"
>
<span>{{ style.icon }}</span>
<span>{{ style.label }}</span>
</div>
</template>
+9
View File
@@ -0,0 +1,9 @@
import { createI18n } from 'vue-i18n';
import ru from '../locales/ru.json';
export const i18n = createI18n({
legacy: false,
locale: 'ru',
fallbackLocale: 'ru',
messages: { ru },
});
+35
View File
@@ -0,0 +1,35 @@
{
"app": {
"title": "Верификатор документов",
"subtitle": "Кооперативная экономика",
"about": "О сервисе"
},
"upload": {
"drop": "Перетащите PDF + .sig или ZIP-архив",
"or": "или",
"choose": "Выбрать файлы",
"hint": "Файлы не покидают ваш браузер — проверка локальная"
},
"verdict": {
"valid": "Документ подлинный",
"invalid": "Документ не прошёл проверку",
"warn": "Проверка частична"
},
"checks": {
"C1": "Целостность файла",
"C2": "Хэш метаданных",
"C3": "Композитный хэш",
"C4": "Криптоподпись",
"C5": "Сертификат подписанта",
"C6": "Привязка ключа к аккаунту",
"C7": "Самоподписанность",
"C8": "Запись в цепи"
},
"legacy": "устаревшая канонизация",
"health": {
"synced": "индекс синхронизирован",
"lagging": "индекс отстаёт",
"offline": "индекс недоступен"
},
"print": "Печать отчёта"
}
+63
View File
@@ -0,0 +1,63 @@
@import 'tailwindcss';
/*
* Дизайн-токены D4.3a — минималистичный пространственный дизайн на плоскостях.
* ВЕДУЩИЙ цвет — бордо #9f1239 (бренд кооперативной экономики). Только chrome
* (хедер/нав/CTA/focus). Вердикт — ОТДЕЛЬНАЯ семантика, бордо НЕ сигнал вердикта.
* Нейтраль — Radix mauve (тёплый серый под красный hue). НЕ Material.
*/
@theme {
/* ВЕДУЩИЙ — бордо (chrome) */
--color-brand-9: #9f1239; /* бордо — ведущий акцент (CTA, активное, focus) */
--color-brand-10: #881337; /* бордо hover */
--color-brand-11: #fb7185; /* бордо-текст на тёмном (rose-400) */
--color-brand-3: #fff1f3; /* бордо-подложка (мягкий фон акцента) */
/* Нейтраль — Radix mauve */
--color-mauve-1: #fdfcfd;
--color-mauve-2: #f9f8f9;
--color-mauve-3: #f3f1f3;
--color-mauve-6: #dcd8de;
--color-mauve-8: #bcb3c4;
--color-mauve-11: #6f6779;
--color-mauve-12: #1c1a20;
/* Вердикт — ОТДЕЛЬНАЯ семантика (не бордо) */
--color-verdict-valid: #30a46c; /* ✅ зелёный (Radix grass-9) */
--color-verdict-invalid: #e5484d; /* ❌ яркий красный (Radix red-9) — ярче бордо, различим */
--color-verdict-warn: #ffa057; /* ⚠ amber */
/* Пространство / радиусы */
--radius-plane: 0.75rem;
--spacing-air: 2rem;
}
:root {
color-scheme: light;
}
body {
margin: 0;
background: var(--color-mauve-1);
color: var(--color-mauve-12);
font-family:
'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
}
/* Плоскость — подложка + hairline-граница, без material-elevation/ripple */
.plane {
background: var(--color-mauve-1);
border: 1px solid var(--color-mauve-6);
border-radius: var(--radius-plane);
}
/* Печать: чистый отчёт без chrome */
@media print {
.no-print {
display: none !important;
}
body {
background: #fff;
}
}