[350-1][@ant] feat: вынести реестры ledger2 в cooptypes, slim-YAML §6 и деплой standards-site на /standards — один TS-источник правды для имён счетов/кошельков (standards-site + desktop/shared/lib/ledger2 станет re-export после мёржа в reports), spec манифеста приведена к тому что реально рендерит фронт (без ролей-enum, без scenario, без Doxygen-id), FocusBar показывает Проводки/Переводы отдельными карточками с tooltip-подсказками из @coopenomics/cooptypes, клик по × и ● даёт описание из YAML, автопан при навигации ←/→, мобильная заглушка <900px, scrollBehavior при переходе на related; публикация через mono publish-docs с base=/standards/ и ссылкой в mkdocs nav, чтобы председатели сразу попадали на визуальный паспорт процесса с docs.coopenomics.world
This commit is contained in:
@@ -84,6 +84,20 @@ jobs:
|
||||
mkdocs build
|
||||
working-directory: ./components/docs
|
||||
|
||||
# ── standards-site → /standards/ на docs.coopenomics.world ───────
|
||||
# Отдельный Vue-сайт с BPMN-графом кооперативных стандартов,
|
||||
# публикуется как поддиректория на том же домене. Vite собирает
|
||||
# с base='/standards/' (см. vite.config.ts), относительные пути
|
||||
# внутри dist/ и hash-router работают корректно.
|
||||
- name: Build standards-site
|
||||
run: pnpm run build
|
||||
working-directory: ./components/contracts/standards-site
|
||||
|
||||
- name: Copy standards-site into docs site/standards
|
||||
run: |
|
||||
mkdir -p ./components/docs/site/standards
|
||||
cp -r ./components/contracts/standards-site/dist/* ./components/docs/site/standards/
|
||||
|
||||
- name: Remove specific large file before publishing
|
||||
run: |
|
||||
# Удаляем конкретный большой файл sdk/typedoc.json
|
||||
|
||||
@@ -79,6 +79,7 @@ actions:
|
||||
# финальный `active` проставляется в таблице `accounts` в действии confirmreg.
|
||||
# Для BPMN-графа мы показываем единый жизненный цикл процесса.
|
||||
entity: "registrator::candidate → registrator::account"
|
||||
entity_human: "Кандидат → пайщик"
|
||||
entity_source: cpp/registrator/src/user/
|
||||
|
||||
states:
|
||||
@@ -237,62 +238,41 @@ documents:
|
||||
stored_in: "(authorization — в параметре действия, не хранится в candidates после удаления записи)"
|
||||
note: "TODO: сверить идентификатор шаблона с fabric/registry.md"
|
||||
|
||||
# ── Секция 6. Операции ledger2 ──────────────────────────────────────────────
|
||||
# ── Секция 6. Операции ──────────────────────────────────────────────────────
|
||||
# Процесс мульти-операционный: две записи ACTION_REGISTRY с process_type = reg.regist.
|
||||
# Обе срабатывают на закрывающем действии confirmreg.
|
||||
# Источник: ledger2/actions.hpp — записи #1 и #2.
|
||||
# Slim-формат: коды счетов и id кошельков → человеческие имена
|
||||
# подставляет фронтенд из `standards-site/src/data/registries.ts`
|
||||
# (зеркало реестра из mono-ai-1/components/desktop/src/shared/lib/ledger2/).
|
||||
operations:
|
||||
- ledger_code: reg.entrfee
|
||||
human_name: Вступительный взнос пайщика
|
||||
wallet_op: ISSUE
|
||||
wallet_from: null
|
||||
wallet_to:
|
||||
id: 3001
|
||||
name: ENTRANCE_FEES
|
||||
human: Вступительные взносы
|
||||
debit:
|
||||
account: 51
|
||||
name: BANK_ACCOUNT
|
||||
human: Расчётный счёт
|
||||
type: ACTIVE
|
||||
credit:
|
||||
account: 86
|
||||
name: TARGET_RECEIPTS
|
||||
human: Целевое финансирование
|
||||
type: PASSIVE
|
||||
wallet_to: 3001 # Вступительные взносы
|
||||
debit: 51 # Расчётный счёт
|
||||
credit: 86 # Целевое финансирование
|
||||
amount_ref: candidate.initial
|
||||
triggered_by: registrator::confirmreg
|
||||
description: >
|
||||
Первичный вход вступительного взноса на кошелёк ENTRANCE_FEES.
|
||||
Двойная запись: Дт 51 «Расчётный счёт» / Кт 86 «Целевое финансирование» —
|
||||
средства поступили на расчётный счёт и квалифицируются как целевые
|
||||
поступления в кооперативный фонд.
|
||||
Первичный вход вступительного взноса на кошелёк «Вступительные взносы».
|
||||
Двойная запись Дт 51 / Кт 86 — средства поступили на расчётный счёт и
|
||||
квалифицируются как целевые поступления в кооперативный фонд.
|
||||
|
||||
- ledger_code: reg.minshare
|
||||
human_name: Минимальный паевой взнос при регистрации
|
||||
wallet_op: ISSUE
|
||||
wallet_from: null
|
||||
wallet_to:
|
||||
id: 2002
|
||||
name: MIN_SHARE_FUND
|
||||
human: Минимальный паевой взнос
|
||||
debit:
|
||||
account: 51
|
||||
name: BANK_ACCOUNT
|
||||
human: Расчётный счёт
|
||||
type: ACTIVE
|
||||
credit:
|
||||
account: 80
|
||||
name: SHARE_FUND
|
||||
human: Паевой фонд (складочный капитал)
|
||||
type: PASSIVE
|
||||
wallet_to: 2002 # Минимальный паевой взнос
|
||||
debit: 51 # Расчётный счёт
|
||||
credit: 80 # Паевой фонд (складочный капитал)
|
||||
amount_ref: candidate.minimum
|
||||
triggered_by: registrator::confirmreg
|
||||
description: >
|
||||
Первичный вход минимального паевого взноса на кошелёк MIN_SHARE_FUND.
|
||||
Двойная запись: Дт 51 «Расчётный счёт» / Кт 80 «Паевой фонд» —
|
||||
средства поступили на расчётный счёт и формируют складочный капитал
|
||||
кооператива.
|
||||
Первичный вход минимального паевого взноса на кошелёк «Минимальный
|
||||
паевой взнос». Двойная запись Дт 51 / Кт 80 — средства поступили
|
||||
на расчётный счёт и формируют складочный капитал кооператива.
|
||||
|
||||
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
|
||||
related:
|
||||
|
||||
@@ -65,6 +65,7 @@ actions:
|
||||
# после completedpst. Узел в графе нужен, чтобы обозначить успешное завершение
|
||||
# и привязать к нему ledger2-операцию.
|
||||
entity: wallet::deposit
|
||||
entity_human: Заявка на взнос
|
||||
entity_source: cpp/wallet/src/deposit/
|
||||
|
||||
states:
|
||||
@@ -164,36 +165,26 @@ scenario:
|
||||
# самим платежом. Секция оставлена пустой намеренно.
|
||||
documents: []
|
||||
|
||||
# ── Секция 6. Операции ledger2 ──────────────────────────────────────────────
|
||||
# ── Секция 6. Операции ──────────────────────────────────────────────────────
|
||||
# Одноактовый процесс: в ACTION_REGISTRY одна запись с process_type=wall.deposit.
|
||||
# Источник: ledger2/actions.hpp — запись #3 (wall.depcpl).
|
||||
# Slim-формат: коды/id → имена подставляет фронтенд из registries.ts.
|
||||
operations:
|
||||
- ledger_code: wall.depcpl
|
||||
human_name: Внесение пайщиком паевого взноса
|
||||
wallet_op: ISSUE
|
||||
wallet_from: null
|
||||
wallet_to:
|
||||
id: 2001
|
||||
name: SHARE_FUND_PAY
|
||||
human: ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
|
||||
debit:
|
||||
account: 51
|
||||
name: BANK_ACCOUNT
|
||||
human: Расчётный счёт
|
||||
type: ACTIVE
|
||||
credit:
|
||||
account: 80
|
||||
name: SHARE_FUND
|
||||
human: Паевой фонд (складочный капитал)
|
||||
type: PASSIVE
|
||||
wallet_to: 2001 # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
|
||||
debit: 51 # Расчётный счёт
|
||||
credit: 80 # Паевой фонд (складочный капитал)
|
||||
amount_ref: deposit.quantity
|
||||
triggered_by: wallet::completedpst
|
||||
description: >
|
||||
Первичный вход средств на кошелёк SHARE_FUND_PAY пайщика.
|
||||
Двойная запись: Дт 51 «Расчётный счёт» / Кт 80 «Паевой фонд» —
|
||||
деньги пайщика поступили на расчётный счёт и увеличили складочный
|
||||
капитал кооператива. Средства доступны пайщику для дальнейших
|
||||
операций (инвестиции в программы, конвертации, возврат).
|
||||
Первичный вход средств на кошелёк пайщика «ЦПП Цифровой Кошелёк».
|
||||
Двойная запись Дт 51 / Кт 80 — деньги пайщика поступили на расчётный
|
||||
счёт и увеличили складочный капитал кооператива. Средства доступны
|
||||
пайщику для дальнейших операций (инвестиции в программы, конвертации,
|
||||
возврат).
|
||||
|
||||
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
|
||||
related:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@vue-flow/controls": "^1.1.3",
|
||||
"@vue-flow/core": "^1.48.2",
|
||||
"@vue-flow/minimap": "^1.5.4",
|
||||
"cooptypes": "workspace:*",
|
||||
"lucide-vue-next": "^0.460.0",
|
||||
"vue": "^3.5.31",
|
||||
"vue-router": "^4.6.4",
|
||||
|
||||
@@ -1,9 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeUnmount, ref } from 'vue';
|
||||
import Sidebar from '@/components/Sidebar.vue';
|
||||
|
||||
const MOBILE_BREAKPOINT = 900;
|
||||
|
||||
const isMobile = ref(false);
|
||||
|
||||
function updateIsMobile(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
isMobile.value = window.innerWidth < MOBILE_BREAKPOINT;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateIsMobile();
|
||||
window.addEventListener('resize', updateIsMobile);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateIsMobile);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<div v-if="isMobile" class="mobile-stub">
|
||||
<div class="mobile-stub__box">
|
||||
<h1>Только для десктопа</h1>
|
||||
<p>
|
||||
Реестр кооперативных стандартов рассчитан на широкие экраны —
|
||||
BPMN-граф процесса не помещается на мобильных устройствах.
|
||||
Откройте сайт с компьютера или планшета.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="app-shell">
|
||||
<aside class="app-sidebar">
|
||||
<Sidebar />
|
||||
</aside>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* Показывает одно из: статус / действие / документ / операцию.
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { Standard, Transition, Ledger2Operation, ContractAction, ProcessDocument } from '@/types/standard';
|
||||
import { getAccount, getWallet } from '@/data/registries';
|
||||
|
||||
const props = defineProps<{
|
||||
standard: Standard;
|
||||
@@ -16,10 +16,10 @@ const props = defineProps<{
|
||||
focusOperation: string | null;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const INITIAL_MARKER = '∅';
|
||||
const START_ID = '__start__';
|
||||
const END_ID = '__end__';
|
||||
const REJECTED_PREFIX = '__rejected__';
|
||||
|
||||
const ROLE_HUMAN: Record<string, string> = {
|
||||
contributor: 'Участник',
|
||||
@@ -95,19 +95,55 @@ const focusedState = computed(() => {
|
||||
});
|
||||
|
||||
const focusMode = computed<
|
||||
'action' | 'document' | 'operation' | 'state' | 'process-start' | 'process-end' | 'none'
|
||||
| 'action'
|
||||
| 'document'
|
||||
| 'operation'
|
||||
| 'state'
|
||||
| 'process-start'
|
||||
| 'process-end'
|
||||
| 'process-rejected'
|
||||
| 'none'
|
||||
>(() => {
|
||||
if (focusedTransition.value) return 'action';
|
||||
if (focusedDocument.value) return 'document';
|
||||
if (focusedOperation.value) return 'operation';
|
||||
if (props.focusStatus === START_ID) return 'process-start';
|
||||
if (props.focusStatus === END_ID) return 'process-end';
|
||||
if (rejectedInfo.value) return 'process-rejected';
|
||||
if (focusedState.value) return 'state';
|
||||
return 'none';
|
||||
});
|
||||
|
||||
const relatedCount = computed(() => (props.standard.related ?? []).length);
|
||||
|
||||
// ── Данные для «завершения отказом» ─────────────────────────────────────
|
||||
interface RejectedInfo {
|
||||
transition: Transition;
|
||||
action: ContractAction | undefined;
|
||||
virtualState: Standard['states'][number] | undefined;
|
||||
}
|
||||
function parseRejectedId(id: string): { from: string; actionShort: string } | null {
|
||||
if (!id.startsWith(REJECTED_PREFIX)) return null;
|
||||
const rest = id.slice(REJECTED_PREFIX.length);
|
||||
const idx = rest.indexOf('__');
|
||||
if (idx < 0) return null;
|
||||
return { from: rest.slice(0, idx), actionShort: rest.slice(idx + 2) };
|
||||
}
|
||||
const rejectedInfo = computed<RejectedInfo | null>(() => {
|
||||
const id = props.focusStatus;
|
||||
if (!id) return null;
|
||||
const parsed = parseRejectedId(id);
|
||||
if (!parsed) return null;
|
||||
const transition = props.standard.transitions.find((t) => {
|
||||
const from = t.from === INITIAL_MARKER ? 'start' : t.from;
|
||||
return from === parsed.from && actionShort(t.action) === parsed.actionShort;
|
||||
});
|
||||
if (!transition) return null;
|
||||
const action = props.standard.actions.find((a) => a.name === transition.action);
|
||||
const virtualState = props.standard.states.find((s) => s.name === transition.to);
|
||||
return { transition, action, virtualState };
|
||||
});
|
||||
|
||||
// ── Данные для «конца процесса» ─────────────────────────────────────────
|
||||
const terminalStates = computed(() => {
|
||||
const vset = virtualStates.value;
|
||||
@@ -121,18 +157,29 @@ const terminalStates = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
function goToState(name: string): void {
|
||||
if (virtualStates.value.has(name)) return;
|
||||
router.push({ query: { s: name } });
|
||||
function accountTitle(code: number | null | undefined): string {
|
||||
if (code == null) return 'Счёт не задан';
|
||||
const meta = getAccount(code);
|
||||
if (!meta) return `${code} (нет в реестре)`;
|
||||
const kind = meta.kind === 'active' ? 'актив' : meta.kind === 'passive' ? 'пассив' : 'актив/пассив';
|
||||
return `${code} · ${meta.name} · ${kind}`;
|
||||
}
|
||||
|
||||
function accLabel(acc: Ledger2Operation['debit'] | Ledger2Operation['credit']): string {
|
||||
return acc ? `${acc.account} ${acc.name}` : '—';
|
||||
function walletTitle(id: number | null | undefined, op: Ledger2Operation['wallet_op']): string {
|
||||
if (id == null) {
|
||||
// null допустим только для ISSUE (из ниоткуда) или CONSUME (в никуда)
|
||||
if (op === 'ISSUE') return 'Выпуск средств извне системы';
|
||||
return 'Кошелёк не задан';
|
||||
}
|
||||
const meta = getWallet(id);
|
||||
if (!meta) return `${id} (нет в реестре)`;
|
||||
return `${id} · ${meta.name}`;
|
||||
}
|
||||
|
||||
function walletLabel(w: Ledger2Operation['wallet_to']): string {
|
||||
return w ? `${w.name} (${w.id})` : '—';
|
||||
function walletDisplayId(id: number | null): string {
|
||||
return id == null ? '∅' : String(id);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -145,6 +192,7 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
|
||||
'focus-bar--op': focusMode === 'operation',
|
||||
'focus-bar--process':
|
||||
focusMode === 'process-start' || focusMode === 'process-end',
|
||||
'focus-bar--rejected': focusMode === 'process-rejected',
|
||||
}"
|
||||
>
|
||||
<!-- Начало процесса -->
|
||||
@@ -157,6 +205,32 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Завершение отказом -->
|
||||
<template v-else-if="focusMode === 'process-rejected' && rejectedInfo">
|
||||
<div class="focus-bar__col focus-bar__col--main">
|
||||
<div class="focus-bar__kicker">Завершение отказом</div>
|
||||
<div class="focus-bar__title-row">
|
||||
<span class="focus-bar__human">
|
||||
{{ rejectedInfo.action?.human ?? actionShort(rejectedInfo.transition.action) }}
|
||||
</span>
|
||||
<code class="focus-bar__code">{{ rejectedInfo.transition.action }}</code>
|
||||
<span class="focus-bar__actor">· {{ roleHuman(rejectedInfo.transition.actor) }}</span>
|
||||
</div>
|
||||
<p v-if="rejectedInfo.action?.purpose" class="focus-bar__desc">
|
||||
{{ rejectedInfo.action.purpose }}
|
||||
</p>
|
||||
<p v-if="rejectedInfo.virtualState" class="focus-bar__desc">
|
||||
<span class="focus-bar__kicker-mini">Результат:</span>
|
||||
виртуальный статус
|
||||
<code class="focus-bar__inline-code">{{ rejectedInfo.virtualState.name }}</code>
|
||||
<template v-if="rejectedInfo.virtualState.human">
|
||||
— <strong>{{ rejectedInfo.virtualState.human }}</strong>
|
||||
</template>.
|
||||
{{ rejectedInfo.virtualState.description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Конец процесса -->
|
||||
<template v-else-if="focusMode === 'process-end'">
|
||||
<div class="focus-bar__col focus-bar__col--main">
|
||||
@@ -185,26 +259,6 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
|
||||
<span class="focus-bar__actor">· {{ roleHuman(focusedTransition.actor) }}</span>
|
||||
</div>
|
||||
<p v-if="focusedAction?.purpose" class="focus-bar__desc">{{ focusedAction.purpose }}</p>
|
||||
</div>
|
||||
|
||||
<div class="focus-bar__col focus-bar__col--meta">
|
||||
<div class="focus-bar__peers">
|
||||
<button
|
||||
class="focus-bar__peer"
|
||||
:disabled="focusedTransition.from === INITIAL_MARKER"
|
||||
@click="goToState(focusedTransition.from === INITIAL_MARKER ? START_ID : focusedTransition.from)"
|
||||
>
|
||||
<code>{{ focusedTransition.from === INITIAL_MARKER ? 'Старт' : focusedTransition.from }}</code>
|
||||
</button>
|
||||
<span class="focus-bar__arrow">→</span>
|
||||
<button
|
||||
class="focus-bar__peer"
|
||||
:disabled="virtualStates.has(focusedTransition.to)"
|
||||
@click="goToState(focusedTransition.to)"
|
||||
>
|
||||
<code>{{ virtualStates.has(focusedTransition.to) ? 'Отклонено' : focusedTransition.to }}</code>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="focusedTransition.guards && focusedTransition.guards.length" class="focus-bar__guards">
|
||||
<span class="focus-bar__kicker-mini">Условия:</span>
|
||||
<span class="focus-bar__guard-list">
|
||||
@@ -214,13 +268,43 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
|
||||
</div>
|
||||
|
||||
<div v-if="opsForFocusedAction.length" class="focus-bar__col focus-bar__col--ops">
|
||||
<div class="focus-bar__kicker-mini">Операции ledger2</div>
|
||||
<div class="focus-bar__kicker-mini">Операция</div>
|
||||
<div class="focus-bar__ops">
|
||||
<span v-for="op in opsForFocusedAction" :key="op.ledger_code" class="focus-bar__op" :title="op.human_name">
|
||||
<code>{{ op.ledger_code }}</code>
|
||||
<span class="focus-bar__op-dr">Дт {{ op.debit ? op.debit.account : '—' }}</span>
|
||||
<span class="focus-bar__op-cr">Кт {{ op.credit ? op.credit.account : '—' }}</span>
|
||||
</span>
|
||||
<div v-for="op in opsForFocusedAction" :key="op.ledger_code" class="focus-bar__op-block">
|
||||
<div class="focus-bar__op-name">{{ op.human_name }}</div>
|
||||
|
||||
<div
|
||||
v-if="op.debit != null || op.credit != null"
|
||||
class="focus-bar__op-sub"
|
||||
>
|
||||
<div class="focus-bar__op-sub-label">Проводки</div>
|
||||
<div class="focus-bar__op-sub-body">
|
||||
<span class="tooltip" :data-tip="accountTitle(op.debit)">
|
||||
Дт <code>{{ op.debit ?? '—' }}</code>
|
||||
</span>
|
||||
<span class="focus-bar__op-sep">/</span>
|
||||
<span class="tooltip" :data-tip="accountTitle(op.credit)">
|
||||
Кт <code>{{ op.credit ?? '—' }}</code>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="op.wallet_op !== 'WALLET_ONLY' && (op.wallet_from != null || op.wallet_to != null)"
|
||||
class="focus-bar__op-sub"
|
||||
>
|
||||
<div class="focus-bar__op-sub-label">Переводы</div>
|
||||
<div class="focus-bar__op-sub-body">
|
||||
<span class="tooltip" :data-tip="walletTitle(op.wallet_from, op.wallet_op)">
|
||||
<code>{{ walletDisplayId(op.wallet_from) }}</code>
|
||||
</span>
|
||||
<span class="focus-bar__arrow">→</span>
|
||||
<span class="tooltip" :data-tip="walletTitle(op.wallet_to, op.wallet_op)">
|
||||
<code>{{ walletDisplayId(op.wallet_to) }}</code>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -244,29 +328,61 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
|
||||
<!-- Операция -->
|
||||
<template v-else-if="focusMode === 'operation' && focusedOperation">
|
||||
<div class="focus-bar__col focus-bar__col--main">
|
||||
<div class="focus-bar__kicker">Операция ledger2</div>
|
||||
<div class="focus-bar__kicker">Операция</div>
|
||||
<div class="focus-bar__title-row">
|
||||
<span class="focus-bar__human">{{ focusedOperation.human_name }}</span>
|
||||
<code class="focus-bar__code">{{ focusedOperation.ledger_code }}</code>
|
||||
<span class="focus-bar__kind-chip">{{ focusedOperation.wallet_op }}</span>
|
||||
</div>
|
||||
<p v-if="focusedOperation.description" class="focus-bar__desc">{{ focusedOperation.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="focus-bar__col focus-bar__col--meta">
|
||||
<dl class="focus-bar__opdata">
|
||||
<div><dt>Кошелёк</dt><dd>{{ walletLabel(focusedOperation.wallet_from) }} → {{ walletLabel(focusedOperation.wallet_to) }}</dd></div>
|
||||
<div><dt>Дт</dt><dd>{{ accLabel(focusedOperation.debit) }}</dd></div>
|
||||
<div><dt>Кт</dt><dd>{{ accLabel(focusedOperation.credit) }}</dd></div>
|
||||
<div><dt>Сумма</dt><dd><code>{{ focusedOperation.amount_ref }}</code></dd></div>
|
||||
</dl>
|
||||
<div
|
||||
v-if="focusedOperation.debit != null || focusedOperation.credit != null"
|
||||
class="focus-bar__op-sub"
|
||||
>
|
||||
<div class="focus-bar__op-sub-label">Проводки</div>
|
||||
<div class="focus-bar__op-sub-body">
|
||||
<span class="tooltip" :data-tip="accountTitle(focusedOperation.debit)">
|
||||
Дт <code>{{ focusedOperation.debit ?? '—' }}</code>
|
||||
</span>
|
||||
<span class="focus-bar__op-sep">/</span>
|
||||
<span class="tooltip" :data-tip="accountTitle(focusedOperation.credit)">
|
||||
Кт <code>{{ focusedOperation.credit ?? '—' }}</code>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="focusedOperation.wallet_op !== 'WALLET_ONLY'
|
||||
&& (focusedOperation.wallet_from != null || focusedOperation.wallet_to != null)"
|
||||
class="focus-bar__op-sub"
|
||||
>
|
||||
<div class="focus-bar__op-sub-label">Переводы</div>
|
||||
<div class="focus-bar__op-sub-body">
|
||||
<span class="tooltip" :data-tip="walletTitle(focusedOperation.wallet_from, focusedOperation.wallet_op)">
|
||||
<code>{{ walletDisplayId(focusedOperation.wallet_from) }}</code>
|
||||
</span>
|
||||
<span class="focus-bar__arrow">→</span>
|
||||
<span class="tooltip" :data-tip="walletTitle(focusedOperation.wallet_to, focusedOperation.wallet_op)">
|
||||
<code>{{ walletDisplayId(focusedOperation.wallet_to) }}</code>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="focus-bar__op-sub">
|
||||
<div class="focus-bar__op-sub-label">Сумма</div>
|
||||
<div class="focus-bar__op-sub-body"><code>{{ focusedOperation.amount_ref }}</code></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Статус -->
|
||||
<template v-else-if="focusMode === 'state' && focusedState">
|
||||
<div class="focus-bar__col focus-bar__col--main">
|
||||
<div class="focus-bar__kicker">Статус</div>
|
||||
<div class="focus-bar__kicker">
|
||||
Статус<template v-if="standard.entity_human"> · {{ standard.entity_human }}</template>
|
||||
</div>
|
||||
<div class="focus-bar__title-row">
|
||||
<code class="focus-bar__code focus-bar__code--state">{{ focusedState.label }}</code>
|
||||
<span v-if="focusedState.human" class="focus-bar__human">{{ focusedState.human }}</span>
|
||||
@@ -296,6 +412,9 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
|
||||
.focus-bar--process { background: var(--accent-soft); border-color: var(--accent-border); }
|
||||
.focus-bar--process .focus-bar__kicker { color: var(--accent); }
|
||||
.focus-bar--process .focus-bar__code { color: var(--accent); border-color: var(--accent-border); }
|
||||
.focus-bar--rejected { background: var(--reject-soft); border-color: var(--reject); }
|
||||
.focus-bar--rejected .focus-bar__kicker { color: var(--reject); }
|
||||
.focus-bar--rejected .focus-bar__code { color: var(--reject); border-color: var(--reject); }
|
||||
.focus-bar__desc--hint {
|
||||
color: var(--text-subtle);
|
||||
font-style: italic;
|
||||
@@ -373,35 +492,81 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
|
||||
display: inline-block; margin-left: 6px; color: var(--text-subtle); font-style: italic;
|
||||
}
|
||||
|
||||
.focus-bar__peers { display: inline-flex; align-items: center; gap: 8px; margin-top: 2px; }
|
||||
.focus-bar__peer {
|
||||
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
||||
padding: 3px 8px; font-family: var(--font-mono); font-size: 12px;
|
||||
color: var(--text); cursor: pointer;
|
||||
}
|
||||
.focus-bar__peer:not(:disabled):hover {
|
||||
background: var(--accent-soft); border-color: var(--accent-border);
|
||||
}
|
||||
.focus-bar__peer:disabled { cursor: default; color: var(--text-muted); }
|
||||
.focus-bar__arrow { color: var(--text-subtle); }
|
||||
|
||||
.focus-bar__guards { margin-top: 4px; display: flex; flex-direction: column; gap: 3px; font-size: 12px; color: var(--text); }
|
||||
.focus-bar__guard-list { display: flex; flex-direction: column; gap: 2px; }
|
||||
.focus-bar__guard::before { content: '· '; color: var(--text-subtle); }
|
||||
|
||||
.focus-bar__ops { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
|
||||
.focus-bar__op {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 3px 8px; background: var(--bg);
|
||||
border: 1px solid var(--edge-focus-border); border-radius: 4px;
|
||||
font-size: 11px; color: var(--text);
|
||||
.focus-bar__ops { display: flex; flex-direction: column; gap: 10px; margin-top: 2px; }
|
||||
.focus-bar__op-block {
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
}
|
||||
.focus-bar__op code {
|
||||
font-family: var(--font-mono); color: var(--edge-focus);
|
||||
font-weight: 600; background: transparent; border: none; padding: 0;
|
||||
.focus-bar__op-block + .focus-bar__op-block {
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--edge-focus-border);
|
||||
}
|
||||
.focus-bar__op-dr, .focus-bar__op-cr {
|
||||
font-family: var(--font-mono); color: var(--text-muted); font-size: 10.5px;
|
||||
.focus-bar__op-name {
|
||||
font-size: 13px; font-weight: 600; color: var(--text);
|
||||
line-height: 1.3;
|
||||
}
|
||||
.focus-bar__op-sub {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 6px 10px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--edge-focus-border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.focus-bar__op-sub-label {
|
||||
font-size: 10px; font-weight: 600; letter-spacing: 0.06em;
|
||||
text-transform: uppercase; color: var(--text-subtle);
|
||||
}
|
||||
.focus-bar__op-sub-body {
|
||||
display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||
font-size: 12px; color: var(--text);
|
||||
}
|
||||
.focus-bar__op-sub-body code {
|
||||
font-family: var(--font-mono); font-size: 11px;
|
||||
padding: 1px 6px; border-radius: 3px;
|
||||
background: var(--edge-focus-soft); border: 1px solid var(--edge-focus-border);
|
||||
color: var(--edge-focus); font-weight: 600;
|
||||
}
|
||||
.focus-bar__op-sep { color: var(--text-subtle); }
|
||||
|
||||
/* ── CSS-tooltip, надёжный (title часто блокируется) ─────────────────────── */
|
||||
.tooltip { position: relative; display: inline-flex; align-items: center; gap: 4px; cursor: help; }
|
||||
.tooltip[data-tip]:hover::after,
|
||||
.tooltip[data-tip]:focus-visible::after {
|
||||
content: attr(data-tip);
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 5px 9px;
|
||||
background: var(--text);
|
||||
color: var(--bg);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tooltip[data-tip]:hover::before,
|
||||
.tooltip[data-tip]:focus-visible::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: calc(100% + 1px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: var(--text);
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.focus-bar__opdata {
|
||||
|
||||
@@ -110,6 +110,54 @@ function onPaneReady(instance: VueFlowStore) {
|
||||
|
||||
watch(() => props.standard.process_type, doFit);
|
||||
|
||||
// ── Авто-пан: держим фокусный узел в видимой области ──────────────────
|
||||
function focusedNodeId(): string | null {
|
||||
if (props.focusAction && vfInstance.value) {
|
||||
const nodes = vfInstance.value.getNodes ?? [];
|
||||
const found = (nodes as Array<{ id: string; type?: string; data?: unknown }>).find(
|
||||
(n) =>
|
||||
n.type === NODE_TYPES.ACTION &&
|
||||
(n.data as { actionName?: string } | undefined)?.actionName === props.focusAction,
|
||||
);
|
||||
if (found) return found.id;
|
||||
}
|
||||
if (props.focusStatus) return props.focusStatus;
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureInView(nodeId: string | null): void {
|
||||
if (!nodeId || !vfInstance.value || !containerRef.value) return;
|
||||
const vf = vfInstance.value;
|
||||
const node = vf.findNode(nodeId);
|
||||
if (!node) return;
|
||||
const vp = vf.getViewport();
|
||||
const cw = containerRef.value.clientWidth;
|
||||
const ch = containerRef.value.clientHeight;
|
||||
const w = node.dimensions?.width ?? 120;
|
||||
const h = node.dimensions?.height ?? 80;
|
||||
const left = vp.x + node.position.x * vp.zoom;
|
||||
const top = vp.y + node.position.y * vp.zoom;
|
||||
const right = left + w * vp.zoom;
|
||||
const bottom = top + h * vp.zoom;
|
||||
const pad = 48;
|
||||
const outX = left < pad || right > cw - pad;
|
||||
const outY = top < pad || bottom > ch - pad;
|
||||
if (!outX && !outY) return;
|
||||
const cx = node.position.x + w / 2;
|
||||
const cy = node.position.y + h / 2;
|
||||
vf.setCenter(cx, cy, { zoom: vp.zoom, duration: 280 });
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.focusStatus, props.focusAction, props.focusDocument, props.focusOperation],
|
||||
() => {
|
||||
// Ждём обновления слоя VueFlow с новым isFocus, затем корректируем viewport.
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => ensureInView(focusedNodeId()));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
watch(containerRef, (el) => {
|
||||
resizeObserver?.disconnect();
|
||||
@@ -128,6 +176,7 @@ function onNodeClick({ node }: NodeMouseEvent) {
|
||||
case NODE_TYPES.START:
|
||||
case NODE_TYPES.STATE:
|
||||
case NODE_TYPES.END:
|
||||
case NODE_TYPES.REJECTED:
|
||||
router.push({ query: { s: node.id } });
|
||||
break;
|
||||
case NODE_TYPES.ACTION: {
|
||||
@@ -136,7 +185,6 @@ function onNodeClick({ node }: NodeMouseEvent) {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// rejected — не навигируем
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,575 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { Standard, Transition, Ledger2Operation } from '@/types/standard';
|
||||
|
||||
const props = defineProps<{
|
||||
standard: Standard;
|
||||
focusStatus: string | null;
|
||||
focusEdge: string | null;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const INITIAL_MARKER = '∅';
|
||||
const START_ID = '__start__';
|
||||
|
||||
function actionShort(name: string): string {
|
||||
return name.split('::').pop() ?? name;
|
||||
}
|
||||
|
||||
// Ищем human-имя действия в manifest.actions
|
||||
function actionHumanFor(name: string): string | undefined {
|
||||
return props.standard.actions.find((a) => a.name === name)?.human;
|
||||
}
|
||||
|
||||
function roleHuman(role: string): string {
|
||||
const map: Record<string, string> = {
|
||||
contributor: 'Участник',
|
||||
chairman: 'Председатель',
|
||||
soviet: 'Совет',
|
||||
soviet_members: 'Члены совета',
|
||||
meet: 'Общее собрание',
|
||||
gateway_operator: 'Оператор платежей',
|
||||
administrator: 'Администратор',
|
||||
chairman_or_soviet: 'Председатель / Совет',
|
||||
};
|
||||
return map[role] ?? role;
|
||||
}
|
||||
|
||||
const focus = computed(() => props.focusStatus);
|
||||
|
||||
// Описание статуса (или метка «Старт», если фокус — стартовая точка)
|
||||
const stateInfo = computed(() => {
|
||||
if (!focus.value || focus.value === '__start__') {
|
||||
return { label: 'Старт', description: 'Процесс ещё не начат — нет исходной записи сущности.' };
|
||||
}
|
||||
const s = props.standard.states.find((x) => x.name === focus.value);
|
||||
return s
|
||||
? { label: s.name, description: s.description, kind: s.kind ?? (s.virtual ? 'virtual' : 'normal') }
|
||||
: { label: focus.value, description: 'Статус не описан в манифесте.' };
|
||||
});
|
||||
|
||||
const virtualStates = computed(() =>
|
||||
new Set(props.standard.states.filter((s) => s.virtual || s.kind === 'virtual').map((s) => s.name)),
|
||||
);
|
||||
|
||||
const incoming = computed<Transition[]>(() => {
|
||||
if (!focus.value || focus.value === '__start__') return [];
|
||||
return props.standard.transitions.filter((t) => t.to === focus.value);
|
||||
});
|
||||
|
||||
const outgoing = computed<Transition[]>(() => {
|
||||
const f = focus.value ?? '__start__';
|
||||
const src = f === '__start__' ? INITIAL_MARKER : f;
|
||||
return props.standard.transitions.filter((t) => t.from === src);
|
||||
});
|
||||
|
||||
// Поиск действия по имени (для описания)
|
||||
function getActionPurpose(actionName: string): string | undefined {
|
||||
return props.standard.actions.find((a) => a.name === actionName)?.purpose;
|
||||
}
|
||||
|
||||
// Поиск операций ledger2, привязанных к action (через triggered_by + явный transitions[].operations)
|
||||
function getOperationsForTransition(t: Transition): Ledger2Operation[] {
|
||||
const byExplicit = t.operations ?? [];
|
||||
const byImplicit = props.standard.operations
|
||||
.filter((op) => op.triggered_by === t.action)
|
||||
.map((op) => op.ledger_code);
|
||||
const codes = new Set([...byExplicit, ...byImplicit]);
|
||||
return props.standard.operations.filter((op) => codes.has(op.ledger_code));
|
||||
}
|
||||
|
||||
function goToState(name: string): void {
|
||||
if (virtualStates.value.has(name)) return;
|
||||
router.push({ query: { s: name } });
|
||||
}
|
||||
|
||||
function goToEdge(edgeId: string): void {
|
||||
router.push({ query: { e: edgeId } });
|
||||
}
|
||||
|
||||
// Поиск transition, соответствующего фокусному edgeId.
|
||||
// edge.id имеет вид `${source}->${target}::${actionShort}`.
|
||||
const focusedTransition = computed<{ t: Transition; edgeId: string } | null>(() => {
|
||||
if (!props.focusEdge) return null;
|
||||
for (const t of props.standard.transitions) {
|
||||
const source = t.from === INITIAL_MARKER ? START_ID : t.from;
|
||||
const short = t.action.split('::').pop() ?? t.action;
|
||||
const id = `${source}->${t.to}::${short}`;
|
||||
if (id === props.focusEdge) return { t, edgeId: id };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const focusedAction = computed(() => {
|
||||
const tr = focusedTransition.value;
|
||||
if (!tr) return null;
|
||||
return props.standard.actions.find((a) => a.name === tr.t.action) ?? null;
|
||||
});
|
||||
|
||||
function accountLabel(
|
||||
acc: Ledger2Operation['debit'] | Ledger2Operation['credit'] | null,
|
||||
): string {
|
||||
if (!acc) return '—';
|
||||
return `${acc.account} ${acc.name}`;
|
||||
}
|
||||
|
||||
function walletLabel(w: Ledger2Operation['wallet_from'] | Ledger2Operation['wallet_to']): string {
|
||||
if (!w) return '—';
|
||||
return `${w.name} (${w.id})`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="focusedTransition" class="details details--edge">
|
||||
<header class="details__head">
|
||||
<div class="details__kicker details__kicker--edge">Фокусное действие</div>
|
||||
<h3 class="details__title">
|
||||
<span v-if="focusedAction?.human" class="details__human">{{ focusedAction.human }}</span>
|
||||
<code class="details__title-code">{{ focusedTransition.t.action }}</code>
|
||||
</h3>
|
||||
<p class="details__desc">{{ focusedAction?.purpose ?? 'Действие контракта.' }}</p>
|
||||
<div class="details__meta">
|
||||
<span class="details__meta-item">
|
||||
<span class="details__label-mini">От</span>
|
||||
<button
|
||||
class="transitions__peer"
|
||||
:disabled="focusedTransition.t.from === INITIAL_MARKER"
|
||||
@click="goToState(focusedTransition.t.from === INITIAL_MARKER ? START_ID : focusedTransition.t.from)"
|
||||
>
|
||||
<code>{{ focusedTransition.t.from === INITIAL_MARKER ? 'Старт' : focusedTransition.t.from }}</code>
|
||||
</button>
|
||||
</span>
|
||||
<span class="transitions__arrow">→</span>
|
||||
<span class="details__meta-item">
|
||||
<span class="details__label-mini">К</span>
|
||||
<button
|
||||
class="transitions__peer"
|
||||
:disabled="virtualStates.has(focusedTransition.t.to)"
|
||||
@click="goToState(focusedTransition.t.to)"
|
||||
>
|
||||
<code>{{ virtualStates.has(focusedTransition.t.to) ? 'Отклонено' : focusedTransition.t.to }}</code>
|
||||
</button>
|
||||
</span>
|
||||
<span class="details__meta-item details__meta-item--actor">
|
||||
<span class="details__label-mini">Вызывает</span>
|
||||
<strong>{{ roleHuman(focusedTransition.t.actor) }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="focusedTransition.t.guards && focusedTransition.t.guards.length" class="details__block">
|
||||
<h4>Условия выполнения</h4>
|
||||
<ul class="bullets">
|
||||
<li v-for="(g, i) in focusedTransition.t.guards" :key="i">{{ g }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="getOperationsForTransition(focusedTransition.t).length" class="details__block">
|
||||
<h4>Операции ledger2</h4>
|
||||
<div
|
||||
v-for="op in getOperationsForTransition(focusedTransition.t)"
|
||||
:key="op.ledger_code"
|
||||
class="transitions__op"
|
||||
>
|
||||
<div class="op__head">
|
||||
<code>{{ op.ledger_code }}</code>
|
||||
<span class="op__kind">{{ op.wallet_op }}</span>
|
||||
</div>
|
||||
<div class="op__body">{{ op.human_name }}</div>
|
||||
<dl class="op__grid">
|
||||
<div><dt>Кошелёк</dt><dd>{{ walletLabel(op.wallet_from) }} → {{ walletLabel(op.wallet_to) }}</dd></div>
|
||||
<div><dt>Дт</dt><dd>{{ accountLabel(op.debit) }}</dd></div>
|
||||
<div><dt>Кт</dt><dd>{{ accountLabel(op.credit) }}</dd></div>
|
||||
<div><dt>Сумма</dt><dd><code>{{ op.amount_ref }}</code></dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else class="details">
|
||||
<header class="details__head">
|
||||
<div class="details__kicker">Фокусный статус</div>
|
||||
<h3 class="details__title">
|
||||
<code>{{ stateInfo.label }}</code>
|
||||
<span v-if="stateInfo.kind === 'final'" class="details__badge">финал</span>
|
||||
<span v-else-if="stateInfo.kind === 'initial'" class="details__badge">старт</span>
|
||||
</h3>
|
||||
<p class="details__desc">{{ stateInfo.description }}</p>
|
||||
</header>
|
||||
|
||||
<!-- Входы -->
|
||||
<div v-if="incoming.length" class="details__block">
|
||||
<h4>Как сюда попадают</h4>
|
||||
<ul class="transitions">
|
||||
<li v-for="t in incoming" :key="`in:${t.from}:${t.action}`">
|
||||
<button
|
||||
class="transitions__peer"
|
||||
:disabled="virtualStates.has(t.from) || t.from === INITIAL_MARKER"
|
||||
@click="goToState(t.from === INITIAL_MARKER ? '__start__' : t.from)"
|
||||
>
|
||||
<code>{{ t.from === INITIAL_MARKER ? 'Старт' : t.from }}</code>
|
||||
</button>
|
||||
<span class="transitions__arrow">→</span>
|
||||
<button
|
||||
type="button"
|
||||
class="transitions__action transitions__action--clickable"
|
||||
:title="`Открыть описание действия ${actionShort(t.action)}`"
|
||||
@click="goToEdge(`${t.from === INITIAL_MARKER ? '__start__' : t.from}->${focus}::${actionShort(t.action)}`)"
|
||||
>
|
||||
<span v-if="actionHumanFor(t.action)" class="transitions__action-human">
|
||||
{{ actionHumanFor(t.action) }}
|
||||
</span>
|
||||
<code class="transitions__action-id">{{ actionShort(t.action) }}</code>
|
||||
<span class="transitions__actor">{{ roleHuman(t.actor) }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Выходы -->
|
||||
<div v-if="outgoing.length" class="details__block">
|
||||
<h4>Что может произойти дальше</h4>
|
||||
<ol class="transitions transitions--out">
|
||||
<li v-for="t in outgoing" :key="`out:${t.to}:${t.action}`" :class="{ 'transitions__row--reject': virtualStates.has(t.to) }">
|
||||
<div class="transitions__row">
|
||||
<button
|
||||
type="button"
|
||||
class="transitions__action transitions__action--clickable"
|
||||
:title="`Открыть описание действия ${actionShort(t.action)}`"
|
||||
@click="goToEdge(`${focus === '__start__' ? '__start__' : focus}->${t.to}::${actionShort(t.action)}`)"
|
||||
>
|
||||
<span v-if="actionHumanFor(t.action)" class="transitions__action-human">
|
||||
{{ actionHumanFor(t.action) }}
|
||||
</span>
|
||||
<code class="transitions__action-id">{{ actionShort(t.action) }}</code>
|
||||
<span class="transitions__actor">{{ roleHuman(t.actor) }}</span>
|
||||
</button>
|
||||
<span class="transitions__arrow">→</span>
|
||||
<button
|
||||
class="transitions__peer"
|
||||
:disabled="virtualStates.has(t.to)"
|
||||
@click="goToState(t.to)"
|
||||
>
|
||||
<code>{{ virtualStates.has(t.to) ? 'Отклонено' : t.to }}</code>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="getActionPurpose(t.action)" class="transitions__purpose">
|
||||
{{ getActionPurpose(t.action) }}
|
||||
</div>
|
||||
|
||||
<div v-if="t.guards && t.guards.length" class="transitions__guards">
|
||||
<span class="transitions__label">Условия:</span>
|
||||
<ul>
|
||||
<li v-for="(g, i) in t.guards" :key="i">{{ g }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-for="op in getOperationsForTransition(t)" :key="op.ledger_code" class="transitions__op">
|
||||
<div class="op__head">
|
||||
<code>{{ op.ledger_code }}</code>
|
||||
<span class="op__kind">{{ op.wallet_op }}</span>
|
||||
</div>
|
||||
<div class="op__body">
|
||||
<span>{{ op.human_name }}</span>
|
||||
</div>
|
||||
<dl class="op__grid">
|
||||
<div><dt>Кошелёк</dt><dd>{{ walletLabel(op.wallet_from) }} → {{ walletLabel(op.wallet_to) }}</dd></div>
|
||||
<div><dt>Дт</dt><dd>{{ accountLabel(op.debit) }}</dd></div>
|
||||
<div><dt>Кт</dt><dd>{{ accountLabel(op.credit) }}</dd></div>
|
||||
<div><dt>Сумма</dt><dd><code>{{ op.amount_ref }}</code></dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div v-if="!incoming.length && !outgoing.length" class="details__empty">
|
||||
У этого статуса нет входов и выходов в манифесте.
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.details {
|
||||
margin-top: 24px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg);
|
||||
padding: 20px 24px;
|
||||
}
|
||||
.details--edge {
|
||||
border-color: var(--edge-focus-border);
|
||||
background: var(--edge-focus-soft);
|
||||
}
|
||||
.details__kicker--edge {
|
||||
color: var(--edge-focus);
|
||||
}
|
||||
.details__title-code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px !important;
|
||||
background: var(--bg) !important;
|
||||
border-color: var(--edge-focus-border) !important;
|
||||
color: var(--edge-focus) !important;
|
||||
}
|
||||
.details__human {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.details__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.details__meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.details__meta-item--actor {
|
||||
margin-left: auto;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
}
|
||||
.details__meta-item strong {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.details__label-mini {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-subtle);
|
||||
}
|
||||
|
||||
.bullets {
|
||||
margin: 4px 0 0;
|
||||
padding-left: 18px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.details__kicker {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-subtle);
|
||||
}
|
||||
.details__title {
|
||||
margin: 4px 0 6px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.details__title code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 17px;
|
||||
background: var(--surface);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.details__badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--accent-border);
|
||||
border-radius: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
.details__desc {
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.details__block {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.details__block h4 {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-subtle);
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.details__empty {
|
||||
margin-top: 16px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.transitions {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.transitions--out { gap: 18px; }
|
||||
.transitions__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.transitions__row--reject { opacity: 0.75; }
|
||||
.transitions__arrow {
|
||||
color: var(--text-subtle);
|
||||
}
|
||||
.transitions__peer {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
transition: background 80ms ease, border-color 80ms ease;
|
||||
}
|
||||
.transitions__peer:not(:disabled):hover {
|
||||
background: var(--accent-soft);
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
.transitions__peer:disabled {
|
||||
cursor: default;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.transitions__action {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
.transitions__action--clickable {
|
||||
cursor: pointer;
|
||||
transition: background 80ms ease, border-color 80ms ease;
|
||||
}
|
||||
.transitions__action--clickable:hover {
|
||||
background: var(--edge-focus-soft);
|
||||
border-color: var(--edge-focus-border);
|
||||
}
|
||||
.transitions__action-human {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
.transitions__action-id {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.transitions__actor {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.transitions__purpose {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin: 6px 0 0 2px;
|
||||
}
|
||||
.transitions__guards {
|
||||
margin-top: 6px;
|
||||
padding: 8px 12px;
|
||||
border-left: 2px solid var(--border-strong);
|
||||
background: var(--surface);
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
.transitions__label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.transitions__guards ul {
|
||||
margin: 4px 0 0;
|
||||
padding-left: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.transitions__op {
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--accent-border);
|
||||
background: var(--accent-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.op__head {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
}
|
||||
.op__head code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.op__kind {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 1px 6px;
|
||||
border: 1px solid var(--accent-border);
|
||||
border-radius: 4px;
|
||||
color: var(--accent);
|
||||
}
|
||||
.op__body {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.op__grid {
|
||||
margin: 8px 0 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 4px 24px;
|
||||
}
|
||||
.op__grid > div {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.op__grid dt {
|
||||
color: var(--text-muted);
|
||||
min-width: 60px;
|
||||
}
|
||||
.op__grid dd {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -26,9 +26,12 @@ defineProps<{
|
||||
|
||||
<style scoped>
|
||||
.end-wrap {
|
||||
width: 128px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.node-end {
|
||||
|
||||
@@ -2,32 +2,76 @@
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
|
||||
defineProps<{
|
||||
data: { label: string; action?: string; fromState?: string; isFocus: boolean };
|
||||
data: {
|
||||
label: string;
|
||||
actionShort?: string;
|
||||
actionName?: string;
|
||||
virtualState?: string;
|
||||
isFocus: boolean;
|
||||
};
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node-rejected" :title="data.action ? `Отклонено: ${data.action}` : 'Отклонено'">
|
||||
<span>{{ data.label }}</span>
|
||||
<Handle type="target" :position="Position.Left" :connectable="false" />
|
||||
<Handle type="source" :position="Position.Right" :connectable="false" />
|
||||
<div class="rejected-wrap">
|
||||
<div
|
||||
class="node-rejected"
|
||||
:class="{ 'node-rejected--focus': data.isFocus }"
|
||||
:title="data.actionShort ? `Отклонено: ${data.actionShort}` : 'Отклонено'"
|
||||
>
|
||||
<span>{{ data.label }}</span>
|
||||
<Handle type="target" :position="Position.Left" :connectable="false" />
|
||||
<Handle type="source" :position="Position.Right" :connectable="false" />
|
||||
</div>
|
||||
<div class="rejected-wrap__caption">завершение отказом</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rejected-wrap {
|
||||
width: 120px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.node-rejected {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--reject-soft);
|
||||
border: 1.5px dashed var(--reject);
|
||||
opacity: 0.55;
|
||||
opacity: 0.75;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: var(--reject);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: opacity 100ms ease, background 100ms ease, box-shadow 100ms ease;
|
||||
}
|
||||
.node-rejected:hover {
|
||||
opacity: 1;
|
||||
background: var(--reject-soft);
|
||||
box-shadow: 0 0 0 3px var(--reject-soft);
|
||||
}
|
||||
.node-rejected--focus {
|
||||
opacity: 1;
|
||||
background: var(--reject-soft);
|
||||
box-shadow: 0 0 0 3px var(--reject-soft);
|
||||
border-style: solid;
|
||||
}
|
||||
.rejected-wrap__caption {
|
||||
font-size: 10px;
|
||||
color: var(--reject);
|
||||
opacity: 0.8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
:deep(.vue-flow__handle) {
|
||||
opacity: 0;
|
||||
|
||||
@@ -25,9 +25,12 @@ defineProps<{
|
||||
|
||||
<style scoped>
|
||||
.start-wrap {
|
||||
width: 128px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.node-start {
|
||||
|
||||
@@ -3,18 +3,24 @@ import { computed } from 'vue';
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
|
||||
const props = defineProps<{
|
||||
data: { label: string; human?: string; description?: string; isFocus: boolean };
|
||||
data: {
|
||||
label: string;
|
||||
human?: string;
|
||||
description?: string;
|
||||
entity?: string;
|
||||
isFocus: boolean;
|
||||
};
|
||||
}>();
|
||||
|
||||
// В квадратике показываем только короткое имя: приоритет data.human,
|
||||
// fallback — description, обрезанный до одной строки.
|
||||
const humanLabel = computed(() => props.data.human ?? '');
|
||||
const entityLabel = computed(() => props.data.entity ?? '');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node-state" :class="{ 'node-state--focus': data.isFocus }">
|
||||
<div class="node-state__label">{{ data.label }}</div>
|
||||
<div v-if="entityLabel" class="node-state__entity">{{ entityLabel }}</div>
|
||||
<div v-if="humanLabel" class="node-state__human">{{ humanLabel }}</div>
|
||||
<div class="node-state__label">{{ data.label }}</div>
|
||||
<Handle type="target" :position="Position.Left" :connectable="false" />
|
||||
<Handle type="source" :position="Position.Right" :connectable="false" />
|
||||
</div>
|
||||
@@ -24,14 +30,14 @@ const humanLabel = computed(() => props.data.human ?? '');
|
||||
.node-state {
|
||||
position: relative;
|
||||
width: 180px;
|
||||
height: 68px;
|
||||
padding: 10px 12px;
|
||||
height: 80px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 3px;
|
||||
background: var(--bg);
|
||||
border: 1.5px solid var(--border-strong);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 1px;
|
||||
cursor: pointer;
|
||||
transition: border-color 100ms ease, box-shadow 100ms ease, background 100ms ease;
|
||||
overflow: hidden;
|
||||
@@ -66,20 +72,35 @@ const humanLabel = computed(() => props.data.human ?? '');
|
||||
background: var(--accent);
|
||||
opacity: 1;
|
||||
}
|
||||
.node-state__label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.node-state__human {
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
color: var(--text-muted);
|
||||
.node-state__entity {
|
||||
font-size: 9.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-subtle);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.node-state__human {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.node-state__label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.1;
|
||||
margin-top: 2px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
:deep(.vue-flow__handle) {
|
||||
opacity: 0;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Реестры ledger2 для standards-site — re-export из общего пакета
|
||||
* `cooptypes` (модуль `Ledger2`).
|
||||
*
|
||||
* Source of truth — в `components/cooptypes/src/ledger2/`; здесь только
|
||||
* тонкие обёртки под прежний API, чтобы не переписывать потребителей.
|
||||
*/
|
||||
|
||||
import { Ledger2 } from 'cooptypes'
|
||||
|
||||
export const LEDGER2_ACCOUNT_REGISTRY = Ledger2.LEDGER2_ACCOUNT_REGISTRY
|
||||
export const LEDGER2_WALLET_REGISTRY = Ledger2.LEDGER2_WALLET_REGISTRY
|
||||
|
||||
export function getAccount(code: number | null | undefined) {
|
||||
return Ledger2.getAccountMeta(code)
|
||||
}
|
||||
|
||||
export function getWallet(id: number | null | undefined) {
|
||||
if (id == null) return undefined
|
||||
return Ledger2.LEDGER2_WALLET_REGISTRY.find((w) => w.id === id)
|
||||
}
|
||||
@@ -42,11 +42,17 @@ export const START_ID = '__start__';
|
||||
export const END_ID = '__end__';
|
||||
export const INITIAL_MARKER = '∅';
|
||||
|
||||
/* В dagre-боксе для start/end оставляем широкий «воздушный» footprint,
|
||||
чтобы кружок не прилипал к первой/последней карточке. Визуальный
|
||||
кружок (48px) центрируется внутри wrapper-а в соответствующем узле. */
|
||||
/* В dagre-боксе для start/end оставляем широкий «воздушный» footprint,
|
||||
чтобы кружок не прилипал к первой/последней карточке. Визуальный
|
||||
кружок (48px) центрируется внутри wrapper-а в соответствующем узле. */
|
||||
const SIZE = {
|
||||
start: { width: 48, height: 48 },
|
||||
state: { width: 180, height: 68 },
|
||||
end: { width: 48, height: 48 },
|
||||
rejected: { width: 40, height: 40 },
|
||||
start: { width: 128, height: 80 },
|
||||
state: { width: 180, height: 80 },
|
||||
end: { width: 128, height: 80 },
|
||||
rejected: { width: 120, height: 70 },
|
||||
action: { width: 170, height: 54 },
|
||||
};
|
||||
|
||||
@@ -260,6 +266,7 @@ function buildLayout(standard: Standard): LayoutResult {
|
||||
|
||||
// Statuses — все рендерятся как прямоугольник `state`.
|
||||
// Финальные статусы теперь подключаются к отдельному ●-финиш-узлу.
|
||||
const entityHuman = standard.entity_human ?? '';
|
||||
for (const s of states) {
|
||||
if (isVirtual(s)) continue;
|
||||
const pos = g.node(s.name);
|
||||
@@ -272,6 +279,7 @@ function buildLayout(standard: Standard): LayoutResult {
|
||||
label: s.name,
|
||||
human: s.human,
|
||||
description: s.description,
|
||||
entity: entityHuman,
|
||||
isFocus: false,
|
||||
},
|
||||
draggable: false,
|
||||
@@ -331,7 +339,7 @@ function buildLayout(standard: Standard): LayoutResult {
|
||||
const hasDocs = (docsByAction.get(t.action) ?? []).length > 0;
|
||||
const hasWalletMove = ops.length > 0;
|
||||
const hasPosting = ops.some(
|
||||
(op) => op.wallet_op !== 'WALLET_ONLY' && (op.debit || op.credit),
|
||||
(op) => op.wallet_op !== 'WALLET_ONLY' && (op.debit != null || op.credit != null),
|
||||
);
|
||||
|
||||
nodes.push({
|
||||
@@ -395,9 +403,14 @@ function buildLayout(standard: Standard): LayoutResult {
|
||||
id: info.targetId,
|
||||
type: NODE_TYPES.REJECTED,
|
||||
position: { x: pos.x - pos.width / 2, y: pos.y - pos.height / 2 },
|
||||
data: { label: '×', action: actionShort(info.t.action), isFocus: false },
|
||||
data: {
|
||||
label: '×',
|
||||
actionShort: actionShort(info.t.action),
|
||||
actionName: info.t.action,
|
||||
virtualState: info.t.to,
|
||||
isFocus: false,
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
targetPosition: TL,
|
||||
sourcePosition: TR,
|
||||
});
|
||||
|
||||
@@ -24,6 +24,16 @@ function relationHuman(r: string): string {
|
||||
return RELATION_HUMAN[r] ?? r;
|
||||
}
|
||||
|
||||
const LIFECYCLE_HUMAN: Record<string, string> = {
|
||||
proposed: 'Предложен',
|
||||
approved: 'Утверждён',
|
||||
active: 'Действующий',
|
||||
deprecated: 'Устаревший',
|
||||
};
|
||||
function lifecycleHuman(s: string): string {
|
||||
return LIFECYCLE_HUMAN[s] ?? s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Для каждой записи `related[]` смотрим, есть ли такой process_type у нас
|
||||
* в индексе — если да, делаем кликабельный RouterLink, иначе просто метку.
|
||||
@@ -92,18 +102,42 @@ const focusStatus = computed<string | null>(() => {
|
||||
|
||||
<div v-else class="process-page">
|
||||
<header class="process-head">
|
||||
<div class="process-head__top">
|
||||
<div class="process-head__title-box">
|
||||
<span class="process-head__kicker">{{ standard.contract }}</span>
|
||||
<h1>{{ standard.title }}</h1>
|
||||
<code class="process-head__code">{{ standard.process_type }}</code>
|
||||
</div>
|
||||
<div class="process-head__meta">
|
||||
<span v-if="standard.area">зона <code>{{ standard.area }}</code></span>
|
||||
<span>статус: <strong>{{ standard.status }}</strong></span>
|
||||
</div>
|
||||
<div class="process-head__title-box">
|
||||
<h1>{{ standard.title }}</h1>
|
||||
<p class="process-head__summary">{{ standard.summary }}</p>
|
||||
</div>
|
||||
<p class="process-head__summary">{{ standard.summary }}</p>
|
||||
|
||||
<dl class="process-head__meta">
|
||||
<div class="meta-item">
|
||||
<dt>Контракт</dt>
|
||||
<dd><code>{{ standard.contract }}</code></dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>Процесс</dt>
|
||||
<dd><code>{{ standard.process_type }}</code></dd>
|
||||
</div>
|
||||
<div v-if="standard.entity_human || standard.entity" class="meta-item meta-item--stacked">
|
||||
<dt>Сущность</dt>
|
||||
<dd>
|
||||
<span v-if="standard.entity_human" class="meta-item__primary">
|
||||
«{{ standard.entity_human }}»
|
||||
</span>
|
||||
<code v-if="standard.entity" class="meta-item__sub-code">{{ standard.entity }}</code>
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="standard.area" class="meta-item">
|
||||
<dt>Зона</dt>
|
||||
<dd><code>{{ standard.area }}</code></dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>Статус стандарта</dt>
|
||||
<dd>
|
||||
<span class="status-badge" :class="`status-badge--${standard.status}`">
|
||||
{{ lifecycleHuman(standard.status) }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</header>
|
||||
|
||||
<ProcessGraph
|
||||
@@ -136,58 +170,108 @@ const focusStatus = computed<string | null>(() => {
|
||||
|
||||
<style scoped>
|
||||
.process-head {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.process-head__top {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.process-head__title-box {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1 1 420px;
|
||||
min-width: 0;
|
||||
}
|
||||
.process-head__title-box h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
margin: 0 0 4px;
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.process-head__kicker {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-subtle);
|
||||
font-weight: 600;
|
||||
}
|
||||
.process-head__code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.process-head__summary {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
line-height: 1.55;
|
||||
max-width: 780px;
|
||||
margin: 6px 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
.process-head__meta {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, auto));
|
||||
gap: 10px 22px;
|
||||
align-content: start;
|
||||
}
|
||||
.meta-item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
.meta-item dt {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-subtle);
|
||||
}
|
||||
.meta-item dd {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
color: var(--text);
|
||||
}
|
||||
.meta-item dd code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
padding: 2px 7px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
}
|
||||
.meta-item--stacked dd {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.meta-item__primary {
|
||||
font-size: 12.5px;
|
||||
color: var(--text);
|
||||
}
|
||||
.meta-item__sub-code {
|
||||
font-size: 10.5px !important;
|
||||
padding: 1px 5px !important;
|
||||
color: var(--text-muted) !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.process-head__meta code {
|
||||
font-family: var(--font-mono);
|
||||
.status-badge--proposed {
|
||||
background: var(--edge-focus-soft);
|
||||
border-color: var(--edge-focus-border);
|
||||
color: var(--edge-focus);
|
||||
}
|
||||
.status-badge--approved,
|
||||
.status-badge--active {
|
||||
background: var(--accent-soft);
|
||||
border-color: var(--accent-border);
|
||||
color: var(--accent);
|
||||
}
|
||||
.status-badge--deprecated {
|
||||
background: var(--reject-soft);
|
||||
border-color: var(--reject);
|
||||
color: var(--reject);
|
||||
}
|
||||
.related {
|
||||
margin-top: 28px;
|
||||
|
||||
@@ -22,4 +22,20 @@ const routes: RouteRecordRaw[] = [
|
||||
export const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from) {
|
||||
// Скроллим к началу только при смене процесса (params.processType).
|
||||
// При смене query (фокуса) не двигаем — user смотрит на ту же область.
|
||||
const changedProcess =
|
||||
to.name !== from.name || to.params.processType !== from.params.processType;
|
||||
if (!changedProcess) return false;
|
||||
if (typeof document !== 'undefined') {
|
||||
const main = document.querySelector('.app-main');
|
||||
if (main) {
|
||||
main.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
return { top: 0 };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -92,6 +92,32 @@ button { font: inherit; cursor: pointer; }
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ── Мобильная заглушка ──────────────────────────────────────────────────── */
|
||||
.mobile-stub {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: var(--bg);
|
||||
z-index: 100;
|
||||
text-align: center;
|
||||
}
|
||||
.mobile-stub__box {
|
||||
max-width: 360px;
|
||||
}
|
||||
.mobile-stub h1 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.mobile-stub p {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
overflow-y: auto;
|
||||
padding: 20px 28px 24px;
|
||||
|
||||
@@ -33,19 +33,11 @@ export type Role =
|
||||
// open для расширения без правки типа
|
||||
| (string & {});
|
||||
|
||||
// ── Элементарные структуры ──────────────────────────────────────────────────
|
||||
export interface WalletRef {
|
||||
id: number;
|
||||
name: string;
|
||||
human: string;
|
||||
}
|
||||
|
||||
export interface AccountRef {
|
||||
account: number; // номер счёта по плану счетов (51, 58, 80, …)
|
||||
name: string; // константа ledger2_accounts (BANK_ACCOUNT …)
|
||||
human: string;
|
||||
type?: 'ACTIVE' | 'PASSIVE';
|
||||
}
|
||||
// ── Элементарные ссылки в ledger2 ───────────────────────────────────────────
|
||||
// В YAML — только числовые коды. Имена подтягиваются из
|
||||
// `data/registries.ts` (зеркало source-of-truth из mono-ai-1).
|
||||
export type AccountCode = number; // код счёта (51, 80, 86, …)
|
||||
export type WalletId = number; // id кошелька (2001, 3001, …)
|
||||
|
||||
// ── Секции стандарта ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -119,15 +111,16 @@ export interface ProcessDocument {
|
||||
note?: string;
|
||||
}
|
||||
|
||||
// §6 Операции ledger2
|
||||
// §6 Операции
|
||||
// В YAML указываются только коды/id. Человекочитаемые имена — из registries.
|
||||
export interface Ledger2Operation {
|
||||
ledger_code: string; // cap.lnissue
|
||||
human_name: string;
|
||||
wallet_op: WalletOp;
|
||||
wallet_from: WalletRef | null;
|
||||
wallet_to: WalletRef | null;
|
||||
debit: AccountRef | null;
|
||||
credit: AccountRef | null;
|
||||
wallet_from: WalletId | null; // id кошелька-источника (null — ISSUE/extern)
|
||||
wallet_to: WalletId | null; // id кошелька-приёмника (null — CONSUME)
|
||||
debit: AccountCode | null; // код счёта (51, 80, …)
|
||||
credit: AccountCode | null;
|
||||
amount_ref: string; // debt.amount
|
||||
triggered_by: string; // capital::debtpaycnfrm
|
||||
description?: string;
|
||||
@@ -159,7 +152,8 @@ export interface Standard {
|
||||
actions: ContractAction[];
|
||||
|
||||
// §3
|
||||
entity?: string;
|
||||
entity?: string; // технический идентификатор (`wallet::deposit`)
|
||||
entity_human?: string; // человекочитаемое имя, им. падеж ед. ч. («Заявка на взнос»)
|
||||
entity_source?: string;
|
||||
states: EntityState[];
|
||||
transitions: Transition[];
|
||||
|
||||
@@ -5,7 +5,10 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ command }) => ({
|
||||
// При dev-сервере — корень; при build-е сайт публикуется как
|
||||
// поддиректория /standards/ на docs.coopenomics.world.
|
||||
base: command === 'build' ? '/standards/' : '/',
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -28,4 +31,4 @@ export default defineConfig({
|
||||
strictPort: false,
|
||||
allowedHosts: true,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -14,3 +14,4 @@ export * as _Common from './common'
|
||||
export * from './contracts'
|
||||
export * as Cooperative from './cooperative'
|
||||
export * as Interfaces from './interfaces'
|
||||
export * as Ledger2 from './ledger2'
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Реестр плана счетов ledger2 — source of truth в контракте:
|
||||
* `components/contracts/cpp/lib/core/ledger2/accounts.hpp`
|
||||
* (`LEDGER2_ACCOUNT_MAP`). В контракте id хранится как `code * 1000`
|
||||
* (51 → 51000), здесь — человеческий код счёта (51).
|
||||
*
|
||||
* При добавлении нового счёта синхронизировать contract-side.
|
||||
*/
|
||||
export type AccountKind = 'active' | 'passive' | 'active_passive'
|
||||
|
||||
export interface AccountMeta {
|
||||
code: number
|
||||
name: string
|
||||
kind: AccountKind
|
||||
}
|
||||
|
||||
export const LEDGER2_ACCOUNT_REGISTRY: readonly AccountMeta[] = [
|
||||
{ code: 4, name: 'Нематериальные активы', kind: 'active' },
|
||||
{ code: 8, name: 'Вложения во внеоборотные активы', kind: 'active' },
|
||||
{ code: 51, name: 'Расчётный счёт', kind: 'active' },
|
||||
{ code: 58, name: 'Финансовые вложения', kind: 'active' },
|
||||
{ code: 80, name: 'Паевой фонд (складочный капитал)', kind: 'passive' },
|
||||
{ code: 86, name: 'Целевое финансирование', kind: 'passive' },
|
||||
] as const
|
||||
|
||||
const accountByCode = new Map<number, AccountMeta>(
|
||||
LEDGER2_ACCOUNT_REGISTRY.map((a) => [a.code, a]),
|
||||
)
|
||||
|
||||
export function getAccountName(code: number | null | undefined): string | undefined {
|
||||
if (code == null) return undefined
|
||||
return accountByCode.get(code)?.name
|
||||
}
|
||||
|
||||
export function getAccountMeta(code: number | null | undefined): AccountMeta | undefined {
|
||||
if (code == null) return undefined
|
||||
return accountByCode.get(code)
|
||||
}
|
||||
|
||||
/** Преобразует stored id (51000) в человеческий code (51). */
|
||||
export function storedIdToCode(storedId: number | null | undefined): number | null {
|
||||
if (storedId == null) return null
|
||||
return Math.round(storedId / 1000)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* ledger2 — реестры счетов и кошельков, зеркало source-of-truth из
|
||||
* смарт-контрактов (`cpp/lib/core/ledger2/`). Используется потребителями
|
||||
* (standards-site, desktop/shared/lib/ledger2) как единый справочник имён
|
||||
* для UI-подсказок.
|
||||
*/
|
||||
|
||||
export * from './accounts'
|
||||
export * from './wallets'
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Реестр кошельков ledger2 — source of truth в контракте:
|
||||
* `components/contracts/cpp/lib/core/ledger2/wallets.hpp`
|
||||
* (`LEDGER2_WALLET_REGISTRY`). При добавлении/переименовании кошелька
|
||||
* синхронизировать обе стороны.
|
||||
*
|
||||
* Используется для tooltip-подсказок в UI (standards-site, desktop).
|
||||
*/
|
||||
export interface WalletMeta {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
|
||||
// id=0 — frontend-only: в контракте нет, но walletop issue/consume использует
|
||||
// 0 как «кошелёк-источник/сток извне системы» (эмиссия / сжигание).
|
||||
{ id: 0, name: 'Выпуск' },
|
||||
{ id: 2001, name: 'ЦПП «Цифровой Кошелёк» — паевые взносы деньгами' },
|
||||
{ id: 2002, name: 'Минимальный паевой взнос' },
|
||||
{ id: 2003, name: 'Паевой фонд — принятые РИД' },
|
||||
{ id: 3001, name: 'Вступительные взносы' },
|
||||
{ id: 3002, name: 'Членские взносы (платформенные)' },
|
||||
{ id: 3003, name: 'Делегатские членские взносы' },
|
||||
{ id: 4001, name: 'Возвраты паевых взносов пайщикам' },
|
||||
{ id: 4002, name: 'Выплаты поставщикам' },
|
||||
{ id: 4051, name: 'Выданные пайщикам беспроцентные займы' },
|
||||
{ id: 5001, name: 'Ручные корректировки' },
|
||||
{ id: 9001, name: 'ЦПП «Благорост» — инвестиции деньгами' },
|
||||
{ id: 9002, name: 'ЦПП «Благорост» — принятые РИД' },
|
||||
{ id: 9003, name: 'ЦПП «Благорост» — имущественные паевые взносы' },
|
||||
{ id: 9004, name: 'ЦПП «Благорост» — членские взносы' },
|
||||
{ id: 10001, name: 'ЦПП «Генератор» — принятый коммит (имущество)' },
|
||||
{ id: 10002, name: 'ЦПП «Генератор» — членские взносы' },
|
||||
{ id: 11001, name: 'ЦПП «Стол Заказов» — общий кошелёк' },
|
||||
] as const
|
||||
|
||||
const walletNameById = new Map<number, string>(
|
||||
LEDGER2_WALLET_REGISTRY.map((w) => [w.id, w.name]),
|
||||
)
|
||||
|
||||
export function getWalletName(id: number | null | undefined): string | undefined {
|
||||
if (id == null) return undefined
|
||||
return walletNameById.get(id)
|
||||
}
|
||||
@@ -96,8 +96,9 @@ markdown_extensions:
|
||||
permalink: true
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
nav:
|
||||
nav:
|
||||
- Введение: index.md
|
||||
- Реестр стандартов: https://docs.coopenomics.world/standards/
|
||||
- Лицензия: new/installation/index.md # Главная страница раздела
|
||||
- Облачная поставка: new/connection/index.md
|
||||
- Установка: new/installation/chairman.md
|
||||
|
||||
Generated
+18
-21
@@ -222,6 +222,9 @@ importers:
|
||||
'@vue-flow/minimap':
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4(@vue-flow/core@1.48.2(vue@3.5.31(typescript@5.9.3)))(vue@3.5.31(typescript@5.9.3))
|
||||
cooptypes:
|
||||
specifier: workspace:*
|
||||
version: link:../../cooptypes
|
||||
lucide-vue-next:
|
||||
specifier: ^0.460.0
|
||||
version: 0.460.0(vue@3.5.31(typescript@5.9.3))
|
||||
@@ -18847,7 +18850,7 @@ snapshots:
|
||||
'@stylistic/eslint-plugin': 2.13.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@vitest/eslint-plugin': 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1))
|
||||
'@vitest/eslint-plugin': 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1))
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
eslint-config-flat-gitignore: 0.1.8
|
||||
eslint-flat-config-utils: 0.3.1
|
||||
@@ -18864,7 +18867,7 @@ snapshots:
|
||||
eslint-plugin-regexp: 2.10.0(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-toml: 0.11.1(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-unicorn: 55.0.0(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-vue: 9.33.0(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-yml: 1.19.1(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.5.31)(eslint@9.39.4(jiti@2.6.1))
|
||||
@@ -18912,7 +18915,7 @@ snapshots:
|
||||
eslint-plugin-regexp: 2.10.0(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-toml: 0.12.0(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-unicorn: 56.0.1(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-vue: 9.33.0(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-yml: 1.19.1(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-processor-vue-blocks: 1.0.0(@vue/compiler-sfc@3.5.31)(eslint@9.39.4(jiti@2.6.1))
|
||||
@@ -26276,18 +26279,6 @@ snapshots:
|
||||
vite: 8.0.3(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.31(typescript@5.9.3)
|
||||
|
||||
'@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1))':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.57.2
|
||||
'@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
vitest: 1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3)(vitest@4.1.1(@opentelemetry/api@1.9.1)(@types/node@20.19.37)(vite@8.0.3(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.57.2
|
||||
@@ -26312,6 +26303,18 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1))':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.57.2
|
||||
'@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
vitest: 1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1))':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.57.2
|
||||
@@ -30587,12 +30590,6 @@ snapshots:
|
||||
semver: 7.7.4
|
||||
strip-indent: 3.0.0
|
||||
|
||||
eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
|
||||
eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0):
|
||||
dependencies:
|
||||
eslint: 7.32.0
|
||||
|
||||
Reference in New Issue
Block a user