Files
dating-app-frontend/src/views/dev/TestView.vue
Oscar ed51218249 feat(src/assets/icons/icon-names.ts): добавляет новый иконку 'sprite' в список иконок
 feat(src/views/dev/TestView.vue): упрощает типизацию ответа от API для загрузки профилей
 feat(src/assets/icons/sprite.svg): добавляет новые иконки в спрайт
♻️ refactor(tsconfig.json): удаляет базовый URL из конфигурации TypeScript
2026-06-24 16:24:41 +03:00

459 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="test-view">
<header class="test-view__header">
<span class="test-view__badge">DEV</span>
<h1 class="test-view__title">
Test Utils
</h1>
<p class="test-view__sub">
Сброс флоу лайков / матчей / чатов
</p>
</header>
<!-- Profiles list -->
<section class="test-card">
<div class="test-card__head">
<h2 class="test-card__title">
Профили
</h2>
<button class="test-btn test-btn--ghost" :disabled="loadingProfiles" @click="loadProfiles">
{{ loadingProfiles ? 'Загрузка...' : 'Обновить' }}
</button>
</div>
<div v-if="loadingProfiles" class="test-card__loading">
Загрузка профилей...
</div>
<div v-else-if="profiles.length === 0" class="test-card__empty">
Профилей нет
</div>
<div v-else class="profile-table">
<div class="profile-table__head">
<label class="profile-table__check">
<input
type="checkbox"
:checked="allSelected"
:indeterminate.prop="someSelected && !allSelected"
@change="toggleAll"
>
</label>
<span>Имя</span>
<span>Телефон</span>
<span>activeChatId</span>
</div>
<label
v-for="p in profiles"
:key="p.id"
class="profile-table__row"
:class="{ 'profile-table__row--selected': selected.has(p.id) }"
>
<span class="profile-table__check">
<input type="checkbox" :value="p.id" :checked="selected.has(p.id)" @change="toggle(p.id)">
</span>
<span class="profile-table__name">{{ p.name }}</span>
<span class="profile-table__mono">{{ p.phone ?? '—' }}</span>
<span class="profile-table__mono" :class="{ 'profile-table__active': !!p.activeChatId }">
{{ p.activeChatId ? `${p.activeChatId.slice(0, 8)}` : '—' }}
</span>
</label>
</div>
</section>
<!-- Actions -->
<section class="test-card">
<h2 class="test-card__title">
Действия
</h2>
<div class="test-actions">
<button
class="test-btn test-btn--warn"
:disabled="selected.size === 0 || resetting"
@click="resetSelected"
>
{{ resetting ? 'Сброс...' : `Сбросить выбранных (${selected.size})` }}
</button>
<button
class="test-btn test-btn--danger"
:disabled="profiles.length === 0 || resetting"
@click="resetAll"
>
{{ resetting ? 'Сброс...' : `Сбросить всех (${profiles.length})` }}
</button>
</div>
</section>
<!-- Result log -->
<section v-if="log.length" class="test-card">
<div class="test-card__head">
<h2 class="test-card__title">
Лог
</h2>
<button class="test-btn test-btn--ghost" @click="log = []">
Очистить
</button>
</div>
<div class="test-log">
<div
v-for="(entry, i) in log"
:key="i"
class="test-log__entry"
:class="`test-log__entry--${entry.type}`"
>
<span class="test-log__time">{{ entry.time }}</span>
<span>{{ entry.message }}</span>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { axiosInstance } from '@/api/client'
interface DevProfile {
id: string
name: string
phone: string | null
activeChatId: string | null
}
interface LogEntry {
type: 'success' | 'error' | 'info'
time: string
message: string
}
const profiles = ref<DevProfile[]>([])
const selected = reactive(new Set<string>())
const loadingProfiles = ref(false)
const resetting = ref(false)
const log = ref<LogEntry[]>([])
const allSelected = computed(() => profiles.value.length > 0 && selected.size === profiles.value.length)
const someSelected = computed(() => selected.size > 0)
function now() {
return new Date().toLocaleTimeString('ru-RU')
}
function addLog(type: LogEntry['type'], message: string) {
log.value.unshift({ type, time: now(), message })
}
async function loadProfiles() {
loadingProfiles.value = true
try {
const res = await axiosInstance.get<DevProfile[]>('/api/v1/dev/profiles')
profiles.value = res.data
addLog('info', `Загружено профилей: ${profiles.value.length}`)
}
catch (e: any) {
addLog('error', `Ошибка загрузки: ${e?.message ?? String(e)}`)
}
finally {
loadingProfiles.value = false
}
}
function toggle(id: string) {
if (selected.has(id))
selected.delete(id)
else selected.add(id)
}
function toggleAll() {
if (allSelected.value) {
selected.clear()
}
else {
profiles.value.forEach(p => selected.add(p.id))
}
}
async function doReset(ids: string[]) {
if (!ids.length)
return
resetting.value = true
try {
const res = await axiosInstance.delete<{ deleted: Record<string, number>, reset: number }>(
`/api/v1/dev/reset-flow?profileIds=${ids.join(',')}`,
)
const { deleted, reset } = res.data
addLog(
'success',
`Сброшено: likes=${deleted.likes}, matches=${deleted.matches}, chats=${deleted.chats}, profiles=${reset}`,
)
await loadProfiles()
selected.clear()
}
catch (e: any) {
addLog('error', `Ошибка сброса: ${e?.message ?? String(e)}`)
}
finally {
resetting.value = false
}
}
function resetSelected() {
doReset([...selected])
}
function resetAll() {
doReset(profiles.value.map(p => p.id))
}
onMounted(loadProfiles)
</script>
<style scoped lang="scss">
.test-view {
max-width: 720px;
margin: 0 auto;
padding: 32px 20px 64px;
display: flex;
flex-direction: column;
gap: 20px;
&__header {
display: flex;
flex-direction: column;
gap: 6px;
}
&__badge {
font-family: var(--font-mono);
font-size: 0.6875rem;
letter-spacing: 0.12em;
color: #c45c3a;
border: 1px solid currentColor;
border-radius: 3px;
padding: 2px 6px;
display: inline-block;
width: fit-content;
}
&__title {
font-family: var(--font-display);
font-size: 1.75rem;
color: var(--color-cream);
margin: 0;
}
&__sub {
font-family: var(--font-mono);
font-size: 0.8125rem;
color: var(--color-muted);
margin: 0;
}
}
.test-card {
background: var(--color-surface-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
&__head {
display: flex;
align-items: center;
justify-content: space-between;
}
&__title {
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-muted);
margin: 0;
}
&__loading,
&__empty {
font-family: var(--font-mono);
font-size: 0.8125rem;
color: var(--color-dim);
padding: 12px 0;
}
}
.profile-table {
display: flex;
flex-direction: column;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
overflow: hidden;
&__head,
&__row {
display: grid;
grid-template-columns: 32px 1fr 1fr 1fr;
align-items: center;
gap: 8px;
padding: 10px 12px;
font-family: var(--font-mono);
font-size: 0.8rem;
}
&__head {
background: var(--color-surface-3);
color: var(--color-muted);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.06em;
border-bottom: 1px solid var(--color-border);
}
&__row {
color: var(--color-cream);
cursor: pointer;
border-bottom: 1px solid var(--color-border);
transition: background var(--transition-fast);
&:last-child {
border-bottom: none;
}
&:hover {
background: var(--color-surface-3);
}
&--selected {
background: rgba(196, 92, 58, 0.08);
}
}
&__check {
display: flex;
align-items: center;
justify-content: center;
input[type='checkbox'] {
width: 14px;
height: 14px;
accent-color: var(--color-signal);
cursor: pointer;
}
}
&__name {
font-family: var(--font-display);
font-size: 0.9rem;
color: var(--color-cream);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__mono {
color: var(--color-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__active {
color: #c45c3a;
}
}
.test-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.test-btn {
font-family: var(--font-mono);
font-size: 0.8125rem;
padding: 8px 16px;
border-radius: var(--radius-sm);
border: 1px solid transparent;
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
&--ghost {
background: none;
border-color: var(--color-border);
color: var(--color-muted);
&:hover:not(:disabled) {
color: var(--color-cream);
border-color: var(--color-cream);
}
}
&--warn {
background: rgba(196, 92, 58, 0.12);
border-color: rgba(196, 92, 58, 0.4);
color: #c45c3a;
&:hover:not(:disabled) {
background: rgba(196, 92, 58, 0.22);
border-color: #c45c3a;
}
}
&--danger {
background: rgba(180, 40, 40, 0.12);
border-color: rgba(180, 40, 40, 0.4);
color: #e05555;
&:hover:not(:disabled) {
background: rgba(180, 40, 40, 0.22);
border-color: #e05555;
}
}
}
.test-log {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 200px;
overflow-y: auto;
&__entry {
display: flex;
gap: 12px;
font-family: var(--font-mono);
font-size: 0.75rem;
padding: 6px 8px;
border-radius: 3px;
&--success {
background: rgba(60, 180, 100, 0.08);
color: #5dbd7e;
}
&--error {
background: rgba(200, 60, 60, 0.08);
color: #e05555;
}
&--info {
background: rgba(100, 120, 160, 0.08);
color: var(--color-muted);
}
}
&__time {
opacity: 0.5;
flex-shrink: 0;
}
}
</style>