🏠 Home / Hub
📊 Projects 02 — Vue CRUD Dashboard
← Projects Menu
Goal: Vue 3 (Composition API) + Vite + Axios သုံးပြီး admin-style CRUD dashboard တစ်ခုဆောက်မယ်။ Products list, search/filter, create/edit form, delete, loading/error states ပါမယ်။
1. Project Setup
npm create vue@latest vue-crud-dashboard
# ✅ TypeScript: No (or Yes if comfortable)
# ✅ Vue Router: Yes
# ✅ Pinia: Yes
cd vue-crud-dashboard
npm install
npm install axios
npm run dev
2. File Structure
src/
├── main.js
├── App.vue
├── router/
│ └── index.js ← route definitions
├── stores/
│ └── productStore.js ← Pinia state
├── services/
│ └── productService.js ← Axios API calls
├── components/
│ ├── ProductTable.vue ← data table with search
│ ├── ProductForm.vue ← create/edit form modal
│ ├── ConfirmDialog.vue ← delete confirmation
│ └── LoadingSpinner.vue
└── pages/
├── ProductsPage.vue ← main CRUD page
└── DashboardPage.vue ← overview stats
3. API Service (productService.js)
// src/services/productService.js
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api',
headers: { 'Content-Type': 'application/json' },
})
// Attach token to every request
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
export const productService = {
getAll: (params) => api.get('/products', { params }),
getOne: (id) => api.get(`/products/${id}`),
create: (data) => api.post('/products', data),
update: (id, data) => api.put(`/products/${id}`, data),
delete: (id) => api.delete(`/products/${id}`),
}
export default api
4. Pinia Store (productStore.js)
// src/stores/productStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { productService } from '@/services/productService'
export const useProductStore = defineStore('products', () => {
const products = ref([])
const loading = ref(false)
const error = ref(null)
const search = ref('')
const filtered = computed(() =>
products.value.filter(p =>
p.name.toLowerCase().includes(search.value.toLowerCase())
)
)
async function fetchAll() {
loading.value = true
error.value = null
try {
const res = await productService.getAll()
products.value = res.data.data ?? res.data
} catch (e) {
error.value = e.response?.data?.message || 'Failed to load products'
} finally {
loading.value = false
}
}
async function save(form, id = null) {
if (id) {
const res = await productService.update(id, form)
const idx = products.value.findIndex(p => p.id === id)
if (idx !== -1) products.value[idx] = res.data
} else {
const res = await productService.create(form)
products.value.push(res.data)
}
}
async function remove(id) {
await productService.delete(id)
products.value = products.value.filter(p => p.id !== id)
}
return { products, loading, error, search, filtered, fetchAll, save, remove }
})
5. ProductsPage.vue
<template>
<div class="products-page">
<div class="page-header">
<h1>Products</h1>
<button @click="openCreate" class="btn-primary">+ Add Product</button>
</div>
<!-- Search & Filter -->
<div class="toolbar">
<input v-model="store.search" placeholder="Search products..." class="search-input">
<select v-model="categoryFilter">
<option value="">All Categories</option>
<option v-for="cat in categories" :key="cat" :value="cat">{{ cat }}</option>
</select>
</div>
<!-- Loading / Error -->
<LoadingSpinner v-if="store.loading" />
<div v-else-if="store.error" class="error-banner">{{ store.error }}</div>
<!-- Table -->
<ProductTable
v-else
:products="store.filtered"
@edit="openEdit"
@delete="confirmDelete"
/>
<!-- Form Modal -->
<ProductForm
v-if="showForm"
:product="editing"
@save="handleSave"
@close="showForm = false"
/>
<!-- Delete Dialog -->
<ConfirmDialog
v-if="deleting"
:message="`Delete ${deleting.name}?`"
@confirm="handleDelete"
@cancel="deleting = null"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useProductStore } from '@/stores/productStore'
import ProductTable from '@/components/ProductTable.vue'
import ProductForm from '@/components/ProductForm.vue'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import LoadingSpinner from '@/components/LoadingSpinner.vue'
const store = useProductStore()
const showForm = ref(false)
const editing = ref(null)
const deleting = ref(null)
const categoryFilter = ref('')
onMounted(() => store.fetchAll())
function openCreate() { editing.value = null; showForm.value = true }
function openEdit(product) { editing.value = { ...product }; showForm.value = true }
function confirmDelete(product) { deleting.value = product }
async function handleSave(form) {
await store.save(form, editing.value?.id)
showForm.value = false
}
async function handleDelete() {
await store.remove(deleting.value.id)
deleting.value = null
}
</script>
6. ProductForm.vue (Modal)
<template>
<div class="modal-overlay" @click.self="$emit('close')">
<div class="modal">
<h2>{{ product ? 'Edit Product' : 'New Product' }}</h2>
<form @submit.prevent="submit">
<div class="field">
<label>Name *</label>
<input v-model="form.name" required placeholder="Product name">
<span class="err">{{ errors.name }}</span>
</div>
<div class="field">
<label>Price *</label>
<input v-model.number="form.price" type="number" min="0" step="0.01" required>
</div>
<div class="field">
<label>Stock</label>
<input v-model.number="form.stock" type="number" min="0">
</div>
<div class="field">
<label>Description</label>
<textarea v-model="form.description" rows="3"></textarea>
</div>
<div class="actions">
<button type="button" @click="$emit('close')">Cancel</button>
<button type="submit" class="btn-primary" :disabled="saving">
{{ saving ? 'Saving...' : 'Save' }}
</button>
</div>
</form>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const props = defineProps(['product'])
const emit = defineEmits(['save', 'close'])
const form = reactive({ name: '', price: 0, stock: 0, description: '', ...props.product })
const errors = reactive({})
const saving = ref(false)
function validate() {
errors.name = form.name.trim() ? '' : 'Name is required'
return !Object.values(errors).some(Boolean)
}
async function submit() {
if (!validate()) return
saving.value = true
try {
await emit('save', { ...form })
} finally {
saving.value = false
}
}
</script>
7. Features Checklist
| Feature | Implementation |
| List products | GET /api/products → Pinia store → table |
| Search | Computed filtered with v-model search |
| Create product | Modal form → POST → push to store |
| Edit product | Modal pre-filled → PUT → update store |
| Delete product | Confirm dialog → DELETE → filter store |
| Loading state | ref loading → spinner component |
| Error state | ref error → error banner |
| Auth token | Axios interceptor → Bearer header |
| Pagination | ?page=1&limit=20 query params |
📌 Study Checklist
- အပေါ်က concept ကို တစ်ကြောင်းချင်းဖတ်ပြီး example ကို ကိုယ်တိုင်ပြန်ရေးကြည့်ပါ။
- Code/command ပါတဲ့ lesson ဆိုရင် value/name/path တစ်ခုခု ပြောင်းပြီး result ဘာကွာလဲ စမ်းပါ။
- မမှတ်မိသေးတဲ့ keyword 3 ခုကို notebook ထဲရေးပြီး နောက် lesson မသွားခင် ပြန်ရှင်းကြည့်ပါ။
- ပြီးရင် Home / Hub ကိုပြန်သွားပြီး next lesson ဆက်သင်ပါ။