Squashed commit message
Some checks are pending
Telegram Mini App Shop Builder / Compute version metadata (push) Waiting to run
Telegram Mini App Shop Builder / Run Frontend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run Backend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run PHP_CodeSniffer (push) Waiting to run
Telegram Mini App Shop Builder / Build module. (push) Blocked by required conditions
Telegram Mini App Shop Builder / release (push) Blocked by required conditions
Some checks are pending
Telegram Mini App Shop Builder / Compute version metadata (push) Waiting to run
Telegram Mini App Shop Builder / Run Frontend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run Backend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run PHP_CodeSniffer (push) Waiting to run
Telegram Mini App Shop Builder / Build module. (push) Blocked by required conditions
Telegram Mini App Shop Builder / release (push) Blocked by required conditions
This commit is contained in:
370
.cursor/rules/vue.md
Normal file
370
.cursor/rules/vue.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# Vue.js 3 Rules
|
||||
|
||||
## Component Structure
|
||||
|
||||
### Template
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ Logical structure -->
|
||||
<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>
|
||||
// ✅ Always use <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>
|
||||
/* ✅ Use scoped styles */
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* ✅ Use :deep() to style nested components */
|
||||
:deep(.p-datatable) {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Component Naming
|
||||
|
||||
```vue
|
||||
<!-- ✅ PascalCase for components -->
|
||||
<CustomerCard />
|
||||
<ProductsList />
|
||||
<OrderDetails />
|
||||
|
||||
<!-- ✅ kebab-case in templates also works -->
|
||||
<customer-card />
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ Always define types and validation
|
||||
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>
|
||||
// ✅ Define emits with types
|
||||
const emit = defineEmits<{
|
||||
update: [id: number, data: object];
|
||||
delete: [id: number];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
// ✅ Or with validation
|
||||
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 for primitives
|
||||
const count = ref(0);
|
||||
const name = ref('');
|
||||
|
||||
// ✅ ref for objects (preferred)
|
||||
const customer = ref({
|
||||
id: null,
|
||||
name: '',
|
||||
email: ''
|
||||
});
|
||||
|
||||
// ✅ Use reactive only when necessary
|
||||
import { reactive } from 'vue';
|
||||
const state = reactive({
|
||||
items: [],
|
||||
loading: false
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Computed Properties
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ Use computed for derived values
|
||||
const filteredItems = computed(() => {
|
||||
return items.value.filter(item => item.isActive);
|
||||
});
|
||||
|
||||
// ✅ Computed with 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>
|
||||
<!-- ✅ Use kebab-case for events -->
|
||||
<Button @click="handleClick" />
|
||||
<Input @input="handleInput" />
|
||||
<Form @submit.prevent="handleSubmit" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// ✅ Name handlers with handle* prefix
|
||||
function handleClick() {
|
||||
// ...
|
||||
}
|
||||
|
||||
function handleInput(event) {
|
||||
// ...
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
// ...
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Conditional Rendering
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ Use v-if for conditional rendering -->
|
||||
<div v-if="loading">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
|
||||
<!-- ✅ v-show for frequent toggling -->
|
||||
<div v-show="hasItems">
|
||||
<ItemsList :items="items" />
|
||||
</div>
|
||||
|
||||
<!-- ✅ v-else for alternatives -->
|
||||
<div v-else>
|
||||
<EmptyState />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Lists
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ Always use :key -->
|
||||
<div v-for="item in items" :key="item.id">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
|
||||
<!-- ✅ For index-based lists -->
|
||||
<div v-for="(item, index) in items" :key="`item-${index}`">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Form Handling
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<!-- ✅ Use v-model -->
|
||||
<InputText v-model="form.name" />
|
||||
<Textarea v-model="form.description" />
|
||||
|
||||
<!-- ✅ For custom components -->
|
||||
<CustomInput v-model="form.email" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const form = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
email: ''
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
// Validation and submit
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## PrimeVue Components
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ Use PrimeVue components in the admin panel -->
|
||||
<DataTable
|
||||
:value="customers"
|
||||
:loading="loading"
|
||||
paginator
|
||||
:rows="20"
|
||||
@page="onPage"
|
||||
>
|
||||
<Column field="name" header="Name" sortable />
|
||||
</DataTable>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
```vue
|
||||
<style scoped>
|
||||
/* ✅ Use scoped -->
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* ✅ :deep() for nested components */
|
||||
:deep(.p-datatable) {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* ✅ :slotted() for slots */
|
||||
:slotted(.header) {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Composition Functions
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ Extract complex logic into 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: 'Error',
|
||||
detail: result.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to load data'
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user