@coopenomics/desktop 2025.11.9-alpha-2 → 2025.11.10-alpha-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coopenomics/desktop",
3
- "version": "2025.11.9-alpha-2",
3
+ "version": "2025.11.10-alpha-1",
4
4
  "description": "A Desktop Project",
5
5
  "productName": "Desktop App",
6
6
  "author": "Alex Ant <dacom.dark.sun@gmail.com>",
@@ -25,9 +25,9 @@
25
25
  "start": "node -r ./alias-resolver.js dist/ssr/index.js"
26
26
  },
27
27
  "dependencies": {
28
- "@coopenomics/controller": "2025.11.9-alpha-2",
29
- "@coopenomics/notifications": "2025.11.9-alpha-2",
30
- "@coopenomics/sdk": "2025.11.9-alpha-2",
28
+ "@coopenomics/controller": "2025.11.10-alpha-1",
29
+ "@coopenomics/notifications": "2025.11.10-alpha-1",
30
+ "@coopenomics/sdk": "2025.11.10-alpha-1",
31
31
  "@dicebear/collection": "^9.0.1",
32
32
  "@dicebear/core": "^9.0.1",
33
33
  "@editorjs/code": "^2.9.3",
@@ -59,7 +59,7 @@
59
59
  "@wharfkit/wallet-plugin-privatekey": "^1.1.0",
60
60
  "axios": "^1.2.1",
61
61
  "compression": "^1.7.4",
62
- "cooptypes": "2025.11.9-alpha-2",
62
+ "cooptypes": "2025.11.10-alpha-1",
63
63
  "dompurify": "^3.1.7",
64
64
  "dotenv": "^16.4.5",
65
65
  "email-regex": "^5.0.0",
@@ -123,5 +123,5 @@
123
123
  "npm": ">= 6.13.4",
124
124
  "yarn": ">= 1.21.1"
125
125
  },
126
- "gitHead": "5acd7ae2c437ce54f356113b5474700114f586ab"
126
+ "gitHead": "634766a08edc036ccf4b08438b14251fa3a8ab73"
127
127
  }
