<script setup>
// Аккаунты Telegram текущего SSO-юзера: карточки + LoginWizard модалка.
// Владение — на бэкенде (accounts.user_id = sub); 404 на чужой id тоже здесь.
import { computed, onMounted, ref } from 'vue'
import {
GnAlert, GnBadge, GnButton, GnCard, GnConfirmDialog,
GnIconButton, GnLoader, GnPageHeader,
} from 'gnexus-ui-kit/vue'
import { useToast } from 'gnexus-ui-kit/vue'
import { api } from '../api.js'
import { maskPhone } from '../format.js'
import { t } from '../i18n/index.js'
import LoginWizard from './LoginWizard.vue'
const toast = useToast()
const accounts = ref([])
const maxAccounts = ref(null)
const loading = ref(true)
const error = ref('')
const wizardOpen = ref(false)
const displayName = (a) =>
a.display_name || (a.username ? '@' + a.username : maskPhone(a.phone))
function statusVariant(status) {
if (status === 'active') return 'success'
if (status === 'logged_out') return 'warning'
return 'danger'
}
async function load() {
try {
const data = await api.accounts()
accounts.value = data.accounts
maxAccounts.value = data.max_accounts
error.value = ''
} catch (e) {
if (e.status !== 401) error.value = e.message
} finally {
loading.value = false
}
}
onMounted(load)
// --- connect / disconnect / delete (логаут Telegram, необратимо) --------------
const busy = ref(0)
async function doConnect(a) {
busy.value = a.id
try {
await api.connectAccount(a.id)
} catch (e) {
toast.danger({ title: t('accounts.connectFailed'), text: e.message })
} finally {
busy.value = 0
await load()
}
}
async function doDisconnect(a) {
busy.value = a.id
try {
await api.disconnectAccount(a.id)
} catch (e) {
toast.danger({ title: t('common.failed'), text: e.message })
} finally {
busy.value = 0
await load()
}
}
const deleteTarget = ref(null)
const deleteOpen = ref(false)
function askDelete(a) {
deleteTarget.value = a
deleteOpen.value = true
}
async function doDelete() {
if (!deleteTarget.value) return
try {
await api.logoutAccount(deleteTarget.value.id)
toast.success({ title: t('accounts.deleted') })
await load()
} catch (e) {
toast.danger({ title: t('common.failed'), text: e.message })
} finally {
deleteTarget.value = null
}
}
// перелогин: logged_out аккаунт нельзя оживить — только новый логин
function reLogin(a) {
wizardOpen.value = true
deleteTarget.value = null
}
</script>
<template>
<GnPageHeader :title="t('accounts.title')" kicker="tgclient" />
<GnLoader v-if="loading" circle :label="t('common.loading')" />
<GnAlert v-else-if="error" variant="danger" style="margin-bottom: 16px">{{ error }}</GnAlert>
<template v-else>
<GnCard :title="t('accounts.title')" class="wide-card">
<div class="head-row">
<p v-if="maxAccounts" class="card-note">
{{ t('accounts.maxAccounts', { n: accounts.length, limit: maxAccounts }) }}
</p>
<GnButton
variant="primary"
size="sm"
icon="ph-plus-circle"
:disabled="maxAccounts && accounts.length >= maxAccounts"
@click="wizardOpen = true"
>
{{ t('accounts.addAccount') }}
</GnButton>
</div>
<p v-if="!accounts.length" class="card-note">
{{ t('accounts.emptyTitle') }} — {{ t('accounts.emptyText') }}
</p>
<div class="account-grid">
<div v-for="a in accounts" :key="a.id" class="account-card card">
<div class="account-head">
<strong class="account-name">{{ displayName(a) }}</strong>
<GnBadge :variant="statusVariant(a.status)" outline>
{{ t('accounts.status' + (a.status === 'active' ? 'Active' : a.status === 'logged_out' ? 'LoggedOut' : 'Error')) }}
</GnBadge>
</div>
<p class="account-meta">{{ maskPhone(a.phone) }}</p>
<p v-if="a.label" class="account-meta">{{ a.label }}</p>
<p class="account-meta">
{{ a.connected ? t('accounts.connectedYes') : t('accounts.connectedNo') }}
· {{ t('accounts.colLastUsed') }}: {{ a.last_used_at ? new Date(a.last_used_at).toLocaleString() : t('time.never') }}
</p>
<div class="account-actions">
<GnIconButton
v-if="a.status === 'active' && !a.connected"
icon="ph ph-plugs-connected"
size="sm"
:label="t('accounts.connect')"
:disabled="busy === a.id"
@click="doConnect(a)"
/>
<GnIconButton
v-if="a.connected"
icon="ph ph-plug-slash"
size="sm"
:label="t('accounts.disconnect')"
:disabled="busy === a.id"
@click="doDisconnect(a)"
/>
<GnIconButton
v-if="a.status === 'logged_out'"
icon="ph ph-arrow-clockwise"
size="sm"
:label="t('accounts.loginAgain')"
@click="reLogin(a)"
/>
<GnIconButton
icon="ph ph-sign-out"
size="sm"
variant="danger"
:label="t('accounts.delete')"
@click="askDelete(a)"
/>
</div>
</div>
</div>
</GnCard>
</template>
<!-- логин-визард: phone → код → (2FA) → done -->
<LoginWizard v-model:open="wizardOpen" @done="load" />
<GnConfirmDialog
v-model:open="deleteOpen"
:title="t('accounts.deleteTitle')"
:message="t('accounts.deleteMessage', { name: deleteTarget?.phone || '' })"
:confirm-text="t('accounts.delete')"
:cancel-text="t('common.cancel')"
confirm-variant="danger"
@confirm="doDelete"
/>
</template>
<style scoped>
/* GnCard по умолчанию — fit-content; страницы тянут карты на всю ширину */
.wide-card {
max-width: none;
width: 100%;
}
.head-row {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 14px;
}
.card-note {
margin: 0;
opacity: 0.75;
font-size: 14px;
}
.account-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.account-card {
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.account-head {
display: flex;
gap: 10px;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
.account-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.account-meta {
margin: 0;
font-size: 13px;
opacity: 0.75;
}
.account-actions {
display: flex;
gap: 6px;
margin-top: 8px;
}
</style>