@coopenomics/desktop 2025.11.18-alpha-1 → 2025.11.19-alpha-2
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 +6 -6
- package/src/entities/ConnectionAgreement/model/store.ts +10 -3
- package/src/features/Provider/api/index.ts +35 -1
- package/src/features/Provider/model/index.ts +65 -2
- package/src/pages/Union/ConnectionAgreement/ConnectionAgreementPage.vue +23 -19
- package/src/widgets/ConnectionAgreementStepper/Steps/DomainValidationStep.vue +4 -5
- package/src/widgets/ConnectionDashboard/ui/AxonWallet.vue +184 -0
- package/src/widgets/ConnectionDashboard/ui/ConnectionDashboard.vue +10 -138
- package/src/widgets/ConnectionDashboard/ui/DomainCard.vue +242 -0
- package/src/widgets/ConnectionDashboard/ui/SubscriptionsCard.vue +193 -0
- package/src/widgets/ConnectionDashboard/ui/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coopenomics/desktop",
|
|
3
|
-
"version": "2025.11.
|
|
3
|
+
"version": "2025.11.19-alpha-2",
|
|
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.19-alpha-2",
|
|
29
|
+
"@coopenomics/notifications": "2025.11.19-alpha-2",
|
|
30
|
+
"@coopenomics/sdk": "2025.11.19-alpha-2",
|
|
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.19-alpha-2",
|
|
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": "7a75365d2f7de18945779d16ceeecfe5a9aa6c8c"
|
|
127
127
|
}
|
|
@@ -135,12 +135,19 @@ export const useConnectionAgreementStore = defineStore(namespace, () => {
|
|
|
135
135
|
try {
|
|
136
136
|
currentInstanceLoading.value = true
|
|
137
137
|
currentInstanceError.value = null
|
|
138
|
-
|
|
138
|
+
const freshInstance = await getCurrentInstance()
|
|
139
|
+
|
|
140
|
+
// Обновляем данные только если получили свежие данные
|
|
141
|
+
// При ошибке оставляем старые данные в currentInstance (они могут быть из localStorage)
|
|
142
|
+
if (freshInstance !== null) {
|
|
143
|
+
currentInstance.value = freshInstance
|
|
144
|
+
}
|
|
145
|
+
|
|
139
146
|
console.log('Текущий инстанс загружен:', currentInstance.value)
|
|
140
147
|
} catch (error: any) {
|
|
141
148
|
currentInstanceError.value = error.message || 'Ошибка загрузки инстанса'
|
|
142
|
-
//
|
|
143
|
-
currentInstance.value
|
|
149
|
+
// НЕ очищаем старые данные при ошибке - они остаются актуальными из localStorage
|
|
150
|
+
// currentInstance.value остается как есть
|
|
144
151
|
console.error('Ошибка при загрузке текущего инстанса:', error)
|
|
145
152
|
} finally {
|
|
146
153
|
currentInstanceLoading.value = false
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { client } from 'src/shared/api/client';
|
|
2
|
-
import { Queries } from '@coopenomics/sdk';
|
|
2
|
+
import { Queries, Mutations } from '@coopenomics/sdk';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Получить подписки пользователя у провайдера
|
|
@@ -10,3 +10,37 @@ export async function loadProviderSubscriptions() {
|
|
|
10
10
|
|
|
11
11
|
return subscriptions;
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Генерирует документ для конвертации в AXON
|
|
16
|
+
*/
|
|
17
|
+
export async function generateConvertToAxonStatement(data: Mutations.Provider.GenerateConvertToAxonStatement.IInput['data'], options?: Mutations.Provider.GenerateConvertToAxonStatement.IInput['options']) {
|
|
18
|
+
const { [Mutations.Provider.GenerateConvertToAxonStatement.name]: generatedDocument } = await client.Mutation(
|
|
19
|
+
Mutations.Provider.GenerateConvertToAxonStatement.mutation,
|
|
20
|
+
{
|
|
21
|
+
variables: {
|
|
22
|
+
data,
|
|
23
|
+
options
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return generatedDocument;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Обрабатывает подписанный документ конвертации в AXON
|
|
33
|
+
*/
|
|
34
|
+
export async function processConvertToAxonStatement(signedDocument: Mutations.Provider.ProcessConvertToAxonStatement.IInput['signedDocument'], convertAmount: string) {
|
|
35
|
+
const { [Mutations.Provider.ProcessConvertToAxonStatement.name]: result } = await client.Mutation(
|
|
36
|
+
Mutations.Provider.ProcessConvertToAxonStatement.mutation,
|
|
37
|
+
{
|
|
38
|
+
variables: {
|
|
39
|
+
signedDocument,
|
|
40
|
+
convertAmount
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { ref, computed } from 'vue';
|
|
2
|
-
import { loadProviderSubscriptions } from '../api';
|
|
2
|
+
import { loadProviderSubscriptions, generateConvertToAxonStatement, processConvertToAxonStatement } from '../api';
|
|
3
3
|
import { useSystemStore } from 'src/entities/System/model';
|
|
4
|
-
import { Queries } from '@coopenomics/sdk';
|
|
4
|
+
import { Queries, Mutations } from '@coopenomics/sdk';
|
|
5
|
+
import { useSignDocument } from 'src/shared/lib/document/model/entity';
|
|
6
|
+
import { SuccessAlert, FailAlert } from 'src/shared/api';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Специфичные данные для подписки на хостинг
|
|
@@ -23,6 +25,9 @@ export type SubscriptionSpecificData = HostingSubscriptionData | null;
|
|
|
23
25
|
*/
|
|
24
26
|
export type ProviderSubscription = Queries.System.GetProviderSubscriptions.IOutput[typeof Queries.System.GetProviderSubscriptions.name][number];
|
|
25
27
|
|
|
28
|
+
// Курс конвертации AXON в валюту системы (RUB)
|
|
29
|
+
export const AXON_GOVERN_RATE = 10; // 1 AXON = 10 RUB
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
32
|
* Composable для работы с подписками провайдера
|
|
28
33
|
*/
|
|
@@ -32,6 +37,9 @@ export function useProviderSubscriptions() {
|
|
|
32
37
|
const isLoading = ref(false);
|
|
33
38
|
const error = ref<string | null>(null);
|
|
34
39
|
|
|
40
|
+
// IP адрес сервера для делегирования домена
|
|
41
|
+
const SERVER_IP = '51.250.114.13';
|
|
42
|
+
|
|
35
43
|
// Получить подписку на хостинг (id=1)
|
|
36
44
|
const hostingSubscription = computed(() =>
|
|
37
45
|
subscriptions.value.find(sub => sub.subscription_type_id === 1)
|
|
@@ -104,5 +112,60 @@ export function useProviderSubscriptions() {
|
|
|
104
112
|
instanceStatus,
|
|
105
113
|
loadSubscriptions,
|
|
106
114
|
startAutoRefresh,
|
|
115
|
+
SERVER_IP,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type IGenerateConvertToAxonStatementInput = Mutations.Provider.GenerateConvertToAxonStatement.IInput['data'];
|
|
120
|
+
export type IGenerateConvertToAxonStatementResult = Mutations.Provider.GenerateConvertToAxonStatement.IOutput[typeof Mutations.Provider.GenerateConvertToAxonStatement.name];
|
|
121
|
+
|
|
122
|
+
export type IProcessConvertToAxonStatementInput = Mutations.Provider.ProcessConvertToAxonStatement.IInput;
|
|
123
|
+
export type IProcessConvertToAxonStatementResult = Mutations.Provider.ProcessConvertToAxonStatement.IOutput[typeof Mutations.Provider.ProcessConvertToAxonStatement.name];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Composable для конвертации валюты в AXON
|
|
127
|
+
*/
|
|
128
|
+
export function useProviderAxonConvert() {
|
|
129
|
+
const loading = ref(false);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Конвертирует указанную сумму в AXON
|
|
133
|
+
*/
|
|
134
|
+
const convertToAxon = async (params: {
|
|
135
|
+
convertAmount: string;
|
|
136
|
+
username: string;
|
|
137
|
+
coopname: string;
|
|
138
|
+
}) => {
|
|
139
|
+
try {
|
|
140
|
+
loading.value = true;
|
|
141
|
+
|
|
142
|
+
// Генерируем документ конвертации
|
|
143
|
+
const generatedDocument = await generateConvertToAxonStatement({
|
|
144
|
+
convert_amount: params.convertAmount,
|
|
145
|
+
username: params.username,
|
|
146
|
+
coopname: params.coopname
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Подписываем документ
|
|
150
|
+
const { signDocument } = useSignDocument();
|
|
151
|
+
const signedDocument = await signDocument(generatedDocument, params.username);
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
// Обрабатываем подписанный документ
|
|
155
|
+
await processConvertToAxonStatement(signedDocument, params.convertAmount);
|
|
156
|
+
|
|
157
|
+
SuccessAlert('Конвертация успешно выполнена');
|
|
158
|
+
return true;
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
FailAlert(error || 'Не удалось выполнить конвертацию');
|
|
161
|
+
return false;
|
|
162
|
+
} finally {
|
|
163
|
+
loading.value = false;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
loading,
|
|
169
|
+
convertToAxon
|
|
107
170
|
};
|
|
108
171
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
|
-
div.row
|
|
2
|
+
div.row.q-pa-md
|
|
3
3
|
div.col-md-12.col-xs-12
|
|
4
4
|
// Лоадер пока идет загрузка данных
|
|
5
5
|
WindowLoader(v-if="isLoading", text="Загрузка данных подключения...")
|
|
@@ -8,7 +8,8 @@ div.row
|
|
|
8
8
|
div(v-else)
|
|
9
9
|
div(v-if="system.info.is_providered")
|
|
10
10
|
//- Показываем дашборд если установка завершена и мы на основной странице
|
|
11
|
-
|
|
11
|
+
div(v-if="isInstallationCompleted && !isOnCompletionRoute").relative
|
|
12
|
+
ConnectionDashboard
|
|
12
13
|
|
|
13
14
|
//- Показываем степпер если идет процесс подключения
|
|
14
15
|
ConnectionAgreementStepper(v-else-if="!isOnCompletionRoute")
|
|
@@ -107,22 +108,14 @@ const init = async () => {
|
|
|
107
108
|
console.log('SYSTEM.info.is_unioned', system.info.is_unioned, connectionAgreement.isInitialized);
|
|
108
109
|
|
|
109
110
|
// Запускаем автообновление инстанса каждые 30 секунд (включает начальную загрузку)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!isLoading) {
|
|
119
|
-
unwatch();
|
|
120
|
-
resolve();
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
111
|
+
// Не ждем завершения первой загрузки, чтобы избежать зависания при недоступности бэкенда
|
|
112
|
+
connectionAgreement.startInstanceAutoRefresh(30000).then((stop) => {
|
|
113
|
+
stopInstanceRefresh = stop;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Даем небольшую паузу для того, чтобы данные могли загрузиться из кэша или быстро
|
|
117
|
+
// Но не ждем обязательно завершения
|
|
118
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
126
119
|
|
|
127
120
|
// Инициализируем persistent store если он еще не инициализирован
|
|
128
121
|
if (!connectionAgreement.isInitialized) {
|
|
@@ -133,8 +126,19 @@ const init = async () => {
|
|
|
133
126
|
const hasInstanceError = connectionAgreement.currentInstanceError;
|
|
134
127
|
|
|
135
128
|
// Определяем шаг на основе текущего прогресса установки (при каждом заходе)
|
|
129
|
+
|
|
130
|
+
// Сначала проверяем, была ли установка уже завершена (даже при ошибке загрузки)
|
|
131
|
+
const isAlreadyCompleted = instance?.progress === 100 && instance?.status === Zeus.InstanceStatus.ACTIVE;
|
|
132
|
+
if (isAlreadyCompleted) {
|
|
133
|
+
console.log('✅ Установка уже завершена ранее, показываем дашборд');
|
|
134
|
+
// Не меняем шаг, оставляем текущий (или устанавливаем специальный шаг для завершенной установки)
|
|
135
|
+
isLoading.value = false;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
136
139
|
if (hasInstanceError) {
|
|
137
|
-
// Если есть ошибка загрузки инстанса,
|
|
140
|
+
// Если есть ошибка загрузки инстанса, но установки не было завершено ранее,
|
|
141
|
+
// начинаем с шага 1 по умолчанию
|
|
138
142
|
console.log('❌ Ошибка загрузки инстанса, устанавливаем шаг 1 по умолчанию');
|
|
139
143
|
connectionAgreement.setCurrentStep(1);
|
|
140
144
|
} else if (instance && typeof instance.progress === 'number' && instance.progress > 0) {
|
|
@@ -68,16 +68,18 @@
|
|
|
68
68
|
</template>
|
|
69
69
|
|
|
70
70
|
<script setup lang="ts">
|
|
71
|
-
import { computed,
|
|
71
|
+
import { computed, withDefaults, onMounted } from 'vue'
|
|
72
72
|
import { copyToClipboard } from 'quasar'
|
|
73
73
|
import { FailAlert, SuccessAlert } from 'src/shared/api'
|
|
74
74
|
import type { IStepProps } from '../model/types'
|
|
75
75
|
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
|
76
|
+
import { useProviderSubscriptions } from 'src/features/Provider/model'
|
|
76
77
|
|
|
77
78
|
const props = withDefaults(defineProps<IStepProps>(), {})
|
|
78
79
|
|
|
79
80
|
const connectionAgreement = useConnectionAgreementStore()
|
|
80
81
|
const { loadCurrentInstance } = connectionAgreement
|
|
82
|
+
const { SERVER_IP } = useProviderSubscriptions()
|
|
81
83
|
|
|
82
84
|
// Получаем данные напрямую из store
|
|
83
85
|
const coop = computed(() => connectionAgreement.coop)
|
|
@@ -98,9 +100,6 @@ onMounted(async () => {
|
|
|
98
100
|
await loadCoopIfNeeded()
|
|
99
101
|
})
|
|
100
102
|
|
|
101
|
-
// IP адрес сервера
|
|
102
|
-
const SERVER_IP = ref('51.250.114.13')
|
|
103
|
-
|
|
104
103
|
const isDone = computed(() => props.isDone)
|
|
105
104
|
|
|
106
105
|
|
|
@@ -118,7 +117,7 @@ const handleReload = async () => {
|
|
|
118
117
|
|
|
119
118
|
const copyIpAddress = async () => {
|
|
120
119
|
try {
|
|
121
|
-
await copyToClipboard(SERVER_IP
|
|
120
|
+
await copyToClipboard(SERVER_IP)
|
|
122
121
|
SuccessAlert('IP адрес скопирован в буфер обмена')
|
|
123
122
|
} catch (error) {
|
|
124
123
|
console.error('Ошибка копирования:', error)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.axon-wallet
|
|
3
|
+
ColorCard(color='purple', @click.stop)
|
|
4
|
+
// Заголовок
|
|
5
|
+
.wallet-header
|
|
6
|
+
.wallet-title
|
|
7
|
+
q-icon(name="account_balance_wallet" size="20px").q-mr-sm
|
|
8
|
+
| Кошелек AXON
|
|
9
|
+
|
|
10
|
+
// Описание
|
|
11
|
+
.wallet-description
|
|
12
|
+
.text-body2.text-grey-7
|
|
13
|
+
| AXON используется для оплаты пакетов документов. Минимально 5 AXON в день, по факту - от использования.
|
|
14
|
+
|
|
15
|
+
// Баланс
|
|
16
|
+
.balance-section
|
|
17
|
+
.balance-value {{ formattedBalance }}
|
|
18
|
+
.balance-label Доступно
|
|
19
|
+
|
|
20
|
+
// Действия
|
|
21
|
+
.actions-section
|
|
22
|
+
.action-buttons.q-pa-sm
|
|
23
|
+
q-btn(
|
|
24
|
+
color="primary"
|
|
25
|
+
icon="add"
|
|
26
|
+
label="Пополнить"
|
|
27
|
+
@click.stop="showDepositDialog = true"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// Диалог пополнения
|
|
31
|
+
q-dialog(v-model="showDepositDialog", @hide="clear")
|
|
32
|
+
ModalBase(title="Пополнение кошелька AXON")
|
|
33
|
+
Form.q-pa-sm(
|
|
34
|
+
:handler-submit="handlerSubmit",
|
|
35
|
+
:is-submitting="isSubmitting",
|
|
36
|
+
button-cancel-txt="Отменить",
|
|
37
|
+
button-submit-txt="Пополнить",
|
|
38
|
+
@cancel="clear"
|
|
39
|
+
)
|
|
40
|
+
q-input(
|
|
41
|
+
v-model="depositAmount",
|
|
42
|
+
standout="bg-teal text-white",
|
|
43
|
+
placeholder="Введите сумму в RUB",
|
|
44
|
+
type="number",
|
|
45
|
+
:min="0",
|
|
46
|
+
:step="10",
|
|
47
|
+
:hint="depositHint",
|
|
48
|
+
:rules="[(val) => val > 0 || 'Сумма должна быть положительной']"
|
|
49
|
+
)
|
|
50
|
+
template(#append)
|
|
51
|
+
span.text-overline RUB
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup lang="ts">
|
|
55
|
+
import { computed, ref } from 'vue';
|
|
56
|
+
import { useSessionStore } from 'src/entities/Session';
|
|
57
|
+
import { ColorCard } from 'src/shared/ui';
|
|
58
|
+
import { Form } from 'src/shared/ui/Form';
|
|
59
|
+
import { ModalBase } from 'src/shared/ui/ModalBase';
|
|
60
|
+
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
|
|
61
|
+
import { useProviderAxonConvert, AXON_GOVERN_RATE } from 'src/features/Provider/model';
|
|
62
|
+
import { useSystemStore } from 'src/entities/System/model';
|
|
63
|
+
|
|
64
|
+
const session = useSessionStore();
|
|
65
|
+
const system = useSystemStore();
|
|
66
|
+
const { convertToAxon } = useProviderAxonConvert();
|
|
67
|
+
|
|
68
|
+
// Диалог пополнения
|
|
69
|
+
const showDepositDialog = ref(false);
|
|
70
|
+
const depositAmount = ref('');
|
|
71
|
+
const isSubmitting = ref(false);
|
|
72
|
+
|
|
73
|
+
// Форматированный баланс AXON
|
|
74
|
+
const formattedBalance = computed(() => {
|
|
75
|
+
const balance = session.blockchainAccount?.core_liquid_balance || '0';
|
|
76
|
+
return formatAsset2Digits(`${balance} AXON`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Подсказка с расчетом AXON
|
|
80
|
+
const depositHint = computed(() => {
|
|
81
|
+
if (!depositAmount.value || parseFloat(depositAmount.value) <= 0) {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rubAmount = parseFloat(depositAmount.value);
|
|
86
|
+
const axonAmount = rubAmount / AXON_GOVERN_RATE;
|
|
87
|
+
return `Будет зачислено: ${formatAsset2Digits(`${axonAmount} AXON`)} (курс: 1 AXON = ${AXON_GOVERN_RATE} RUB)`;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Закрыть диалог
|
|
91
|
+
const clear = () => {
|
|
92
|
+
showDepositDialog.value = false;
|
|
93
|
+
depositAmount.value = '';
|
|
94
|
+
isSubmitting.value = false;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Обработчик пополнения (конвертация RUB в AXON)
|
|
98
|
+
const handlerSubmit = async () => {
|
|
99
|
+
isSubmitting.value = true;
|
|
100
|
+
try {
|
|
101
|
+
// Выполняем конвертацию RUB в AXON
|
|
102
|
+
const success = await convertToAxon({
|
|
103
|
+
convertAmount: depositAmount.value,
|
|
104
|
+
username: session.username || '',
|
|
105
|
+
coopname: system.info.coopname || ''
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (success) {
|
|
109
|
+
clear();
|
|
110
|
+
} else {
|
|
111
|
+
isSubmitting.value = false;
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(error);
|
|
115
|
+
isSubmitting.value = false;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<style lang="scss" scoped>
|
|
121
|
+
.axon-wallet {
|
|
122
|
+
padding: 8px;
|
|
123
|
+
|
|
124
|
+
// Переопределяем отступ ColorCard только для этого виджета
|
|
125
|
+
:deep(.color-card) {
|
|
126
|
+
margin-bottom: 0 !important;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.wallet-header {
|
|
130
|
+
margin-bottom: 8px;
|
|
131
|
+
|
|
132
|
+
.wallet-title {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
font-size: 14px;
|
|
136
|
+
font-weight: 600;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.wallet-description {
|
|
141
|
+
margin-bottom: 12px;
|
|
142
|
+
|
|
143
|
+
.text-body2 {
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
line-height: 1.4;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.balance-section {
|
|
150
|
+
padding: 12px;
|
|
151
|
+
background: rgba(255, 255, 255, 0.15);
|
|
152
|
+
border-radius: 8px;
|
|
153
|
+
margin-bottom: 12px;
|
|
154
|
+
|
|
155
|
+
.balance-label {
|
|
156
|
+
font-size: 11px;
|
|
157
|
+
opacity: 0.8;
|
|
158
|
+
margin-bottom: 4px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.balance-value {
|
|
162
|
+
font-size: 18px;
|
|
163
|
+
font-weight: 700;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.actions-section {
|
|
168
|
+
.action-buttons {
|
|
169
|
+
display: flex;
|
|
170
|
+
justify-content: center;
|
|
171
|
+
|
|
172
|
+
.action-btn {
|
|
173
|
+
min-width: 120px;
|
|
174
|
+
height: 36px;
|
|
175
|
+
border-radius: 8px;
|
|
176
|
+
|
|
177
|
+
&:hover {
|
|
178
|
+
background: rgba(255, 255, 255, 0.1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
</style>
|
|
@@ -1,154 +1,26 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
2
|
div.connection-dashboard
|
|
3
|
-
//- Заголовок с поздравлением
|
|
4
|
-
.text-center.q-pa-lg.q-mb-lg
|
|
5
|
-
q-icon(name="celebration" color="positive" size="64px").q-mb-md
|
|
6
|
-
.text-h5.q-mb-sm Подключение активно
|
|
7
|
-
.text-body1.text-grey-7 Ваш кооператив успешно развернут на платформе
|
|
8
|
-
|
|
9
3
|
//- Основная информация
|
|
10
|
-
.row.q-
|
|
4
|
+
.row.q-mb-lg.justify-center
|
|
5
|
+
//- Карточка баланса AXON
|
|
6
|
+
.col-12.col-md-4
|
|
7
|
+
AxonWallet
|
|
8
|
+
|
|
11
9
|
//- Карточка домена
|
|
12
10
|
.col-12.col-md-6
|
|
13
|
-
|
|
14
|
-
q-card-section.q-pa-lg
|
|
15
|
-
.flex.items-center.q-mb-md
|
|
16
|
-
q-icon(name="domain" color="primary" size="32px").q-mr-md
|
|
17
|
-
.text-subtitle1.text-weight-medium Домен
|
|
11
|
+
DomainCard
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.row.q-mt-md
|
|
22
|
-
.col-6
|
|
23
|
-
.text-caption.text-grey-7 Статус
|
|
24
|
-
.text-body2.text-weight-medium
|
|
25
|
-
q-chip(color="positive" text-color="white" size="sm") Активен
|
|
26
|
-
.col-6
|
|
27
|
-
.text-caption.text-grey-7 Делегирование
|
|
28
|
-
.text-body2.text-weight-medium
|
|
29
|
-
q-chip(
|
|
30
|
-
:color="instance?.is_delegated ? 'positive' : 'grey'"
|
|
31
|
-
text-color="white"
|
|
32
|
-
size="sm"
|
|
33
|
-
) {{ instance?.is_delegated ? 'Настроено' : 'Не настроено' }}
|
|
34
|
-
|
|
35
|
-
//- Карточка блокчейн-статуса
|
|
13
|
+
//- Карточка подписок
|
|
14
|
+
.row.justify-center
|
|
36
15
|
.col-12.col-md-6
|
|
37
|
-
|
|
38
|
-
q-card-section.q-pa-lg
|
|
39
|
-
.flex.items-center.q-mb-md
|
|
40
|
-
q-icon(name="link" color="primary" size="32px").q-mr-md
|
|
41
|
-
.text-subtitle1.text-weight-medium Блокчейн
|
|
42
|
-
|
|
43
|
-
.text-h6.q-mb-sm {{ getBlockchainStatusLabel }}
|
|
16
|
+
SubscriptionsCard
|
|
44
17
|
|
|
45
|
-
.row.q-mt-md
|
|
46
|
-
.col-6
|
|
47
|
-
.text-caption.text-grey-7 Статус членства
|
|
48
|
-
.text-body2.text-weight-medium
|
|
49
|
-
q-chip(
|
|
50
|
-
:color="getBlockchainStatusColor"
|
|
51
|
-
text-color="white"
|
|
52
|
-
size="sm"
|
|
53
|
-
) {{ instance?.blockchain_status || '—' }}
|
|
54
|
-
.col-6
|
|
55
|
-
.text-caption.text-grey-7 Установка
|
|
56
|
-
.text-body2.text-weight-medium {{ instance?.progress || 0 }}%
|
|
57
18
|
|
|
58
|
-
//- Дополнительные карточки (заглушки)
|
|
59
|
-
.row.q-col-gutter-lg
|
|
60
|
-
//- Карточка подписок (заглушка)
|
|
61
|
-
.col-12.col-md-4
|
|
62
|
-
q-card(flat).full-height.bg-grey-1
|
|
63
|
-
q-card-section.q-pa-lg
|
|
64
|
-
.flex.items-center.q-mb-md
|
|
65
|
-
q-icon(name="subscriptions" color="primary" size="28px").q-mr-sm
|
|
66
|
-
.text-subtitle2.text-weight-medium Подписки
|
|
67
|
-
|
|
68
|
-
.text-body2.text-grey-7.q-mb-md
|
|
69
|
-
| Управление активными подписками на услуги платформы
|
|
70
|
-
|
|
71
|
-
q-btn(
|
|
72
|
-
flat
|
|
73
|
-
color="primary"
|
|
74
|
-
label="Скоро"
|
|
75
|
-
disable
|
|
76
|
-
size="sm"
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
//- Карточка баланса AXON (заглушка)
|
|
80
|
-
.col-12.col-md-4
|
|
81
|
-
q-card(flat).full-height.bg-grey-1
|
|
82
|
-
q-card-section.q-pa-lg
|
|
83
|
-
.flex.items-center.q-mb-md
|
|
84
|
-
q-icon(name="account_balance_wallet" color="primary" size="28px").q-mr-sm
|
|
85
|
-
.text-subtitle2.text-weight-medium Кошелек AXON
|
|
86
|
-
|
|
87
|
-
.text-body2.text-grey-7.q-mb-md
|
|
88
|
-
| Баланс токенов для оплаты услуг платформы
|
|
89
|
-
|
|
90
|
-
q-btn(
|
|
91
|
-
flat
|
|
92
|
-
color="primary"
|
|
93
|
-
label="Скоро"
|
|
94
|
-
disable
|
|
95
|
-
size="sm"
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
//- Карточка настроек (заглушка)
|
|
99
|
-
.col-12.col-md-4
|
|
100
|
-
q-card(flat).full-height.bg-grey-1
|
|
101
|
-
q-card-section.q-pa-lg
|
|
102
|
-
.flex.items-center.q-mb-md
|
|
103
|
-
q-icon(name="settings" color="primary" size="28px").q-mr-sm
|
|
104
|
-
.text-subtitle2.text-weight-medium Настройки
|
|
105
|
-
|
|
106
|
-
.text-body2.text-grey-7.q-mb-md
|
|
107
|
-
| Параметры подключения и конфигурация платформы
|
|
108
|
-
|
|
109
|
-
q-btn(
|
|
110
|
-
flat
|
|
111
|
-
color="primary"
|
|
112
|
-
label="Скоро"
|
|
113
|
-
disable
|
|
114
|
-
size="sm"
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
//- Информация о платформе
|
|
118
|
-
.q-mt-xl
|
|
119
|
-
q-card(flat).bg-grey-1
|
|
120
|
-
q-card-section.q-pa-lg
|
|
121
|
-
.text-subtitle2.q-mb-md.text-weight-medium О платформе
|
|
122
|
-
.text-body2.text-grey-8
|
|
123
|
-
| Платформа кооперативной экономики предоставляет цифровые инструменты для управления кооперативом,
|
|
124
|
-
| учета деятельности членов, проведения собраний, голосований и других операций в соответствии
|
|
125
|
-
| с законодательством о кооперации.
|
|
126
19
|
</template>
|
|
127
20
|
|
|
128
21
|
<script setup lang="ts">
|
|
129
|
-
import {
|
|
130
|
-
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
|
131
|
-
|
|
132
|
-
const connectionAgreement = useConnectionAgreementStore()
|
|
133
|
-
|
|
134
|
-
// Получаем instance напрямую из store
|
|
135
|
-
const instance = computed(() => connectionAgreement.currentInstance)
|
|
136
|
-
|
|
137
|
-
// Цвет статуса блокчейна
|
|
138
|
-
const getBlockchainStatusColor = computed(() => {
|
|
139
|
-
if (instance.value?.blockchain_status === 'active') return 'positive'
|
|
140
|
-
if (instance.value?.blockchain_status === 'pending') return 'warning'
|
|
141
|
-
if (instance.value?.blockchain_status === 'blocked') return 'negative'
|
|
142
|
-
return 'grey'
|
|
143
|
-
})
|
|
22
|
+
import { AxonWallet, DomainCard, SubscriptionsCard } from './index'
|
|
144
23
|
|
|
145
|
-
// Метка статуса блокчейна
|
|
146
|
-
const getBlockchainStatusLabel = computed(() => {
|
|
147
|
-
if (instance.value?.blockchain_status === 'active') return 'Подключен к блокчейну'
|
|
148
|
-
if (instance.value?.blockchain_status === 'pending') return 'Ожидание подключения'
|
|
149
|
-
if (instance.value?.blockchain_status === 'blocked') return 'Заблокирован'
|
|
150
|
-
return 'Неизвестно'
|
|
151
|
-
})
|
|
152
24
|
</script>
|
|
153
25
|
|
|
154
26
|
<style lang="scss" scoped>
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.domain-card
|
|
3
|
+
ColorCard(color='blue', @click.stop)
|
|
4
|
+
// Заголовок
|
|
5
|
+
.domain-header
|
|
6
|
+
.domain-title
|
|
7
|
+
q-icon(name="domain" size="20px").q-mr-sm
|
|
8
|
+
| Подключение
|
|
9
|
+
|
|
10
|
+
// Отображение или редактирование домена
|
|
11
|
+
.domain-display
|
|
12
|
+
.flex.items-center
|
|
13
|
+
.domain-text.full-width
|
|
14
|
+
template(v-if="!isEditing")
|
|
15
|
+
div.q-pa-md.text-h6.q-mr-sm {{ coop.publicCooperativeData?.announce || '—' }}
|
|
16
|
+
q-btn(
|
|
17
|
+
flat
|
|
18
|
+
round
|
|
19
|
+
icon="edit"
|
|
20
|
+
size="sm"
|
|
21
|
+
color="primary"
|
|
22
|
+
@click="startEdit"
|
|
23
|
+
)
|
|
24
|
+
q-tooltip Редактировать домен
|
|
25
|
+
div(v-else).q-pa-sm
|
|
26
|
+
.domain-warning.q-mb-md.full-width
|
|
27
|
+
.text-caption.text-orange-8.q-mb-sm
|
|
28
|
+
| ⚠️ Убедитесь, что домен делегирован IP-адрес: {{ SERVER_IP }}.
|
|
29
|
+
| Обновление домена перезагрузит цифровой кооператив.
|
|
30
|
+
| Все данные будут сохранены.
|
|
31
|
+
|
|
32
|
+
q-input(
|
|
33
|
+
v-model="domainValue"
|
|
34
|
+
placeholder="Введите домен"
|
|
35
|
+
outlined
|
|
36
|
+
dense
|
|
37
|
+
autofocus
|
|
38
|
+
@keyup.enter="saveDomain"
|
|
39
|
+
@keyup.escape="cancelEdit"
|
|
40
|
+
:rules="[(val) => !!val || 'Домен обязателен']"
|
|
41
|
+
).full-width.q-mt-md
|
|
42
|
+
template(#prepend)
|
|
43
|
+
q-btn(
|
|
44
|
+
flat
|
|
45
|
+
round
|
|
46
|
+
icon="close"
|
|
47
|
+
size="sm"
|
|
48
|
+
color="negative"
|
|
49
|
+
@click="cancelEdit"
|
|
50
|
+
)
|
|
51
|
+
q-tooltip Отменить
|
|
52
|
+
template(#append)
|
|
53
|
+
q-btn(
|
|
54
|
+
flat
|
|
55
|
+
round
|
|
56
|
+
icon="check"
|
|
57
|
+
size="sm"
|
|
58
|
+
color="positive"
|
|
59
|
+
@click="saveDomain"
|
|
60
|
+
)
|
|
61
|
+
q-tooltip Сохранить
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
.row
|
|
65
|
+
.col-6
|
|
66
|
+
.text-caption.text-grey-7 Членство в союзе
|
|
67
|
+
.text-body2.text-weight-medium
|
|
68
|
+
q-chip(
|
|
69
|
+
:color="getMembershipStatusColor"
|
|
70
|
+
outline
|
|
71
|
+
size="sm"
|
|
72
|
+
) {{ getMembershipStatusLabel }}
|
|
73
|
+
.col-6
|
|
74
|
+
.text-caption.text-grey-7 Домен делегирован
|
|
75
|
+
.text-body2.text-weight-medium
|
|
76
|
+
q-chip(
|
|
77
|
+
:color="isDelegatingLoading ? 'grey' : (instance?.is_delegated ? 'positive' : 'grey')"
|
|
78
|
+
outline
|
|
79
|
+
size="sm"
|
|
80
|
+
)
|
|
81
|
+
template(v-if="isDelegatingLoading")
|
|
82
|
+
q-spinner(color="white" size="16px")
|
|
83
|
+
template(v-else)
|
|
84
|
+
q-icon(
|
|
85
|
+
v-if="!instance?.is_delegated"
|
|
86
|
+
name="refresh"
|
|
87
|
+
size="14px"
|
|
88
|
+
class="q-ml-xs rotating-icon"
|
|
89
|
+
color="grey-5"
|
|
90
|
+
)
|
|
91
|
+
span {{ instance?.is_delegated ? 'Да' : 'Обновляем' }}
|
|
92
|
+
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<script setup lang="ts">
|
|
96
|
+
import { computed, ref, watch } from 'vue'
|
|
97
|
+
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
|
98
|
+
import { useCooperativeStore } from 'src/entities/Cooperative'
|
|
99
|
+
import { useUpdateCoop } from 'src/features/Cooperative/UpdateCoop'
|
|
100
|
+
import { FailAlert, SuccessAlert } from 'src/shared/api'
|
|
101
|
+
import { useSessionStore } from 'src/entities/Session'
|
|
102
|
+
import { ColorCard } from 'src/shared/ui'
|
|
103
|
+
import { useProviderSubscriptions } from 'src/features/Provider/model'
|
|
104
|
+
|
|
105
|
+
const connectionAgreement = useConnectionAgreementStore()
|
|
106
|
+
|
|
107
|
+
const coop = useCooperativeStore()
|
|
108
|
+
const { updateCoop } = useUpdateCoop()
|
|
109
|
+
const session = useSessionStore()
|
|
110
|
+
const { SERVER_IP } = useProviderSubscriptions()
|
|
111
|
+
// Получаем instance напрямую из store
|
|
112
|
+
const instance = computed(() => connectionAgreement.currentInstance)
|
|
113
|
+
|
|
114
|
+
// Цвет статуса членства
|
|
115
|
+
const getMembershipStatusColor = computed(() => {
|
|
116
|
+
if (instance.value?.blockchain_status === 'active') return 'positive'
|
|
117
|
+
if (instance.value?.blockchain_status === 'pending') return 'warning'
|
|
118
|
+
if (instance.value?.blockchain_status === 'blocked') return 'negative'
|
|
119
|
+
return 'grey'
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Метка статуса членства
|
|
123
|
+
const getMembershipStatusLabel = computed(() => {
|
|
124
|
+
if (instance.value?.blockchain_status === 'active') return 'Активно'
|
|
125
|
+
if (instance.value?.blockchain_status === 'pending') return 'Ожидает подтверждения'
|
|
126
|
+
if (instance.value?.blockchain_status === 'blocked') return 'Заблокировано'
|
|
127
|
+
return 'Неизвестно'
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Состояние редактирования домена
|
|
131
|
+
const isEditing = ref(false)
|
|
132
|
+
const domainValue = ref('')
|
|
133
|
+
const isDelegatingLoading = ref(false)
|
|
134
|
+
|
|
135
|
+
// Загружаем данные кооператива при монтировании
|
|
136
|
+
coop.loadPublicCooperativeData(session.username)
|
|
137
|
+
|
|
138
|
+
// Синхронизируем значение домена при изменении данных кооператива
|
|
139
|
+
watch(() => coop?.publicCooperativeData?.announce, (newAnnounce) => {
|
|
140
|
+
if (newAnnounce && !isEditing.value) {
|
|
141
|
+
domainValue.value = newAnnounce
|
|
142
|
+
}
|
|
143
|
+
}, { immediate: true })
|
|
144
|
+
|
|
145
|
+
// Начать редактирование
|
|
146
|
+
const startEdit = () => {
|
|
147
|
+
isEditing.value = true
|
|
148
|
+
domainValue.value = coop?.publicCooperativeData?.announce || ''
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Отменить редактирование
|
|
152
|
+
const cancelEdit = () => {
|
|
153
|
+
isEditing.value = false
|
|
154
|
+
domainValue.value = coop?.publicCooperativeData?.announce || ''
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Сохранить домен
|
|
158
|
+
const saveDomain = async () => {
|
|
159
|
+
if (!domainValue.value.trim()) {
|
|
160
|
+
FailAlert('Домен не может быть пустым')
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!coop.publicCooperativeData) {
|
|
165
|
+
FailAlert('Не удалось получить данные кооператива')
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
isDelegatingLoading.value = true
|
|
171
|
+
|
|
172
|
+
await updateCoop({
|
|
173
|
+
coopname: session.username,
|
|
174
|
+
username: session.username,
|
|
175
|
+
initial: coop.publicCooperativeData.initial,
|
|
176
|
+
minimum: coop.publicCooperativeData.minimum,
|
|
177
|
+
org_initial: coop.publicCooperativeData.org_initial,
|
|
178
|
+
org_minimum: coop.publicCooperativeData.org_minimum,
|
|
179
|
+
announce: domainValue.value.trim(), // Обновляем announce (домен)
|
|
180
|
+
description: coop.publicCooperativeData.description
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// Перезагружаем данные
|
|
184
|
+
await coop.loadPublicCooperativeData(session.username)
|
|
185
|
+
await connectionAgreement.loadCurrentInstance()
|
|
186
|
+
|
|
187
|
+
isEditing.value = false
|
|
188
|
+
SuccessAlert('Домен успешно обновлен')
|
|
189
|
+
} catch (error: any) {
|
|
190
|
+
FailAlert(`Ошибка при обновлении домена: ${error.message}`)
|
|
191
|
+
} finally {
|
|
192
|
+
isDelegatingLoading.value = false
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
</script>
|
|
196
|
+
|
|
197
|
+
<style lang="scss" scoped>
|
|
198
|
+
.domain-card {
|
|
199
|
+
padding: 8px;
|
|
200
|
+
|
|
201
|
+
// Переопределяем отступ ColorCard только для этого виджета
|
|
202
|
+
:deep(.color-card) {
|
|
203
|
+
margin-bottom: 0 !important;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.domain-header {
|
|
207
|
+
margin-bottom: 12px;
|
|
208
|
+
|
|
209
|
+
.domain-title {
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
font-size: 14px;
|
|
213
|
+
font-weight: 600;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.domain-display {
|
|
218
|
+
.domain-text {
|
|
219
|
+
display: flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
|
|
222
|
+
.text-h6 {
|
|
223
|
+
margin-right: 8px;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Анимация вращения иконки обновления
|
|
229
|
+
.rotating-icon {
|
|
230
|
+
animation: rotate 2s linear infinite;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@keyframes rotate {
|
|
234
|
+
from {
|
|
235
|
+
transform: rotate(0deg);
|
|
236
|
+
}
|
|
237
|
+
to {
|
|
238
|
+
transform: rotate(360deg);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
</style>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.subscriptions-card
|
|
3
|
+
ColorCard(color='orange', @click.stop)
|
|
4
|
+
// Заголовок
|
|
5
|
+
.subscriptions-header
|
|
6
|
+
.subscriptions-title
|
|
7
|
+
q-icon(name="subscriptions" size="20px").q-mr-sm
|
|
8
|
+
| Подписки
|
|
9
|
+
|
|
10
|
+
// Список подписок
|
|
11
|
+
.subscriptions-list
|
|
12
|
+
template(v-if="isLoading")
|
|
13
|
+
.text-center.q-pa-md
|
|
14
|
+
q-spinner(color="orange" size="24px")
|
|
15
|
+
.text-caption.text-grey-7.q-mt-sm Загрузка подписок...
|
|
16
|
+
|
|
17
|
+
template(v-else-if="subscriptions.length > 0")
|
|
18
|
+
q-list(separator)
|
|
19
|
+
q-item(
|
|
20
|
+
v-for="subscription in subscriptions"
|
|
21
|
+
:key="subscription.id"
|
|
22
|
+
)
|
|
23
|
+
q-item-section(avatar)
|
|
24
|
+
q-icon(
|
|
25
|
+
:name="getSubscriptionIcon(subscription)"
|
|
26
|
+
:color="getSubscriptionColor(subscription)"
|
|
27
|
+
size="20px"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
q-item-section
|
|
31
|
+
q-item-label {{ getSubscriptionTypeName(subscription.subscription_type_id) }}
|
|
32
|
+
q-item-label.caption.text-grey-7 {{ getSubscriptionStatusText(subscription) }}
|
|
33
|
+
|
|
34
|
+
q-item-section(side)
|
|
35
|
+
.text-weight-medium {{ formatPrice(subscription.price) }}
|
|
36
|
+
.text-caption.text-grey-7 {{ currencySymbol }}/месяц
|
|
37
|
+
|
|
38
|
+
template(v-else-if="error")
|
|
39
|
+
.text-center.q-pa-md
|
|
40
|
+
.text-negative Ошибка загрузки подписок
|
|
41
|
+
.text-caption.text-grey-7.q-mt-sm {{ error }}
|
|
42
|
+
|
|
43
|
+
template(v-else)
|
|
44
|
+
.text-center.q-pa-md
|
|
45
|
+
.text-grey-6 Нет активных подписок
|
|
46
|
+
.text-caption.text-grey-7.q-mt-sm Подписки появятся после подключения услуг платформы
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup lang="ts">
|
|
50
|
+
import { onMounted, computed } from 'vue'
|
|
51
|
+
import { useProviderSubscriptions } from 'src/features/Provider/model'
|
|
52
|
+
import { useSystemStore } from 'src/entities/System/model'
|
|
53
|
+
import { ColorCard } from 'src/shared/ui'
|
|
54
|
+
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits'
|
|
55
|
+
|
|
56
|
+
const {
|
|
57
|
+
subscriptions,
|
|
58
|
+
isLoading,
|
|
59
|
+
error,
|
|
60
|
+
loadSubscriptions
|
|
61
|
+
} = useProviderSubscriptions()
|
|
62
|
+
const { info } = useSystemStore()
|
|
63
|
+
|
|
64
|
+
// Загружаем подписки при монтировании
|
|
65
|
+
onMounted(async () => {
|
|
66
|
+
await loadSubscriptions()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Форматирование цены
|
|
70
|
+
const formatPrice = (price: number | string) => {
|
|
71
|
+
const priceStr = typeof price === 'number' ? price.toString() : price
|
|
72
|
+
const currencySymbol = info.symbols?.root_govern_symbol || 'AXON'
|
|
73
|
+
return formatAsset2Digits(`${priceStr} ${currencySymbol}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Получение символа валюты для отображения
|
|
77
|
+
const currencySymbol = computed(() => info.symbols?.root_govern_symbol || 'AXON')
|
|
78
|
+
|
|
79
|
+
// Получение названия типа подписки
|
|
80
|
+
const getSubscriptionTypeName = (typeId: number) => {
|
|
81
|
+
switch (typeId) {
|
|
82
|
+
case 1: return 'Хостинг'
|
|
83
|
+
case 2: return 'База данных'
|
|
84
|
+
case 3: return 'API'
|
|
85
|
+
default: return `Подписка ${typeId}`
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Получение иконки для статуса подписки
|
|
90
|
+
const getSubscriptionIcon = (subscription: any) => {
|
|
91
|
+
// Для хостинга проверяем specific_data
|
|
92
|
+
if (subscription.subscription_type_id === 1) {
|
|
93
|
+
const specificData = subscription.specific_data
|
|
94
|
+
if (specificData?.is_valid && specificData?.is_delegated) return 'check_circle'
|
|
95
|
+
if (specificData?.progress > 0 && specificData?.progress < 100) return 'hourglass_top'
|
|
96
|
+
return 'schedule'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Для других типов подписок проверяем instance_status
|
|
100
|
+
switch (subscription.instance_status) {
|
|
101
|
+
case 'active': return 'check_circle'
|
|
102
|
+
case 'pending': return 'schedule'
|
|
103
|
+
case 'installing': return 'hourglass_top'
|
|
104
|
+
case 'error': return 'error'
|
|
105
|
+
case 'inactive': return 'pause_circle'
|
|
106
|
+
default: return 'help'
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Получение цвета для статуса подписки
|
|
111
|
+
const getSubscriptionColor = (subscription: any) => {
|
|
112
|
+
// Для хостинга проверяем specific_data
|
|
113
|
+
if (subscription.subscription_type_id === 1) {
|
|
114
|
+
const specificData = subscription.specific_data
|
|
115
|
+
if (specificData?.is_valid && specificData?.is_delegated) return 'positive'
|
|
116
|
+
if (specificData?.progress > 0 && specificData?.progress < 100) return 'warning'
|
|
117
|
+
return 'grey'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Для других типов подписок проверяем instance_status
|
|
121
|
+
switch (subscription.instance_status) {
|
|
122
|
+
case 'active': return 'positive'
|
|
123
|
+
case 'pending': return 'grey'
|
|
124
|
+
case 'installing': return 'warning'
|
|
125
|
+
case 'error': return 'negative'
|
|
126
|
+
case 'inactive': return 'grey'
|
|
127
|
+
default: return 'grey'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Получение текста статуса подписки
|
|
132
|
+
const getSubscriptionStatusText = (subscription: any) => {
|
|
133
|
+
// Для хостинга показываем прогресс
|
|
134
|
+
if (subscription.subscription_type_id === 1) {
|
|
135
|
+
const specificData = subscription.specific_data
|
|
136
|
+
if (specificData?.is_valid && specificData?.is_delegated) {
|
|
137
|
+
return 'Активна'
|
|
138
|
+
}
|
|
139
|
+
if (specificData?.progress > 0 && specificData?.progress < 100) {
|
|
140
|
+
return `Устанавливается (${specificData.progress}%)`
|
|
141
|
+
}
|
|
142
|
+
return 'Ожидает настройки'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Для других типов подписок
|
|
146
|
+
switch (subscription.instance_status) {
|
|
147
|
+
case 'active': return 'Активна'
|
|
148
|
+
case 'pending': return 'Ожидает активации'
|
|
149
|
+
case 'installing': return 'Устанавливается'
|
|
150
|
+
case 'error': return 'Ошибка'
|
|
151
|
+
case 'inactive': return 'Неактивна'
|
|
152
|
+
default: return 'Неизвестен'
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<style lang="scss" scoped>
|
|
158
|
+
.subscriptions-card {
|
|
159
|
+
padding: 8px;
|
|
160
|
+
|
|
161
|
+
// Переопределяем отступ ColorCard только для этого виджета
|
|
162
|
+
:deep(.color-card) {
|
|
163
|
+
margin-bottom: 0 !important;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.subscriptions-header {
|
|
167
|
+
margin-bottom: 16px;
|
|
168
|
+
|
|
169
|
+
.subscriptions-title {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
font-size: 14px;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.subscriptions-list {
|
|
178
|
+
.q-list {
|
|
179
|
+
background: transparent;
|
|
180
|
+
border-radius: 8px;
|
|
181
|
+
|
|
182
|
+
.q-item {
|
|
183
|
+
padding: 12px 16px;
|
|
184
|
+
border-radius: 8px;
|
|
185
|
+
|
|
186
|
+
&:hover {
|
|
187
|
+
background: rgba(255, 255, 255, 0.05);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
</style>
|