---
name: Vue Component Development
description: Hướng dẫn phát triển Vue 3 components theo patterns của dự án LADA Express
---

# Vue Component Development Skill

## Mục đích

Skill này cung cấp hướng dẫn chi tiết để phát triển Vue 3 components theo đúng patterns và best practices được sử dụng trong dự án LADA Express.

## Công nghệ sử dụng

- **Vue 3.4+** với Composition API (`<script setup>`)
- **Vite** cho build tool
- **Bootstrap 5** cho styling
- **VueDatePicker** cho date range selection
- **v-select** cho dropdown components
- **VeeValidate + Yup** cho form validation

## Patterns chính

### 1. Component Structure

Mỗi Vue component nên tuân theo cấu trúc sau:

```vue
<template>
  <!-- Template markup -->
  <div>
    <!-- Breadcrumb -->
    <div class="breadcrumb-header justify-content-between">
      <!-- ... -->
    </div>

    <!-- Search/Filter Card -->
    <div class="row row-sm">
      <div class="col-sm-12 col-md-12 col-lg-12 col-xl-12 grid-margin">
        <div class="card card-search shadow-none mb-3">
          <form class="card-body" @submit.prevent="search">
            <!-- Form fields -->
          </form>
        </div>
      </div>
    </div>

    <!-- Data Display Card -->
    <div class="row row-sm">
      <div class="col-sm-12 col-md-12 col-lg-12 col-xl-12 grid-margin">
        <div class="card card-table shadow-none">
          <div class="card-header">
            <h5 class="card-title mb-0">
              <i class="fas fa-table me-2"></i>
              Tiêu đề
            </h5>
          </div>
          <div class="card-body card-table-body">
            <!-- Table or content -->
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue';
// Import services và utilities

// Data
const loading = ref(false);

// Search model
const searchModel = reactive({
  Pagination: {
    Total: 0,
    PageIndex: 1,
    PageSize: 25
  },
  Sort: {
    Predicate: '',
    Reverse: true
  },
  Search: {
    PredicateObject: {
      // Search fields
    }
  }
});

// Functions
async function search() {
  loading.value = true;
  try {
    // API call
  } catch (error) {
    console.error(error);
  } finally {
    loading.value = false;
  }
}

onMounted(() => {
  // Initialize
});
</script>
```

### 2. Date Range Picker Pattern

Sử dụng VueDatePicker với DateRangeSidebar:

```vue
<template>
  <div class="col-lg-4 col-md-6 mb-3">
    <label>Thời gian</label>
    <VueDatePicker 
      ref="dateRangePicker" 
      v-model="date" 
      :enable-time-picker="false" 
      range
      multi-calendars 
      auto-apply 
      :format="format" 
      @update:model-value="handleDateRange"
      :min-date="new Date(2025, 7, 10)">
      <template #left-sidebar>
        <DateRangeSidebar @change-date="onChangeDate"></DateRangeSidebar>
      </template>
    </VueDatePicker>
  </div>
</template>

<script setup>
import VueDatePicker from '@vuepic/vue-datepicker';
import { DateRangeSidebar } from '@/components/index';
import { ref } from 'vue';

const date = ref();
const dateRangePicker = ref(VueDatePicker);

const format = (dates) => {
  let start = dates[0];
  let end = dates[1];
  const dayStart = start.getDate();
  const monthStart = start.getMonth() + 1;
  const yearStart = start.getFullYear();
  const dayEnd = end.getDate();
  const monthEnd = end.getMonth() + 1;
  const yearEnd = end.getFullYear();

  return `${dayStart}/${monthStart}/${yearStart} - ${dayEnd}/${monthEnd}/${yearEnd}`;
}

function handleDateRange(modelData) {
  if (modelData != null) {
    const endDate = modelData[1];
    const startDate = modelData[0];
    
    searchModel.Search.PredicateObject.StartDate = startDate;
    searchModel.Search.PredicateObject.EndDate = endDate;
  } else {
    searchModel.Search.PredicateObject.StartDate = null;
    searchModel.Search.PredicateObject.EndDate = null;
  }
}

async function onChangeDate(newDates) {
  date.value = newDates;
  dateRangePicker.value.closeMenu();
  handleDateRange(newDates);
}
</script>
```

