[794-8][@ant] feat: verifier-spa отчёт, печать, i18n и дизайн-токены бордо — презентация вердикта в фирменном минималистичном дизайн-языке кооперативной экономики (плоскости, ведущий цвет бордо только chrome)
CI / build (push) Failing after 7s
CI / build (push) Failing after 7s
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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": "Печать отчёта"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user