ваше сообщение коммита

This commit is contained in:
2025-06-01 15:13:52 +03:00
parent 03ea1cf726
commit 2507d776e0
32 changed files with 1832 additions and 445 deletions

View File

@@ -20,7 +20,7 @@
<script setup>
import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
import { RouterView } from 'vue-router';
import { useAuth } from './composables/useAuth';
import { useAuth, provideAuth } from './composables/useAuth';
import { fetchTokenBalances } from './services/tokens';
import eventBus from './utils/eventBus';
@@ -158,6 +158,8 @@
}
});
});
provideAuth();
</script>
<style>

View File

@@ -38,7 +38,7 @@
<script setup>
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits } from 'vue';
import { useAuth } from '../composables/useAuth';
import { useAuthContext } from '../composables/useAuth';
import { useAuthFlow } from '../composables/useAuthFlow';
import { useNotifications } from '../composables/useNotifications';
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
@@ -53,7 +53,7 @@ import NotificationDisplay from './NotificationDisplay.vue';
// 1. ИСПОЛЬЗОВАНИЕ COMPOSABLES
// =====================================================================
const auth = useAuth();
const auth = useAuthContext();
const { notifications, showSuccessMessage, showErrorMessage } = useNotifications();
// Определяем props, которые будут приходить от родительского View

View File

