elearning/frontend_management/pages/instructor/courses/[id]/edit.vue

348 lines
11 KiB
Vue

<template>
<div>
<!-- Header -->
<div class="flex items-center gap-4 mb-6">
<q-btn
flat
round
icon="arrow_back"
@click="navigateTo(`/instructor/courses/${route.params.id}`)"
/>
<div>
<h1 class="text-2xl font-bold text-primary-600">แกไขหลกสตร</h1>
<p class="text-gray-600 mt-1">แกไขขอมลหลกสตรของค</p>
</div>
</div>
<!-- Loading -->
<div v-if="initialLoading" class="flex justify-center py-20">
<q-spinner-dots size="50px" color="primary" />
</div>
<!-- Form -->
<div v-else class="bg-white rounded-xl shadow-sm p-6">
<q-form @submit="handleSubmit">
<!-- Basic Info -->
<h2 class="text-lg font-semibold text-primary-600 mb-4">อมลพนฐาน</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<q-input
v-model="form.title.th"
label="ชื่อหลักสูตร (ภาษาไทย) *"
outlined
:rules="[val => !!val || 'กรุณากรอกชื่อหลักสูตร']"
/>
<q-input
v-model="form.title.en"
label="ชื่อหลักสูตร (English) *"
outlined
:rules="[val => !!val || 'Please enter course title']"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<q-input
v-model="form.slug"
label="Slug (URL) *"
outlined
hint="ใช้สำหรับ URL เช่น javascript-basics"
:rules="[val => !!val || 'กรุณากรอก slug']"
/>
<q-select
v-model="form.category_id"
:options="categoryOptions"
label="หมวดหมู่ *"
outlined
emit-value
map-options
:rules="[val => !!val || 'กรุณาเลือกหมวดหมู่']"
/>
</div>
<div class="mb-6">
<q-input
v-model="form.description.th"
label="คำอธิบาย (ภาษาไทย)"
type="textarea"
outlined
autogrow
rows="3"
/>
</div>
<div class="mb-6">
<q-input
v-model="form.description.en"
label="คำอธิบาย (English)"
type="textarea"
outlined
autogrow
rows="3"
/>
</div>
<!-- Pricing -->
<q-separator class="my-6" />
<h2 class="text-lg font-semibold text-primary-600 mb-4">ราคาและการตงค</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<q-toggle
v-model="form.is_free"
label="หลักสูตรฟรี"
color="primary"
/>
<q-input
v-if="!form.is_free"
v-model.number="form.price"
label="ราคา (บาท)"
type="number"
outlined
prefix="฿"
:rules="[val => form.is_free || val > 0 || 'กรุณากรอกราคา']"
/>
<q-toggle
v-model="form.have_certificate"
label="มีใบประกาศนียบัตร"
color="primary"
/>
</div>
<!-- Thumbnail -->
<q-separator class="my-6" />
<h2 class="text-lg font-semibold text-primary-600 mb-4">ปภาพปก</h2>
<div class="mb-6">
<div class="flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-lg p-6 bg-gray-50 hover:bg-gray-100 transition-colors cursor-pointer" @click="triggerThumbnailUpload">
<div v-if="form.thumbnail_url" class="relative group w-full max-w-md aspect-video">
<img
:src="form.thumbnail_url"
:key="form.thumbnail_url"
alt="Thumbnail preview"
class="w-full h-full object-cover rounded-lg shadow-sm"
@error="form.thumbnail_url = ''"
/>
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center rounded-lg">
<q-icon name="photo_camera" color="white" size="40px" />
<span class="text-white ml-2">เปลยนรปภาพ</span>
</div>
<div v-if="uploadingThumbnail" class="absolute inset-0 bg-white/80 flex items-center justify-center rounded-lg">
<q-spinner color="primary" size="2em" />
</div>
</div>
<div v-else class="text-center py-8">
<q-icon name="add_photo_alternate" size="48px" class="text-gray-400 mb-2" />
<div class="text-gray-600 font-medium">คลกเพออพโหลดรปภาพปก</div>
<div class="text-xs text-gray-400 mt-1">ขนาดแนะนำ 1280x720 (16:9) งส 5MB</div>
</div>
<input
ref="thumbnailInputRef"
type="file"
accept="image/*"
class="hidden"
@change="handleThumbnailUpload"
/>
</div>
</div>
<!-- Actions -->
<div class="flex justify-end gap-3 mt-8">
<q-btn
flat
label="ยกเลิก"
color="grey-7"
@click="navigateTo(`/instructor/courses/${route.params.id}`)"
/>
<q-btn
type="submit"
label="บันทึกการแก้ไข"
color="primary"
:loading="saving"
/>
</div>
</q-form>
</div>
</div>
</template>
<script setup lang="ts">
import { useQuasar } from 'quasar';
import { instructorService, type CreateCourseRequest } from '~/services/instructor.service';
import { adminService, type CategoryResponse } from '~/services/admin.service';
definePageMeta({
layout: 'instructor',
middleware: ['auth']
});
const $q = useQuasar();
const route = useRoute();
// Data
const initialLoading = ref(true);
const saving = ref(false);
const uploadingThumbnail = ref(false);
const thumbnailInputRef = ref<HTMLInputElement | null>(null);
const categories = ref<CategoryResponse[]>([]);
// Form
const form = ref<CreateCourseRequest>({
category_id: 0,
title: { th: '', en: '' },
slug: '',
description: { th: '', en: '' },
thumbnail_url: null,
price: 0,
is_free: true,
have_certificate: true
});
// Category options
const categoryOptions = computed(() =>
categories.value.map(cat => ({
label: cat.name.th,
value: cat.id
}))
);
// Methods
const fetchData = async () => {
initialLoading.value = true;
try {
const courseId = parseInt(route.params.id as string);
// Fetch course and categories in parallel
const [course, cats] = await Promise.all([
instructorService.getCourseById(courseId),
adminService.getCategories()
]);
categories.value = cats;
// Populate form with course data
form.value = {
category_id: course.category_id,
title: { ...course.title },
slug: course.slug,
description: { ...course.description },
thumbnail_url: course.thumbnail_url,
price: parseFloat(course.price),
is_free: course.is_free,
have_certificate: course.have_certificate
};
} catch (error) {
$q.notify({
type: 'negative',
message: 'ไม่สามารถโหลดข้อมูลหลักสูตรได้',
position: 'top'
});
navigateTo('/instructor/courses');
} finally {
initialLoading.value = false;
}
};
const handleSubmit = async () => {
saving.value = true;
try {
const courseId = parseInt(route.params.id as string);
// Set price to 0 if free
if (form.value.is_free) {
form.value.price = 0;
}
await instructorService.updateCourse(courseId, form.value);
$q.notify({
type: 'positive',
message: 'บันทึกการแก้ไขสำเร็จ',
position: 'top'
});
navigateTo(`/instructor/courses/${courseId}`);
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่',
position: 'top'
});
} finally {
saving.value = false;
}
};
const triggerThumbnailUpload = () => {
thumbnailInputRef.value?.click();
};
const handleThumbnailUpload = async (event: Event) => {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
$q.notify({
type: 'warning',
message: 'กรุณาเลือกไฟล์รูปภาพเท่านั้น',
position: 'top'
});
return;
}
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
$q.notify({
type: 'warning',
message: 'ไฟล์มีขนาดใหญ่เกิน 5MB',
position: 'top'
});
return;
}
uploadingThumbnail.value = true;
try {
const courseId = parseInt(route.params.id as string);
// Clear current thumbnail first to force complete re-mount
form.value.thumbnail_url = null;
// Upload the file
await instructorService.uploadCourseThumbnail(courseId, file);
// Wait for Vue to unmount old img
await nextTick();
// Re-fetch course data to get fresh presigned URL from backend
const updatedCourse = await instructorService.getCourseById(courseId);
// Add cache buster to force reload
if (updatedCourse.thumbnail_url) {
// For presigned URLs, we cannot append query parameters as it invalidates the signature
// The presigned URL itself should be unique enough or handle caching differently
form.value.thumbnail_url = updatedCourse.thumbnail_url;
}
$q.notify({
type: 'positive',
message: 'อัพโหลดรูปภาพปกเรียบร้อย',
position: 'top'
});
// Clear input value to allow re-uploading same file if needed
input.value = '';
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.message || 'เกิดข้อผิดพลาดในการอัพโหลดรูปภาพ',
position: 'top'
});
} finally {
uploadingThumbnail.value = false;
}
};
// Lifecycle
onMounted(() => {
fetchData();
});
</script>