This commit is contained in:
2026-04-30 01:17:17 +04:00
parent dc0f0409af
commit ed78bb603b
24 changed files with 1554 additions and 410 deletions

View File

@@ -0,0 +1,128 @@
<div class="page">
<div class="card">
<div class="card__header">
<a class="back" routerLink="/" aria-label="Назад">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6" />
</svg>
</a>
<h1 class="card__title">Новый Фастчек</h1>
<p class="card__subtitle">Укажите сумму для пополнения</p>
</div>
<div class="card__body">
<!-- Payment methods -->
<div class="field">
<span class="field__label">Способ оплаты</span>
<div class="methods">
<button type="button" class="method" [class.method--active]="payment() === 'sbp'"
(click)="selectPayment('sbp', true)" aria-label="СБП">
<img class="method__logo"
src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg"
alt="СБП" />
</button>
<button type="button" class="method method--disabled" disabled aria-label="WeChat Pay">
<img class="method__logo" src="/wechat-pay.svg" alt="WeChat Pay" />
</button>
<button type="button" class="method method--disabled" disabled aria-label="Visa">
<img class="method__logo" src="/visa.svg" alt="Visa" />
</button>
<button type="button" class="method method--disabled" disabled aria-label="MasterCard">
<img class="method__logo" src="/mastercard.svg" alt="Mastercard" />
</button>
</div>
</div>
<!-- Currencies -->
<div class="field">
<span class="field__label">Валюта</span>
<div class="currencies">
<button type="button" class="chip" [class.chip--active]="currency() === 'RUB'"
(click)="selectCurrency('RUB', true)">
<!-- <span class="chip__flag">🇷🇺</span> -->
<span class="chip__sign"></span>
<span class="chip__code">RUB</span>
</button>
<button type="button" class="chip chip--disabled" disabled>
<!-- <span class="chip__flag">🇨🇳</span> -->
<span class="chip__sign">¥</span>
<span class="chip__code">CNY</span>
</button>
<button type="button" class="chip chip--disabled" disabled>
<!-- <span class="chip__flag">🇺🇸</span> -->
<span class="chip__sign">$</span>
<span class="chip__code">USD</span>
</button>
<button type="button" class="chip chip--disabled" disabled>
<!-- <span class="chip__flag">🇪🇺</span> -->
<span class="chip__sign"></span>
<span class="chip__code">EUR</span>
</button>
<button type="button" class="chip chip--disabled" disabled>
<!-- <span class="chip__flag">🇦🇲</span> -->
<span class="chip__sign">֏</span>
<span class="chip__code">AMD</span>
</button>
</div>
</div>
<div class="field">
<label class="field__label" for="amount">Сумма платежа</label>
<div class="input-wrap" [class.input-wrap--error]="error()">
<span class="input-wrap__prefix"></span>
<input
id="amount"
type="number"
class="input-wrap__input"
[ngModel]="amount()"
(ngModelChange)="onAmountChange($event)"
min="1"
step="1"
inputmode="numeric"
placeholder="0"
autofocus
/>
</div>
@if (error()) {
<span class="field__error">{{ error() }}</span>
}
</div>
<div class="field">
<label class="field__label" for="note">Примечание</label>
<textarea
id="note"
class="note-input"
[ngModel]="note()"
(ngModelChange)="onNoteChange($event)"
placeholder="Причина платежа..."
rows="3"
maxlength="500"
></textarea>
</div>
<button class="pay-btn" type="button" (click)="createCheck()" [disabled]="loading()">
<span class="pay-btn__icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5v14M5 12h14" />
</svg>
</span>
{{ loading() ? 'Создание…' : 'Создать Фастчек' }}
</button>
</div>
<div class="card__footer">
<span class="secure-badge">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
Защищённое соединение
</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,163 @@
@use './../../../shared' as *;
.card__header {
position: relative;
}
.back {
position: absolute;
top: 18px;
left: 18px;
width: 38px;
height: 38px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
color: #fff;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.22);
text-decoration: none;
transition: background 0.15s;
z-index: 1;
&:hover { background: rgba(255, 255, 255, 0.28); }
&:active { background: rgba(255, 255, 255, 0.36); }
}
.currency-badge {
display: flex;
align-items: center;
gap: 10px;
background: #f1f5f9;
border-radius: 12px;
padding: 12px 16px;
margin-bottom: 18px;
&__flag { font-size: 22px; line-height: 1; }
&__code { font-size: 15px; font-weight: 700; color: #0f172a; }
&__name { font-size: 13px; color: #64748b; margin-left: auto; }
}
// ─── Methods row ────────────────────────────────────────────────────────────
.methods {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.method {
display: flex;
align-items: center;
justify-content: center;
height: 56px;
padding: 8px;
border-radius: 12px;
border: 2px solid #e2e8f0;
background: #fff;
cursor: pointer;
transition: border-color .15s, background .15s, transform .1s, box-shadow .15s;
&__logo {
max-width: 100%;
max-height: 28px;
object-fit: contain;
display: block;
pointer-events: none;
}
&:hover:not(:disabled):not(.method--disabled) {
border-color: #cbd5e1;
}
&:active:not(:disabled) { transform: scale(.97); }
&--active {
border-color: #2563eb;
background: rgba(37, 99, 235, .06);
box-shadow: 0 0 0 3px rgba(37, 99, 235, .1);
}
&--disabled,
&:disabled {
cursor: not-allowed;
background: #f8fafc;
.method__logo {
filter: grayscale(1);
opacity: .45;
}
}
}
// ─── Currency chips ─────────────────────────────────────────────────────────
.currencies {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 14px;
height: 38px;
border-radius: 999px;
border: 2px solid #e2e8f0;
background: #f8fafc;
color: #475569;
font-family: inherit;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: border-color .15s, background .15s, color .15s;
&__flag { font-size: 16px; line-height: 1; }
&__sign {
font-size: 15px;
font-weight: 800;
color: #1e40af;
line-height: 1;
}
&__code { letter-spacing: .3px; }
&--active {
border-color: #2563eb;
background: rgba(37, 99, 235, .08);
color: #1e40af;
}
&--disabled,
&:disabled {
opacity: .45;
cursor: not-allowed;
color: #94a3b8;
.chip__sign { color: #94a3b8; }
}
}
.note-input {
width: 100%;
border: 2px solid #e2e8f0;
border-radius: 14px;
background: #f8fafc;
padding: 14px 16px;
font-size: 15px;
font-weight: 500;
color: #0f172a;
font-family: inherit;
resize: vertical;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
line-height: 1.5;
&::placeholder { color: #cbd5e1; font-weight: 400; }
&:focus {
border-color: #2563eb;
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12);
background: #fff;
}
}

View File

@@ -0,0 +1,103 @@
import { Component, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { FastcheckService } from '../../fastcheck.service';
import { FASTCHECK_API } from '../../api';
interface CreateFastcheckResponse {
fastcheck: string;
expiration: string;
code: string;
Status: boolean;
}
type PaymentMethod = 'sbp' | 'wechat' | 'visa' | 'master';
type Currency = 'RUB' | 'CNY' | 'USD' | 'EUR' | 'AMD';
@Component({
selector: 'app-create-page',
imports: [FormsModule, RouterLink],
templateUrl: './create-page.html',
styleUrl: './create-page.scss'
})
export class CreatePage {
private http = inject(HttpClient);
private store = inject(FastcheckService);
private router = inject(Router);
amount = signal<number>(10);
note = signal<string>('');
error = signal<string>('');
loading = signal<boolean>(false);
payment = signal<PaymentMethod>('sbp');
currency = signal<Currency>('RUB');
/** sessionID for the Authorization header. Comes from ?session=... or websession. */
private get sessionId(): string {
return new URLSearchParams(window.location.search).get('session') ?? '';
}
selectPayment(method: PaymentMethod, enabled: boolean): void {
if (!enabled) return;
this.payment.set(method);
}
selectCurrency(c: Currency, enabled: boolean): void {
if (!enabled) return;
this.currency.set(c);
}
createCheck(): void {
const val = this.amount();
if (!val || val <= 0) {
this.error.set('Введите корректную сумму');
return;
}
this.error.set('');
this.loading.set(true);
const headers: Record<string, string> = {};
if (this.sessionId) {
headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId });
}
this.http
.post<CreateFastcheckResponse>(
`${FASTCHECK_API}/fastcheck`,
{ amount: val, currency: this.currency() },
{ headers }
)
.subscribe({
next: (res) => {
this.loading.set(false);
if (res?.fastcheck) {
this.store.setCreated({
fastcheck: res.fastcheck,
code: res.code,
amount: val,
expiration: res.expiration
});
this.router.navigate(['/']);
} else {
this.error.set('Не удалось создать Фастчек.');
}
},
error: () => {
this.loading.set(false);
this.error.set('Ошибка при создании Фастчека. Попробуйте ещё раз.');
}
});
}
onAmountChange(value: number): void {
this.amount.set(value);
if (value > 0) this.error.set('');
}
onNoteChange(value: string): void {
this.note.set(value);
}
}