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
371 lines
6.5 KiB
Markdown
371 lines
6.5 KiB
Markdown
# Vue.js 3 Rules
|
||
|
||
## Component Structure
|
||
|
||
### Template
|
||
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<style scoped>
|
||
/* ✅ Используй scoped стили */
|
||
.container {
|
||
padding: 1rem;
|
||
}
|
||
|
||
/* ✅ Используй :deep() для доступа к дочерним компонентам */
|
||
:deep(.p-datatable) {
|
||
border: 1px solid #ccc;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
## Component Naming
|
||
|
||
```vue
|
||
<!-- ✅ PascalCase для компонентов -->
|
||
<CustomerCard />
|
||
<ProductsList />
|
||
<OrderDetails />
|
||
|
||
<!-- ✅ kebab-case в шаблоне тоже работает -->
|
||
<customer-card />
|
||
```
|
||
|
||
## Props
|
||
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<template>
|
||
<!-- ✅ Используй PrimeVue компоненты в админке -->
|
||
<DataTable
|
||
:value="customers"
|
||
:loading="loading"
|
||
paginator
|
||
:rows="20"
|
||
@page="onPage"
|
||
>
|
||
<Column field="name" header="Name" sortable />
|
||
</DataTable>
|
||
</template>
|
||
```
|
||
|
||
## Styling
|
||
|
||
```vue
|
||
<style scoped>
|
||
/* ✅ Используй scoped -->
|
||
.container {
|
||
padding: 1rem;
|
||
}
|
||
|
||
/* ✅ :deep() для дочерних компонентов */
|
||
:deep(.p-datatable) {
|
||
border: 1px solid #ccc;
|
||
}
|
||
|
||
/* ✅ :slotted() для слотов */
|
||
:slotted(.header) {
|
||
font-weight: bold;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
## Composition Functions
|
||
|
||
```vue
|
||
<script setup>
|
||
// ✅ Выноси сложную логику в composables
|
||
import { useCustomers } from '@/composables/useCustomers.js';
|
||
|
||
const {
|
||
customers,
|
||
loading,
|
||
loadCustomers,
|
||
totalRecords
|
||
} = useCustomers();
|
||
|
||
onMounted(() => {
|
||
loadCustomers();
|
||
});
|
||
</script>
|
||
```
|
||
|
||
## Error Handling
|
||
|
||
```vue
|
||
<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>
|
||
```
|
||
|