feat: Implement instructor module with course management pages and API services.
This commit is contained in:
parent
07ab43a785
commit
a24f8c4982
6 changed files with 135 additions and 27 deletions
|
|
@ -112,26 +112,36 @@
|
|||
<h2 class="text-lg font-semibold text-primary-600 mb-4">รูปภาพปก</h2>
|
||||
|
||||
<div class="mb-6">
|
||||
<q-input
|
||||
v-model="form.thumbnail_url"
|
||||
label="URL รูปภาพปก"
|
||||
outlined
|
||||
hint="ใส่ URL รูปภาพ (optional)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="image" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<!-- Preview thumbnail if exists -->
|
||||
<div v-if="form.thumbnail_url" class="mb-6">
|
||||
<img
|
||||
:src="form.thumbnail_url"
|
||||
alt="Thumbnail preview"
|
||||
class="max-w-xs rounded-lg shadow"
|
||||
@error="form.thumbnail_url = ''"
|
||||
/>
|
||||
<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 -->
|
||||
|
|
@ -170,6 +180,8 @@ 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
|
||||
|
|
@ -259,6 +271,76 @@ const handleSubmit = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -15,9 +15,15 @@
|
|||
<div class="text-xs text-gray-500">{{ authStore.user?.role || 'INSTRUCTOR' }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-primary-100 rounded-full flex items-center justify-center text-2xl cursor-pointer hover:bg-primary-200 transition"
|
||||
class="w-12 h-12 bg-primary-100 rounded-full flex items-center justify-center text-2xl cursor-pointer hover:bg-primary-200 transition overflow-hidden"
|
||||
>
|
||||
<q-icon name="person" />
|
||||
<img
|
||||
v-if="authStore.user?.avatarUrl"
|
||||
:src="authStore.user.avatarUrl"
|
||||
alt=""
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<q-icon v-else name="person" />
|
||||
<q-menu>
|
||||
<q-list style="min-width: 200px">
|
||||
<!-- User Info Header -->
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 mb-1">ชื่อ-นามสกุล</div>
|
||||
<div class="text-lg font-semibold text-gray-900">{{ profile.fullName }}</div>
|
||||
<div class="text-lg text-gray-900">{{ profile.fullName }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
@ -65,9 +65,7 @@
|
|||
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 mb-1">ตำแหน่ง</div>
|
||||
<div class="text-lg text-gray-900">
|
||||
<q-badge color="primary">{{ profile.roleName || getRoleLabel(profile.role) }}</q-badge>
|
||||
</div>
|
||||
<div class="text-lg text-gray-900">{{ profile.roleName || getRoleLabel(profile.role) }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue