новый дизайн

This commit is contained in:
2026-07-25 18:36:45 +04:00
parent df7f98fe8b
commit c43a73e148
54 changed files with 4234 additions and 1110 deletions
+263
View File
@@ -0,0 +1,263 @@
<script setup>
import { Head, useForm, usePage } from '@inertiajs/vue3';
import { computed, ref } from 'vue';
import PortalLayout from '../../Layouts/PortalLayout.vue';
import { arrowSvg } from '../Landing/Components/symbols';
const props = defineProps({
site: { type: Object, required: true },
auth: { type: Object, required: true },
urls: { type: Object, required: true },
csrfToken: { type: String, required: true },
flash: { type: Object, default: () => ({}) },
account: { type: Object, required: true },
characters: { type: Array, default: () => [] },
client: { type: Object, required: true },
wallet: { type: Object, required: true },
});
const page = usePage();
const sharedErrors = computed(() => page.props.errors || {});
const activeSection = ref('overview');
const passwordForm = useForm({
current_password: '',
password: '',
password_confirmation: '',
});
const sections = [
{ id: 'overview', label: 'Обзор', number: '01' },
{ id: 'characters', label: 'Персонажи', number: '02' },
{ id: 'wallet', label: 'Слёзы Элуны', number: '03' },
{ id: 'security', label: 'Безопасность', number: '04' },
];
function goTo(id) {
activeSection.value = id;
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function updatePassword() {
passwordForm.post(props.urls.password, {
preserveScroll: true,
onSuccess: () => passwordForm.reset(),
onError: () => {
passwordForm.reset('password', 'password_confirmation');
goTo('security');
},
});
}
</script>
<template>
<Head>
<title>Личный кабинет — {{ site.name }}</title>
<meta name="description" content="Персонажи, баланс и настройки игрового аккаунта MoonWell." />
</Head>
<PortalLayout :site="site" :auth="auth" :urls="urls" :flash="flash">
<section class="portal-welcome wrap">
<div>
<span class="eyebrow">Личный кабинет</span>
<h1 class="h-display">С возвращением, <em>{{ account.username }}</em></h1>
<p class="lead">Твой аккаунт, герои и всё необходимое для следующего путешествия.</p>
</div>
<div class="portal-welcome__status">
<span :class="['portal-status-dot', { danger: account.locked }]"></span>
<span>
<small>Статус аккаунта</small>
<strong>{{ account.status_label }}</strong>
</span>
</div>
</section>
<div class="portal-dashboard wrap">
<aside class="portal-sidebar">
<span class="portal-sidebar__label">Навигация</span>
<button
v-for="section in sections"
:key="section.id"
:class="{ active: activeSection === section.id }"
type="button"
@click="goTo(section.id)"
>
<span>{{ section.number }}</span>
{{ section.label }}
</button>
<div class="portal-sidebar__balance">
<small>Баланс</small>
<strong>{{ wallet.formatted_amount }}</strong>
<span>Слёз Элуны</span>
</div>
</aside>
<div class="portal-content">
<section id="overview" class="portal-section">
<div class="portal-section__head">
<div>
<span class="eyebrow">Обзор</span>
<h2 class="h-serif">Данные аккаунта</h2>
</div>
<a v-if="auth.is_admin" class="btn btn-ghost portal-small-button" :href="urls.admin">Открыть админку</a>
</div>
<div class="portal-grid portal-grid--account">
<article class="portal-card portal-account-card corners">
<dl class="portal-stats">
<div><dt>Логин</dt><dd>{{ account.username }}</dd></div>
<div><dt>Email</dt><dd>{{ account.email }}</dd></div>
<div><dt>Уровень доступа</dt><dd>{{ account.access_label }}</dd></div>
<div><dt>Регистрация</dt><dd>{{ account.joined_at }}</dd></div>
<div><dt>Последний вход</dt><dd>{{ account.last_login_at }}</dd></div>
<div><dt>Персонажей</dt><dd>{{ characters.length }}</dd></div>
</dl>
</article>
<article class="portal-card portal-launcher-card corners">
<span class="eyebrow">Лаунчер</span>
<h3 class="h-serif">Врата в Азерот</h3>
<p>Лаунчер автоматически скачает клиент, настроит realmlist и установит обновления.</p>
<a class="btn btn-primary" :href="urls.client">
Скачать лаунчер <span v-html="arrowSvg"></span>
</a>
<span class="portal-file-note">{{ client.filename }}</span>
</article>
</div>
</section>
<section id="characters" class="portal-section">
<div class="portal-section__head">
<div>
<span class="eyebrow">Мои персонажи</span>
<h2 class="h-serif">Герои аккаунта</h2>
</div>
<span class="portal-section__count">{{ characters.length }} {{ characters.length === 1 ? 'герой' : 'героев' }}</span>
</div>
<div v-if="characters.length" class="portal-character-grid">
<article v-for="character in characters" :key="character.name" class="portal-card portal-character-card">
<div class="portal-character-card__head">
<span class="portal-character-level"><span>{{ character.level }}</span></span>
<span :class="['portal-online', { online: character.online }]">
{{ character.online ? 'Онлайн' : 'Оффлайн' }}
</span>
</div>
<h3 class="h-serif">{{ character.name }}</h3>
<p>{{ character.race }} · {{ character.class }}</p>
<dl>
<div><dt>Пол</dt><dd>{{ character.gender }}</dd></div>
<div><dt>Золото</dt><dd>{{ character.money }}</dd></div>
<div><dt>Наработано</dt><dd>{{ character.played }}</dd></div>
</dl>
</article>
</div>
<article v-else class="portal-card portal-empty">
<span class="portal-empty__moon">☾</span>
<h3 class="h-serif">Персонажей пока нет</h3>
<p>Создай первого героя в игре — он появится здесь автоматически.</p>
<a class="btn btn-primary" :href="urls.client">Скачать лаунчер</a>
</article>
</section>
<section id="wallet" class="portal-section">
<div class="portal-section__head">
<div>
<span class="eyebrow">Слёзы Элуны</span>
<h2 class="h-serif">Баланс и операции</h2>
</div>
</div>
<div class="portal-grid portal-grid--wallet">
<article class="portal-card portal-wallet-card corners">
<span class="portal-card-label">Доступно сейчас</span>
<div class="portal-balance">{{ wallet.formatted_amount }}</div>
<p>Слёз Элуны</p>
<form class="portal-deposit" method="POST" :action="urls.deposit">
<input type="hidden" name="_token" :value="csrfToken" />
<label class="portal-field">
<span>Сумма пополнения, ₽</span>
<input
type="number"
name="amount"
:min="wallet.minimum_deposit"
:max="wallet.maximum_deposit"
step="0.01"
:value="wallet.deposit_amount"
required
/>
<small v-if="sharedErrors.amount" class="portal-field-error">{{ sharedErrors.amount }}</small>
</label>
<button class="btn btn-primary" type="submit">
Пополнить <span v-html="arrowSvg"></span>
</button>
</form>
<div class="portal-rate">
<span>1 ₽ = {{ wallet.tears_per_ruble }} Слёз Элуны</span>
<span>От {{ wallet.minimum_deposit }} до {{ wallet.formatted_maximum_deposit }} ₽</span>
</div>
</article>
<article class="portal-card portal-history-card">
<span class="portal-card-label">Последние операции</span>
<div v-if="wallet.transactions.length" class="portal-history">
<div v-for="transaction in wallet.transactions" :key="transaction.id" class="portal-history__row">
<span>
<strong>{{ transaction.label }}</strong>
<small>{{ transaction.created_at }}</small>
</span>
<strong :class="{ positive: transaction.is_positive }">
{{ transaction.is_positive ? '+' : '' }}{{ transaction.formatted_amount }}
</strong>
</div>
</div>
<div v-else class="portal-history-empty">Операций пока нет.</div>
</article>
</div>
</section>
<section id="security" class="portal-section">
<div class="portal-section__head">
<div>
<span class="eyebrow">Безопасность</span>
<h2 class="h-serif">Сменить пароль</h2>
</div>
</div>
<div class="portal-card portal-security-card corners">
<div class="portal-security-card__copy">
<h3 class="h-serif">Защити свой аккаунт</h3>
<p>После обновления новый пароль сразу начнёт действовать и в игре, и в личном кабинете.</p>
<ul>
<li>От 8 до 32 символов</li>
<li>Хотя бы одна буква и одна цифра</li>
<li>Не используй пароль от почты</li>
</ul>
</div>
<form class="portal-password-form" @submit.prevent="updatePassword">
<label :class="['portal-field', { 'has-error': passwordForm.errors.current_password }]">
<span>Текущий пароль</span>
<input v-model="passwordForm.current_password" type="password" autocomplete="current-password" maxlength="32" required />
<small v-if="passwordForm.errors.current_password">{{ passwordForm.errors.current_password }}</small>
</label>
<label :class="['portal-field', { 'has-error': passwordForm.errors.password }]">
<span>Новый пароль</span>
<input v-model="passwordForm.password" type="password" autocomplete="new-password" maxlength="32" required />
<small v-if="passwordForm.errors.password">{{ passwordForm.errors.password }}</small>
</label>
<label :class="['portal-field', { 'has-error': passwordForm.errors.password }]">
<span>Повтори новый пароль</span>
<input v-model="passwordForm.password_confirmation" type="password" autocomplete="new-password" maxlength="32" required />
</label>
<button class="btn btn-primary portal-submit" type="submit" :disabled="passwordForm.processing">
{{ passwordForm.processing ? 'Обновляем…' : 'Обновить пароль' }}
<span v-html="arrowSvg"></span>
</button>
</form>
</div>
</section>
</div>
</div>
</PortalLayout>
</template>