@@ -18,7 +18,7 @@
<script setup>
import { defineProps, defineEmits, onMounted, onBeforeUnmount, watch } from 'vue';
import { useAuth } from '../composables/useAuth';
import { useAuthContext } from '../composables/useAuth';
import eventBus from '../utils/eventBus';
const props = defineProps({
@@ -35,7 +35,7 @@ const toggleSidebar = () => {
};
// Обработка аутентификации
const auth = useAuth();
const auth = useAuthContext();
const { isAuthenticated } = auth;
// Мониторинг изменений статуса аутентификации

View File

@@ -129,7 +129,7 @@ import { useRouter } from 'vue-router';
import eventBus from '../utils/eventBus';
import EmailConnect from './identity/EmailConnect.vue';
import TelegramConnect from './identity/TelegramConnect.vue';
import { useAuth } from '@/composables/useAuth';
import { useAuthContext } from '@/composables/useAuth';
const router = useRouter();
const props = defineProps({
@@ -144,7 +144,7 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue', 'wallet-auth', 'disconnect-wallet', 'telegram-auth', 'email-auth', 'cancel-email-auth']);
const { deleteIdentity } = useAuth();
const { deleteIdentity } = useAuthContext();
// Обработчики событий
const handleWalletAuth = () => {

View File

@@ -0,0 +1,12 @@
<template>
<input type="checkbox" :checked="value === true || value === 'true'" disabled />
</template>
<script setup>
defineProps({ value: [Boolean, String, Number] });
</script>
<style scoped>
input[type='checkbox'] {
pointer-events: none;
accent-color: #1976d2;
}
</style>

View File

@@ -0,0 +1,16 @@
<template>
<span>{{ formatted }}</span>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({ value: String });
const formatted = computed(() => {
if (!props.value) return '';
const d = new Date(props.value);
if (isNaN(d)) return props.value;
return d.toISOString().slice(0, 10);
});
</script>
<style scoped>
span { color: #3a3a3a; }
</style>

View File

@@ -0,0 +1,9 @@
<template>
<span>{{ value }}</span>
</template>
<script setup>
defineProps({ value: [String, Number] });
</script>
<style scoped>
span { font-variant-numeric: tabular-nums; }
</style>

View File

@@ -0,0 +1,20 @@
<template>
<span v-if="Array.isArray(value)">
<span v-for="(v, i) in value" :key="i" class="select-tag">{{ v }}</span>
</span>
<span v-else>{{ value }}</span>
</template>
<script setup>
defineProps({ value: [String, Array], options: [Array, Object] });
</script>
<style scoped>
.select-tag {
display: inline-block;
background: #e0f3ff;
color: #1976d2;
border-radius: 6px;
padding: 2px 8px;
margin-right: 4px;
font-size: 0.95em;
}
</style>

View File

@@ -0,0 +1,9 @@
<template>
<span>{{ value }}</span>
</template>
<script setup>
defineProps({ value: String });
</script>
<style scoped>
span { white-space: pre-line; }
</style>

View File

@@ -38,10 +38,10 @@
<script setup>
import { ref, computed } from 'vue';
import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth';
import { useAuthContext } from '@/composables/useAuth';
const emit = defineEmits(['close', 'success']);
const { linkIdentity } = useAuth();
const { linkIdentity } = useAuthContext();
const email = ref('');
const code = ref('');

View File

@@ -17,11 +17,11 @@
<script setup>
import { ref, computed } from 'vue';
import { useAuth } from '@/composables/useAuth';
import { useAuthContext } from '@/composables/useAuth';
import { connectWithWallet } from '@/services/wallet';
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const { linkIdentity } = useAuthContext();
const isLoading = ref(false);
const error = ref('');

View File

@@ -0,0 +1,111 @@
<template>
<div class="create-table-modal">
<div class="modal-header">
<h3>Создать новую таблицу</h3>
<button class="close-btn" @click="$emit('close')">×</button>
</div>
<form @submit.prevent="createTable">
<div class="form-group">
<label>Название таблицы</label>
<input v-model="name" required maxlength="255" />
</div>
<div class="form-group">
<label>Описание</label>
<textarea v-model="description" maxlength="500"></textarea>
</div>
<div class="modal-actions">
<button class="btn btn-success" type="submit" :disabled="isLoading">Создать</button>
<button class="btn btn-secondary" type="button" @click="$emit('close')">Отмена</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue';
import tablesService from '../../services/tablesService';
const emit = defineEmits(['close', 'table-created']);
const name = ref('');
const description = ref('');
const isLoading = ref(false);
const error = ref('');
async function createTable() {
if (!name.value.trim()) return;
isLoading.value = true;
error.value = '';
try {
await tablesService.createTable({ name: name.value, description: description.value });
emit('table-created');
emit('close');
} catch (e) {
error.value = 'Ошибка создания таблицы';
} finally {
isLoading.value = false;
}
}
</script>
<style scoped>
.create-table-modal {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 16px rgba(0,0,0,0.13);
padding: 28px 22px 18px 22px;
max-width: 400px;
margin: 40px auto;
position: relative;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #bbb;
transition: color 0.2s;
}
.close-btn:hover {
color: #333;
}
.form-group {
margin-bottom: 16px;
}
input, textarea {
width: 100%;
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 1rem;
}
.modal-actions {
display: flex;
gap: 12px;
margin-top: 10px;
}
.btn {
padding: 7px 18px;
border-radius: 6px;
font-size: 1rem;
border: none;
cursor: pointer;
}
.btn-success {
background: #28a745;
color: #fff;
}
.btn-secondary {
background: #bbb;
color: #fff;
}
.error {
color: #dc3545;
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,276 @@
<template>
<div class="dynamic-table-editor">
<div class="editor-header">
<input v-model="tableName" @blur="saveTableName" class="table-title-input" />
<textarea v-model="tableDesc" @blur="saveTableDesc" class="table-desc-input" />
<button class="close-btn" @click="$emit('close')">×</button>
</div>
<div v-if="isLoading" class="loading">Загрузка...</div>
<div v-else>
<TableColumnsDraggable
:columns="columns"
@update-column="updateColumn"
@delete-column="deleteColumn"
@edit-options="openOptionsEditor"
@columns-reordered="reorderColumns"
/>
<SelectOptionsEditor
v-if="showOptionsEditor"
:options="editingOptions"
@update:options="saveOptions"
/>
<div class="table-controls">
<button class="btn btn-success" @click="addColumn">Добавить столбец</button>
<button class="btn btn-success" @click="addRow">Добавить строку</button>
</div>
<table class="dynamic-table">
<thead>
<tr>
<th v-for="col in columns" :key="col.id">{{ col.name }}</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<td v-for="col in columns" :key="col.id">
<template v-if="col.type === 'select'">
<select v-model="cellEdits[`${row.id}_${col.id}`]" @change="saveCell(row.id, col.id)">
<option v-for="opt in col.options || []" :key="opt" :value="opt">{{ opt }}</option>
</select>
</template>
<template v-else>
<input :value="cellValue(row.id, col.id)" @input="onCellInput(row.id, col.id, $event.target.value)" @blur="saveCell(row.id, col.id)" />
</template>
</td>
<td>
<button class="btn btn-danger btn-sm" @click="deleteRow(row.id)">Удалить</button>
</td>
</tr>
</tbody>
</table>
<div v-if="!rows.length || !columns.length" class="empty-table">Нет данных. Добавьте столбцы и строки.</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import tablesService from '../../services/tablesService';
import TableColumnsDraggable from './TableColumnsDraggable.vue';
import SelectOptionsEditor from './SelectOptionsEditor.vue';
const props = defineProps({ tableId: { type: Number, required: true } });
const emit = defineEmits(['close']);
const isLoading = ref(true);
const columns = ref([]);
const rows = ref([]);
const cellValues = ref([]);
const cellEdits = ref({});
const tableName = ref('');
const tableDesc = ref('');
const showOptionsEditor = ref(false);
const editingCol = ref(null);
const editingOptions = ref([]);
function loadTable() {
isLoading.value = true;
tablesService.getTable(props.tableId)
.then(res => {
columns.value = res.columns;
rows.value = res.rows;
cellValues.value = res.cellValues;
cellEdits.value = {};
tableName.value = res.columns.length ? res.columns[0].table_name : '';
tableDesc.value = res.columns.length ? res.columns[0].table_description : '';
})
.finally(() => { isLoading.value = false; });
}
function addColumn() {
const name = prompt('Название столбца:');
if (!name) return;
tablesService.addColumn(props.tableId, { name, type: 'text' }).then(loadTable);
}
function addRow() {
tablesService.addRow(props.tableId).then(loadTable);
}
function deleteColumn(colId) {
if (!confirm('Удалить столбец?')) return;
tablesService.deleteColumn(colId).then(loadTable);
}
function deleteRow(rowId) {
if (!confirm('Удалить строку?')) return;
tablesService.deleteRow(rowId).then(loadTable);
}
function cellValue(rowId, colId) {
const key = `${rowId}_${colId}`;
if (cellEdits.value[key] !== undefined) return cellEdits.value[key];
const found = cellValues.value.find(c => c.row_id === rowId && c.column_id === colId);
return found ? found.value : '';
}
function saveCell(rowId, colId) {
const key = `${rowId}_${colId}`;
const value = cellEdits.value[key];
tablesService.saveCell({ row_id: rowId, column_id: colId, value }).then(loadTable);
}
function updateColumn(col) {
try {
// Убеждаемся, что options - это массив или null
let options = col.options;
if (typeof options === 'string') {
try {
options = JSON.parse(options);
} catch {
options = [];
}
}
tablesService.updateColumn(col.id, {
name: col.name,
type: col.type,
options: options,
order: col.order
}).then(loadTable).catch(err => {
console.error('Ошибка обновления столбца:', err);
alert('Ошибка обновления столбца');
});
} catch (err) {
console.error('Ошибка updateColumn:', err);
}
}
function reorderColumns(newColumns) {
// Сохраняем новый порядок столбцов последовательно
const updatePromises = newColumns.map((col, idx) =>
tablesService.updateColumn(col.id, { order: idx })
);
Promise.all(updatePromises)
.then(() => loadTable())
.catch(err => {
console.error('Ошибка переупорядочивания столбцов:', err);
alert('Ошибка переупорядочивания столбцов');
});
}
function onCellInput(rowId, colId, value) {
const key = `${rowId}_${colId}`;
cellEdits.value[key] = value;
}
function openOptionsEditor(col) {
editingCol.value = col;
editingOptions.value = Array.isArray(col.options) ? [...col.options] : [];
showOptionsEditor.value = true;
}
function saveOptions(newOptions) {
if (!editingCol.value) return;
tablesService.updateColumn(editingCol.value.id, { options: newOptions }).then(() => {
showOptionsEditor.value = false;
loadTable();
});
}
function saveTableName() {
tablesService.updateTable(props.tableId, { name: tableName.value });
}
function saveTableDesc() {
tablesService.updateTable(props.tableId, { description: tableDesc.value });
}
watch([columns, rows], () => {
cellEdits.value = {};
});
onMounted(loadTable);
</script>
<style scoped>
.dynamic-table-editor {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 16px rgba(0,0,0,0.13);
padding: 28px 22px 18px 22px;
max-width: 900px;
margin: 40px auto;
position: relative;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
gap: 12px;
}
.table-title-input {
font-size: 1.3em;
font-weight: bold;
border: none;
outline: none;
background: transparent;
flex: 1;
padding: 4px 8px;
border-radius: 4px;
}
.table-desc-input {
font-size: 0.9em;
border: 1px solid #ccc;
outline: none;
padding: 4px 8px;
border-radius: 4px;
resize: none;
height: 60px;
width: 200px;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #bbb;
transition: color 0.2s;
}
.close-btn:hover {
color: #333;
}
.table-controls {
margin-bottom: 16px;
display: flex;
gap: 12px;
}
.dynamic-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 18px;
}
.dynamic-table th, .dynamic-table td {
border: 1px solid #eee;
padding: 6px 10px;
text-align: left;
}
.dynamic-table th {
background: #f5f7fa;
}
.btn {
padding: 5px 12px;
border-radius: 6px;
font-size: 0.95rem;
border: none;
cursor: pointer;
}
.btn-success {
background: #28a745;
color: #fff;
}
.btn-danger {
background: #dc3545;
color: #fff;
}
.btn-sm {
font-size: 0.85rem;
padding: 2px 8px;
}
.loading {
color: #888;
margin: 16px 0;
}
.empty-table {
color: #aaa;
text-align: center;
margin: 24px 0;
font-size: 1.1em;
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<div class="dynamic-tables-modal">
<div class="modal-header">
<h2>Пользовательские таблицы</h2>
<button class="close-btn" @click="$emit('close')">×</button>
</div>
<UserTablesList @open-table="openTable" @table-deleted="onTableDeleted" />
<DynamicTableEditor v-if="selectedTable" :table-id="selectedTable.id" @close="closeEditor" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import UserTablesList from './UserTablesList.vue';
import DynamicTableEditor from './DynamicTableEditor.vue';
const selectedTable = ref(null);
function openTable(table) {
selectedTable.value = table;
}
function closeEditor() {
selectedTable.value = null;
}
function onTableDeleted(deletedTableId) {
if (selectedTable.value && selectedTable.value.id === deletedTableId) {
selectedTable.value = null;
}
}
</script>
<style scoped>
.dynamic-tables-modal {
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
padding: 32px 24px 24px 24px;
max-width: 900px;
margin: 40px auto;
position: relative;
overflow-x: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
.close-btn {
background: none;
border: none;
font-size: 2rem;
cursor: pointer;
color: #bbb;
transition: color 0.2s;
}
.close-btn:hover {
color: #333;
}
.tables-list-block {
margin-bottom: 18px;
}
.tables-list {
list-style: none;
padding: 0;
}
.tables-list li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.btn {
margin-left: 12px;
}
.loading {
color: #888;
margin: 16px 0;
}
.user-table-block {
margin-bottom: 32px;
border: 1px solid #eee;
border-radius: 8px;
padding: 18px 12px;
background: #fafbfc;
}
.user-table-full {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
}
.user-table-full th, .user-table-full td {
border: 1px solid #e0e0e0;
padding: 6px 10px;
text-align: left;
}
.user-table-full th {
background: #f5f5f5;
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<div class="select-options-editor">
<h4>Опции для select</h4>
<ul>
<li v-for="(opt, idx) in localOptions" :key="idx">
<input v-model="localOptions[idx]" @blur="emitOptions" class="option-input" />
<button class="btn btn-danger btn-xs" @click="removeOption(idx)">×</button>
</li>
</ul>
<button class="btn btn-success btn-xs" @click="addOption">Добавить опцию</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
options: { type: Array, default: () => [] }
});
const emit = defineEmits(['update:options']);
const localOptions = ref([...props.options]);
watch(() => props.options, (val) => {
localOptions.value = [...val];
});
function addOption() {
localOptions.value.push('');
emitOptions();
}
function removeOption(idx) {
localOptions.value.splice(idx, 1);
emitOptions();
}
function emitOptions() {
emit('update:options', localOptions.value.filter(opt => opt.trim() !== ''));
}
</script>
<style scoped>
.select-options-editor {
background: #f8fafc;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 12px;
max-width: 320px;
}
ul {
list-style: none;
padding: 0;
margin: 0 0 8px 0;
}
li {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.option-input {
flex: 1 1 80px;
border: 1px solid #b0b0b0;
border-radius: 5px;
padding: 2px 6px;
}
.btn-xs {
font-size: 0.8em;
padding: 2px 6px;
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="columns-container">
<div v-for="(col, index) in localColumns" :key="col.id" class="column-header">
<input v-model="col.name" @blur="updateColumn(col)" class="col-name-input" :placeholder="'Название'" />
<select v-model="col.type" @change="updateColumn(col)" class="col-type-select">
<option value="text">Текст</option>
<option value="select">Список</option>
</select>
<button class="btn btn-danger btn-sm" @click="$emit('delete-column', col.id)">×</button>
<button v-if="col.type==='select'" class="btn btn-secondary btn-xs" @click="$emit('edit-options', col)">Опции</button>
<div class="reorder-buttons">
<button v-if="index > 0" class="btn btn-light btn-xs" @click="moveColumn(index, -1)"></button>
<button v-if="index < localColumns.length - 1" class="btn btn-light btn-xs" @click="moveColumn(index, 1)"></button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
columns: { type: Array, required: true }
});
const emit = defineEmits(['update:columns', 'update-column', 'delete-column', 'edit-options', 'columns-reordered']);
const localColumns = ref([...props.columns]);
watch(() => props.columns, (val) => {
localColumns.value = [...val];
});
function updateColumn(col) {
emit('update-column', col);
}
function moveColumn(index, direction) {
const newIndex = index + direction;
if (newIndex >= 0 && newIndex < localColumns.value.length) {
const columns = [...localColumns.value];
[columns[index], columns[newIndex]] = [columns[newIndex], columns[index]];
localColumns.value = columns;
emit('columns-reordered', localColumns.value);
}
}
</script>
<style scoped>
.columns-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
}
.column-header {
background: #f5f7fa;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 8px 10px;
display: flex;
align-items: center;
gap: 6px;
min-width: 180px;
position: relative;
}
.col-name-input {
flex: 1 1 80px;
border: 1px solid #b0b0b0;
border-radius: 5px;
padding: 2px 6px;
}
.col-type-select {
border-radius: 5px;
padding: 2px 6px;
}
.btn {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8em;
border: none;
cursor: pointer;
color: #fff;
}
.btn-danger {
background: #dc3545;
}
.btn-secondary {
background: #6c757d;
}
.btn-light {
background: #f8f9fa;
color: #333;
border: 1px solid #ddd;
}
.btn-xs {
font-size: 0.7em;
padding: 2px 6px;
}
.btn-sm {
font-size: 0.75em;
padding: 3px 6px;
}
.reorder-buttons {
display: flex;
gap: 2px;
margin-left: 4px;
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<div class="user-tables-list">
<div class="header-block">
<h2>Пользовательские таблицы</h2>
<button class="btn btn-success" @click="showCreateTable = true">Создать таблицу</button>
</div>
<div v-if="isLoading" class="loading">Загрузка...</div>
<div v-else>
<div v-if="tables.length === 0" class="empty-block">Нет таблиц. Создайте первую!</div>
<div v-else class="tables-cards">
<div v-for="table in tables" :key="table.id" class="table-card">
<div class="table-card-header">
<input v-if="editingTableId === table.id" v-model="editName" @blur="saveName(table)" @keyup.enter="saveName(table)" class="table-name-input" />
<h3 v-else @dblclick="startEditName(table)">{{ table.name }}</h3>
<div class="table-card-actions">
<button class="btn btn-info btn-sm" @click="$emit('open-table', table)">Открыть</button>
<button class="btn btn-warning btn-sm" @click="startEditName(table)">Переименовать</button>
<button class="btn btn-danger btn-sm" @click="deleteTable(table)">Удалить</button>
</div>
</div>
<div class="table-card-desc">
<textarea v-if="editingDescId === table.id" v-model="editDesc" @blur="saveDesc(table)" @keyup.enter="saveDesc(table)" class="table-desc-input" />
<p v-else @dblclick="startEditDesc(table)">{{ table.description || 'Без описания' }}</p>
</div>
</div>
</div>
</div>
<CreateTableModal v-if="showCreateTable" @close="showCreateTable = false" @table-created="onTableCreated" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import tablesService from '../../services/tablesService';
import CreateTableModal from './CreateTableModal.vue';
const emit = defineEmits(['open-table', 'table-deleted']);
const tables = ref([]);
const isLoading = ref(false);
const showCreateTable = ref(false);
const editingTableId = ref(null);
const editName = ref('');
const editingDescId = ref(null);
const editDesc = ref('');
function loadTables() {
isLoading.value = true;
tablesService.getTables()
.then(res => {
tables.value = [...res]; // Создаем новый массив для принудительного обновления
})
.finally(() => { isLoading.value = false; });
}
function onTableCreated() {
showCreateTable.value = false;
loadTables();
}
function startEditName(table) {
editingTableId.value = table.id;
editName.value = table.name;
}
function saveName(table) {
if (editName.value && editName.value !== table.name) {
tablesService.updateTable(table.id, { name: editName.value })
.then(loadTables);
}
editingTableId.value = null;
}
function startEditDesc(table) {
editingDescId.value = table.id;
editDesc.value = table.description || '';
}
function saveDesc(table) {
tablesService.updateTable(table.id, { description: editDesc.value })
.then(loadTables);
editingDescId.value = null;
}
function deleteTable(table) {
console.log('deleteTable called with:', table);
if (!confirm(`Удалить таблицу "${table.name}"?`)) {
console.log('User cancelled deletion');
return;
}
console.log('User confirmed deletion, proceeding...');
// Немедленно удаляем из локального списка для быстрой реакции UI
tables.value = tables.value.filter(t => t.id !== table.id);
console.log('Removed from local list, making API call...');
tablesService.deleteTable(table.id)
.then((result) => {
console.log('Таблица удалена:', result);
// Уведомляем родительский компонент об удалении
emit('table-deleted', table.id);
// Принудительно обновляем список с сервера для синхронизации
loadTables();
})
.catch((error) => {
console.error('Ошибка удаления таблицы:', error);
alert('Ошибка при удалении таблицы');
// При ошибке восстанавливаем список с сервера
loadTables();
});
}
onMounted(loadTables);
</script>
<style scoped>
.user-tables-list {
padding: 18px 8px;
}
.header-block {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
.tables-cards {
display: flex;
flex-wrap: wrap;
gap: 18px;
}
.table-card {
background: #f8fafc;
border: 1px solid #e0e0e0;
border-radius: 10px;
padding: 16px 18px;
min-width: 260px;
max-width: 340px;
flex: 1 1 260px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.table-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.table-card-actions {
display: flex;
gap: 6px;
}
.table-card-desc {
margin-top: 10px;
}
.table-name-input, .table-desc-input {
width: 100%;
font-size: 1.1em;
border: 1px solid #b0b0b0;
border-radius: 6px;
padding: 4px 8px;
}
.empty-block {
color: #888;
text-align: center;
margin: 32px 0;
}
.loading {
color: #888;
margin: 16px 0;
}
</style>

View File

@@ -1,474 +1,492 @@
import { ref, onMounted, onUnmounted } from 'vue';
import { ref, onMounted, onUnmounted, provide, inject } from 'vue';
import axios from '../api/axios';
export function useAuth() {
const isAuthenticated = ref(false);
const authType = ref(null);
const userId = ref(null);
const address = ref(null);
const telegramId = ref(null);
const isAdmin = ref(false);
const email = ref(null);
const processedGuestIds = ref([]);
const identities = ref([]);
const tokenBalances = ref([]);
// === SINGLETON STATE ===
const isAuthenticated = ref(false);
const authType = ref(null);
const userId = ref(null);
const address = ref(null);
const telegramId = ref(null);
const isAdmin = ref(false);
const email = ref(null);
const processedGuestIds = ref([]);
const identities = ref([]);
const tokenBalances = ref([]);
// Функция для обновления списка идентификаторов
const updateIdentities = async () => {
if (!isAuthenticated.value || !userId.value) return;
// Функция для обновления списка идентификаторов
const updateIdentities = async () => {
if (!isAuthenticated.value || !userId.value) return;
try {
const response = await axios.get('/api/auth/identities');
if (response.data.success) {
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
const filteredIdentities = response.data.identities
.filter((identity) => identity.provider !== 'guest')
.reduce((acc, identity) => {
// Для каждого типа провайдера оставляем только один идентификатор
const existingIdentity = acc.find((i) => i.provider === identity.provider);
if (!existingIdentity) {
acc.push(identity);
}
return acc;
}, []);
try {
const response = await axios.get('/api/auth/identities');
if (response.data.success) {
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
const filteredIdentities = response.data.identities
.filter((identity) => identity.provider !== 'guest')
.reduce((acc, identity) => {
// Для каждого типа провайдера оставляем только один идентификатор
const existingIdentity = acc.find((i) => i.provider === identity.provider);
if (!existingIdentity) {
acc.push(identity);
}
return acc;
}, []);
// Сравниваем новый отфильтрованный список с текущим значением
const currentProviders = identities.value.map(id => id.provider).sort();
const newProviders = filteredIdentities.map(id => id.provider).sort();
const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders);
// Сравниваем новый отфильтрованный список с текущим значением
const currentProviders = identities.value.map(id => id.provider).sort();
const newProviders = filteredIdentities.map(id => id.provider).sort();
const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders);
// Обновляем реактивное значение
identities.value = filteredIdentities;
console.log('User identities updated:', identities.value);
// Обновляем реактивное значение
identities.value = filteredIdentities;
console.log('User identities updated:', identities.value);
// Если список идентификаторов изменился, принудительно проверяем аутентификацию,
// чтобы обновить authType и другие связанные данные (например, telegramId)
if (identitiesChanged) {
console.log('Identities changed, forcing auth check.');
await checkAuth(); // Вызываем checkAuth для обновления полного состояния
}
// Если список идентификаторов изменился, принудительно проверяем аутентификацию,
// чтобы обновить authType и другие связанные данные (например, telegramId)
if (identitiesChanged) {
console.log('Identities changed, forcing auth check.');
await checkAuth(); // Вызываем checkAuth для обновления полного состояния
}
} catch (error) {
console.error('Error fetching user identities:', error);
}
};
} catch (error) {
console.error('Error fetching user identities:', error);
}
};
// Периодическое обновление идентификаторов
let identitiesInterval;
// Периодическое обновление идентификаторов
let identitiesInterval;
const startIdentitiesPolling = () => {
if (identitiesInterval) return;
identitiesInterval = setInterval(updateIdentities, 30000); // Обновляем каждые 30 секунд
};
const startIdentitiesPolling = () => {
if (identitiesInterval) return;
identitiesInterval = setInterval(updateIdentities, 30000); // Обновляем каждые 30 секунд
};
const stopIdentitiesPolling = () => {
if (identitiesInterval) {
clearInterval(identitiesInterval);
identitiesInterval = null;
const stopIdentitiesPolling = () => {
if (identitiesInterval) {
clearInterval(identitiesInterval);
identitiesInterval = null;
}
};
const checkTokenBalances = async (address) => {
try {
const response = await axios.get(`/api/auth/check-tokens/${address}`);
if (response.data.success) {
tokenBalances.value = response.data.balances;
return response.data.balances;
}
};
return null;
} catch (error) {
console.error('Error checking token balances:', error);
return null;
}
};
const checkTokenBalances = async (address) => {
try {
const response = await axios.get(`/api/auth/check-tokens/${address}`);
if (response.data.success) {
tokenBalances.value = response.data.balances;
return response.data.balances;
}
return null;
} catch (error) {
console.error('Error checking token balances:', error);
return null;
}
};
const updateAuth = async ({
authenticated,
authType: newAuthType,
userId: newUserId,
address: newAddress,
telegramId: newTelegramId,
isAdmin: newIsAdmin,
email: newEmail,
}) => {
const wasAuthenticated = isAuthenticated.value;
const previousUserId = userId.value;
const updateAuth = async ({
console.log('updateAuth called with:', {
authenticated,
authType: newAuthType,
userId: newUserId,
address: newAddress,
telegramId: newTelegramId,
isAdmin: newIsAdmin,
email: newEmail,
}) => {
const wasAuthenticated = isAuthenticated.value;
const previousUserId = userId.value;
newAuthType,
newUserId,
newAddress,
newTelegramId,
newIsAdmin,
newEmail,
});
console.log('updateAuth called with:', {
// Убедимся, что переменные являются реактивными
isAuthenticated.value = authenticated === true;
authType.value = newAuthType || null;
userId.value = newUserId || null;
address.value = newAddress || null;
telegramId.value = newTelegramId || null;
isAdmin.value = newIsAdmin === true;
email.value = newEmail || null;
// Кэшируем данные аутентификации
localStorage.setItem(
'authData',
JSON.stringify({
authenticated,
newAuthType,
newUserId,
newAddress,
newTelegramId,
newIsAdmin,
newEmail,
});
authType: newAuthType,
userId: newUserId,
address: newAddress,
telegramId: newTelegramId,
isAdmin: newIsAdmin,
email: newEmail,
})
);
// Убедимся, что переменные являются реактивными
isAuthenticated.value = authenticated === true;
authType.value = newAuthType || null;
userId.value = newUserId || null;
address.value = newAddress || null;
telegramId.value = newTelegramId || null;
isAdmin.value = newIsAdmin === true;
email.value = newEmail || null;
// Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса
if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) {
await checkTokenBalances(newAddress);
}
// Кэшируем данные аутентификации
localStorage.setItem(
'authData',
JSON.stringify({
authenticated,
authType: newAuthType,
userId: newUserId,
address: newAddress,
telegramId: newTelegramId,
isAdmin: newIsAdmin,
email: newEmail,
})
);
// Обновляем идентификаторы при любом изменении аутентификации
if (authenticated) {
await updateIdentities();
startIdentitiesPolling();
} else {
stopIdentitiesPolling();
identities.value = [];
}
// Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса
if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) {
await checkTokenBalances(newAddress);
}
console.log('Auth updated:', {
authenticated: isAuthenticated.value,
userId: userId.value,
address: address.value,
telegramId: telegramId.value,
email: email.value,
isAdmin: isAdmin.value,
});
// Обновляем идентификаторы при любом изменении аутентификации
if (authenticated) {
await updateIdentities();
startIdentitiesPolling();
} else {
stopIdentitiesPolling();
identities.value = [];
}
// Если пользователь только что аутентифицировался или сменил аккаунт,
// пробуем связать сообщения
if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) {
console.log('Auth change detected, linking messages');
linkMessages();
}
};
console.log('Auth updated:', {
authenticated: isAuthenticated.value,
userId: userId.value,
address: address.value,
telegramId: telegramId.value,
email: email.value,
isAdmin: isAdmin.value,
});
// Функция для связывания сообщений после успешной авторизации
const linkMessages = async () => {
try {
if (isAuthenticated.value) {
console.log('Linking messages after authentication');
// Если пользователь только что аутентифицировался или сменил аккаунт,
// пробуем связать сообщения
if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) {
console.log('Auth change detected, linking messages');
linkMessages();
}
};
// Проверка, есть ли гостевой ID для обработки
const localGuestId = localStorage.getItem('guestId');
// Функция для связывания сообщений после успешной авторизации
const linkMessages = async () => {
try {
if (isAuthenticated.value) {
console.log('Linking messages after authentication');
// Если гостевого ID нет или он уже был обработан, пропускаем запрос
if (!localGuestId || processedGuestIds.value.includes(localGuestId)) {
console.log('No new guest IDs to process or already processed');
return {
success: true,
message: 'No new guest IDs to process',
processedIds: processedGuestIds.value,
};
}
// Проверка, есть ли гостевой ID для обработки
const localGuestId = localStorage.getItem('guestId');
// Создаем объект с идентификаторами для передачи на сервер
const identifiersData = {
userId: userId.value,
guestId: localGuestId,
};
// Добавляем все доступные идентификаторы
if (address.value) identifiersData.address = address.value;
if (email.value) identifiersData.email = email.value;
if (telegramId.value) identifiersData.telegramId = telegramId.value;
console.log('Sending link-guest-messages request with data:', identifiersData);
/* Удаляем ненужный вызов
try {
// Отправляем запрос на связывание сообщений
const response = await axios.post('/api/auth/link-guest-messages', identifiersData);
if (response.data.success) {
console.log('Messages linked successfully:', response.data);
// Обновляем список обработанных guestIds из ответа сервера
if (response.data.processedIds && Array.isArray(response.data.processedIds)) {
processedGuestIds.value = [...response.data.processedIds];
console.log('Updated processed guest IDs from server:', processedGuestIds.value);
}
// В качестве запасного варианта также обрабатываем старый формат ответа
else if (response.data.results && Array.isArray(response.data.results)) {
const newProcessedIds = response.data.results
.filter((result) => result.guestId)
.map((result) => result.guestId);
if (newProcessedIds.length > 0) {
processedGuestIds.value = [
...new Set([...processedGuestIds.value, ...newProcessedIds]),
];
console.log('Updated processed guest IDs from results:', processedGuestIds.value);
}
}
// Очищаем гостевые сообщения из localStorage после успешного связывания
localStorage.removeItem('guestMessages');
localStorage.removeItem('guestId');
// Если гостевого ID нет или он уже был обработан, пропускаем запрос
if (!localGuestId || processedGuestIds.value.includes(localGuestId)) {
console.log('No new guest IDs to process or already processed');
return {
success: true,
message: 'No new guest IDs to process',
processedIds: processedGuestIds.value,
};
}
// Создаем объект с идентификаторами для передачи на сервер
const identifiersData = {
userId: userId.value,
guestId: localGuestId,
} catch (error) {
console.error('Error linking messages:', error);
return {
success: false,
error: error.message,
};
}
*/
// Предполагаем, что бэкенд автоматически связывает сообщения
// Очищаем данные гостя локально
console.log('Assuming backend handles message linking. Clearing local guest data.');
localStorage.removeItem('guestMessages');
localStorage.removeItem('guestId');
// Добавляем текущий guestId в обработанные, чтобы не пытаться отправить его снова
if(localGuestId) {
updateProcessedGuestIds([localGuestId]);
}
return { success: true, message: 'Local guest data cleared.' };
// Добавляем все доступные идентификаторы
if (address.value) identifiersData.address = address.value;
if (email.value) identifiersData.email = email.value;
if (telegramId.value) identifiersData.telegramId = telegramId.value;
}
console.log('Sending link-guest-messages request with data:', identifiersData);
return { success: false, message: 'Not authenticated' };
} catch (error) {
console.error('Error in linkMessages:', error);
return { success: false, error: error.message };
}
};
/* Удаляем ненужный вызов
try {
// Отправляем запрос на связывание сообщений
const response = await axios.post('/api/auth/link-guest-messages', identifiersData);
const checkAuth = async () => {
try {
const response = await axios.get('/api/auth/check');
console.log('Auth check response:', response.data);
if (response.data.success) {
console.log('Messages linked successfully:', response.data);
const wasAuthenticated = isAuthenticated.value;
const previousUserId = userId.value;
const previousAuthType = authType.value;
// Обновляем список обработанных guestIds из ответа сервера
if (response.data.processedIds && Array.isArray(response.data.processedIds)) {
processedGuestIds.value = [...response.data.processedIds];
console.log('Updated processed guest IDs from server:', processedGuestIds.value);
}
// В качестве запасного варианта также обрабатываем старый формат ответа
else if (response.data.results && Array.isArray(response.data.results)) {
const newProcessedIds = response.data.results
.filter((result) => result.guestId)
.map((result) => result.guestId);
// Обновляем данные авторизации через updateAuth вместо прямого изменения
await updateAuth({
authenticated: response.data.authenticated,
authType: response.data.authType,
userId: response.data.userId,
address: response.data.address,
telegramId: response.data.telegramId,
email: response.data.email,
isAdmin: response.data.isAdmin,
});
if (newProcessedIds.length > 0) {
processedGuestIds.value = [
...new Set([...processedGuestIds.value, ...newProcessedIds]),
];
console.log('Updated processed guest IDs from results:', processedGuestIds.value);
}
}
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
if (response.data.authenticated) {
// Сначала обновляем идентификаторы, чтобы иметь актуальные данные
await updateIdentities();
// Очищаем гостевые сообщения из localStorage после успешного связывания
localStorage.removeItem('guestMessages');
localStorage.removeItem('guestId');
// Если пользователь только что аутентифицировался или сменил аккаунт,
// связываем гостевые сообщения с его аккаунтом
if (!wasAuthenticated || (previousUserId && previousUserId !== response.data.userId)) {
// Немедленно связываем сообщения
const linkResult = await linkMessages();
console.log('Link messages result on auth change:', linkResult);
return {
success: true,
processedIds: processedGuestIds.value,
};
}
} catch (error) {
console.error('Error linking messages:', error);
return {
success: false,
error: error.message,
};
// Если пользователь только что аутентифицировался через Telegram,
// обновляем историю чата без перезагрузки страницы
if (response.data.authType === 'telegram' && previousAuthType !== 'telegram') {
console.log('Telegram auth detected, loading message history');
// Отправляем событие для загрузки истории чата
window.dispatchEvent(new CustomEvent('load-chat-history'));
}
*/
// Предполагаем, что бэкенд автоматически связывает сообщения
// Очищаем данные гостя локально
console.log('Assuming backend handles message linking. Clearing local guest data.');
localStorage.removeItem('guestMessages');
localStorage.removeItem('guestId');
// Добавляем текущий guestId в обработанные, чтобы не пытаться отправить его снова
if(localGuestId) {
updateProcessedGuestIds([localGuestId]);
}
return { success: true, message: 'Local guest data cleared.' };
}
return { success: false, message: 'Not authenticated' };
} catch (error) {
console.error('Error in linkMessages:', error);
return { success: false, error: error.message };
}
};
const checkAuth = async () => {
try {
const response = await axios.get('/api/auth/check');
console.log('Auth check response:', response.data);
const wasAuthenticated = isAuthenticated.value;
const previousUserId = userId.value;
const previousAuthType = authType.value;
// Обновляем данные авторизации через updateAuth вместо прямого изменения
await updateAuth({
authenticated: response.data.authenticated,
authType: response.data.authType,
userId: response.data.userId,
address: response.data.address,
telegramId: response.data.telegramId,
email: response.data.email,
isAdmin: response.data.isAdmin,
});
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
if (response.data.authenticated) {
// Сначала обновляем идентификаторы, чтобы иметь актуальные данные
await updateIdentities();
// Если пользователь только что аутентифицировался или сменил аккаунт,
// связываем гостевые сообщения с его аккаунтом
if (!wasAuthenticated || (previousUserId && previousUserId !== response.data.userId)) {
// Немедленно связываем сообщения
const linkResult = await linkMessages();
console.log('Link messages result on auth change:', linkResult);
// Если пользователь только что аутентифицировался через Telegram,
// обновляем историю чата без перезагрузки страницы
if (response.data.authType === 'telegram' && previousAuthType !== 'telegram') {
console.log('Telegram auth detected, loading message history');
// Отправляем событие для загрузки истории чата
window.dispatchEvent(new CustomEvent('load-chat-history'));
}
}
// Обновляем отображение подключенного состояния в UI
updateConnectionDisplay(true, response.data.authType, response.data);
} else {
// Обновляем отображение отключенного состояния
updateConnectionDisplay(false);
}
return response.data;
} catch (error) {
console.error('Error checking auth:', error);
// В случае ошибки сбрасываем состояние аутентификации
updateConnectionDisplay(false);
return { authenticated: false };
}
};
const disconnect = async () => {
try {
// Удаляем все идентификаторы перед выходом
await axios.post('/api/auth/logout');
// Обновляем состояние в памяти
updateAuth({
authenticated: false,
authType: null,
userId: null,
address: null,
telegramId: null,
email: null,
isAdmin: false,
});
// Обновляем отображение подключенного состояния в UI
updateConnectionDisplay(true, response.data.authType, response.data);
} else {
// Обновляем отображение отключенного состояния
updateConnectionDisplay(false);
}
// Очищаем списки идентификаторов
identities.value = [];
processedGuestIds.value = [];
return response.data;
} catch (error) {
console.error('Error checking auth:', error);
// В случае ошибки сбрасываем состояние аутентификации
updateConnectionDisplay(false);
return { authenticated: false };
}
};
// Очищаем localStorage полностью
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('userId');
localStorage.removeItem('address');
localStorage.removeItem('isAdmin');
localStorage.removeItem('guestId');
localStorage.removeItem('guestMessages');
localStorage.removeItem('telegramId');
localStorage.removeItem('email');
const disconnect = async () => {
try {
// Удаляем все идентификаторы перед выходом
await axios.post('/api/auth/logout');
// Удаляем класс подключенного кошелька
// Обновляем состояние в памяти
updateAuth({
authenticated: false,
authType: null,
userId: null,
address: null,
telegramId: null,
email: null,
isAdmin: false,
});
// Обновляем отображение отключенного состояния
updateConnectionDisplay(false);
// Очищаем списки идентификаторов
identities.value = [];
processedGuestIds.value = [];
// Очищаем localStorage полностью
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('userId');
localStorage.removeItem('address');
localStorage.removeItem('isAdmin');
localStorage.removeItem('guestId');
localStorage.removeItem('guestMessages');
localStorage.removeItem('telegramId');
localStorage.removeItem('email');
// Удаляем класс подключенного кошелька
document.body.classList.remove('wallet-connected');
console.log('User disconnected successfully and all identifiers cleared');
return { success: true };
} catch (error) {
console.error('Error disconnecting:', error);
return { success: false, error: error.message };
}
};
// Обновляем список обработанных guestIds
const updateProcessedGuestIds = (ids) => {
if (Array.isArray(ids)) {
processedGuestIds.value = [...new Set([...processedGuestIds.value, ...ids])].slice(-20);
}
};
// Функция для обновления отображения подключения в UI
const updateConnectionDisplay = (isConnected, authType, authData = {}) => {
try {
console.log('Updating connection display:', { isConnected, authType, authData });
if (isConnected) {
document.body.classList.add('wallet-connected');
const authDisplayEl = document.getElementById('auth-display');
if (authDisplayEl) {
let displayText = 'Подключено';
if (authType === 'wallet' && authData.address) {
const shortAddress = `${authData.address.substring(0, 6)}...${authData.address.substring(authData.address.length - 4)}`;
displayText = `Кошелек: <strong>${shortAddress}</strong>`;
} else if (authType === 'email' && authData.email) {
displayText = `Email: <strong>${authData.email}</strong>`;
} else if (authType === 'telegram' && authData.telegramId) {
displayText = `Telegram: <strong>${authData.telegramUsername || authData.telegramId}</strong>`;
}
authDisplayEl.innerHTML = displayText;
authDisplayEl.style.display = 'inline-block';
}
// Скрываем кнопки авторизации и показываем кнопку выхода
const authButtonsEl = document.getElementById('auth-buttons');
const logoutButtonEl = document.getElementById('logout-button');
if (authButtonsEl) authButtonsEl.style.display = 'none';
if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block';
} else {
document.body.classList.remove('wallet-connected');
console.log('User disconnected successfully and all identifiers cleared');
return { success: true };
} catch (error) {
console.error('Error disconnecting:', error);
return { success: false, error: error.message };
}
};
// Обновляем список обработанных guestIds
const updateProcessedGuestIds = (ids) => {
if (Array.isArray(ids)) {
processedGuestIds.value = [...new Set([...processedGuestIds.value, ...ids])];
}
};
// Функция для обновления отображения подключения в UI
const updateConnectionDisplay = (isConnected, authType, authData = {}) => {
try {
console.log('Updating connection display:', { isConnected, authType, authData });
if (isConnected) {
document.body.classList.add('wallet-connected');
const authDisplayEl = document.getElementById('auth-display');
if (authDisplayEl) {
let displayText = 'Подключено';
if (authType === 'wallet' && authData.address) {
const shortAddress = `${authData.address.substring(0, 6)}...${authData.address.substring(authData.address.length - 4)}`;
displayText = `Кошелек: <strong>${shortAddress}</strong>`;
} else if (authType === 'email' && authData.email) {
displayText = `Email: <strong>${authData.email}</strong>`;
} else if (authType === 'telegram' && authData.telegramId) {
displayText = `Telegram: <strong>${authData.telegramUsername || authData.telegramId}</strong>`;
}
authDisplayEl.innerHTML = displayText;
authDisplayEl.style.display = 'inline-block';
}
// Скрываем кнопки авторизации и показываем кнопку выхода
const authButtonsEl = document.getElementById('auth-buttons');
const logoutButtonEl = document.getElementById('logout-button');
if (authButtonsEl) authButtonsEl.style.display = 'none';
if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block';
} else {
document.body.classList.remove('wallet-connected');
// Скрываем отображение аутентификации
const authDisplayEl = document.getElementById('auth-display');
if (authDisplayEl) {
authDisplayEl.style.display = 'none';
}
// Показываем кнопки авторизации и скрываем кнопку выхода
const authButtonsEl = document.getElementById('auth-buttons');
const logoutButtonEl = document.getElementById('logout-button');
if (authButtonsEl) authButtonsEl.style.display = 'flex';
if (logoutButtonEl) logoutButtonEl.style.display = 'none';
// Скрываем отображение аутентификации
const authDisplayEl = document.getElementById('auth-display');
if (authDisplayEl) {
authDisplayEl.style.display = 'none';
}
} catch (error) {
console.error('Error updating connection display:', error);
// Показываем кнопки авторизации и скрываем кнопку выхода
const authButtonsEl = document.getElementById('auth-buttons');
const logoutButtonEl = document.getElementById('logout-button');
if (authButtonsEl) authButtonsEl.style.display = 'flex';
if (logoutButtonEl) logoutButtonEl.style.display = 'none';
}
};
} catch (error) {
console.error('Error updating connection display:', error);
}
};
onMounted(async () => {
await checkAuth();
onMounted(async () => {
await checkAuth();
});
// Очищаем интервал при размонтировании компонента
onUnmounted(() => {
stopIdentitiesPolling();
});
/**
* Связывает новый идентификатор с текущим аккаунтом пользователя
* @param {string} type - Тип идентификатора (wallet, email, telegram)
* @param {string} value - Значение идентификатора
* @returns {Promise<Object>} - Результат операции
*/
const linkIdentity = async (type, value) => {
const response = await axios.post('/api/link', {
type,
value,
});
return response.data;
};
// Очищаем интервал при размонтировании компонента
onUnmounted(() => {
stopIdentitiesPolling();
});
/**
* Удаляет идентификатор пользователя
* @param {string} provider - Тип идентификатора (wallet, email, telegram)
* @param {string} providerId - Значение идентификатора
* @returns {Promise<Object>} - Результат операции
*/
const deleteIdentity = async (provider, providerId) => {
const response = await axios.delete(`/api/${provider}/${encodeURIComponent(providerId)}`);
return response.data;
};
/**
* Связывает новый идентификатор с текущим аккаунтом пользователя
* @param {string} type - Тип идентификатора (wallet, email, telegram)
* @param {string} value - Значение идентификатора
* @returns {Promise<Object>} - Результат операции
*/
const linkIdentity = async (type, value) => {
const response = await axios.post('/api/link', {
type,
value,
});
return response.data;
};
// === SINGLETON API ===
const authApi = {
isAuthenticated,
authType,
userId,
address,
isAdmin,
telegramId,
email,
identities,
processedGuestIds,
tokenBalances,
updateAuth,
checkAuth,
disconnect,
linkMessages,
updateIdentities,
updateProcessedGuestIds,
updateConnectionDisplay,
linkIdentity,
deleteIdentity,
};
/**
* Удаляет идентификатор пользователя
* @param {string} provider - Тип идентификатора (wallet, email, telegram)
* @param {string} providerId - Значение идентификатора
* @returns {Promise<Object>} - Результат операции
*/
const deleteIdentity = async (provider, providerId) => {
const response = await axios.delete(`/api/${provider}/${encodeURIComponent(providerId)}`);
return response.data;
};
// === PROVIDE/INJECT HELPERS ===
const AUTH_KEY = Symbol('auth');
return {
isAuthenticated,
authType,
userId,
address,
isAdmin,
telegramId,
email,
identities,
processedGuestIds,
tokenBalances,
updateAuth,
checkAuth,
disconnect,
linkMessages,
updateIdentities,
updateProcessedGuestIds,
updateConnectionDisplay,
linkIdentity,
deleteIdentity,
};
export function provideAuth() {
provide(AUTH_KEY, authApi);
}
export function useAuthContext() {
const ctx = inject(AUTH_KEY);
if (!ctx) throw new Error('Auth context not provided!');
return ctx;
}
// === useAuth теперь просто возвращает singleton ===
export function useAuth() {
return authApi;
}

View File

@@ -1,12 +1,12 @@
import { ref, onUnmounted } from 'vue';
import api from '../api/axios';
import { useAuth } from './useAuth';
import { useAuthContext } from './useAuth';
import { useNotifications } from './useNotifications';
export function useAuthFlow(options = {}) {
const { onAuthSuccess } = options; // Callback после успешной аутентификации/привязки
const auth = useAuth();
const auth = useAuthContext();
const { showSuccessMessage, showErrorMessage } = useNotifications();
// Состояния Telegram

View File

@@ -1,10 +1,10 @@
import { ref, watch, onUnmounted } from 'vue';
import { fetchTokenBalances } from '../services/tokens';
import { useAuth } from './useAuth'; // Предполагаем, что useAuth предоставляет identities
import { useAuthContext } from './useAuth'; // Предполагаем, что useAuth предоставляет identities
import eventBus from '../utils/eventBus';
export function useTokenBalances() {
const auth = useAuth(); // Получаем доступ к состоянию аутентификации
const auth = useAuthContext(); // Получаем доступ к состоянию аутентификации
const tokenBalances = ref([]); // теперь массив объектов
const isLoadingTokens = ref(false);
let balanceUpdateInterval = null;

View File

@@ -0,0 +1,57 @@
import axios from 'axios';
const api = '/api/tables';
export default {
async getTables() {
const res = await axios.get(`${api}?_t=${Date.now()}`);
return res.data;
},
async createTable(data) {
const res = await axios.post(api, data);
return res.data;
},
async getTable(id) {
const res = await axios.get(`${api}/${id}`);
return res.data;
},
async addColumn(tableId, data) {
const res = await axios.post(`${api}/${tableId}/columns`, data);
return res.data;
},
async addRow(tableId) {
const res = await axios.post(`${api}/${tableId}/rows`);
return res.data;
},
async saveCell(data) {
const res = await axios.post(`${api}/cell`, data);
return res.data;
},
async deleteColumn(columnId) {
const res = await axios.delete(`${api}/column/${columnId}`);
return res.data;
},
async deleteRow(rowId) {
const res = await axios.delete(`${api}/row/${rowId}`);
return res.data;
},
async updateColumn(columnId, data) {
const res = await axios.patch(`${api}/column/${columnId}`, data);
return res.data;
},
async updateTable(id, data) {
const res = await axios.patch(`${api}/${id}`, data);
return res.data;
},
async deleteTable(id) {
console.log('tablesService.deleteTable called with id:', id);
try {
const res = await axios.delete(`${api}/${id}`);
console.log('Delete response:', res.data);
return res.data;
} catch (error) {
console.error('Error in deleteTable service:', error);
throw error;
}
}
};

View File

@@ -22,13 +22,20 @@
</div>
<ContactTable v-if="showContacts" :contacts="contacts" @close="showContacts = false" @show-details="openContactDetails" />
<ContactDetails v-if="showContactDetails" :contact="selectedContact" @close="showContactDetails = false" @contact-deleted="onContactDeleted" />
<div class="crm-tables-block">
<h2>Таблицы</h2>
<button class="btn btn-info" @click="showTables = true">
<i class="fas fa-table"></i> Подробнее
</button>
</div>
<DynamicTablesModal v-if="showTables" @close="showTables = false" />
</div>
</BaseLayout>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, defineProps, defineEmits, computed, watch } from 'vue';
import { useAuth } from '../composables/useAuth';
import { useAuthContext } from '../composables/useAuth';
import { useRouter } from 'vue-router';
import { setToStorage } from '../utils/storage';
import BaseLayout from '../components/BaseLayout.vue';
@@ -38,6 +45,7 @@ import ContactTable from '../components/ContactTable.vue';
import contactsService from '../services/contactsService.js';
import DleManagement from '../components/DleManagement.vue';
import ContactDetails from '../components/ContactDetails.vue';
import DynamicTablesModal from '../components/tables/DynamicTablesModal.vue';
// Определяем props
const props = defineProps({
@@ -50,7 +58,7 @@ const props = defineProps({
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const auth = useAuth();
const auth = useAuthContext();
const router = useRouter();
const isLoading = ref(true);
const dleList = ref([]);
@@ -62,6 +70,7 @@ const contacts = ref([]);
const isLoadingContacts = ref(false);
const selectedContact = ref(null);
const showContactDetails = ref(false);
const showTables = ref(false);
// Функция для перехода на домашнюю страницу и открытия боковой панели
const goToHomeAndShowSidebar = () => {
@@ -281,4 +290,24 @@ strong {
font-size: 1rem;
padding: 8px 18px;
}
.crm-tables-block {
margin: 32px 0 24px 0;
padding: 24px;
background: #f8fafc;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
display: flex;
align-items: center;
justify-content: space-between;
}
.crm-tables-block h2 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
}
.crm-tables-block .btn {
font-size: 1rem;
padding: 8px 18px;
}
</style>

View File

@@ -20,7 +20,7 @@
<script setup>
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits } from 'vue';
import { useAuth } from '../composables/useAuth';
import { useAuthContext } from '../composables/useAuth';
import { useChat } from '../composables/useChat';
import { connectWithWallet } from '../services/wallet';
import eventBus from '../utils/eventBus';
@@ -44,7 +44,7 @@
// 1. ИСПОЛЬЗОВАНИЕ COMPOSABLES
// =====================================================================
const auth = useAuth();
const auth = useAuthContext();
// =====================================================================
// 2. СОСТОЯНИЯ КОМПОНЕНТА

View File

@@ -31,7 +31,7 @@
<script setup>
import { ref, onMounted, watch, onBeforeUnmount, computed, defineProps, defineEmits } from 'vue';
import { useAuth } from '../composables/useAuth';
import { useAuthContext } from '../composables/useAuth';
import { useRouter } from 'vue-router';
import { getFromStorage, setToStorage } from '../utils/storage';
import BaseLayout from '../components/BaseLayout.vue';
@@ -48,7 +48,7 @@ const props = defineProps({
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const auth = useAuth();
const auth = useAuthContext();
const router = useRouter();
const isLoading = ref(true);

View File

@@ -316,12 +316,12 @@
<script setup>
import { reactive, onMounted, computed, ref, watch } from 'vue';
import axios from 'axios'; // Предполагаем, что axios доступен
import { useAuth } from '@/composables/useAuth'; // Импортируем composable useAuth
import { useAuthContext } from '@/composables/useAuth'; // Импортируем composable useAuth
import dleService from '@/services/dleService';
import useBlockchainNetworks from '@/composables/useBlockchainNetworks'; // Импортируем composable для работы с сетями
// TODO: Импортировать API
const { address, isAdmin, auth, user } = useAuth(); // Получаем объект адреса и статус админа
const { address, isAdmin, auth, user } = useAuthContext(); // Получаем объект адреса и статус админа
// Инициализация composable для работы с сетями блокчейн
const {