✨ feat(src/views/dev/TestView.vue): добавляет тестовый интерфейс для сброса флоу лайков, матчей и чатов
✨ feat(src/composables/useUi.ts): добавляет возможность действия для уведомлений в тостах
This commit is contained in:
@@ -8,6 +8,7 @@ export interface Toast {
|
|||||||
type: ToastType
|
type: ToastType
|
||||||
message: string
|
message: string
|
||||||
duration?: number
|
duration?: number
|
||||||
|
action?: { label: string, onClick: () => void }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Tag = TagResponseDto
|
export type Tag = TagResponseDto
|
||||||
@@ -16,16 +17,16 @@ export type District = DistrictResponseDto
|
|||||||
export type Greeting = GreetingDto
|
export type Greeting = GreetingDto
|
||||||
|
|
||||||
const toasts = ref<Toast[]>([])
|
const toasts = ref<Toast[]>([])
|
||||||
const sidebarExpanded = ref(false)
|
const sidebarExpanded = ref(true)
|
||||||
const tags = ref<Tag[]>([])
|
const tags = ref<Tag[]>([])
|
||||||
const cities = ref<City[]>([])
|
const cities = ref<City[]>([])
|
||||||
const districts = reactive<Record<string, District[]>>({})
|
const districts = reactive<Record<string, District[]>>({})
|
||||||
const greetings = ref<Greeting[]>([])
|
const greetings = ref<Greeting[]>([])
|
||||||
const referencesLoaded = ref(false)
|
const referencesLoaded = ref(false)
|
||||||
|
|
||||||
function addToast(message: string, type: ToastType = 'info', duration = 4000) {
|
function addToast(message: string, type: ToastType = 'info', duration = 4000, action?: Toast['action']) {
|
||||||
const id = `${Date.now()}-${Math.random()}`
|
const id = `${Date.now()}-${Math.random()}`
|
||||||
toasts.value.push({ id, type, message, duration })
|
toasts.value.push({ id, type, message, duration, action })
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
setTimeout(removeToast, duration, id)
|
setTimeout(removeToast, duration, id)
|
||||||
}
|
}
|
||||||
|
|||||||
458
src/views/dev/TestView.vue
Normal file
458
src/views/dev/TestView.vue
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
<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<{ data: 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<{ data: { 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>
|
||||||
Reference in New Issue
Block a user