package/src/app/App.vue CHANGED
@@ -33,11 +33,23 @@ const { showDialog } = useNotificationPermissionDialog();
33
33
  onMounted(async () => {
34
34
  try {
35
35
  console.log('systemInfo', info)
36
- // Если мы в SPA режиме (hash mode) и pathname не является главной страницей
37
- // и при этом hash пустой или указывает не на текущий pathname
38
- if (typeof window !== 'undefined' && window.location.pathname !== '/' &&
39
- (!window.location.hash || !window.location.hash.includes(window.location.pathname))) {
40
- console.log('URL needs hash correction');
36
+ // Проверяем, нужно ли корректировать URL для hash роутера
37
+ // Выполняем только в клиентском режиме с hash роутером
38
+ const isClientMode = process.env.CLIENT === 'true';
39
+ const isHashRouter = process.env.VUE_ROUTER_MODE === 'hash';
40
+ const isNotSSR = process.env.SERVER !== 'true';
41
+ const hasWindow = typeof window !== 'undefined';
42
+
43
+ const shouldCorrectHashUrl =
44
+ isClientMode &&
45
+ isHashRouter &&
46
+ isNotSSR &&
47
+ hasWindow &&
48
+ window.location.pathname !== '/' &&
49
+ (!window.location.hash || !window.location.hash.includes(window.location.pathname));
50
+
51
+ if (shouldCorrectHashUrl) {
52
+ console.log('URL needs hash correction for hash router mode');
41
53
  const newUrl = window.location.origin + '/#' + window.location.pathname + window.location.search;
42
54
  console.log('Redirecting to:', newUrl);
43
55
  window.location.replace(newUrl);
package/src/env.d.ts CHANGED
@@ -5,6 +5,8 @@ declare namespace NodeJS {
5
5
  NODE_ENV: string;
6
6
  VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined;
7
7
  VUE_ROUTER_BASE: string | undefined;
8
+ CLIENT?: string;
9
+ SERVER?: string;
8
10
  }
9
11
  }
10
12
 
@@ -80,14 +80,17 @@ export function useNotificationPermissionDialog() {
80
80
  const handleAllow = async () => {
81
81
  try {
82
82
  store.isPermissionDialogProcessing = true;
83
-
83
+ console.log('handleAllow');
84
84
  // Подписываемся на уведомления (это включает запрос разрешения)
85
85
  const success = await subscribe();
86
-
86
+ console.log('success', success);
87
87
  if (success) {
88
+ console.log('saveChoice granted');
88
89
  saveChoice('granted');
90
+ console.log('hideDialog');
89
91
  hideDialog();
90
92
  } else {
93
+ console.log('saveChoice denied');
91
94
  // Если подписка не удалась, но пользователь согласился,
92
95
  // все равно сохраняем выбор чтобы не показывать диалог снова
93
96
  saveChoice('denied');
@@ -95,6 +98,7 @@ export function useNotificationPermissionDialog() {
95
98
  }
96
99
  } catch (error) {
97
100
  console.error('Ошибка при разрешении уведомлений:', error);
101
+ console.log('saveChoice denied');
98
102
  saveChoice('denied');
99
103
  hideDialog();
100
104
  } finally {
@@ -83,7 +83,16 @@ export function useWebPushNotifications() {
83
83
  throw new Error('Push уведомления не поддерживаются');
84
84
  }
85
85
 
86
- const permission = await Notification.requestPermission();
86
+ // Добавляем timeout на случай, если пользователь не взаимодействует с диалогом
87
+ const permission = await Promise.race([
88
+ Notification.requestPermission(),
89
+ new Promise<never>((_, reject) => {
90
+ setTimeout(() => {
91
+ reject(new Error('Пользователь не ответил на запрос разрешения в течение 30 секунд'));
92
+ }, 15000);
93
+ }),
94
+ ]);
95
+
87
96
  store.support.permission = permission;
88
97
  store.support.hasPermission = permission === 'granted';
89
98
  store.support.canSubscribe =
@@ -150,7 +159,16 @@ export function useWebPushNotifications() {
150
159
  }
151
160
 
152
161
  console.log('Форсированный запрос разрешения...');
153
- const permission = await Notification.requestPermission();
162
+
163
+ // Добавляем timeout на случай, если пользователь не взаимодействует с диалогом
164
+ const permission = await Promise.race([
165
+ Notification.requestPermission(),
166
+ new Promise<never>((_, reject) => {
167
+ setTimeout(() => {
168
+ reject(new Error('Пользователь не ответил на запрос разрешения в течение 30 секунд'));
169
+ }, 30000);
170
+ }),
171
+ ]);
154
172
 
155
173
  store.support.permission = permission;
156
174
  store.support.hasPermission = permission === 'granted';
@@ -267,7 +285,7 @@ export function useWebPushNotifications() {
267
285
  // Создаем новую подписку
268
286
  const subscription = await registration.pushManager.subscribe({
269
287
  userVisibleOnly: true,
270
- applicationServerKey: applicationServerKey,
288
+ applicationServerKey: applicationServerKey as any,
271
289
  });
272
290
 
273
291
  return subscription;
@@ -1,138 +1,54 @@
1
1
  <template lang="pug">
2
- div.row.justify-center.q-pa-md
3
- div.col-md-8.col-xs-12
2
+ div.row.q-pa-md
3
+ div.col-md-12.col-xs-12
4
4
  div(v-if="system.info.is_providered")
5
- div(v-if="is_finish == false")
6
- div(v-if="!signedDocument")
7
- div(v-if="html")
8
- DocumentHtmlReader(:html="html")
9
- q-btn(@click="sign" color="primary") подписать
10
-
11
- div(v-else)
12
- Loader(:text='`Готовим соглашение...`')
13
-
14
- div(v-else)
15
- p.text-h6 Предварительная настройка
16
- p Пожалуйста, укажите домен для установки MONO. Также, укажите суммы вступительных и минимальных паевых взносов для физических лиц, юридических лиц и индивидуальных предпринимателей:
17
-
18
- AddCooperativeForm(:document="signedDocument" @finish="finish").q-pt-md
19
- div(v-if="is_finish == true && coop")
20
-
21
- p.text-h6 Кооператив на подключении
22
- p Статус:
23
- q-badge(v-if="coop.status == 'pending'" color="orange").q-ml-sm ожидание
24
- q-badge(v-if="coop.status == 'active'" color="teal").q-ml-sm активен
25
- q-badge(v-if="coop.status == 'blocked'" color="red").q-ml-sm заблокирован
26
-
27
- q-btn(@click="reload" color="primary" size="sm").q-ml-md
28
- q-icon(name="refresh")
29
- span обновить
5
+ ConnectionAgreementStepper(
6
+ :initial-step="currentStep"
7
+ :is-finish="is_finish"
8
+ :signed-document="signedDocument"
9
+ :coop="coop"
10
+ :html="html"
11
+ :domain-valid="domainValid"
12
+ :installation-progress="installationProgress"
13
+ :instance-status="instanceStatus"
14
+ :subscriptions-loading="subscriptionsLoading"
15
+ :subscriptions-error="subscriptionsError"
16
+ @step-change="handleStepChange"
17
+ @tariff-selected="handleTariffSelected"
18
+ @tariff-deselected="handleTariffDeselected"
19
+ @continue="handleContinue"
20
+ @sign="sign"
21
+ @finish="finish"
22
+ @reload="reload"
23
+ )
30
24
 
31
- div(v-else)
25
+ div(v-else).row
32
26
  //- Заглушка для недоступного провайдера
33
- q-banner(
34
- :class="'text-white bg-blue-500'"
35
- rounded
36
- )
37
- template(v-slot:avatar)
38
- q-icon(name="info" color="white")
39
- span Для подключения к Кооперативной Экономике обратитесь в ПК ВОСХОД
40
- q-btn(
41
- flat
42
- color="white"
43
- label="Перейти на сайт"
44
- @click="openProviderWebsite"
45
- size="sm"
46
- ).q-ml-md
47
-
48
- div(v-if="system.info.is_providered").q-mt-md
49
- p.text-subtitle1 Статус подписки на хостинг:
50
-
51
- //- Статус подписки (только если провайдер доступен)
52
- div
53
- div(
54
- v-if="subscriptionsError"
55
- ).q-mb-md
56
- q-banner(
57
- :class="'text-white bg-red-500'"
58
- rounded
59
- )
60
- template(v-slot:avatar)
61
- q-icon(name="error" color="white")
62
- span {{ subscriptionsError }}
63
-
64
- div.flex.items-center.q-gutter-sm
65
- div
66
- span.text-body2 Валидность домена:
67
- q-badge(
68
- v-if="domainValid === true"
69
- color="green"
70
- ).q-ml-sm валиден
71
- q-badge(
72
- v-if="domainValid === false"
73
- color="red"
74
- ).q-ml-sm не валиден
75
- q-badge(
76
- v-if="domainValid === null && !subscriptionsLoading"
77
- color="grey"
78
- ).q-ml-sm неизвестно
79
- q-badge(
80
- v-if="subscriptionsLoading"
81
- color="blue"
82
- ).q-ml-sm загрузка...
83
-
84
- div
85
- span.text-body2 Прогресс установки:
86
- q-badge(
87
- v-if="installationProgress !== null"
88
- :color="installationProgress === 100 ? 'green' : 'orange'"
89
- ).q-ml-sm {{ installationProgress }}%
90
- q-badge(
91
- v-if="installationProgress === null && !subscriptionsLoading"
92
- color="grey"
93
- ).q-ml-sm неизвестно
94
- q-badge(
95
- v-if="subscriptionsLoading"
96
- color="blue"
97
- ).q-ml-sm загрузка...
98
-
99
- div
100
- span.text-body2 Статус сервера:
101
- q-badge(
102
- v-if="instanceStatus"
103
- :color="instanceStatus === 'active' ? 'green' : instanceStatus === 'error' ? 'red' : 'orange'"
104
- ).q-ml-sm {{ instanceStatus }}
105
- q-badge(
106
- v-if="!instanceStatus && !subscriptionsLoading"
107
- color="grey"
108
- ).q-ml-sm неизвестно
109
- q-badge(
110
- v-if="subscriptionsLoading"
111
- color="blue"
112
- ).q-ml-sm загрузка...
113
-
114
- p Пожалуйста, перешлите инструкцию ниже вашему техническому специалисту. После её выполнения, мы автоматически выполним запуск. Далее, Вам необходимо завершить установку уже на Вашем сайте следуя инструкциям, представленным там.
115
-
116
- q-card(flat bordered).q-pa-sm
117
- p.text-bold Инструкция
118
- div.flex.justify-between
119
- span {{instruction}}
120
- q-btn(size="sm" icon="fas fa-copy" flat @click="copy")
27
+ div.col-md-12.col-xs-12
28
+ ColorCard(color="blue")
29
+ .text-center.q-pa-md
30
+ q-icon(name="fas fa-info-circle" size="2rem").q-mb-sm
31
+ .text-h6.q-mb-md Информация о подключении
32
+ p Для подключения к платформе Кооперативной Экономики обратитесь в ПК ВОСХОД.
33
+ q-btn(
34
+ color="primary"
35
+ label="Перейти на сайт"
36
+ @click="openProviderWebsite"
37
+ size="md"
38
+ ).q-mt-md
121
39
 
122
40
  </template>
123
41
  <script setup lang="ts">
124
42
  import { DigitalDocument } from 'src/shared/lib/document';
125
43
  import { useSessionStore } from 'src/entities/Session';
126
44
  import { useSystemStore } from 'src/entities/System/model';
127
- import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
128
45
  import { computed, ref, onMounted, onUnmounted } from 'vue';
129
- import { Loader } from 'src/shared/ui/Loader';
130
- import { AddCooperativeForm } from 'src/features/Union/AddCooperative';
131
46
  import { useLoadCooperatives } from 'src/features/Union/LoadCooperatives';
132
47
  import { useProviderSubscriptions } from 'src/features/Provider';
133
- import { copyToClipboard } from 'quasar';
134
- import { SuccessAlert } from 'src/shared/api';
135
48
  import { Cooperative } from 'cooptypes';
49
+ import { ConnectionAgreementStepper } from 'src/widgets/ConnectionAgreementStepper';
50
+ import { ColorCard } from 'src/shared/ui';
51
+
136
52
 
137
53
  const session = useSessionStore()
138
54
  const system = useSystemStore()
@@ -148,23 +64,40 @@ const {
148
64
  } = useProviderSubscriptions()
149
65
 
150
66
  const coop = ref()
151
- const instruction = computed(() => `Создайте A-запись домена ${coop.value?.announce} на IP-адрес: 51.250.114.13`)
152
67
 
153
68
  const html = computed(() => document.value?.data?.html)
154
69
  const signedDocument = computed(() => document.value?.signedDocument)
155
70
  const is_finish = ref(false)
156
71
 
72
+ // Управление шагом степпера
73
+ const currentStep = ref(1)
74
+
75
+ const handleStepChange = (step: number) => {
76
+ currentStep.value = step
77
+ }
78
+
79
+ const handleTariffSelected = (tariff: any) => {
80
+ // Здесь можно сохранить выбранный тариф
81
+ console.log('Selected tariff:', tariff)
82
+ }
83
+
84
+ const handleTariffDeselected = () => {
85
+ // Здесь можно обработать снятие выбора тарифа
86
+ console.log('Tariff deselected')
87
+ }
88
+
157
89
  // Остановка автообновления при размонтировании компонента
158
90
  let stopRefresh: (() => void) | null = null
159
91
 
160
- const copy = () => {
161
- copyToClipboard(instruction.value)
162
- .then(() => {
163
- SuccessAlert('Инструкция скопирована в буфер')
164
- })
165
- .catch((e) => {
166
- console.log(e)
92
+ const handleContinue = async () => {
93
+ // Если документ еще не сгенерирован, генерируем его
94
+ if (!document.value.data?.html && !coop.value) {
95
+ await document.value.generate({
96
+ registry_id: Cooperative.Registry.CoopenomicsAgreement.registry_id,
97
+ coopname: 'voskhod',
98
+ username: session.username,
167
99
  })
100
+ }
168
101
  }
169
102
 
170
103
  const openProviderWebsite = () => {
@@ -199,13 +132,16 @@ const init = async () => {
199
132
 
200
133
  coop.value = await loadOneCooperative(session.username)
201
134
 
202
- if (!coop.value)
135
+ if (!coop.value) {
203
136
  await document.value.generate({
204
137
  registry_id: Cooperative.Registry.CoopenomicsAgreement.registry_id,
205
138
  coopname: 'voskhod',
206
139
  username: session.username,
207
140
  })
208
- else is_finish.value = true
141
+ } else {
142
+ is_finish.value = true
143
+ currentStep.value = 4 // Переходим на последний шаг если кооператив уже создан
144
+ }
209
145
  }
210
146
 
211
147
  const sign = async() => {
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ import { computed, withDefaults } from 'vue'
3
+ import type { IStepProps } from '../model/types'
4
+ import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader'
5
+ import { Loader } from 'src/shared/ui/Loader'
6
+
7
+ const props = withDefaults(defineProps<IStepProps & {
8
+ html?: string
9
+ onSign?: () => void
10
+ onBack?: () => void
11
+ }>(), {})
12
+
13
+ const emits = defineEmits<{
14
+ back: []
15
+ sign: []
16
+ }>()
17
+
18
+ const isActive = computed(() => props.isActive)
19
+ const isDone = computed(() => props.isDone)
20
+
21
+ const handleSign = () => {
22
+ emits('sign')
23
+ }
24
+
25
+ const handleBack = () => {
26
+ emits('back')
27
+ }
28
+ </script>
29
+
30
+ <template lang="pug">
31
+ q-step(
32
+ :name="2"
33
+ title="Соглашение о подключении"
34
+ icon="description"
35
+ :done="isDone"
36
+ )
37
+ .q-pa-md
38
+ template(v-if="html")
39
+ DocumentHtmlReader(:html="html")
40
+ template(v-else)
41
+ Loader(:text='`Готовим соглашение...`')
42
+
43
+ q-stepper-navigation.q-gutter-sm(v-if="html")
44
+ q-btn(
45
+ v-if="isActive"
46
+ color="grey-6"
47
+ flat
48
+ label="Назад"
49
+ @click="handleBack"
50
+ )
51
+ q-btn(
52
+ v-if="isActive"
53
+ color="primary"
54
+ label="Подписать"
55
+ @click="handleSign"
56
+ )
57
+ </template>
@@ -0,0 +1,51 @@
1
+ <script setup lang="ts">
2
+ import { withDefaults } from 'vue'
3
+ import type { IStepProps } from '../model/types'
4
+ import { AddCooperativeForm } from 'src/features/Union/AddCooperative'
5
+
6
+ withDefaults(defineProps<IStepProps & {
7
+ signedDocument?: any
8
+ onFinish?: () => void
9
+ onBack?: () => void
10
+ }>(), {})
11
+
12
+ const emits = defineEmits<{
13
+ back: []
14
+ finish: []
15
+ }>()
16
+
17
+ const handleFinish = () => {
18
+ emits('finish')
19
+ }
20
+
21
+ const handleBack = () => {
22
+ emits('back')
23
+ }
24
+ </script>
25
+
26
+ <template lang="pug">
27
+ q-step(
28
+ :name="3"
29
+ title="Настройка кооператива"
30
+ icon="settings"
31
+ :done="isDone"
32
+ )
33
+ .q-pa-md
34
+ p.text-h6.q-mb-md Предварительная настройка
35
+ p.q-mb-md
36
+ | Пожалуйста, укажите домен для установки Цифрового Кооператива. Также, укажите суммы вступительных и минимальных паевых взносов для физических лиц, юридических лиц и индивидуальных предпринимателей:
37
+
38
+ AddCooperativeForm(
39
+ v-if="signedDocument"
40
+ :document="signedDocument"
41
+ @finish="handleFinish"
42
+ )
43
+
44
+ q-stepper-navigation.q-gutter-sm(v-if="signedDocument")
45
+ q-btn(
46
+ color="grey-6"
47
+ flat
48
+ label="Назад"
49
+ @click="handleBack"
50
+ )
51
+ </template>
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ import { computed, withDefaults, ref } from 'vue'
3
+ import type { IStepProps } from '../model/types'
4
+ import { TariffSelector, type ITariff } from '../Tariffs'
5
+
6
+ const props = withDefaults(defineProps<IStepProps & {
7
+ onContinue?: () => void
8
+ }>(), {})
9
+
10
+ const emits = defineEmits<{
11
+ tariffSelected: [tariff: ITariff]
12
+ tariffDeselected: []
13
+ }>()
14
+
15
+ const isActive = computed(() => props.isActive)
16
+ const isDone = computed(() => props.isDone)
17
+
18
+ const selectedTariff = ref<ITariff | null>(null)
19
+
20
+ const canContinue = computed(() => selectedTariff.value !== null)
21
+
22
+ const handleTariffSelected = (tariff: ITariff) => {
23
+ selectedTariff.value = tariff
24
+ emits('tariffSelected', tariff)
25
+ }
26
+
27
+ const handleTariffDeselected = () => {
28
+ selectedTariff.value = null
29
+ emits('tariffDeselected')
30
+ }
31
+
32
+ const handleContinue = () => {
33
+ if (canContinue.value && props.onContinue) {
34
+ props.onContinue()
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <template lang="pug">
40
+ q-step(
41
+ :name="1"
42
+ title="Выберите тариф"
43
+ icon="info"
44
+ :done="isDone"
45
+ )
46
+ .q-pa-md
47
+
48
+ TariffSelector(
49
+ :disabled="!isActive"
50
+ @tariff-selected="handleTariffSelected"
51
+ @tariff-deselected="handleTariffDeselected"
52
+ )
53
+
54
+ q-stepper-navigation.q-gutter-sm
55
+ q-btn(
56
+ v-if="isActive"
57
+ color="primary"
58
+ :disable="!canContinue"
59
+ label="Продолжить"
60
+ @click="handleContinue"
61
+ )
62
+ </template>
@@ -0,0 +1,163 @@
1
+ <script setup lang="ts">
2
+ import { computed, withDefaults } from 'vue'
3
+ import type { IStepProps } from '../model/types'
4
+ import { copyToClipboard } from 'quasar'
5
+ import { SuccessAlert } from 'src/shared/api'
6
+
7
+ const props = withDefaults(defineProps<IStepProps & {
8
+ coop?: any
9
+ domainValid?: boolean | null
10
+ installationProgress?: number | null
11
+ instanceStatus?: string | null
12
+ subscriptionsLoading?: boolean
13
+ subscriptionsError?: string | null
14
+ onReload?: () => void
15
+ onBack?: () => void
16
+ }>(), {
17
+ domainValid: null,
18
+ installationProgress: null,
19
+ instanceStatus: null,
20
+ subscriptionsLoading: false,
21
+ subscriptionsError: null
22
+ })
23
+
24
+ const emits = defineEmits<{
25
+ back: []
26
+ reload: []
27
+ }>()
28
+
29
+ const isDone = computed(() => props.isDone)
30
+
31
+ const handleBack = () => {
32
+ emits('back')
33
+ }
34
+
35
+ const handleReload = () => {
36
+ emits('reload')
37
+ }
38
+
39
+ const instruction = computed(() => `Создайте A-запись домена ${props.coop?.announce} на IP-адрес: 51.250.114.13`)
40
+
41
+ const copy = () => {
42
+ copyToClipboard(instruction.value)
43
+ .then(() => {
44
+ SuccessAlert('Инструкция скопирована в буфер')
45
+ })
46
+ .catch((e) => {
47
+ console.log(e)
48
+ })
49
+ }
50
+ </script>
51
+
52
+ <template lang="pug">
53
+ q-step(
54
+ :name="4"
55
+ title="Установка кооператива"
56
+ icon="hourglass_top"
57
+ :done="isDone"
58
+ )
59
+ .q-pa-md
60
+ p.text-h6.q-mb-md Кооператив на подключении
61
+ p.q-mb-md Статус:
62
+ q-badge(
63
+ v-if="coop?.status == 'pending'"
64
+ color="orange"
65
+ ).q-ml-sm ожидание
66
+ q-badge(
67
+ v-if="coop?.status == 'active'"
68
+ color="teal"
69
+ ).q-ml-sm активен
70
+ q-badge(
71
+ v-if="coop?.status == 'blocked'"
72
+ color="red"
73
+ ).q-ml-sm заблокирован
74
+
75
+ q-btn(
76
+ color="grey-6"
77
+ size="sm"
78
+ flat
79
+ label="Назад"
80
+ @click="handleBack"
81
+ )
82
+ q-btn(
83
+ color="primary"
84
+ size="sm"
85
+ icon="refresh"
86
+ @click="handleReload"
87
+ ).q-ml-md
88
+ span обновить
89
+
90
+ .q-mt-md
91
+ p.text-subtitle1 Статус подписки на хостинг:
92
+
93
+ div.flex.items-center.q-gutter-sm.q-mt-sm
94
+ div
95
+ span.text-body2 Валидность домена:
96
+ q-badge(
97
+ v-if="domainValid === true"
98
+ color="green"
99
+ ).q-ml-sm валиден
100
+ q-badge(
101
+ v-if="domainValid === false"
102
+ color="red"
103
+ ).q-ml-sm не валиден
104
+ q-badge(
105
+ v-if="domainValid === null && !subscriptionsLoading"
106
+ color="grey"
107
+ ).q-ml-sm неизвестно
108
+ q-badge(
109
+ v-if="subscriptionsLoading"
110
+ color="blue"
111
+ ).q-ml-sm загрузка...
112
+
113
+ div
114
+ span.text-body2 Прогресс установки:
115
+ q-badge(
116
+ v-if="installationProgress !== null"
117
+ :color="installationProgress === 100 ? 'green' : 'orange'"
118
+ ).q-ml-sm {{ installationProgress }}%
119
+ q-badge(
120
+ v-if="installationProgress === null && !subscriptionsLoading"
121
+ color="grey"
122
+ ).q-ml-sm неизвестно
123
+ q-badge(
124
+ v-if="subscriptionsLoading"
125
+ color="blue"
126
+ ).q-ml-sm загрузка...
127
+
128
+ div
129
+ span.text-body2 Статус сервера:
130
+ q-badge(
131
+ v-if="instanceStatus"
132
+ :color="instanceStatus === 'active' ? 'green' : instanceStatus === 'error' ? 'red' : 'orange'"
133
+ ).q-ml-sm {{ instanceStatus }}
134
+ q-badge(
135
+ v-if="!instanceStatus && !subscriptionsLoading"
136
+ color="grey"
137
+ ).q-ml-sm неизвестно
138
+ q-badge(
139
+ v-if="subscriptionsLoading"
140
+ color="blue"
141
+ ).q-ml-sm загрузка...
142
+
143
+ .q-mt-md
144
+ div(
145
+ v-if="subscriptionsError"
146
+ ).q-mb-md
147
+ q-banner(
148
+ :class="'text-white bg-red-500'"
149
+ rounded
150
+ )
151
+ template(v-slot:avatar)
152
+ q-icon(name="error" color="white")
153
+ span {{ subscriptionsError }}
154
+
155
+ p.q-mt-md
156
+ | Пожалуйста, перешлите инструкцию ниже вашему техническому специалисту. После её выполнения, мы автоматически выполним запуск. Далее, Вам необходимо завершить установку уже на Вашем сайте следуя инструкциям, представленным там.
157
+
158
+ q-card(flat bordered).q-pa-sm.q-mt-md
159
+ p.text-bold Инструкция
160
+ div.flex.justify-between
161
+ span {{ instruction }}
162
+ q-btn(size="sm" icon="fas fa-copy" flat @click="copy")
163
+ </template>
@@ -0,0 +1,4 @@
1
+ export { default as IntroStep } from './IntroStep.vue'
2
+ export { default as AgreementStep } from './AgreementStep.vue'
3
+ export { default as FormStep } from './FormStep.vue'
4
+ export { default as WaitingStep } from './WaitingStep.vue'
@@ -0,0 +1,278 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ export interface ITariff {
5
+ id: string
6
+ name: string
7
+ description: string
8
+ features: string[]
9
+ price: string
10
+ additionalCosts?: string[]
11
+ }
12
+
13
+ const props = defineProps<{
14
+ tariff: ITariff
15
+ selected?: boolean
16
+ disabled?: boolean
17
+ }>()
18
+
19
+ const emits = defineEmits<{
20
+ select: [tariffId: string]
21
+ deselect: [tariffId: string]
22
+ }>()
23
+
24
+ const isSelected = computed(() => props.selected)
25
+ const isDisabled = computed(() => props.disabled)
26
+
27
+ const cardClasses = computed(() => {
28
+ const classes = ['tariff-card', 'cursor-pointer', 'transition-all', 'duration-200']
29
+
30
+ if (isSelected.value) {
31
+ classes.push('tariff-selected')
32
+ }
33
+
34
+ if (isDisabled.value) {
35
+ classes.push('tariff-disabled')
36
+ }
37
+
38
+ return classes.join(' ')
39
+ })
40
+
41
+ const handleSelect = () => {
42
+ if (!isDisabled.value) {
43
+ // Если тариф уже выбран, снимаем выбор, иначе выбираем
44
+ if (isSelected.value) {
45
+ emits('deselect', props.tariff.id)
46
+ } else {
47
+ emits('select', props.tariff.id)
48
+ }
49
+ }
50
+ }
51
+ </script>
52
+
53
+ <template lang="pug">
54
+ div.tariff-card-container
55
+ .tariff-card(
56
+ :class="cardClasses"
57
+ @click="handleSelect"
58
+ )
59
+ .tariff-header
60
+ .tariff-checkmark(v-if="isSelected")
61
+ q-icon(name="check_circle" size="24px" color="white")
62
+ .tariff-title
63
+ h6.text-h6 {{ tariff.name }}
64
+
65
+ .tariff-content
66
+ p.text-body2.text-grey-7.q-mb-md.text-center {{ tariff.description }}
67
+
68
+ .tariff-features
69
+ .feature-item(v-for="feature in tariff.features" :key="feature")
70
+ q-icon(name="check" size="16px" color="positive").q-mr-xs
71
+ span {{ feature }}
72
+
73
+ template(v-if="tariff.additionalCosts && tariff.additionalCosts.length")
74
+ .tariff-additional.q-mt-md
75
+ p.text-subtitle2.text-grey-8 Дополнительные расходы:
76
+ .additional-item(v-for="cost in tariff.additionalCosts" :key="cost")
77
+ q-icon(name="add_circle_outline" size="14px" color="info").q-mr-xs
78
+ span.text-caption {{ cost }}
79
+
80
+ .tariff-footer
81
+ .tariff-price
82
+ .price-display {{ tariff.price }}
83
+ .price-period(v-if="tariff.price !== 'Бесплатно'") в месяц
84
+ .select-hint.text-caption.text-grey-6
85
+ | Нажмите для выбора
86
+ </template>
87
+
88
+ <style scoped>
89
+ .tariff-card-container {
90
+ position: relative;
91
+ height: 100%;
92
+ }
93
+
94
+ .tariff-card {
95
+ position: relative;
96
+ height: 100%;
97
+ min-height: 360px; /* Уменьшенная минимальная высота */
98
+ padding: 24px;
99
+ border-radius: 16px;
100
+ background: white;
101
+ border: 2px solid #f0f0f0;
102
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
103
+ cursor: pointer;
104
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
105
+ overflow: hidden;
106
+ display: flex;
107
+ flex-direction: column;
108
+ }
109
+
110
+ .tariff-card:hover:not(.tariff-disabled):not(.tariff-selected) {
111
+ transform: translateY(-4px);
112
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
113
+ border-color: #e0e0e0;
114
+ }
115
+
116
+ .tariff-selected {
117
+ border-color: var(--q-accent);
118
+ background: linear-gradient(135deg, rgba(25, 118, 210, 0.05) 0%, rgba(25, 118, 210, 0.02) 100%);
119
+ box-shadow:
120
+ 0 8px 32px rgba(25, 118, 210, 0.2),
121
+ 0 0 0 1px var(--q-accent);
122
+ transform: translateY(-2px);
123
+ }
124
+
125
+ .tariff-disabled {
126
+ opacity: 0.6;
127
+ cursor: not-allowed !important;
128
+ pointer-events: none;
129
+ }
130
+
131
+ .tariff-header {
132
+ position: relative;
133
+ height: 40px; /* Фиксированная высота для предотвращения прыжков */
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ margin-bottom: 16px;
138
+ }
139
+
140
+ .tariff-checkmark {
141
+ position: absolute;
142
+ top: 50%;
143
+ right: 0;
144
+ transform: translateY(-50%) scale(0.8);
145
+ background: var(--q-accent);
146
+ border-radius: 50%;
147
+ padding: 4px;
148
+ box-shadow: 0 2px 8px rgba(25, 118, 210, 0.3);
149
+ opacity: 0;
150
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
151
+ }
152
+
153
+ .tariff-selected .tariff-checkmark {
154
+ opacity: 1;
155
+ transform: translateY(-50%) scale(1);
156
+ }
157
+
158
+ .tariff-title {
159
+ text-align: center;
160
+ margin-right: 36px; /* Отступ для иконки */
161
+ }
162
+
163
+ .tariff-content {
164
+ flex: 1;
165
+ display: flex;
166
+ flex-direction: column;
167
+ }
168
+
169
+ .tariff-features {
170
+ flex: 1;
171
+ margin-bottom: 16px;
172
+ }
173
+
174
+ .feature-item {
175
+ display: flex;
176
+ align-items: center;
177
+ margin-bottom: 8px;
178
+ padding: 4px 0;
179
+ transition: color 0.2s ease;
180
+ }
181
+
182
+ .feature-item:hover {
183
+ color: var(--q-primary);
184
+ }
185
+
186
+ .tariff-additional {
187
+ border-top: 1px solid #f5f5f5;
188
+ padding-top: 16px;
189
+ }
190
+
191
+ .additional-item {
192
+ display: flex;
193
+ align-items: center;
194
+ margin-bottom: 6px;
195
+ color: #666;
196
+ font-size: 12px;
197
+ }
198
+
199
+ .tariff-footer {
200
+ position: relative;
201
+ height: 80px; /* Увеличенная высота для богатого оформления */
202
+ margin-top: auto;
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: flex-end;
206
+ padding: 20px 32px 20px 12px;
207
+ }
208
+
209
+ .tariff-price {
210
+ display: flex;
211
+ flex-direction: column;
212
+ align-items: flex-end;
213
+ gap: 4px;
214
+ text-align: right;
215
+ }
216
+
217
+ .price-display {
218
+ font-size: 36px;
219
+ font-weight: 600;
220
+ letter-spacing: -1px;
221
+ background: linear-gradient(135deg, var(--q-accent) 0%, rgba(25, 118, 210, 0.8) 100%);
222
+ -webkit-background-clip: text;
223
+ -webkit-text-fill-color: transparent;
224
+ background-clip: text;
225
+ line-height: 1;
226
+ text-shadow: 0 2px 4px rgba(25, 118, 210, 0.3);
227
+ }
228
+
229
+ .price-period {
230
+ font-size: 12px;
231
+ font-weight: 400;
232
+ color: #888;
233
+ letter-spacing: 0.5px;
234
+ text-transform: uppercase;
235
+ opacity: 0.8;
236
+ }
237
+
238
+ .select-hint {
239
+ position: absolute;
240
+ bottom: 8px;
241
+ left: 50%;
242
+ transform: translateX(-50%);
243
+ color: #999;
244
+ font-style: italic;
245
+ opacity: 0;
246
+ animation: pulse 2s infinite;
247
+ pointer-events: none;
248
+ transition: opacity 0.3s ease;
249
+ }
250
+
251
+ .tariff-card:not(.tariff-selected):not(.tariff-disabled) .select-hint {
252
+ opacity: 1;
253
+ }
254
+
255
+ @keyframes pulse {
256
+ 0%, 100% {
257
+ opacity: 0.6;
258
+ }
259
+ 50% {
260
+ opacity: 1;
261
+ }
262
+ }
263
+
264
+ /* Responsive adjustments */
265
+ @media (max-width: 599px) {
266
+ .tariff-card {
267
+ padding: 16px;
268
+ }
269
+
270
+ .tariff-header {
271
+ margin-bottom: 12px;
272
+ }
273
+
274
+ .tariff-title h6 {
275
+ font-size: 1rem;
276
+ }
277
+ }
278
+ </style>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import { TariffCard, type ITariff } from './index'
4
+
5
+ // Доступные тарифы
6
+ const availableTariffs: ITariff[] = [
7
+ {
8
+ id: 'test',
9
+ name: 'Тестовый',
10
+ description: 'Единственный тариф на период бета-тестирования платформы',
11
+ price: '1500 RUB',
12
+ features: [
13
+ '150 AXON на счёт кооператива',
14
+ 'достаточно для 100 пакетов документов',
15
+ 'и 50 регистраций пайщиков',
16
+ 'Хостинг на изолированном сервере',
17
+ 'Техническая поддержка'
18
+ ],
19
+ additionalCosts: [
20
+ '5 AXON в день списывается со счёта кооператива',
21
+ '1 AXON списывается за каждый пакет документов',
22
+ '1 AXON списывается за регистрацию нового пайщика',
23
+ ]
24
+ }
25
+ ]
26
+
27
+ defineProps<{
28
+ disabled?: boolean
29
+ }>()
30
+
31
+ const emits = defineEmits<{
32
+ tariffSelected: [tariff: ITariff]
33
+ tariffDeselected: []
34
+ }>()
35
+
36
+ const selectedTariffId = ref<string>('')
37
+
38
+ const selectedTariff = computed(() => {
39
+ return availableTariffs.find(tariff => tariff.id === selectedTariffId.value)
40
+ })
41
+
42
+ const handleTariffSelect = (tariffId: string) => {
43
+ selectedTariffId.value = tariffId
44
+ const tariff = availableTariffs.find(t => t.id === tariffId)
45
+ if (tariff) {
46
+ emits('tariffSelected', tariff)
47
+ }
48
+ }
49
+
50
+ const handleTariffDeselect = (tariffId: string) => {
51
+ if (selectedTariffId.value === tariffId) {
52
+ selectedTariffId.value = ''
53
+ emits('tariffDeselected')
54
+ }
55
+ }
56
+
57
+ // Экспортируем для использования в родительском компоненте
58
+ defineExpose({
59
+ selectedTariff,
60
+ hasSelection: computed(() => !!selectedTariffId.value)
61
+ })
62
+ </script>
63
+
64
+ <template lang="pug">
65
+ div
66
+ .text-center.q-mb-lg
67
+ //- h5.text-h5.q-mb-sm Выберите тариф
68
+ p.text-body2.text-grey-7 Выберите подходящий тариф для вашего кооператива
69
+
70
+ .tariff-grid
71
+ div(v-for="tariff in availableTariffs" :key="tariff.id")
72
+ TariffCard(
73
+ :tariff="tariff"
74
+ :selected="selectedTariffId === tariff.id"
75
+ :disabled="disabled"
76
+ @select="handleTariffSelect"
77
+ @deselect="handleTariffDeselect"
78
+ )</template>
79
+
80
+ <style scoped>
81
+ .tariff-grid {
82
+ display: grid;
83
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
84
+ gap: 24px;
85
+ max-width: 800px;
86
+ margin: 0 auto;
87
+ }
88
+
89
+ @media (max-width: 599px) {
90
+ .tariff-grid {
91
+ grid-template-columns: 1fr;
92
+ gap: 16px;
93
+ }
94
+ }
95
+ </style>
@@ -0,0 +1,3 @@
1
+ export { default as TariffCard } from './TariffCard.vue'
2
+ export { default as TariffSelector } from './TariffSelector.vue'
3
+ export type { ITariff } from './TariffCard.vue'
@@ -0,0 +1,2 @@
1
+ export { ConnectionAgreementStepper } from './ui'
2
+ export type { IStepperProps, IStepProps } from './model'
@@ -0,0 +1 @@
1
+ export type { IStepperProps, IStepProps } from './types'
@@ -0,0 +1,13 @@
1
+ export interface IStepperProps {
2
+ currentStep: number
3
+ isFinish: boolean
4
+ signedDocument: any
5
+ coop: any
6
+ html: string
7
+ }
8
+
9
+ export interface IStepProps {
10
+ currentStep: number
11
+ isActive: boolean
12
+ isDone: boolean
13
+ }
@@ -0,0 +1,123 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import { IntroStep, AgreementStep, FormStep, WaitingStep } from '../Steps/index'
4
+
5
+ const props = defineProps<{
6
+ initialStep?: number
7
+ isFinish: boolean
8
+ signedDocument: any
9
+ coop: any
10
+ html?: string
11
+ domainValid?: boolean | null
12
+ installationProgress?: number | null
13
+ instanceStatus?: string | null
14
+ subscriptionsLoading?: boolean
15
+ subscriptionsError?: string | null
16
+ }>()
17
+
18
+ const emits = defineEmits<{
19
+ stepChange: [step: number]
20
+ tariffSelected: [tariff: any]
21
+ tariffDeselected: []
22
+ continue: []
23
+ sign: []
24
+ finish: []
25
+ reload: []
26
+ }>()
27
+
28
+ const currentStep = ref(props.initialStep || 1)
29
+
30
+ // Управление шагами
31
+ const goToNext = () => {
32
+ if (currentStep.value < 4) {
33
+ currentStep.value++
34
+ emits('stepChange', currentStep.value)
35
+ }
36
+ }
37
+
38
+ const goToPrev = () => {
39
+ if (currentStep.value > 1) {
40
+ currentStep.value--
41
+ emits('stepChange', currentStep.value)
42
+ }
43
+ }
44
+
45
+ const handleContinue = () => {
46
+ goToNext()
47
+ emits('continue')
48
+ }
49
+
50
+ const handleSign = () => {
51
+ goToNext()
52
+ emits('sign')
53
+ }
54
+
55
+ const handleFinish = () => {
56
+ goToNext()
57
+ emits('finish')
58
+ }
59
+
60
+ const handleReload = () => {
61
+ emits('reload')
62
+ }
63
+
64
+ const handleTariffSelected = (tariff: any) => {
65
+ emits('tariffSelected', tariff)
66
+ }
67
+
68
+ const handleTariffDeselected = () => {
69
+ emits('tariffDeselected')
70
+ }
71
+ </script>
72
+
73
+ <template lang="pug">
74
+ div
75
+ q-stepper(
76
+ :model-value="currentStep"
77
+ flat
78
+ vertical
79
+ color="accent"
80
+ animated
81
+ done-color="teal"
82
+ )
83
+ IntroStep(
84
+ :current-step="currentStep"
85
+ :is-active="currentStep === 1"
86
+ :is-done="currentStep > 1"
87
+ @continue="handleContinue"
88
+ @tariff-selected="handleTariffSelected"
89
+ @tariff-deselected="handleTariffDeselected"
90
+ )
91
+
92
+ AgreementStep(
93
+ :current-step="currentStep"
94
+ :is-active="currentStep === 2"
95
+ :is-done="currentStep > 2"
96
+ :html="html"
97
+ @back="goToPrev"
98
+ @sign="handleSign"
99
+ )
100
+
101
+ FormStep(
102
+ :current-step="currentStep"
103
+ :is-active="currentStep === 3"
104
+ :is-done="currentStep > 3"
105
+ :signed-document="signedDocument"
106
+ @back="goToPrev"
107
+ @finish="handleFinish"
108
+ )
109
+
110
+ WaitingStep(
111
+ :current-step="currentStep"
112
+ :is-active="currentStep === 4"
113
+ :is-done="false"
114
+ :coop="coop"
115
+ :domain-valid="domainValid"
116
+ :installation-progress="installationProgress"
117
+ :instance-status="instanceStatus"
118
+ :subscriptions-loading="subscriptionsLoading"
119
+ :subscriptions-error="subscriptionsError"
120
+ @back="goToPrev"
121
+ @reload="handleReload"
122
+ )
123
+ </template>
@@ -0,0 +1 @@
1
+ export { default as ConnectionAgreementStepper } from './ConnectionAgreementStepper.vue'