@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coopenomics/desktop",
3
- "version": "2025.11.18-alpha-1",
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.18-alpha-1",
29
- "@coopenomics/notifications": "2025.11.18-alpha-1",
30
- "@coopenomics/sdk": "2025.11.18-alpha-1",
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.18-alpha-1",
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": "86172790d1a19def4062384a0aa1f67f40ca51cf"
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
- currentInstance.value = await getCurrentInstance()
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 = null
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
- ConnectionDashboard(v-if="isInstallationCompleted && !isOnCompletionRoute")
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
- stopInstanceRefresh = await connectionAgreement.startInstanceAutoRefresh(30000);
111
-
112
- // Ждем завершения загрузки данных инстанса (независимо от того, есть инстанс или нет)
113
- if (connectionAgreement.currentInstanceLoading) {
114
- await new Promise<void>((resolve) => {
115
- const unwatch = watch(
116
- () => connectionAgreement.currentInstanceLoading,
117
- (isLoading) => {
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
- // Если есть ошибка загрузки инстанса, начинаем с шага 1 по умолчанию
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, ref, withDefaults, onMounted } from 'vue'
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.value)
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-col-gutter-lg.q-mb-lg
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
- q-card(flat).full-height.bg-grey-1
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
- .text-h6.text-primary.q-mb-sm {{ instance?.domain || '—' }}
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
- q-card(flat).full-height.bg-grey-1
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 { computed } from 'vue'
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>
@@ -1,2 +1,5 @@
1
1
  export { default as ConnectionDashboard } from './ConnectionDashboard.vue'
2
+ export { default as AxonWallet } from './AxonWallet.vue'
3
+ export { default as DomainCard } from './DomainCard.vue'
4
+ export { default as SubscriptionsCard } from './SubscriptionsCard.vue'
2
5