Files
interview-demo-code/.cursor/rules/vue.md
Nikita Kiselev 9a93cc7342 feat: add Telegram customers management system with admin panel
Implement comprehensive Telegram customers storage and management functionality:

Backend:
- Add database migration for telecart_customers table with indexes
- Create TelegramCustomer model with CRUD operations
- Implement TelegramCustomerService for business logic
- Add TelegramCustomerHandler for API endpoint (saveOrUpdate)
- Add TelegramCustomersHandler for admin API (getCustomers with pagination, filtering, sorting)
- Add SendMessageHandler for sending messages to customers via Telegram
- Create custom exceptions: TelegramCustomerNotFoundException, TelegramCustomerWriteNotAllowedException
- Refactor TelegramInitDataDecoder to separate decoding logic
- Add TelegramHeader enum for header constants
- Update SignatureValidator to use TelegramInitDataDecoder
- Register new routes in bastion/routes.php and src/routes.php

Frontend (Admin):
- Add CustomersView.vue component with PrimeVue DataTable
- Implement advanced filtering (text, date, boolean filters)
- Add column visibility toggle functionality
- Add global search with debounce
- Implement message sending dialog with validation
- Add Russian locale for PrimeVue components
- Add navigation link in App.vue
- Register route in router

Frontend (SPA):
- Add saveTelegramCustomer utility function
- Integrate automatic customer data saving on app initialization
- Extract user data from Telegram.WebApp.initDataUnsafe

The system automatically saves/updates customer data when users access the Telegram Mini App,
and provides admin interface for viewing, filtering, and messaging customers.

BREAKING CHANGE: None
2025-11-23 21:30:51 +03:00

6.5 KiB
Raw Blame History

Vue.js 3 Rules

Component Structure

Template

<template>
  <!--  Логическая структура -->
  <div class="container">
    <header>
      <h2>{{ title }}</h2>
    </header>
    
    <main>
      <DataTable :value="items" />
    </main>
    
    <footer>
      <Button @click="handleSave">Save</Button>
    </footer>
  </div>
</template>

Script Setup

<script setup>
// ✅ Всегда используй <script setup>
import { ref, computed, onMounted } from 'vue';
import DataTable from 'primevue/datatable';
import { apiGet } from '@/utils/http.js';

// Props
const props = defineProps({
  title: {
    type: String,
    required: true
  }
});

// Emits
const emit = defineEmits(['update', 'delete']);

// State
const items = ref([]);
const loading = ref(false);

// Computed
const totalItems = computed(() => items.value.length);

// Methods
async function loadItems() {
  loading.value = true;
  try {
    const result = await apiGet('getItems');
    items.value = result.data || [];
  } finally {
    loading.value = false;
  }
}

// Lifecycle
onMounted(() => {
  loadItems();
});
</script>

Styles

<style scoped>
/* ✅ Используй scoped стили */
.container {
  padding: 1rem;
}

/* ✅ Используй :deep() для доступа к дочерним компонентам */
:deep(.p-datatable) {
  border: 1px solid #ccc;
}
</style>

Component Naming

<!--  PascalCase для компонентов -->
<CustomerCard />
<ProductsList />
<OrderDetails />

<!--  kebab-case в шаблоне тоже работает -->
<customer-card />

Props

<script setup>
// ✅ Всегда определяй типы и валидацию
const props = defineProps({
  customerId: {
    type: Number,
    required: true,
    validator: (value) => value > 0
  },
  showDetails: {
    type: Boolean,
    default: false
  },
  items: {
    type: Array,
    default: () => []
  }
});
</script>

Emits

<script setup>
// ✅ Определяй emits с типами
const emit = defineEmits<{
  update: [id: number, data: object];
  delete: [id: number];
  cancel: [];
}>();

// ✅ Или с валидацией
const emit = defineEmits({
  update: (id: number, data: object) => {
    if (id > 0 && typeof data === 'object') {
      return true;
    }
    console.warn('Invalid emit arguments');
    return false;
  }
});
</script>

Reactive State

<script setup>
// ✅ ref для примитивов
const count = ref(0);
const name = ref('');

// ✅ ref для объектов (предпочтительно)
const customer = ref({
  id: null,
  name: '',
  email: ''
});

// ✅ reactive только если нужно
import { reactive } from 'vue';
const state = reactive({
  items: [],
  loading: false
});
</script>

Computed Properties

<script setup>
// ✅ Используй computed для производных значений
const filteredItems = computed(() => {
  return items.value.filter(item => item.isActive);
});

// ✅ Computed с getter/setter
const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (value) => {
    const parts = value.split(' ');
    firstName.value = parts[0];
    lastName.value = parts[1];
  }
});
</script>

Event Handlers

<template>
  <!--  Используй kebab-case для событий -->
  <Button @click="handleClick" />
  <Input @input="handleInput" />
  <Form @submit.prevent="handleSubmit" />
</template>

<script setup>
// ✅ Именуй обработчики с префиксом handle
function handleClick() {
  // ...
}

function handleInput(event) {
  // ...
}

function handleSubmit() {
  // ...
}
</script>

Conditional Rendering

<template>
  <!--  Используй v-if для условного рендеринга -->
  <div v-if="loading">
    <LoadingSpinner />
  </div>
  
  <!--  v-show для частых переключений -->
  <div v-show="hasItems">
    <ItemsList :items="items" />
  </div>
  
  <!--  v-else для альтернатив -->
  <div v-else>
    <EmptyState />
  </div>
</template>

Lists

<template>
  <!--  Всегда используй :key -->
  <div v-for="item in items" :key="item.id">
    {{ item.name }}
  </div>
  
  <!--  Для индексов -->
  <div v-for="(item, index) in items" :key="`item-${index}`">
    {{ item.name }}
  </div>
</template>

Form Handling

<template>
  <form @submit.prevent="handleSubmit">
    <!--  Используй v-model -->
    <InputText v-model="form.name" />
    <Textarea v-model="form.description" />
    
    <!--  Для кастомных компонентов -->
    <CustomInput v-model="form.email" />
  </form>
</template>

<script setup>
const form = ref({
  name: '',
  description: '',
  email: ''
});

function handleSubmit() {
  // Валидация и отправка
}
</script>

PrimeVue Components

<template>
  <!--  Используй PrimeVue компоненты в админке -->
  <DataTable
    :value="customers"
    :loading="loading"
    paginator
    :rows="20"
    @page="onPage"
  >
    <Column field="name" header="Name" sortable />
  </DataTable>
</template>

Styling

<style scoped>
/* ✅ Используй scoped -->
.container {
  padding: 1rem;
}

/* ✅ :deep() для дочерних компонентов */
:deep(.p-datatable) {
  border: 1px solid #ccc;
}

/* ✅ :slotted() для слотов */
:slotted(.header) {
  font-weight: bold;
}
</style>

Composition Functions

<script setup>
// ✅ Выноси сложную логику в composables
import { useCustomers } from '@/composables/useCustomers.js';

const {
  customers,
  loading,
  loadCustomers,
  totalRecords
} = useCustomers();

onMounted(() => {
  loadCustomers();
});
</script>

Error Handling

<script setup>
import { useToast } from 'primevue';

const toast = useToast();

async function loadData() {
  try {
    const result = await apiGet('endpoint');
    if (result.success) {
      data.value = result.data;
    } else {
      toast.add({
        severity: 'error',
        summary: 'Ошибка',
        detail: result.error
      });
    }
  } catch (error) {
    console.error('Error:', error);
    toast.add({
      severity: 'error',
      summary: 'Ошибка',
      detail: 'Не удалось загрузить данные'
    });
  }
}
</script>