### 3. Form Validation Pattern

Sử dụng VeeValidate với Yup schema:

```vue
<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    <div class="mb-3">
      <label>Tên <span class="text-danger">*</span></label>
      <Field name="name" type="text" class="form-control" />
      <ErrorMessage name="name" class="text-danger" />
    </div>
    
    <button type="submit" class="btn btn-primary">
      Lưu
    </button>
  </Form>
</template>

<script setup>
import { Form, Field, ErrorMessage } from 'vee-validate';
import * as yup from 'yup';

const schema = yup.object({
  name: yup.string().required('Vui lòng nhập tên'),
  email: yup.string().email('Email không hợp lệ').required('Vui lòng nhập email'),
});

async function onSubmit(values) {
  // Handle form submission
}
</script>
```

### 4. Service Pattern

Tạo service files trong `src/services/`:

```javascript
import apiClient from '@/infrastructure/apiClient';
import apiPath from '@/infrastructure/apiPath';

export default {
  async search(searchModel) {
    try {
      const response = await apiClient.post(apiPath.yourModule.search, searchModel);
      return response.data;
    } catch (error) {
      console.error('Error in search:', error);
      throw error;
    }
  },

  async getById(id) {
    try {
      const response = await apiClient.get(`${apiPath.yourModule.getById}${id}`);
      return response.data;
    } catch (error) {
      console.error('Error in getById:', error);
      throw error;
    }
  },

  async createOrUpdate(model) {
    try {
      const response = await apiClient.post(apiPath.yourModule.createOrUpdate, model);
      return response.data;
    } catch (error) {
      console.error('Error in createOrUpdate:', error);
      throw error;
    }
  },

  async delete(id) {
    try {
      const response = await apiClient.delete(`${apiPath.yourModule.delete}${id}`);
      return response.data;
    } catch (error) {
      console.error('Error in delete:', error);
      throw error;
    }
  }
};
```

### 5. Formatting Utilities

Sử dụng các helper functions cho formatting:

```javascript
function formatNumber(value) {
  if (value == null || value == undefined) return '0';
  return new Intl.NumberFormat('vi-VN').format(value);
}

function formatCurrency(value) {
  if (value == null || value == undefined) return '0 ₫';
  return new Intl.NumberFormat('vi-VN', {
    style: 'currency',
    currency: 'VND'
  }).format(value);
}

function formatPercent(value) {
  if (value == null || value == undefined) return '0.00';
  return parseFloat(value).toFixed(2);
}

function formatDate(date) {
  if (!date) return '';
  return moment(date).format('DD/MM/YYYY');
}

function formatDateTime(date) {
  if (!date) return '';
  return moment(date).format('DD/MM/YYYY HH:mm');
}
```

### 6. Toast Notifications

Sử dụng vue-toastification:

```javascript
import { useToast } from 'vue-toastification';

const toast = useToast();

// Success
toast.success('Thao tác thành công');

// Error
toast.error('Có lỗi xảy ra: ' + error.message);

// Info
toast.info('Thông tin');

// Warning
toast.warning('Cảnh báo');
```

## Best Practices

### 1. Naming Conventions

- **Components**: PascalCase (e.g., `UserProfile.vue`)
- **Services**: camelCase với suffix Service (e.g., `userService.js`)
- **Stores**: camelCase với suffix Store (e.g., `userStore.js`)
- **Variables**: camelCase (e.g., `searchModel`)
- **Constants**: UPPER_SNAKE_CASE (e.g., `API_BASE_URL`)

### 2. File Organization

```
src/
├── views/
│   └── moduleName/
│       ├── index.vue          # List/Grid view
│       ├── form.vue           # Create/Edit form
│       └── components/        # Module-specific components
├── services/
│   └── moduleNameService.js   # API service
├── components/
│   └── shared/                # Shared components
└── stores/
    └── moduleNameStore.js     # Pinia store (if needed)
```

### 3. Reactive Data

