@coopenomics/desktop 2025.11.12-alpha-1 → 2025.11.13-alpha-3
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/extensions/participant/install.ts +16 -1
- package/package.json +6 -6
- package/src/entities/ConnectionAgreement/model/store.ts +4 -2
- package/src/pages/Union/ConnectionAgreement/ConnectionAgreementPage.vue +139 -91
- package/src/pages/Union/ConnectionAgreement/InstallationCompletedPage.vue +221 -0
- package/src/pages/Union/ConnectionAgreement/index.ts +1 -0
- package/src/widgets/ConnectionAgreementStepper/Steps/AgreementStep.vue +1 -1
- package/src/widgets/ConnectionAgreementStepper/Steps/ApprovalWaitingStep.vue +202 -50
- package/src/widgets/ConnectionAgreementStepper/Steps/DomainValidationStep.vue +4 -4
- package/src/widgets/ConnectionAgreementStepper/Steps/FormStep.vue +6 -2
- package/src/widgets/ConnectionAgreementStepper/Steps/InstallationStep.vue +342 -68
- package/src/widgets/ConnectionAgreementStepper/Steps/IntroStep.vue +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { markRaw } from 'vue';
|
|
2
2
|
import { ProfilePage } from 'src/pages/User/ProfilePage';
|
|
3
3
|
import { WalletPage } from 'src/pages/User/WalletPage';
|
|
4
|
-
import { ConnectionAgreementPage } from 'src/pages/Union/ConnectionAgreement';
|
|
4
|
+
import { ConnectionAgreementPage, InstallationCompletedPage } from 'src/pages/Union/ConnectionAgreement';
|
|
5
5
|
import { UserPaymentMethodsPage } from 'src/pages/User/PaymentMethodsPage';
|
|
6
6
|
import { ContactsPage } from 'src/pages/Contacts';
|
|
7
7
|
import { ListOfMeetsPage } from 'src/pages/Cooperative/ListOfMeets';
|
|
@@ -64,6 +64,21 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
|
|
64
64
|
path: '/:coopname/connect',
|
|
65
65
|
name: 'connect',
|
|
66
66
|
component: markRaw(ConnectionAgreementPage),
|
|
67
|
+
children: [
|
|
68
|
+
{
|
|
69
|
+
path: 'completed',
|
|
70
|
+
name: 'installation-completed',
|
|
71
|
+
component: markRaw(InstallationCompletedPage),
|
|
72
|
+
meta: {
|
|
73
|
+
title: 'Установка завершена',
|
|
74
|
+
icon: 'fas fa-check-circle',
|
|
75
|
+
roles: ['user'],
|
|
76
|
+
conditions: 'isCoop === true && coopname === "voskhod"',
|
|
77
|
+
requiresAuth: true,
|
|
78
|
+
hidden: true,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
67
82
|
},
|
|
68
83
|
{
|
|
69
84
|
meta: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coopenomics/desktop",
|
|
3
|
-
"version": "2025.11.
|
|
3
|
+
"version": "2025.11.13-alpha-3",
|
|
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.
|
|
29
|
-
"@coopenomics/notifications": "2025.11.
|
|
30
|
-
"@coopenomics/sdk": "2025.11.
|
|
28
|
+
"@coopenomics/controller": "2025.11.13-alpha-3",
|
|
29
|
+
"@coopenomics/notifications": "2025.11.13-alpha-3",
|
|
30
|
+
"@coopenomics/sdk": "2025.11.13-alpha-3",
|
|
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.
|
|
62
|
+
"cooptypes": "2025.11.13-alpha-3",
|
|
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": "
|
|
126
|
+
"gitHead": "dab94657557e501109b96a18e0e00db0c00d111e"
|
|
127
127
|
}
|
|
@@ -139,14 +139,16 @@ export const useConnectionAgreementStore = defineStore(namespace, () => {
|
|
|
139
139
|
console.log('Текущий инстанс загружен:', currentInstance.value)
|
|
140
140
|
} catch (error: any) {
|
|
141
141
|
currentInstanceError.value = error.message || 'Ошибка загрузки инстанса'
|
|
142
|
+
// Очищаем старые данные при ошибке - они больше не актуальны
|
|
143
|
+
currentInstance.value = null
|
|
142
144
|
console.error('Ошибка при загрузке текущего инстанса:', error)
|
|
143
145
|
} finally {
|
|
144
146
|
currentInstanceLoading.value = false
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
const startInstanceAutoRefresh = (intervalMs = 30000) => { // 30 секунд по умолчанию
|
|
149
|
-
loadCurrentInstance() // Первая загрузка
|
|
150
|
+
const startInstanceAutoRefresh = async (intervalMs = 30000) => { // 30 секунд по умолчанию
|
|
151
|
+
await loadCurrentInstance() // Первая загрузка
|
|
150
152
|
|
|
151
153
|
const interval = setInterval(() => {
|
|
152
154
|
loadCurrentInstance()
|
|
@@ -1,134 +1,182 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
2
|
div.row.q-pa-md
|
|
3
3
|
div.col-md-12.col-xs-12
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
q-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
4
|
+
// Лоадер пока идет загрузка данных
|
|
5
|
+
WindowLoader(v-if="isLoading", text="Загрузка данных подключения...")
|
|
6
|
+
|
|
7
|
+
// Основной контент после загрузки
|
|
8
|
+
div(v-else)
|
|
9
|
+
div(v-if="system.info.is_providered")
|
|
10
|
+
//- Показываем дашборд если установка завершена и мы на основной странице
|
|
11
|
+
ConnectionDashboard(v-if="isInstallationCompleted && !isOnCompletionRoute")
|
|
12
|
+
|
|
13
|
+
//- Показываем степпер если идет процесс подключения
|
|
14
|
+
ConnectionAgreementStepper(v-else-if="!isOnCompletionRoute")
|
|
15
|
+
|
|
16
|
+
//- Router view для дочерних страниц (завершение установки) только на дочерних маршрутах
|
|
17
|
+
router-view(v-if="isOnCompletionRoute")
|
|
18
|
+
|
|
19
|
+
div(v-else).row
|
|
20
|
+
//- Заглушка для недоступного провайдера
|
|
21
|
+
div.col-md-12.col-xs-12
|
|
22
|
+
ColorCard(color="blue")
|
|
23
|
+
.text-center.q-pa-md
|
|
24
|
+
q-icon(name="fas fa-info-circle" size="2rem").q-mb-sm
|
|
25
|
+
.text-h6.q-mb-md Подключение к Кооперативной Экономике
|
|
26
|
+
p Для запуска вашего Цифрового Кооператива и подключения к платформе Кооперативной Экономики обратитесь в ПК ВОСХОД.
|
|
27
|
+
q-btn(
|
|
28
|
+
color="primary"
|
|
29
|
+
label="Перейти на сайт"
|
|
30
|
+
@click="openProviderWebsite"
|
|
31
|
+
size="md"
|
|
32
|
+
).q-mt-md
|
|
29
33
|
|
|
30
34
|
</template>
|
|
31
35
|
<script setup lang="ts">
|
|
32
|
-
import { computed, onMounted, onUnmounted, watch } from 'vue';
|
|
36
|
+
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
|
37
|
+
import { useRouter } from 'vue-router';
|
|
33
38
|
import { useSystemStore } from 'src/entities/System/model';
|
|
34
39
|
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement';
|
|
35
40
|
import { ConnectionAgreementStepper } from 'src/widgets/ConnectionAgreementStepper';
|
|
36
41
|
import { ConnectionDashboard } from 'src/widgets/ConnectionDashboard';
|
|
37
42
|
import { ColorCard } from 'src/shared/ui';
|
|
38
|
-
import {
|
|
43
|
+
import { WindowLoader } from 'src/shared/ui/Loader';
|
|
44
|
+
import { Zeus } from '@coopenomics/sdk';
|
|
39
45
|
|
|
40
|
-
const
|
|
41
|
-
const
|
|
46
|
+
const router = useRouter();
|
|
47
|
+
const system = useSystemStore();
|
|
48
|
+
const connectionAgreement = useConnectionAgreementStore();
|
|
49
|
+
|
|
50
|
+
// Лоадер состояния
|
|
51
|
+
const isLoading = ref(true);
|
|
42
52
|
|
|
43
53
|
// Остановка автообновления при размонтировании компонента
|
|
44
|
-
let stopInstanceRefresh: (() => void) | null = null
|
|
54
|
+
let stopInstanceRefresh: (() => void) | null = null;
|
|
55
|
+
|
|
56
|
+
// Редирект теперь делает только InstallationStep.vue
|
|
45
57
|
|
|
46
58
|
// Проверка завершения установки
|
|
47
59
|
const isInstallationCompleted = computed(() => {
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
// После загрузки данных проверяем статус установки
|
|
61
|
+
if (!isLoading.value) {
|
|
62
|
+
const instance = connectionAgreement.currentInstance;
|
|
63
|
+
return instance?.progress === 100 && instance?.status === Zeus.InstanceStatus.ACTIVE;
|
|
64
|
+
}
|
|
65
|
+
return false; // Во время загрузки считаем, что установка не завершена
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Проверка, находимся ли мы на маршруте завершения установки
|
|
69
|
+
const isOnCompletionRoute = computed(() => {
|
|
70
|
+
return router.currentRoute.value.name === 'installation-completed';
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Переменная для отслеживания предыдущего состояния завершения установки
|
|
74
|
+
let wasInstallationCompleted = false;
|
|
75
|
+
|
|
76
|
+
// Флаг для отслеживания, был ли уже показан степпер (означает, что пользователь видел процесс установки)
|
|
77
|
+
let hasShownStepper = false;
|
|
78
|
+
|
|
79
|
+
// Следим за завершением установки для редиректа
|
|
80
|
+
watch(isInstallationCompleted, (isCompleted) => {
|
|
81
|
+
// Редирект только при переходе из незавершенного состояния в завершенное
|
|
82
|
+
// и только если пользователь уже видел степпер (т.е. установка шла в реальном времени)
|
|
83
|
+
if (isCompleted && !wasInstallationCompleted && hasShownStepper && !isOnCompletionRoute.value) {
|
|
84
|
+
console.log('🎉 Установка завершена в реальном времени! → переадресация на страницу завершения')
|
|
85
|
+
router.push({ name: 'installation-completed' })
|
|
86
|
+
}
|
|
87
|
+
wasInstallationCompleted = isCompleted
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Следим за показом степпера
|
|
91
|
+
watch(() => !isInstallationCompleted.value && !isLoading.value && !isOnCompletionRoute.value, (isShowingStepper) => {
|
|
92
|
+
if (isShowingStepper) {
|
|
93
|
+
hasShownStepper = true
|
|
94
|
+
}
|
|
50
95
|
})
|
|
51
96
|
|
|
52
97
|
const openProviderWebsite = () => {
|
|
53
|
-
window.open('https://цифровой-кооператив.рф', '_blank')
|
|
54
|
-
}
|
|
98
|
+
window.open('https://цифровой-кооператив.рф', '_blank');
|
|
99
|
+
};
|
|
55
100
|
|
|
56
101
|
const init = async () => {
|
|
57
102
|
// Инициализация имеет смысл только если провайдер доступен
|
|
58
|
-
if (!system.info.is_providered)
|
|
103
|
+
if (!system.info.is_providered) {
|
|
104
|
+
isLoading.value = false;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
59
107
|
|
|
60
108
|
// Инициализируем persistent store если он еще не инициализирован
|
|
61
109
|
if (!connectionAgreement.isInitialized) {
|
|
62
|
-
connectionAgreement.setInitialized(true)
|
|
110
|
+
connectionAgreement.setInitialized(true);
|
|
63
111
|
}
|
|
64
112
|
|
|
113
|
+
// Запускаем автообновление инстанса каждые 30 секунд (включает начальную загрузку)
|
|
114
|
+
stopInstanceRefresh = await connectionAgreement.startInstanceAutoRefresh(30000);
|
|
65
115
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Запускаем автообновление инстанса каждые 30 секунд
|
|
70
|
-
stopInstanceRefresh = connectionAgreement.startInstanceAutoRefresh(30000)
|
|
71
|
-
}
|
|
116
|
+
// Скрываем лоадер после загрузки данных
|
|
117
|
+
isLoading.value = false;
|
|
118
|
+
};
|
|
72
119
|
|
|
73
120
|
// Watch за изменением currentInstance для автоматического перехода между шагами
|
|
74
|
-
watch(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
121
|
+
watch(
|
|
122
|
+
() => connectionAgreement.currentInstance,
|
|
123
|
+
(instance) => {
|
|
124
|
+
// Не обрабатываем изменения если идет загрузка или есть ошибка
|
|
125
|
+
if (connectionAgreement.currentInstanceLoading || connectionAgreement.currentInstanceError) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!instance) return;
|
|
130
|
+
|
|
131
|
+
const currentStep = connectionAgreement.currentStep;
|
|
132
|
+
|
|
133
|
+
console.log('📊 Instance обновлен:', {
|
|
134
|
+
step: currentStep,
|
|
135
|
+
is_valid: instance.is_valid,
|
|
136
|
+
is_delegated: instance.is_delegated,
|
|
137
|
+
blockchain_status: instance.blockchain_status,
|
|
138
|
+
progress: instance.progress,
|
|
139
|
+
status: instance.status,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Логика автоматических переходов (только для шагов 4, 5, 6)
|
|
143
|
+
if (currentStep === 4) {
|
|
144
|
+
// Шаг 4: Проверка домена
|
|
145
|
+
if (instance.is_valid && instance.is_delegated) {
|
|
146
|
+
// Домен валиден и делегирован
|
|
147
|
+
if (instance.blockchain_status === 'active') {
|
|
148
|
+
// Можно переходить сразу к установке
|
|
149
|
+
console.log('✅ Домен готов и blockchain_status активен → переход к шагу 6');
|
|
150
|
+
connectionAgreement.setCurrentStep(6);
|
|
151
|
+
} else {
|
|
152
|
+
// Ожидаем подтверждения от союза
|
|
153
|
+
console.log('⏳ Домен готов, но ожидаем подтверждения → переход к шагу 5');
|
|
154
|
+
connectionAgreement.setCurrentStep(5);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} else if (currentStep === 5) {
|
|
158
|
+
// Шаг 5: Ожидание подтверждения от союза
|
|
93
159
|
if (instance.blockchain_status === 'active') {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
connectionAgreement.setCurrentStep(6)
|
|
97
|
-
} else {
|
|
98
|
-
// Ожидаем подтверждения от союза
|
|
99
|
-
console.log('⏳ Домен готов, но ожидаем подтверждения → переход к шагу 5')
|
|
100
|
-
connectionAgreement.setCurrentStep(5)
|
|
160
|
+
console.log('✅ Подтверждение получено → переход к шагу 6');
|
|
161
|
+
connectionAgreement.setCurrentStep(6);
|
|
101
162
|
}
|
|
102
163
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
connectionAgreement.setCurrentStep(6)
|
|
108
|
-
}
|
|
109
|
-
} else if (currentStep === 6) {
|
|
110
|
-
// Шаг 6: Установка
|
|
111
|
-
if (instance.progress === 100 && instance.status === Zeus.InstanceStatus.ACTIVE) {
|
|
112
|
-
console.log('🎉 Установка завершена!')
|
|
113
|
-
// Не переходим автоматически, просто покажется дашборд через computed
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}, { deep: true })
|
|
164
|
+
// Редирект на страницу завершения теперь делает только InstallationStep.vue
|
|
165
|
+
},
|
|
166
|
+
{ deep: true }
|
|
167
|
+
);
|
|
117
168
|
|
|
118
169
|
// Lifecycle хуки
|
|
119
170
|
onMounted(() => {
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
// Если провайдер недоступен - ничего не делаем, показываем заглушку
|
|
125
|
-
})
|
|
171
|
+
// Делаем инициализацию при монтировании компонента
|
|
172
|
+
init();
|
|
173
|
+
});
|
|
126
174
|
|
|
127
175
|
onUnmounted(() => {
|
|
128
176
|
// Останавливаем автообновление инстанса при размонтировании компонента
|
|
129
177
|
if (stopInstanceRefresh) {
|
|
130
|
-
stopInstanceRefresh()
|
|
131
|
-
stopInstanceRefresh = null
|
|
178
|
+
stopInstanceRefresh();
|
|
179
|
+
stopInstanceRefresh = null;
|
|
132
180
|
}
|
|
133
|
-
})
|
|
181
|
+
});
|
|
134
182
|
</script>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
div.row.q-pa-md
|
|
3
|
+
div.col-md-12.col-xs-12
|
|
4
|
+
.completion-container
|
|
5
|
+
.completion-card
|
|
6
|
+
.completion-header
|
|
7
|
+
.celebration-icon
|
|
8
|
+
q-icon(name="celebration" color="positive" size="80px")
|
|
9
|
+
.sparkle.sparkle-1
|
|
10
|
+
.sparkle.sparkle-2
|
|
11
|
+
.sparkle.sparkle-3
|
|
12
|
+
.completion-title.text-h5.text-positive.q-mt-lg Установка завершена!
|
|
13
|
+
.completion-subtitle.text-body1.text-grey-7.q-mt-sm
|
|
14
|
+
| Ваш Цифровой Кооператив успешно развернут и подключен к платформе
|
|
15
|
+
|
|
16
|
+
.completion-details.q-mt-xl
|
|
17
|
+
.detail-item
|
|
18
|
+
q-icon(name="check_circle" color="positive" size="24px").q-mr-md
|
|
19
|
+
.detail-text
|
|
20
|
+
.text-body2.text-weight-medium Серверная инфраструктура
|
|
21
|
+
.text-caption.text-grey-6 Полностью настроена и оптимизирована
|
|
22
|
+
|
|
23
|
+
.detail-item.q-mt-md
|
|
24
|
+
q-icon(name="check_circle" color="positive" size="24px").q-mr-md
|
|
25
|
+
.detail-text
|
|
26
|
+
.text-body2.text-weight-medium Блокчейн-узел
|
|
27
|
+
.text-caption.text-grey-6 Развернут, синхронизирован и подключен к сети
|
|
28
|
+
|
|
29
|
+
.detail-item.q-mt-md
|
|
30
|
+
q-icon(name="check_circle" color="positive" size="24px").q-mr-md
|
|
31
|
+
.detail-text
|
|
32
|
+
.text-body2.text-weight-medium Базы данных
|
|
33
|
+
.text-caption.text-grey-6 Инициализированы и готовы к работе
|
|
34
|
+
|
|
35
|
+
.detail-item.q-mt-md
|
|
36
|
+
q-icon(name="check_circle" color="positive" size="24px").q-mr-md
|
|
37
|
+
.detail-text
|
|
38
|
+
.text-body2.text-weight-medium Сервисы кооператива
|
|
39
|
+
.text-caption.text-grey-6 Запущены и функционируют
|
|
40
|
+
|
|
41
|
+
.next-steps.q-mt-xl
|
|
42
|
+
.text-subtitle2.text-weight-medium.q-mb-md Что дальше?
|
|
43
|
+
.text-body2.text-grey-8.q-mb-lg
|
|
44
|
+
| Для завершения настройки необходимо выполнить финальную конфигурацию на сайте вашего Цифрового Кооператива. Нажмите кнопку ниже, чтобы перейти к управлению подключением, где сможете просматривать статус подключения, управлять подписками и настраивать параметры платформы.
|
|
45
|
+
|
|
46
|
+
q-btn(
|
|
47
|
+
color="primary"
|
|
48
|
+
label="Перейти к управлению подключением"
|
|
49
|
+
@click="goToDashboard"
|
|
50
|
+
size="lg"
|
|
51
|
+
unelevated
|
|
52
|
+
no-caps
|
|
53
|
+
).q-mt-md
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<script setup lang="ts">
|
|
57
|
+
import { useRouter } from 'vue-router'
|
|
58
|
+
|
|
59
|
+
const router = useRouter()
|
|
60
|
+
|
|
61
|
+
const goToDashboard = () => {
|
|
62
|
+
router.push({ name: 'connect' })
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<style scoped>
|
|
67
|
+
.completion-container {
|
|
68
|
+
display: flex;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
align-items: center;
|
|
71
|
+
min-height: 80vh;
|
|
72
|
+
padding: 2rem 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.completion-card {
|
|
76
|
+
max-width: 600px;
|
|
77
|
+
width: 100%;
|
|
78
|
+
text-align: center;
|
|
79
|
+
padding: 3rem;
|
|
80
|
+
border-radius: 24px;
|
|
81
|
+
border: 1px solid rgba(76, 175, 80, 0.1);
|
|
82
|
+
box-shadow:
|
|
83
|
+
0 12px 40px rgba(0, 0, 0, 0.08),
|
|
84
|
+
0 4px 16px rgba(0, 0, 0, 0.04);
|
|
85
|
+
position: relative;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.completion-card::before {
|
|
90
|
+
content: '';
|
|
91
|
+
position: absolute;
|
|
92
|
+
top: 0;
|
|
93
|
+
left: 0;
|
|
94
|
+
right: 0;
|
|
95
|
+
height: 4px;
|
|
96
|
+
background: linear-gradient(90deg, var(--q-positive) 0%, #4CAF50 50%, var(--q-positive) 100%);
|
|
97
|
+
background-size: 200% 100%;
|
|
98
|
+
animation: completion-shimmer 3s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@keyframes completion-shimmer {
|
|
102
|
+
0%, 100% { background-position: -100% 0; }
|
|
103
|
+
50% { background-position: 100% 0; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.completion-header {
|
|
107
|
+
position: relative;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.celebration-icon {
|
|
111
|
+
position: relative;
|
|
112
|
+
display: inline-block;
|
|
113
|
+
margin-bottom: 1rem;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.sparkle {
|
|
117
|
+
position: absolute;
|
|
118
|
+
width: 8px;
|
|
119
|
+
height: 8px;
|
|
120
|
+
background: var(--q-positive);
|
|
121
|
+
border-radius: 50%;
|
|
122
|
+
animation: sparkle 2s infinite ease-in-out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.sparkle-1 {
|
|
126
|
+
top: -10px;
|
|
127
|
+
right: -10px;
|
|
128
|
+
animation-delay: 0s;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.sparkle-2 {
|
|
132
|
+
top: 20px;
|
|
133
|
+
left: -15px;
|
|
134
|
+
animation-delay: 0.5s;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.sparkle-3 {
|
|
138
|
+
bottom: -5px;
|
|
139
|
+
right: 15px;
|
|
140
|
+
animation-delay: 1s;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@keyframes sparkle {
|
|
144
|
+
0%, 100% {
|
|
145
|
+
opacity: 0;
|
|
146
|
+
transform: scale(0);
|
|
147
|
+
}
|
|
148
|
+
50% {
|
|
149
|
+
opacity: 1;
|
|
150
|
+
transform: scale(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.completion-title {
|
|
155
|
+
font-weight: 700;
|
|
156
|
+
letter-spacing: -0.5px;
|
|
157
|
+
margin-bottom: 0.5rem;
|
|
158
|
+
background: linear-gradient(135deg, var(--q-positive) 0%, #4CAF50 100%);
|
|
159
|
+
-webkit-background-clip: text;
|
|
160
|
+
-webkit-text-fill-color: transparent;
|
|
161
|
+
background-clip: text;
|
|
162
|
+
text-shadow: 0 2px 4px rgba(76, 175, 80, 0.3);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.completion-details {
|
|
166
|
+
margin-top: 2rem;
|
|
167
|
+
text-align: left;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.detail-item {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
padding: 1rem;
|
|
174
|
+
background: rgba(76, 175, 80, 0.02);
|
|
175
|
+
border-radius: 12px;
|
|
176
|
+
border: 1px solid rgba(76, 175, 80, 0.1);
|
|
177
|
+
transition: all 0.3s ease;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.detail-item:hover {
|
|
181
|
+
transform: translateX(4px);
|
|
182
|
+
background: rgba(76, 175, 80, 0.05);
|
|
183
|
+
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.detail-text {
|
|
187
|
+
margin-left: 1rem;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.next-steps {
|
|
191
|
+
margin-top: 2rem;
|
|
192
|
+
text-align: left;
|
|
193
|
+
padding: 1.5rem;
|
|
194
|
+
background: rgba(0, 0, 0, 0.02);
|
|
195
|
+
border-radius: 12px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Адаптивность */
|
|
199
|
+
@media (max-width: 768px) {
|
|
200
|
+
.completion-card {
|
|
201
|
+
padding: 2rem;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.detail-item {
|
|
205
|
+
flex-direction: column;
|
|
206
|
+
text-align: center;
|
|
207
|
+
gap: 0.5rem;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.completion-container {
|
|
211
|
+
min-height: 70vh;
|
|
212
|
+
padding: 1rem 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@media (max-width: 480px) {
|
|
217
|
+
.completion-card {
|
|
218
|
+
padding: 1.5rem;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
</style>
|