Initial commit of Vue Website Template

This commit is contained in:
Flook 2025-07-09 02:58:14 +07:00
parent 376a04cc0f
commit 89242ec738
5 changed files with 581 additions and 176 deletions

View File

@ -1,7 +1,6 @@
// src/components/TabNews.vue // src/components/TabNews.vue
<template> <template>
<div class="p-4"> <div class="p-4">
<!-- วขอขาว -->
<div class="mb-6 px-2 md:px-4"> <div class="mb-6 px-2 md:px-4">
<h4 <h4
class="inline-block text-white text-2xl md:text-3xl font-semibold px-6 py-2 bg-[#1b3872] rounded shadow-md" class="inline-block text-white text-2xl md:text-3xl font-semibold px-6 py-2 bg-[#1b3872] rounded shadow-md"
@ -10,7 +9,6 @@
</h4> </h4>
</div> </div>
<!-- Tabs -->
<div role="tablist" class="flex flex-wrap space-x-2 border-b border-gray-300"> <div role="tablist" class="flex flex-wrap space-x-2 border-b border-gray-300">
<button <button
v-for="(tab, index) in tabs" v-for="(tab, index) in tabs"
@ -26,7 +24,6 @@
</button> </button>
</div> </div>
<!-- Tab Content -->
<div class="mt-4"> <div class="mt-4">
<div <div
v-for="tab in tabs" v-for="tab in tabs"
@ -53,13 +50,13 @@
<div class="card-body p-4 flex flex-col justify-between flex-1"> <div class="card-body p-4 flex flex-col justify-between flex-1">
<router-link <router-link
:to="`/content/${item.id}`" :to="`/tab-news-content/${item.id}`"
class="card-title text-base text-gray-800 hover:underline line-clamp-2 mb-2" class="card-title text-base text-gray-800 hover:underline line-clamp-2 mb-2"
> >
{{ appStore.checkLang.isTh ? item.title_th : item.title_en || 'No Title' }} {{ appStore.checkLang.isTh ? item.title_th : item.title_en || 'No Title' }}
</router-link> </router-link>
<p class="text-sm text-gray-600 line-clamp-2 flex-grow"> <p class="text-sm text-gray-600 line-clamp-3 flex-grow">
{{ truncateDetail(appStore.checkLang.isTh ? item.detail_th : item.detail_en || '', 80) }} {{ stripHtmlAndTruncate(appStore.checkLang.isTh ? item.detail_th : item.detail_en || '', 120) }}
</p> </p>
<div class="text-xs text-gray-400 mt-2"> <div class="text-xs text-gray-400 mt-2">
<span v-if="item.release_date">{{ formatDate(item.release_date) }}</span> <span v-if="item.release_date">{{ formatDate(item.release_date) }}</span>
@ -76,7 +73,7 @@
<div class="flex justify-end mt-6"> <div class="flex justify-end mt-6">
<router-link <router-link
v-if="newsByTab[tab.category]?.total > 6" v-if="newsByTab[tab.category]?.total > 6"
:to="`/news/${tab.category}`" :to="`/tab-news/${tab.category}`"
class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md" class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md"
> >
{{ appStore.checkLang.isTh ? more : more_en }} {{ appStore.checkLang.isTh ? more : more_en }}
@ -107,9 +104,15 @@ const newsByTab = ref({});
const more = "อ่านข่าวต่อ"; const more = "อ่านข่าวต่อ";
const more_en = "Read More"; const more_en = "Read More";
const truncateDetail = (text, maxLength) => { // HTML tags String truncate
if (!text) return ''; const stripHtmlAndTruncate = (htmlText, maxLength) => {
return text.length <= maxLength ? text : text.substring(0, maxLength) + '...'; if (!htmlText) return '';
// 1. DOMParser parse HTML string
const doc = new DOMParser().parseFromString(htmlText, 'text/html');
// 2. textContent HTML tags
const plainText = doc.body.textContent || "";
// 3. truncate text
return plainText.length <= maxLength ? plainText : plainText.substring(0, maxLength) + '...';
}; };
const formatDate = (dateString) => { const formatDate = (dateString) => {
@ -128,6 +131,7 @@ const formatDate = (dateString) => {
const fetchNewsForTab = async (category) => { const fetchNewsForTab = async (category) => {
try { try {
// limit 6 TabNews
const { data, total } = await appStore.fetchTabNews(category, 6); const { data, total } = await appStore.fetchTabNews(category, 6);
newsByTab.value[category] = { data, total }; newsByTab.value[category] = { data, total };
} catch (error) { } catch (error) {
@ -138,15 +142,19 @@ const fetchNewsForTab = async (category) => {
const changeTab = async (category) => { const changeTab = async (category) => {
activeTab.value = category; activeTab.value = category;
if (!newsByTab.value[category]) { // Tab fetch
// API Tab
if (!newsByTab.value[category] || newsByTab.value[category].data.length === 0) {
await fetchNewsForTab(category); await fetchNewsForTab(category);
} }
}; };
// Initial data fetch for all tabs on component mount
onMounted(async () => { onMounted(async () => {
for (const tab of tabs.value) { // Fetch news for all tabs initially
await fetchNewsForTab(tab.category); // Use Promise.all to fetch all tabs concurrently for better performance
} await Promise.all(tabs.value.map(tab => fetchNewsForTab(tab.category)));
}); });
</script> </script>
@ -155,4 +163,11 @@ onMounted(async () => {
overflow: hidden; overflow: hidden;
background-color: #f0f0f0; background-color: #f0f0f0;
} }
</style> /* เพิ่ม line-clamp สำหรับ p ใน card-body ให้แสดงได้ 3 บรรทัด */
.line-clamp-3 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
</style>

View File

@ -9,9 +9,10 @@ import LandingLayout from "@/layouts/LandingLayout.vue";
import LandingPageView from "@/views/LandingPageView.vue"; // View สำหรับหน้า Landing Page import LandingPageView from "@/views/LandingPageView.vue"; // View สำหรับหน้า Landing Page
import HomeView from "@/views/HomeView.vue"; // View สำหรับหน้า Home หลัก (ที่มี Calousels, News ฯลฯ) import HomeView from "@/views/HomeView.vue"; // View สำหรับหน้า Home หลัก (ที่มี Calousels, News ฯลฯ)
import ContentView from "@/views/ContentView.vue"; // View สำหรับแสดงรายละเอียดข่าว import ContentView from "@/views/ContentView.vue"; // View สำหรับแสดงรายละเอียด เรื่องราวดี ๆ ที่อยากบอกต่อ
import NewsCategoryView from "@/views/NewsCategoryView.vue"; // View สำหรับแสดงรายการข่าวตามหมวดหมู่ import NewsCategoryView from "@/views/NewsCategoryView.vue"; // View สำหรับแสดงรายการข่าวตามหมวดหมู่
import NotFoundView from '@/views/NotFoundView.vue'; import NotFoundView from '@/views/NotFoundView.vue'; // View สำหรับแสดงหน้า Not Found
import TabContentView from "@/views/TabContentView.vue"; // // View สำหรับแสดงรายละเอียดข่าวสารอับเดต
const routes = [ const routes = [
// --- Route สำหรับหน้า Landing Page (ใช้ LandingLayout) --- // --- Route สำหรับหน้า Landing Page (ใช้ LandingLayout) ---
@ -46,11 +47,18 @@ const routes = [
}, },
// !!! เพิ่ม Route สำหรับ NewsCategoryView !!! // !!! เพิ่ม Route สำหรับ NewsCategoryView !!!
{ {
path: 'news/:category', // Path สำหรับข่าวตามหมวดหมู่ เช่น /news/RTAFNews path: 'tab-news/:category', // Path สำหรับข่าวตามหมวดหมู่ เช่น /news/RTAFNews
name: 'NewsCategoryView', name: 'NewsCategoryView',
component: NewsCategoryView, component: NewsCategoryView,
props: true // ส่งค่า parameter (category) เป็น props ไปยัง component ได้ props: true // ส่งค่า parameter (category) เป็น props ไปยัง component ได้
}, },
// !!! เพิ่ม Route สำหรับ ContentView !!!
{
path: 'tab-news-content/:id', // Path สำหรับรายละเอียดข่าว เช่น /news-content/1, /news-content/2
name: 'TabContentView',
component: TabContentView,
props: true // ส่งค่า parameter (id) เป็น props ไปยัง component ได้
},
// ... ( routes อื่นๆ ที่อาจจะใช้ DefaultLayout ) // ... ( routes อื่นๆ ที่อาจจะใช้ DefaultLayout )
// *** เส้นทาง 404 (ต้องอยู่สุดท้ายเสมอ!) *** // *** เส้นทาง 404 (ต้องอยู่สุดท้ายเสมอ!) ***

View File

@ -7,10 +7,10 @@ export const useAppStore = defineStore('app', {
state: () => ({ state: () => ({
isTh: true, isTh: true,
tabs: [ // ตรวจสอบว่า tabs array ใน store ตรงกับ TabNews.vue tabs: [ // ตรวจสอบว่า tabs array ใน store ตรงกับ TabNews.vue
{ title: "ข่าวประชาสัมพันธ์หน่วยงาน", title_en: "RTAF News", category: "HumanTechNews" }, { title: "ข่าวประชาสัมพันธ์หน่วยงาน", title_en: "Human Tech News", category: "HumanTechNews" },
{ title: "ข่าวประชาสัมพันธ์ภายใน", title_en: "RTAF Organization News", category: "OrgNews" }, { title: "ข่าวประชาสัมพันธ์ภายใน", title_en: "Organization News", category: "OrgNews" },
{ title: "ข่าวนวัตกรรม", title_en: "RTAF Service News", category: "InnovationNews" }, { title: "ข่าวนวัตกรรม", title_en: "Innovation News", category: "InnovationNews" },
{ title: "Press Release", title_en: "RTAF Press Release", category: "GeneralPublic" }, { title: "Press Release", title_en: "Press Release", category: "GeneralPublic" },
{ title: "ข่าวบริการประชาชน", title_en: "Public Service News", category: "EventActivities" }, { title: "ข่าวบริการประชาชน", title_en: "Public Service News", category: "EventActivities" },
], ],
// *** ข้อมูลสำหรับ Header *** // *** ข้อมูลสำหรับ Header ***
@ -228,115 +228,211 @@ export const useAppStore = defineStore('app', {
mockNewsData : [ mockNewsData : [
{ {
id: 1, id: 1,
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 1", // RTAF News title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 1: การพัฒนาระบบ AI สำหรับการบิน",
title_en: "RTAF News Item 1", title_en: "RTAF News Item 1: AI System Development for Aviation",
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 1 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for RTAF News Item 1 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p><strong>กองทพอากาศ</strong> (AI) </p>
image: { url: "/uploads/news_1.jpg" }, <h3>ตถประสงคหลกของโครงการ</h3>
<ul>
<li>เพมความแมนยำในการวเคราะหอมลการบ</li>
<li>ลดระยะเวลาในการตรวจสอบและบำรงรกษา</li>
<li>ยกระดบความปลอดภยในการปฏภารก</li>
</ul>
<figure style="text-align: center;">
<img src="/uploads/news_1.jpg" alt="การบำรุงรักษาอากาศยาน" style="max-width:90%; height:auto; margin: 15px auto; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);">
<figcaption style="font-style: italic; color: #555; margin-top: 5px;">ภาพ: เจาหนาทกำลงตรวจสอบอากาศยานดวยระบบใหม</figcaption>
</figure>
<h4><strong>แผนงานในระยะตอไป</strong></h4>
<ol>
<li>ทดสอบระบบ AI ในสภาพแวดลอมจร</li>
<li>รวบรวมขอมลและปรบปรงอลกอร</li>
<li>ขยายผลการใชงานไปยงหนวยงานตางๆ ของกองทพอากาศ</li>
</ol>
<p>คาดวาระบบ AI จะพรอมใชงานเตมรปแบบภายในป 2569 งจะนำมาซงประโยชนมหาศาลตอการปฏงานและลดคาใชายในระยะยาว</p>
<blockquote>
"การลงทุนในเทคโนโลยีวันนี้ คือรากฐานความมั่นคงของชาติในวันหน้า"
</blockquote>
<p>หากมอสงสยหรอตองการขอมลเพมเต สามารถตดตอสอบถามไดายประชาสมพนธ กองทพอากาศ.</p>
`,
detail_en: `
<p>The <strong>Royal Thai Air Force (RTAF)</strong> has launched a pilot project to develop an Artificial Intelligence (AI) system to enhance operational efficiency in aviation and aircraft maintenance.</p>
<h3>Key Objectives of the Project</h3>
<ul>
<li>Increase accuracy in flight data analysis.</li>
<li>Reduce inspection and maintenance time.</li>
<li>Improve safety in mission operations.</li>
</ul>
<figure style="text-align: center;">
<img src="/uploads/news_1.jpg" alt="Aircraft Maintenance" style="max-width:90%; height:auto; margin: 15px auto; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);">
<figcaption style="font-style: italic; color: #555; margin-top: 5px;">Image: Officers inspecting aircraft with the new system.</figcaption>
</figure>
<h4>Next Phase Plans</h4>
<ol>
<li>Test the AI system in a real environment.</li>
<li>Collect data and refine algorithms.</li>
<li>Expand implementation to various RTAF units.</li>
</ol>
<p>It is expected that this AI system will be fully operational by 2026, bringing immense benefits to operations and long-term cost reduction.</p>
<blockquote>
"Investing in technology today is the foundation of national security tomorrow."
</blockquote>
<p>For more information or inquiries, please contact the RTAF Public Relations Department.</p>
`,
image: { url: "/uploads/news_1.jpg" }, // ภาพปก
release_date: "07/01/2025", release_date: "07/01/2025",
active: true, active: true,
active_en: true, active_en: true,
type: "HumanTechNews", // <-- ให้ตรงกับ category ใน TabNews.vue type: "HumanTechNews",
feature: false, feature: true, // ตั้งเป็น feature เพื่อให้ปรากฏเด่นถ้าคุณมีส่วนแสดง feature news
}, },
{ {
id: 2, id: 2,
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 2", title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 2: พิธีมอบรางวัลบุคลากรดีเด่น",
title_en: "RTAF News Item 2", title_en: "RTAF News Item 2: Outstanding Personnel Awards Ceremony",
detail_th: "ข่าวนี้มีรายละเอียดที่ยาวมากจริงๆ เพื่อทดสอบการ truncate detail. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", detail_th: `
detail_en: "This news has very long details to test detail truncation. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", <p>กองทพอากาศจดพมอบรางวลและประกาศเกยรตณแกคลากรดเด ประจำป 2568 โดยมเขารบรางวลจากหลากหลายหนวยงาน</p>
<p>ดขนอยางสมเกยรต องประชมใหญ กองบญชาการกองทพอากาศ</p>
<p>การมอบรางวลนดขนเปนประจำทกป เพอยกยองและเปนขวญกำลงใจใหแกกำลงพลทมเทปฏหนาทอยางเตมความสามารถ</p>
`,
detail_en: `
<p>The Royal Thai Air Force held an awards ceremony and commendation for outstanding personnel for the year 2025, with recipients from various units.</p>
<p>The ceremony was honorably held in the grand conference hall of the Royal Thai Air Force Headquarters.</p>
<p>This award ceremony is held annually to recognize and boost the morale of personnel who dedicate themselves to their duties with full capability.</p>
`,
image: { url: "/uploads/news_2.jpg" }, image: { url: "/uploads/news_2.jpg" },
release_date: "07/03/2025", release_date: "07/03/2025",
active: true, active: true,
active_en: true, active_en: true,
type: "HumanTechNews", // <-- ให้ตรงกับ category ใน TabNews.vue type: "HumanTechNews",
feature: false, feature: false,
}, },
{ {
id: 3, id: 3,
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 1", // RTAF Organization News title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 1: การฝึกอบรมด้านความปลอดภัยไซเบอร์",
title_en: "RTAF Org News Item 1", title_en: "RTAF Org News Item 1: Cybersecurity Training",
detail_th: "รายละเอียดข่าว นขต.ทอ. ชิ้นที่ 1 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for RTAF Org News Item 1 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p>นขต.ทอ. ดการฝกอบรมเชงปฏการดานความปลอดภยไซเบอร ใหแกเจาหนาท เพอเสรมสรางความรและทกษะในการปองกนภยคกคามทางไซเบอรเพมข.</p>
<p>การฝกอบรมครอบคลมหวขอสำค เช การปองกนฟชช, การเขารหสขอม, และการรบมอกบการโจมตแบบ DDoS.</p>
`,
detail_en: `
<p>RTAF units organized a practical cybersecurity training for officers to enhance knowledge and skills in preventing increasing cyber threats.</p>
<p>The training covered key topics such as phishing prevention, data encryption, and handling DDoS attacks.</p>
`,
image: { url: "/uploads/news_3.jpg" }, image: { url: "/uploads/news_3.jpg" },
release_date: "06/15/2025", release_date: "06/15/2025",
active: true, active: true,
active_en: true, active_en: true,
type: "OrgNews", // <-- ให้ตรงกับ category ใน TabNews.vue type: "OrgNews",
feature: false, feature: false,
}, },
{ {
id: 4, id: 4,
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 2", title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 2: โครงการจิตอาสาพัฒนาชุมชน",
title_en: "RTAF Org News Item 2", title_en: "RTAF Org News Item 2: Community Development Volunteer Project",
detail_th: "รายละเอียดข่าว นขต.ทอ. ชิ้นที่ 2 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for RTAF Org News Item 2 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p>กำลงพลกองทพอากาศเขารวมโครงการจตอาสา "เราทำความดีด้วยหัวใจ" เพอพฒนาและปรบปรงภศนของชมชนใกลเคยง</p>
<p>จกรรมประกอบดวยการทำความสะอาดพนทสาธารณะ, ปลกตนไม, และชวยเหลอผงอาย.</p>
`,
detail_en: `
<p>RTAF personnel participated in the "We Do Good Deeds with Our Hearts" volunteer project to develop and improve the landscape of nearby communities.</p>
<p>Activities included cleaning public areas, planting trees, and assisting the elderly.</p>
`,
image: { url: "/uploads/news_4.jpg" }, image: { url: "/uploads/news_4.jpg" },
release_date: "07/02/2025", release_date: "07/02/2025",
active: true, active: true,
active_en: true, active_en: true,
type: "OrgNews", // <-- ให้ตรงกับ category ใน TabNews.vue type: "OrgNews",
feature: false, feature: false,
}, },
{ {
id: 5, id: 5,
title_th: "ข่าวนวัตกรรม ชิ้นที่ 1", // RTAF Service News (หรือชื่อที่เหมาะสมกับ InnovationNews) title_th: "ข่าวนวัตกรรม ชิ้นที่ 1: โดรนสำรวจไร้คนขับเพื่อภารกิจกู้ภัย",
title_en: "RTAF Service News Item 1", title_en: "RTAF Service News Item 1: Unmanned Survey Drones for Rescue Missions",
detail_th: "รายละเอียดข่าวนวัตกรรม ชิ้นที่ 1 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for RTAF Service News Item 1 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p>กองทพอากาศไดนำโดรนสำรวจรนใหมมาทดสอบเพอใชในภารกจคนหาและก</p>
<p>โดรนเหลานมาพรอมกบเทคโนโลยเซนเซอรความรอนและกลองความละเอยดส ทำใหสามารถปฏงานในพนทเขาถงยากไดอยางมประสทธภาพ.</p>
`,
detail_en: `
<p>The RTAF has begun testing a new generation of survey drones for use in search and rescue missions.</p>
<p>These drones are equipped with thermal sensors and high-resolution cameras, enabling efficient operations in hard-to-reach areas.</p>
`,
image: { url: "/uploads/news_5.jpg" }, image: { url: "/uploads/news_5.jpg" },
release_date: "07/04/2025", release_date: "07/04/2025",
active: true, active: true,
active_en: true, active_en: true,
type: "InnovationNews", // <-- ให้ตรงกับ category ใน TabNews.vue type: "InnovationNews",
feature: false, feature: false,
}, },
{ {
id: 6, id: 6,
title_th: "ข่าวนวัตกรรม ชิ้นที่ 2", title_th: "ข่าวนวัตกรรม ชิ้นที่ 2: ระบบจำลองการบินเสมือนจริง",
title_en: "RTAF Service News Item 2", title_en: "RTAF Service News Item 2: Virtual Reality Flight Simulator",
detail_th: "รายละเอียดข่าวนวัตกรรม ชิ้นที่ 2 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for RTAF Service News Item 2 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p>กองทพอากาศไดดตงระบบจำลองการบนเสมอนจร (VR Flight Simulator) เพอฝกอบรมนกบนใหนเคยกบสถานการณกเฉนตางๆ อนปฏการจร.</p>
<p>ระบบนวยลดตนทนการฝกอบรมและเพมความปลอดภยในการฝกฝน.</p>
`,
detail_en: `
<p>The RTAF has installed a new Virtual Reality (VR) Flight Simulator to train pilots to familiarize themselves with various emergency situations before actual operations.</p>
<p>This system helps reduce training costs and enhances safety during training.</p>
`,
image: { url: "/uploads/news_5.jpg" }, image: { url: "/uploads/news_5.jpg" },
release_date: "07/05/2025", release_date: "07/05/2025",
active: true, active: true,
active_en: true, active_en: true,
type: "InnovationNews", // <-- ให้ตรงกับ category ใน TabNews.vue type: "InnovationNews",
feature: false, feature: false,
}, },
{ {
id: 7, id: 7,
title_th: "Press Release ชิ้นที่ 1", // Press Release title_th: "Press Release ชิ้นที่ 1: การเยือนของคณะผู้แทนจากมิตรประเทศ",
title_en: "RTAF Press Release Item 1", title_en: "RTAF Press Release Item 1: Visit of Delegation from Allied Country",
detail_th: "รายละเอียด Press Release ชิ้นที่ 1 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for RTAF Press Release Item 1 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p>คณะผแทนระดบสงจากมตรประเทศเดนทางเยอนกองทพอากาศ เพอกระชบความสมพนธและหารอความรวมมอทางทหาร.</p>
<p>การเยอนครงนเนนยำถงความสำคญของการแลกเปลยนความรและประสบการณระหวางก.</p>
`,
detail_en: `
<p>A high-level delegation from an allied country visited the Royal Thai Air Force to strengthen relations and discuss military cooperation.</p>
<p>This visit emphasized the importance of exchanging knowledge and experiences between the two nations.</p>
`,
image: { url: "/uploads/news_4.jpg" }, image: { url: "/uploads/news_4.jpg" },
release_date: "06/20/2025", release_date: "06/20/2025",
active: true, active: true,
active_en: true, active_en: true,
type: "GeneralPublic", // <-- ให้ตรงกับ category ใน TabNews.vue type: "GeneralPublic",
feature: false, feature: false,
}, },
{ {
id: 8, id: 8,
title_th: "ข่าวบริการประชาชน ชิ้นที่ 1", // Public Service News title_th: "ข่าวบริการประชาชน ชิ้นที่ 1: โครงการบริจาคโลหิตประจำปี",
title_en: "Public Service News Item 1", title_en: "Public Service News Item 1: Annual Blood Donation Drive",
detail_th: "รายละเอียดข่าวบริการประชาชน ชิ้นที่ 1 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for Public Service News Item 1 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p>กองทพอากาศขอเชญชวนประชาชนรวมบรจาคโลหตในโครงการ "รวมใจบริจาคโลหิตเพื่อเพื่อนมนุษย์" ประจำป 2568.</p>
<p>โครงการนดขนเพอสำรองโลหตใหบโรงพยาบาลและผวยทองการ.</p>
`,
detail_en: `
<p>The Royal Thai Air Force invites the public to participate in the "Unite for Humanity Blood Donation" project for the year 2025.</p>
<p>This project aims to provide blood reserves for hospitals and patients in need.</p>
`,
image: { url: "/uploads/news_3.jpg" }, image: { url: "/uploads/news_3.jpg" },
release_date: "07/06/2025", release_date: "07/06/2025",
active: true, active: true,
active_en: true, active_en: true,
type: "EventActivities", // <-- ให้ตรงกับ category ใน TabNews.vue type: "EventActivities",
feature: false, feature: false,
}, },
// เพิ่มเติมเพื่อให้มีข้อมูลเพียงพอสำหรับแต่ละแท็บ (อย่างน้อย 6 ชิ้นในแต่ละประเภท เพื่อให้ปุ่ม Read More แสดง) // เพิ่มเติมเพื่อให้มีข้อมูลเพียงพอสำหรับแต่ละแท็บ (อย่างน้อย 6 ชิ้นในแต่ละประเภท เพื่อให้ปุ่ม Read More แสดง)
{ {
id: 9, id: 9,
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 3", title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 3: สัมมนาเชิงปฏิบัติการเทคโนโลยีดิจิทัล",
title_en: "RTAF News Item 3", title_en: "RTAF News Item 3: Digital Technology Workshop",
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 3. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for RTAF News Item 3. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p>กองทพอากาศไดดสมมนาเชงปฏการเกยวกบเทคโนโลยลลาส เพอใหคลากรไดเรยนรและปรบตวเขากบการเปลยนแปลงทางเทคโนโลย</p>
<p>การสมมนามงเนนท AI, Big Data, และ Cybersecurity งเปนสงสำคญในยคปจจ</p>
`,
detail_en: `
<p>The RTAF hosted a workshop on the latest digital technologies, allowing personnel to learn and adapt to technological changes.</p>
<p>The seminar focused on AI, Big Data, and Cybersecurity, which are crucial in the current era.</p>
`,
image: { url: "/uploads/news_2.jpg" }, image: { url: "/uploads/news_2.jpg" },
release_date: "07/07/2025", release_date: "07/07/2025",
active: true, active: true,
@ -346,10 +442,16 @@ export const useAppStore = defineStore('app', {
}, },
{ {
id: 10, id: 10,
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 4", title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 4: โครงการอนุรักษ์สิ่งแวดล้อม",
title_en: "RTAF News Item 4", title_en: "RTAF News Item 4: Environmental Conservation Project",
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 4. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for RTAF News Item 4. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p>กองทพอากาศจดกจกรรมโครงการอนกษงแวดลอม โดยการปลกปาและทำความสะอาดพนทสาธารณะรอบคายทหาร</p>
<p>จกรรมนเปนสวนหนงของความรบผดชอบตอสงคมของกองทพอากาศ</p>
`,
detail_en: `
<p>The RTAF organized an environmental conservation project by planting trees and cleaning public areas around military camps.</p>
<p>This activity is part of the RTAF's social responsibility.</p>
`,
image: { url: "/uploads/news_1.jpg" }, image: { url: "/uploads/news_1.jpg" },
release_date: "07/07/2025", release_date: "07/07/2025",
active: true, active: true,
@ -359,10 +461,16 @@ export const useAppStore = defineStore('app', {
}, },
{ {
id: 11, id: 11,
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 5", title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 5: การพัฒนาศักยภาพกำลังพล",
title_en: "RTAF News Item 5", title_en: "RTAF News Item 5: Personnel Capability Development",
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 5. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for RTAF News Item 5. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p>กองทพอากาศมงมนในการพฒนาศกยภาพของกำลงพล านการฝกอบรมและหลกสตรตางๆ</p>
<p>เพอเพมพนความรและทกษะใหนตอสถานการณจจ.</p>
`,
detail_en: `
<p>The RTAF is committed to developing the capabilities of its personnel through various training and courses.</p>
<p>To enhance knowledge and skills to keep pace with current situations.</p>
`,
image: { url: "/uploads/news_1.jpg" }, image: { url: "/uploads/news_1.jpg" },
release_date: "07/07/2025", release_date: "07/07/2025",
active: true, active: true,
@ -372,10 +480,16 @@ export const useAppStore = defineStore('app', {
}, },
{ {
id: 12, id: 12,
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 6", title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 6: การซ้อมรบร่วมประจำปี",
title_en: "RTAF News Item 6", title_en: "RTAF News Item 6: Annual Joint Military Exercise",
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 6. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for RTAF News Item 6. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p>กองทพอากาศเขารวมการซอมรบรวมกบกองทพบกและกองทพเร เพอเสรมสรางความรวมมอและบรณาการการทำงาน</p>
<p>การซอมรบดำเนนไปอยางเขมขนและประสบความสำเรจตามวตถประสงค.</p>
`,
detail_en: `
<p>The RTAF participated in a joint military exercise with the Royal Thai Army and Royal Thai Navy to enhance cooperation and work integration.</p>
<p>The exercise was conducted intensively and achieved its objectives successfully.</p>
`,
image: { url: "/uploads/news_2.jpg" }, image: { url: "/uploads/news_2.jpg" },
release_date: "07/07/2025", release_date: "07/07/2025",
active: true, active: true,
@ -387,8 +501,14 @@ export const useAppStore = defineStore('app', {
id: 13, id: 13,
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 7 (สำหรับปุ่ม More)", title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 7 (สำหรับปุ่ม More)",
title_en: "RTAF News Item 7 (for More button)", title_en: "RTAF News Item 7 (for More button)",
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 7. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", detail_th: `
detail_en: "Details for RTAF News Item 7. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", <p>รายละเอยดขาวประชาสมพนธหนวยงาน นท 7 เพมเตมสำหรบทดสอบป 'ดูเพิ่มเติม'</p>
<p>อมลนแสดงใหเหนวาสามารถโหลดเนอหาเพมขนมาไดเรอยๆ</p>
`,
detail_en: `
<p>Details for RTAF News Item 7, further content for testing the 'Load More' button.</p>
<p>This data demonstrates that more content can be continuously loaded.</p>
`,
image: { url: "/uploads/news_3.jpg" }, image: { url: "/uploads/news_3.jpg" },
release_date: "07/07/2025", release_date: "07/07/2025",
active: true, active: true,
@ -396,14 +516,18 @@ export const useAppStore = defineStore('app', {
type: "HumanTechNews", type: "HumanTechNews",
feature: false, feature: false,
}, },
// เพิ่มข้อมูลสำหรับแต่ละ category ใน tabs ของคุณให้มีจำนวนเพียงพอที่จะแสดงผล
// ตัวอย่างเช่น สำหรับ OrgNews
{ {
id: 14, id: 14,
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 3", title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 3: โครงการจัดสวัสดิการบุคลากร",
title_en: "RTAF Org News Item 3", title_en: "RTAF Org News Item 3: Personnel Welfare Project",
detail_th: "รายละเอียดข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 3.", detail_th: `
detail_en: "Details for RTAF Org News Item 3.", <p>นขต.ทอ. ไดเรมโครงการจดสวสดการใหม เพอดแลบคลากรและครอบคร</p>
<p>โครงการนรวมถงการสนบสนนดานสขภาพ, การศกษา, และทอยอาศ.</p>
`,
detail_en: `
<p>RTAF units have initiated a new welfare project to care for personnel and their families.</p>
<p>This project includes support for health, education, and housing.</p>
`,
image: { url: "/uploads/news_4.jpg" }, image: { url: "/uploads/news_4.jpg" },
release_date: "07/07/2025", release_date: "07/07/2025",
active: true, active: true,
@ -413,10 +537,16 @@ export const useAppStore = defineStore('app', {
}, },
{ {
id: 15, id: 15,
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 4", title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 4: การปรับปรุงโครงสร้างองค์กร",
title_en: "RTAF Org News Item 4", title_en: "RTAF Org News Item 4: Organizational Structure Improvement",
detail_th: "รายละเอียดข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 4.", detail_th: `
detail_en: "Details for RTAF Org News Item 4.", <p>การปรบปรงโครงสรางองคกรภายในกองทพอากาศ เพอเพมประสทธภาพในการบรหารจดการ</p>
<p>และรองรบภารกจทบซอนมากยงข.</p>
`,
detail_en: `
<p>The internal organizational structure of the RTAF has been improved to enhance management efficiency</p>
<p>and accommodate more complex missions.</p>
`,
image: { url: "/uploads/news_5.jpg" }, image: { url: "/uploads/news_5.jpg" },
release_date: "07/07/2025", release_date: "07/07/2025",
active: true, active: true,
@ -426,10 +556,16 @@ export const useAppStore = defineStore('app', {
}, },
{ {
id: 16, id: 16,
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 5", title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 5: การจัดตั้งศูนย์นวัตกรรม",
title_en: "RTAF Org News Item 5", title_en: "RTAF Org News Item 5: Innovation Center Establishment",
detail_th: "รายละเอียดข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 5.", detail_th: `
detail_en: "Details for RTAF Org News Item 5.", <p>กองทพอากาศไดดตงศนยนวตกรรมแหงใหม เพอเปนแหลงบมเพาะและพฒนาแนวคดใหม านการทหารและเทคโนโลย</p>
<p>นยจะเปนตวขบเคลอนสำคญในการสรางสรรคงประดษฐและนวตกรรม.</p>
`,
detail_en: `
<p>The RTAF has established a new innovation center to foster and develop new concepts in military and technology.</p>
<p>This center will be a crucial driver in creating inventions and innovations.</p>
`,
image: { url: "/uploads/news_6.jpg" }, image: { url: "/uploads/news_6.jpg" },
release_date: "07/07/2025", release_date: "07/07/2025",
active: true, active: true,
@ -439,10 +575,16 @@ export const useAppStore = defineStore('app', {
}, },
{ {
id: 17, id: 17,
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 6", title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 6: กิจกรรมส่งเสริมสุขภาพ",
title_en: "RTAF Org News Item 6", title_en: "RTAF Org News Item 6: Health Promotion Activities",
detail_th: "รายละเอียดข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 6.", detail_th: `
detail_en: "Details for RTAF Org News Item 6.", <p>การจดกจกรรมสงเสรมสขภาพสำหรบกำลงพลและครอบคร เพอสรางเสรมสขภาพทและลดความเสยงจากโรคตางๆ</p>
<p>จกรรมรวมถงการตรวจสขภาพประจำป, การออกกำลงกาย, และการใหความรานโภชนาการ.</p>
`,
detail_en: `
<p>Health promotion activities are organized for personnel and their families to promote good health and reduce the risk of various diseases.</p>
<p>Activities include annual health check-ups, physical exercise, and nutrition education.</p>
`,
image: { url: "/uploads/news_1.jpg" }, image: { url: "/uploads/news_1.jpg" },
release_date: "07/07/2025", release_date: "07/07/2025",
active: true, active: true,
@ -454,8 +596,14 @@ export const useAppStore = defineStore('app', {
id: 18, id: 18,
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 7 (สำหรับปุ่ม More)", title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 7 (สำหรับปุ่ม More)",
title_en: "RTAF Org News Item 7 (for More button)", title_en: "RTAF Org News Item 7 (for More button)",
detail_th: "รายละเอียดข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 7.", detail_th: `
detail_en: "Details for RTAF Org News Item 7.", <p>รายละเอยดขาวประชาสมพนธภายใน นท 7 เพอทดสอบป 'ดูเพิ่มเติม' ในหมวดหมาวภายใน</p>
<p>อขอมลเพมเตมทสามารถโหลดเขามาได</p>
`,
detail_en: `
<p>Details for RTAF Org News Item 7 for testing the 'Load More' button in the internal news category.</p>
<p>This is additional data that can be loaded.</p>
`,
image: { url: "/uploads/news_2.jpg" }, image: { url: "/uploads/news_2.jpg" },
release_date: "07/07/2025", release_date: "07/07/2025",
active: true, active: true,
@ -520,44 +668,51 @@ export const useAppStore = defineStore('app', {
this.isTh = !this.isTh; this.isTh = !this.isTh;
}, },
// *** Action ใหม่: สำหรับ TabNews โดยเฉพาะ (return { data, total }) *** // *** ฟังก์ชันกลางสำหรับดึงข่าวจาก mockNewsData ที่รองรับการกรอง, เรียง, และแบ่งหน้า ***
async fetchTabNews(category, limit = 6) { async fetchNewsFromMockData(options = {}) {
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate API delay await new Promise(resolve => setTimeout(resolve, 100)); // Simulate API delay
const { category, limit = 6, page = 1, isFeature = null } = options;
const isTh = this.isTh; const isTh = this.isTh;
// const currentDate = new Date(); // ไม่ต้องใช้แล้ว
// currentDate.setHours(0, 0, 0, 0); // ไม่ต้องใช้แล้ว
// ขั้นตอนที่ 1: กรองข้อมูลทั้งหมดที่เข้าเงื่อนไข (active, type) ก่อนที่จะ apply limit let filteredNews = this.mockNewsData.filter(item => {
const allMatchingNews = this.mockNewsData.filter(item => {
const isActive = isTh ? item.active : item.active_en; const isActive = isTh ? item.active : item.active_en;
const isCorrectType = item.type === category; let match = isActive;
// ลบส่วนการตรวจสอบวันที่ออกไป // กรองตาม category (type) ถ้ามีการระบุ
// const releaseDateParts = item.release_date ? item.release_date.split('/') : null; if (category) {
// const itemReleaseDate = releaseDateParts ? new Date(parseInt(releaseDateParts[2]), parseInt(releaseDateParts[0]) - 1, parseInt(releaseDateParts[1])) : new Date(0); match = match && item.type === category;
// itemReleaseDate.setHours(0, 0, 0, 0); }
// return isActive && isCorrectType && (itemReleaseDate <= currentDate); // เงื่อนไขเก่า // กรองตาม feature ถ้ามีการระบุ
return isActive && isCorrectType; // เงื่อนไขใหม่: ไม่สนใจวันที่ if (isFeature !== null) {
match = match && item.feature === isFeature;
}
return match;
}); });
// เก็บจำนวนรวมทั้งหมดก่อนการจำกัด (total count) // เรียงลำดับจากวันที่ใหม่สุดไปเก่าสุด
const totalCount = allMatchingNews.length; filteredNews.sort((a, b) => {
const dateA = a.release_date ? new Date(a.release_date.split('/').reverse().join('-')) : new Date(0);
// ขั้นตอนที่ 2: เรียงลำดับข้อมูลที่กรองแล้ว (ยังคงเรียงตามวันที่จากใหม่ไปเก่า) const dateB = b.release_date ? new Date(b.release_date.split('/').reverse().join('-')) : new Date(0);
allMatchingNews.sort((a, b) => { return dateB.getTime() - dateA.getTime(); // ใหม่ไปเก่า
const dateA = a.release_date ? new Date(parseInt(a.release_date.split('/')[2]), parseInt(a.release_date.split('/')[0]) - 1, parseInt(a.release_date.split('/')[1])) : new Date(0);
dateA.setHours(0, 0, 0, 0);
const dateB = b.release_date ? new Date(parseInt(b.release_date.split('/')[2]), parseInt(b.release_date.split('/')[0]) - 1, parseInt(b.release_date.split('/')[1])) : new Date(0);
dateB.setHours(0, 0, 0, 0);
return dateB.getTime() - dateA.getTime();
}); });
// ขั้นตอนที่ 3: จำกัดจำนวนตาม _limit สำหรับข้อมูลที่จะแสดงผล const totalCount = filteredNews.length;
const data = allMatchingNews.slice(0, limit);
return { data: data, total: totalCount }; // ส่ง Object กลับไปสำหรับ TabNews // คำนวณ offset สำหรับ pagination
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedData = filteredNews.slice(startIndex, endIndex);
return { data: paginatedData, total: totalCount };
},
// *** อัปเดต fetchTabNews ให้เรียกใช้ fetchNewsFromMockData ***
async fetchTabNews(category, limit = 6, page = 1) { // เพิ่ม page parameter
return this.fetchNewsFromMockData({ category, limit, page });
}, },
// *** Action เดิม: find (ยังคง return เป็น Array เหมือนเดิมสำหรับ endpoint อื่นๆ) *** // *** Action เดิม: find (ยังคง return เป็น Array เหมือนเดิมสำหรับ endpoint อื่นๆ) ***
@ -627,34 +782,30 @@ export const useAppStore = defineStore('app', {
return dateA - dateB; return dateA - dateB;
}); });
const limitMatch = queryParams.match(/_limit=(\d+)/);
if (limitMatch) {
data = data.slice(0, parseInt(limitMatch[1]));
}
break;
case 'tabNews':
data = this.mockNewsData.filter(item => {
const isActive = isTh ? item.active : item.active_en;
let match = isActive;
if (queryParams.includes('feature=true')) {
match = match && item.feature;
} else if (queryParams.includes('feature=false')) {
match = match && !item.feature;
}
return match;
});
data.sort((a, b) => {
const dateA = new Date(a.release_date.split('/').reverse().join('-'));
const dateB = new Date(b.release_date.split('/').reverse().join('-'));
return dateA - dateB;
});
const limit = queryParams.match(/_limit=(\d+)/); const limit = queryParams.match(/_limit=(\d+)/);
if (limit) { if (limit) {
data = data.slice(0, parseInt(limit[1])); data = data.slice(0, parseInt(limit[1]));
} }
break; break;
case 'tabNews':
// ใช้ fetchNewsFromMockData เพื่อจัดการการกรอง/แบ่งหน้า
// ต้องแยก queryParams ออกมาเป็น object ก่อน
const limitMatch = queryParams.match(/_limit=(\d+)/);
const isFeatureMatch = queryParams.includes('feature=true') ? true : (queryParams.includes('feature=false') ? false : null);
// หาก find('news') หรือ find('contents') ต้องการดึงทั้งหมดโดยไม่แบ่งหน้า
// เราก็สามารถเรียก fetchNewsFromMockData โดยไม่ส่ง limit/page
// หรือถ้าต้องการแบ่งหน้าในอนาคต ก็สามารถส่ง limit/page ได้
const result = await this.fetchNewsFromMockData({
// category: คุณอาจจะต้องเพิ่ม parameter 'category' เข้าไปใน queryParams string ด้วย ถ้าต้องการกรอง
limit: limitMatch ? parseInt(limitMatch[1]) : undefined, // ไม่จำกัดถ้าไม่มี limit ใน queryParams
isFeature: isFeatureMatch,
// หากต้องการ page สำหรับ find('news') ต้องส่งเข้ามาใน queryParams string ด้วย
// เช่น find('news', '_limit=5&_page=2')
page: queryParams.match(/_page=(\d+)/) ? parseInt(queryParams.match(/_page=(\d+)/)[1]) : 1
});
data = result.data; // find action เดิม return แค่ data Array
break;
default: default:
console.warn(`Endpoint ${endpoint} not mocked or handled by specific action.`); console.warn(`Endpoint ${endpoint} not mocked or handled by specific action.`);
data = []; data = [];

View File

@ -11,7 +11,7 @@
:key="item.id" :key="item.id"
class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow duration-300" class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow duration-300"
> >
<router-link :to="`/content/${item.id}`" class="block"> <router-link :to="`/tab-news-content/${item.id}`" class="block">
<div class="w-full h-48 overflow-hidden"> <div class="w-full h-48 overflow-hidden">
<img <img
v-if="item.image && item.image.url" v-if="item.image && item.image.url"
@ -20,15 +20,15 @@
:alt="appStore.checkLang.isTh ? item.title_th : item.title_en" :alt="appStore.checkLang.isTh ? item.title_th : item.title_en"
/> />
<div v-else class="w-full h-full bg-gray-200 flex items-center justify-center text-gray-500"> <div v-else class="w-full h-full bg-gray-200 flex items-center justify-center text-gray-500">
No Image {{ appStore.checkLang.isTh ? 'ไม่มีรูปภาพ' : 'No Image' }}
</div> </div>
</div> </div>
<div class="p-4"> <div class="p-4">
<h3 class="font-semibold text-lg text-gray-800 hover:text-primary-focus leading-tight mb-2"> <h3 class="font-semibold text-lg text-gray-800 hover:text-primary-focus leading-tight mb-2 line-clamp-2">
{{ appStore.checkLang.isTh ? item.title_th : item.title_en }} {{ appStore.checkLang.isTh ? item.title_th : item.title_en }}
</h3> </h3>
<p class="text-sm text-gray-600 leading-snug mb-3"> <p class="text-sm text-gray-600 leading-snug mb-3 line-clamp-3">
{{ truncateDetail(appStore.checkLang.isTh ? item.detail_th : item.detail_en, 120) }} {{ stripHtmlAndTruncate(appStore.checkLang.isTh ? item.detail_th : item.detail_en || '', 120) }}
</p> </p>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
<span v-if="item.release_date">{{ formatDate(item.release_date) }}</span> <span v-if="item.release_date">{{ formatDate(item.release_date) }}</span>
@ -37,7 +37,17 @@
</router-link> </router-link>
</div> </div>
</div> </div>
<div v-else class="text-center text-gray-600 py-16">
<div v-if="newsList.length > 0 && newsList.length < totalNews" class="flex justify-center mt-8">
<button
@click="loadMoreNews"
class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md"
>
{{ appStore.checkLang.isTh ? 'ดูเพิ่มเติม' : 'Load More' }}
</button>
</div>
<div v-else-if="newsList.length === 0 && !isLoading" class="text-center text-gray-600 py-16">
<p class="text-2xl font-semibold mb-4"> <p class="text-2xl font-semibold mb-4">
{{ appStore.checkLang.isTh ? 'ไม่พบข่าวสารในหมวดหมู่นี้ หรือข่าวสารนี้ไม่พร้อมใช้งานในภาษาปัจจุบัน' : 'No news found in this category or not available in the current language.' }} {{ appStore.checkLang.isTh ? 'ไม่พบข่าวสารในหมวดหมู่นี้ หรือข่าวสารนี้ไม่พร้อมใช้งานในภาษาปัจจุบัน' : 'No news found in this category or not available in the current language.' }}
</p> </p>
@ -45,6 +55,15 @@
{{ appStore.checkLang.isTh ? 'กลับหน้าหลัก' : 'Back to Home' }} {{ appStore.checkLang.isTh ? 'กลับหน้าหลัก' : 'Back to Home' }}
</router-link> </router-link>
</div> </div>
<div v-else-if="isLoading" class="text-center text-gray-500 py-16">
<p class="text-xl flex items-center justify-center">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ appStore.checkLang.isTh ? 'กำลังโหลดข่าวสาร...' : 'Loading news...' }}
</p>
</div>
</div> </div>
</template> </template>
@ -57,15 +76,18 @@ import { RouterLink } from 'vue-router';
const appStore = useAppStore(); const appStore = useAppStore();
const route = useRoute(); const route = useRoute();
const newsList = ref([]); const newsList = ref([]);
const totalNews = ref(0);
const currentPage = ref(1);
const newsPerPage = 6;
const isLoading = ref(false);
// Computed property // Computed property
const categoryTitleTh = computed(() => { const categoryTitleTh = computed(() => {
// appStore.tabs Array
if (appStore.tabs && Array.isArray(appStore.tabs) && appStore.tabs.length > 0) { if (appStore.tabs && Array.isArray(appStore.tabs) && appStore.tabs.length > 0) {
const tab = appStore.tabs.find(t => t.category === route.params.category); const tab = appStore.tabs.find(t => t.category === route.params.category);
return tab ? tab.title : 'ไม่ระบุหมวดหมู่'; return tab ? tab.title : 'ไม่ระบุหมวดหมู่';
} }
return 'กำลังโหลด...'; return appStore.checkLang.isTh ? 'กำลังโหลด...' : 'Loading...';
}); });
const categoryTitleEn = computed(() => { const categoryTitleEn = computed(() => {
@ -73,13 +95,18 @@ const categoryTitleEn = computed(() => {
const tab = appStore.tabs.find(t => t.category === route.params.category); const tab = appStore.tabs.find(t => t.category === route.params.category);
return tab ? tab.title_en : 'Unspecified Category'; return tab ? tab.title_en : 'Unspecified Category';
} }
return 'Loading...'; return appStore.checkLang.isTh ? 'Loading...' : 'Loading...';
}); });
const truncateDetail = (text, maxLength) => { // ** HTML tags String truncate **
if (!text) return ''; const stripHtmlAndTruncate = (htmlText, maxLength) => {
if (text.length <= maxLength) return text; if (!htmlText) return '';
return text.substring(0, maxLength) + '...'; // 1. DOMParser parse HTML string
const doc = new DOMParser().parseFromString(htmlText, 'text/html');
// 2. textContent HTML tags
const plainText = doc.body.textContent || "";
// 3. truncate text
return plainText.length <= maxLength ? plainText : plainText.substring(0, maxLength) + '...';
}; };
const formatDate = (dateString) => { const formatDate = (dateString) => {
@ -96,38 +123,74 @@ const formatDate = (dateString) => {
return dateString; return dateString;
}; };
const fetchNewsForCategory = async (category) => { const fetchNewsForCategory = async (category, pageToLoad = 1) => {
isLoading.value = true;
try { try {
// appStore.find active, active_en, release_date feature const result = await appStore.fetchNewsFromMockData({
const allNews = await appStore.find('tabNews', ''); // filter category: category,
limit: newsPerPage,
const filteredNews = allNews.filter(item => { page: pageToLoad,
return item.type === category; // parameter fetchNewsFromMockData
// lang: appStore.isTh ? 'th' : 'en'
}); });
newsList.value = filteredNews; if (pageToLoad === 1) {
newsList.value = result.data;
} else {
newsList.value = [...newsList.value, ...result.data];
}
totalNews.value = result.total;
currentPage.value = pageToLoad;
} catch (error) { } catch (error) {
console.error(`Error fetching news for category ${category}:`, error); console.error(`Error fetching news for category ${category}:`, error);
newsList.value = []; newsList.value = [];
totalNews.value = 0;
} finally {
isLoading.value = false;
} }
}; };
const loadMoreNews = () => {
if (newsList.value.length < totalNews.value && !isLoading.value) {
fetchNewsForCategory(route.params.category, currentPage.value + 1);
}
};
onMounted(() => { onMounted(() => {
fetchNewsForCategory(route.params.category); fetchNewsForCategory(route.params.category, 1);
}); });
// Watch route params changes to refetch news for new category
watch(() => route.params.category, (newCategory) => { watch(() => route.params.category, (newCategory) => {
fetchNewsForCategory(newCategory); newsList.value = []; // Clear current news
currentPage.value = 1; // Reset to first page
totalNews.value = 0; // Reset total
fetchNewsForCategory(newCategory, 1);
}); });
// Watch for language changes and re-fetch the news list // Watch language changes to refetch news list
watch(() => appStore.checkLang.isTh, () => { // **** watch(() => appStore.isTh, () => {
fetchNewsForCategory(route.params.category); newsList.value = []; // Clear current news
currentPage.value = 1; // Reset to first page
totalNews.value = 0; // Reset total
fetchNewsForCategory(route.params.category, 1);
}); });
</script> </script>
<style scoped> <style scoped>
/* เพิ่ม line-clamp สำหรับ p และ h3 ใน card */
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
</style> </style>

View File

@ -0,0 +1,168 @@
// src/views/TabContentView.vue
<template>
<div class="container mx-auto p-4 md:p-8">
<div v-if="newsItem" class="bg-white shadow-lg rounded-lg overflow-hidden">
<div class="relative w-full h-64 md:h-96 overflow-hidden">
<img
v-if="newsItem.image && newsItem.image.url"
:src="`${appStore.imageBaseUrl}${newsItem.image.url}`"
:alt="appStore.checkLang.isTh ? newsItem.title_th : newsItem.title_en"
class="w-full h-full object-cover object-center"
loading="lazy"
/>
<div v-else class="w-full h-full bg-gray-200 flex items-center justify-center text-gray-500 text-lg">
{{ appStore.checkLang.isTh ? 'ไม่มีรูปภาพ' : 'No Image Available' }}
</div>
</div>
<div class="p-6 md:p-8">
<nav class="text-sm text-gray-600 mb-4" aria-label="Breadcrumb">
<ol class="list-none p-0 inline-flex">
<li class="flex items-center">
<router-link to="/home" class="text-blue-600 hover:underline">
{{ appStore.checkLang.isTh ? 'หน้าหลัก' : 'Home' }}
</router-link>
<svg class="fill-current w-3 h-3 mx-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 75.253c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.476 239.029c9.373 9.372 9.373 24.568 0 33.942z"/></svg>
</li>
<li class="flex items-center">
<router-link :to="`/tab-news/${newsItem.type}`" class="text-blue-600 hover:underline">
{{ appStore.checkLang.isTh ? categoryTitleTh : categoryTitleEn }}
</router-link>
<svg class="fill-current w-3 h-3 mx-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 75.253c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.476 239.029c9.373 9.372 9.373 24.568 0 33.942z"/></svg>
</li>
<li>
<span class="text-gray-500">{{ appStore.checkLang.isTh ? newsItem.title_th : newsItem.title_en }}</span>
</li>
</ol>
</nav>
<h1 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4 leading-tight">
{{ appStore.checkLang.isTh ? newsItem.title_th : newsItem.title_en }}
</h1>
<p class="text-sm text-gray-500 mb-6">
<span v-if="newsItem.release_date">
{{ appStore.checkLang.isTh ? 'เผยแพร่เมื่อ' : 'Published on' }}:
<span class="font-medium text-gray-700">{{ formatDate(newsItem.release_date) }}</span>
</span>
</p>
<div class="prose lg:prose-lg max-w-3xl mx-auto text-gray-800 leading-relaxed break-words">
<div v-html="appStore.checkLang.isTh ? newsItem.detail_th : newsItem.detail_en"></div>
</div>
<div class="mt-8 pt-4 border-t border-gray-200 flex justify-end">
<button class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M15 8a3 3 0 10-2.977-2.909L8 7.239V4.75a3 3 0 00-3-3H4.75a3 3 0 00-3 3v.475a3 3 0 003 3V12h-.25a2.75 2.75 0 000 5.5h.25a2.75 2.75 0 000-5.5H4.75a.75.75 0 010-1.5h.25a.75.75 0 01.75.75v.25c0 .285.068.558.196.804l3.153-1.636a3 3 0 003.545-5.908L15 8zM5 4.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zm9.5 9.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM15 6a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z"></path></svg>
{{ appStore.checkLang.isTh ? 'แชร์' : 'Share' }}
</button>
</div>
</div>
</div>
<div v-else class="text-center text-gray-600 py-16">
<p class="text-2xl font-semibold mb-4">
{{ appStore.checkLang.isTh ? 'ไม่พบข่าวสารนี้ หรือข่าวสารนี้ไม่พร้อมใช้งานในภาษาปัจจุบัน' : 'News not found or not available in the current language.' }}
</p>
<router-link to="/home" class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md">
{{ appStore.checkLang.isTh ? 'กลับหน้าหลัก' : 'Back to Home' }}
</router-link>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useAppStore } from '@/stores/app.js';
import { RouterLink } from 'vue-router';
const appStore = useAppStore();
const route = useRoute();
const newsItem = ref(null);
// Computed property Breadcrumbs
const categoryTitleTh = computed(() => {
if (newsItem.value && appStore.tabs && Array.isArray(appStore.tabs) && appStore.tabs.length > 0) {
const tab = appStore.tabs.find(t => t.category === newsItem.value.type);
return tab ? tab.title : 'ไม่ระบุหมวดหมู่';
}
return appStore.checkLang.isTh ? 'กำลังโหลด...' : 'Loading...';
});
const categoryTitleEn = computed(() => {
if (newsItem.value && appStore.tabs && Array.isArray(appStore.tabs) && appStore.tabs.length > 0) {
const tab = appStore.tabs.find(t => t.category === newsItem.value.type);
return tab ? tab.title_en : 'Unspecified Category';
}
return appStore.checkLang.isTh ? 'Loading...' : 'Loading...';
});
const fetchNewsDetail = async (id) => {
// API await axios.get(`/api/news/${id}`);
// mock data store
const foundNews = appStore.mockNewsData.find(item => item.id == id);
if (foundNews) {
// active active_en
if (appStore.isTh && foundNews.active) {
newsItem.value = foundNews;
} else if (!appStore.isTh && foundNews.active_en) {
newsItem.value = foundNews;
} else {
newsItem.value = null; // active
}
} else {
newsItem.value = null;
}
};
const formatDate = (dateString) => {
if (!dateString) return '';
const parts = dateString.split('/'); // Assumes MM/DD/YYYY
if (parts.length === 3) {
const [month, day, year] = parts;
const dateObj = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
const formattedDay = String(dateObj.getDate()).padStart(2, '0');
const formattedMonth = String(dateObj.getMonth() + 1).padStart(2, '0');
const formattedYear = dateObj.getFullYear();
return `${formattedDay}/${formattedMonth}/${formattedYear}`;
}
return dateString;
};
onMounted(() => {
fetchNewsDetail(route.params.id);
});
watch(() => route.params.id, (newId) => {
fetchNewsDetail(newId);
});
watch(() => appStore.isTh, () => {
fetchNewsDetail(route.params.id);
});
</script>
<style scoped>
/* ตัวอย่างการกำหนดความกว้างของเนื้อหา */
.prose {
max-width: 70ch; /* จำกัดความกว้างของข้อความให้อ่านง่ายขึ้น */
}
/* ปรับปรุงการแสดงผลรูปภาพภายในเนื้อหา (ถ้าอยู่ใน detail_th/en) */
.prose img {
max-width: 100%;
height: auto;
display: block; /* ทำให้รูปภาพเป็น block level */
margin: 1em auto; /* จัดกึ่งกลางและมีระยะห่าง */
border-radius: 0.5rem; /* ตัวอย่าง: ขอบมน */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* ตัวอย่าง: เพิ่มเงา */
}
/* สำหรับภาพปก */
.object-cover {
object-fit: cover;
object-position: center; /* ทำให้รูปภาพอยู่ตรงกลางเมื่อถูก crop */
}
</style>