- Sử dụng `ref()` cho primitive values
- Sử dụng `reactive()` cho objects/arrays
- Sử dụng `computed()` cho derived values
- Sử dụng `watch()` hoặc `watchEffect()` cho side effects

### 4. Error Handling

```javascript
async function loadData() {
  loading.value = true;
  try {
    const result = await service.getData();
    if (result && result.Result === 1) {
      // Success
      data.value = result.Items;
    } else {
      toast.error('Lỗi: ' + (result.Message || 'Không thể tải dữ liệu'));
    }
  } catch (error) {
    console.error('Error loading data:', error);
    toast.error('Có lỗi xảy ra khi tải dữ liệu');
  } finally {
    loading.value = false;
  }
}
```

### 5. Loading States

```vue
<template>
  <!-- Loading spinner -->
  <tbody v-if="loading">
    <tr>
      <td colspan="10" class="tx-semibold text-center text-default">
        <span class="spinner-border spinner-border-sm"></span>
      </td>
    </tr>
  </tbody>

  <!-- Empty state -->
  <tbody v-if="!loading && (!data || data.length <= 0)">
    <tr>
      <td colspan="10" class="tx-semibold text-center text-danger">
        Không tìm thấy dữ liệu!
      </td>
    </tr>
  </tbody>

  <!-- Data -->
  <tbody v-if="!loading && data && data.length > 0">
    <tr v-for="item in data" :key="item.Id">
      <!-- Table cells -->
    </tr>
  </tbody>
</template>
```

### 6. LadaDialog Modal Pattern

Sử dụng `LadaDialog` cho các pop-up/modal thay vì cấu trúc Bootstrap Modal tĩnh. Triển khai `<LadaDialog>` ở component cha (như danh sách) và truyền Component content vào `#body`.

**Tại component cha (`index.vue`):**
```vue
<template>
  <LadaDialog :visible="modalControl.show" size="modal-lg">
    <template #header>
      <div class="modal-header">
        <h5 class="modal-title">Tiêu đề Modal</h5>
        <button @click="closeModal" type="button" class="close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
    </template>
    <template #body>
      <ChildComponent :id="selectedId" @cancel="closeModal" />
    </template>
  </LadaDialog>
</template>

<script setup>
import { reactive, ref } from 'vue';
import { LadaDialog } from '@/components/index';
import ChildComponent from './ChildComponent.vue';

const modalControl = reactive({ show: false });
const selectedId = ref(0);

function openModal(id) {
  selectedId.value = id;
  modalControl.show = true;
}

function closeModal() {
  modalControl.show = false;
  // loadData(); // Reload nếu cần thiết
}
</script>
```

**Tại component con (`ChildComponent.vue`):**
- Không cần khai báo thẻ html của modal, component sẽ được tự động đưa vào thân (`#body`) của `LadaDialog`.
- Sử dụng `props` để nhận dữ liệu `id` từ component cha. Phát sự kiện `cancel` hoặc `saved` khi có hành động thành công.

## Common Components

### Global Components (Đã đăng ký trong main.js)

- `VueDatePicker` - Date picker
- `v-select` - Select dropdown
- `LadaDialog` - Modal dialog
- `LadaAccordion` - Accordion
- `Loader` - Loading spinner
- `pager` - Pagination
- `editor` - TinyMCE editor
- `blockui` - Block UI overlay
- `empty` - Empty state
- `OrderStatus` - Order status badge
- `LoadingOverlay` - Loading overlay

### Import khi cần

```javascript
import { DateRangeSidebar } from '@/components/index';
import VChart from 'vue-echarts';
```

## Checklist khi tạo component mới

- [ ] Sử dụng `<script setup>` với Composition API
- [ ] Import các dependencies cần thiết
- [ ] Định nghĩa reactive data với `ref()` hoặc `reactive()`
- [ ] Tạo search model với cấu trúc chuẩn (Pagination, Sort, Search)
- [ ] Implement error handling với try-catch
- [ ] Sử dụng toast notifications cho feedback
- [ ] Thêm loading states
- [ ] Format dữ liệu hiển thị (currency, date, number)
- [ ] Sử dụng Bootstrap classes cho styling
- [ ] Test responsive trên mobile
