Initial commit of Vue Website Template
56
.gitignore
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# ==============================
|
||||||
|
# ⚙️ Node / Vite
|
||||||
|
# ==============================
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
*.local
|
||||||
|
.env
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🧾 Logs
|
||||||
|
# ==============================
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🧠 IDEs
|
||||||
|
# ==============================
|
||||||
|
# 👉 IntelliJ / WebStorm
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
out/
|
||||||
|
|
||||||
|
# 👉 VSCode (ถ้าใช้ร่วม)
|
||||||
|
.vscode/
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🖼 macOS / OS-specific
|
||||||
|
# ==============================
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🐳 Docker (ถ้าใช้)
|
||||||
|
# ==============================
|
||||||
|
docker-compose.override.yml
|
||||||
|
*.pid
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🧪 Testing (ถ้าใช้)
|
||||||
|
# ==============================
|
||||||
|
coverage/
|
||||||
|
*.tsbuildinfo
|
||||||
|
*.test.*
|
||||||
|
*.spec.*
|
||||||
|
playwright-report/
|
||||||
|
cypress/videos/
|
||||||
|
cypress/screenshots/
|
||||||
BIN
Picture1.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
Picture2.png
Normal file
|
After Width: | Height: | Size: 316 KiB |
81
README.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# 🚀 เทมเพลตเว็บไซต์องค์กร (Organizational Website Template)
|
||||||
|
|
||||||
|
> **เว็บไซต์องค์กร**
|
||||||
|
> เว็บไซต์ข่าวสารและประชาสัมพันธ์ขององค์กร นำเสนอข้อมูลอัปเดตเกี่ยวกับกิจกรรม นวัตกรรม และเรื่องราวดี ๆ ที่เราอยากบอกต่อ ทั้งในภาษาไทยและภาษาอังกฤษ
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ คุณสมบัติหลัก
|
||||||
|
|
||||||
|
- 🔖 **ข่าวสารตามหมวดหมู่** เช่น ข่าวประชาสัมพันธ์, ข่าวบริการประชาชน, ข่าวนวัตกรรม
|
||||||
|
- 🌟 **ข่าวเด่น (Feature Story)**: เรื่องราวดี ๆ ที่น่าสนใจและสร้างแรงบันดาลใจ
|
||||||
|
- 🌐 **รองรับหลายภาษา**: ภาษาไทย และภาษาอังกฤษ
|
||||||
|
- 📰 **หน้าแสดงข่าวรายละเอียด**: พร้อมรูปภาพและเนื้อหาฉบับเต็ม
|
||||||
|
- 📱 **Responsive Design**: แสดงผลได้ทุกอุปกรณ์ (มือถือ แท็บเล็ต คอมพิวเตอร์)
|
||||||
|
- ⚙️ **จัดการสถานะด้วย Pinia Store**
|
||||||
|
- 📦 **Component-based**: ใช้โครงสร้างที่จัดระเบียบดี พร้อมนำกลับมาใช้ซ้ำ
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ เทคโนโลยีที่ใช้
|
||||||
|
|
||||||
|
| เทคโนโลยี | รายละเอียด |
|
||||||
|
|-----------|-------------|
|
||||||
|
| [Vue 3](https://vuejs.org/) | JavaScript Framework |
|
||||||
|
| [Vite](https://vitejs.dev/) | Frontend Build Tool ที่รวดเร็ว |
|
||||||
|
| [Pinia](https://pinia.vuejs.org/) | State Management |
|
||||||
|
| [Vue Router](https://router.vuejs.org/) | Routing สำหรับ SPA |
|
||||||
|
| [Tailwind CSS](https://tailwindcss.com/) | Utility-first CSS Framework |
|
||||||
|
| JavaScript (ESNext) | ใช้ Feature ใหม่ของ ECMAScript |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 วิธีติดตั้งและใช้งาน
|
||||||
|
|
||||||
|
### 🧩 ข้อกำหนดเบื้องต้น
|
||||||
|
|
||||||
|
- ติดตั้ง [Node.js](https://nodejs.org/) (แนะนำเวอร์ชัน LTS)
|
||||||
|
- ติดตั้ง npm หรือ yarn
|
||||||
|
|
||||||
|
### 📥 ติดตั้งโปรเจกต์
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-username/your-repo-name.git
|
||||||
|
cd your-repo-name
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### ▶️ เริ่มต้น Development Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 โครงสร้างโปรเจกต์
|
||||||
|
|
||||||
|
```bash
|
||||||
|
project/
|
||||||
|
├── public/ # ไฟล์ static (เช่น logo, favicon)
|
||||||
|
├── src/
|
||||||
|
│ ├── assets/ # ไฟล์ภาพ/ไอคอน/ฟอนต์
|
||||||
|
│ ├── components/ # Vue components เช่น NewsItem, Tabs
|
||||||
|
│ ├── router/ # การตั้งค่า Routing (Vue Router)
|
||||||
|
│ ├── stores/ # Pinia stores (จัดการสถานะ)
|
||||||
|
│ ├── views/ # หน้าแสดงผล เช่น HomeView, NewsView
|
||||||
|
│ └── main.js # Entry Point ของแอป
|
||||||
|
├── vite.config.js # การตั้งค่า Vite
|
||||||
|
├── .gitignore # ไฟล์ที่ Git จะไม่ติดตาม
|
||||||
|
└── README.md # ไฟล์คำอธิบายโปรเจกต์นี้
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
> `MIT License` © 2025 softwarecraft.tech
|
||||||
|
|
||||||
|
## **การมีส่วนร่วม (Contributing)**
|
||||||
|
```markdown
|
||||||
|
## 🤝 การมีส่วนร่วม
|
||||||
|
|
||||||
|
ยินดีต้อนรับทุกการมีส่วนร่วม 🎉
|
||||||
|
หากคุณพบปัญหา, มีข้อเสนอแนะ หรืออยากพัฒนาเพิ่ม
|
||||||
|
สามารถแจ้งได้ที่ admin@softwarecraft.tech ได้เลยครับ!
|
||||||
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>HumanTech</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2329
package-lock.json
generated
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "vue_web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"axios": "^1.10.0",
|
||||||
|
"daisyui": "^5.0.43",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
"vue": "^3.5.17",
|
||||||
|
"vue-router": "^4.5.1",
|
||||||
|
"vue3-carousel": "^0.16.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
|
"vite": "^7.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/icon.svg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/images/Enter.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/images/MainBanner.jpg
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
public/images/moroccan-flower.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/uploads/airlift_patient.jpg
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
public/uploads/document_2.pdf
Normal file
BIN
public/uploads/main_banner_1.jpg
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
public/uploads/main_banner_2.jpg
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
public/uploads/main_banner_3.jpg
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
public/uploads/main_banner_4.jpg
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
public/uploads/news_1.jpg
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
public/uploads/news_2.jpg
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
public/uploads/news_3.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
public/uploads/news_4.jpg
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
public/uploads/news_5.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
public/uploads/news_6.jpg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
public/uploads/slider_banner_1.jpg
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
public/uploads/slider_banner_2.jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
public/uploads/slider_banner_3.jpg
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
public/uploads/slider_banner_4.jpg
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
public/uploads/slider_banner_5.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
public/uploads/slider_banner_6.jpg
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
public/uploads/weblink-1.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/uploads/weblink-2.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/uploads/weblink-3.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/uploads/weblink-4.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
public/uploads/weblink-5.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/uploads/weblink-6.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
38
src/App.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// App.vue
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// ตรวจสอบธีมที่บันทึกไว้ใน localStorage (ถ้ามี) หรือใช้ 'light' เป็นค่าเริ่มต้น
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
|
|
||||||
|
const themeController = document.querySelector('.theme-controller');
|
||||||
|
if (themeController) {
|
||||||
|
// ตั้งค่า toggle ให้ตรงกับธีมที่โหลดมา
|
||||||
|
themeController.checked = (savedTheme === themeController.value);
|
||||||
|
|
||||||
|
themeController.addEventListener('change', (e) => {
|
||||||
|
const newTheme = e.target.checked ? e.target.value : 'light'; // ใช้ 'light' เป็น default เมื่อ toggle ปิด
|
||||||
|
document.documentElement.setAttribute('data-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme); // บันทึกธีมลง localStorage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* สไตล์ที่นี่จะเป็น Global CSS ที่มีผลต่อทั้งแอปพลิเคชัน
|
||||||
|
ควรจะย้ายไปที่นี่ หรือใน src/assets/main.css
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
6
src/assets/main.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@import url("https://fonts.googleapis.com/css2?family=Noto%20Sans%20Thai:wght@400;500;600;700;800;900&display=swap");
|
||||||
|
@import 'vue3-carousel/dist/carousel.css';
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans Thai', sans-serif;
|
||||||
|
}
|
||||||
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
57
src/components/BannerCalousels.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// src/components/BannerCalousels.vue
|
||||||
|
<template>
|
||||||
|
<div class="w-full overflow-hidden p-4">
|
||||||
|
<!-- หัวข้อด้านบน -->
|
||||||
|
<div class="mb-4 px-2 md:px-4">
|
||||||
|
<h4
|
||||||
|
class="inline-block text-white text-2xl md:text-3xl font-semibold px-6 py-2 bg-[#1b3872] rounded shadow-md"
|
||||||
|
>
|
||||||
|
{{ appStore.checkLang.isTh ? 'โอกาสทางอาชีพ / แบนเนอร์ประชาสัมพันธ์' : 'Career Opportunities / Public Relations Banners' }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Carousel -->
|
||||||
|
<Carousel
|
||||||
|
:items-to-show="4"
|
||||||
|
:wrap-around="true"
|
||||||
|
:autoplay="5000"
|
||||||
|
:pause-autoplay-on-hover="true"
|
||||||
|
:navigation-enabled="false"
|
||||||
|
:pagination-enabled="false"
|
||||||
|
class="w-full h-32 md:h-40"
|
||||||
|
>
|
||||||
|
<Slide v-for="(image, key) in images" :key="key">
|
||||||
|
<a
|
||||||
|
:href="image.link ? image.link : '#'"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="block w-full h-full"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="`${appStore.imageBaseUrl}${image.image.url}`"
|
||||||
|
alt="Slider Banner"
|
||||||
|
class="w-full h-full object-cover rounded-md shadow-sm"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</Slide>
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { Carousel, Slide } from 'vue3-carousel';
|
||||||
|
// อย่าลืม import 'vue3-carousel/dist/carousel.css'; หากยังไม่ได้ import ในไฟล์หลัก (main.js/ts)
|
||||||
|
import { useAppStore } from '@/stores/app';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const images = ref([]);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
images.value = await appStore.find('slider-banners');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
85
src/components/Banners.vue
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// src/components/Banners.vue
|
||||||
|
<template>
|
||||||
|
<div class="p-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div class="col-span-1 md:col-span-1">
|
||||||
|
<div v-for="(image, key) in feeds" :key="key" class="mb-4">
|
||||||
|
<a :href="image.link" target="_blank" rel="noopener noreferrer" class="block">
|
||||||
|
<img
|
||||||
|
:src="`${appStore.imageBaseUrl}${image.image.url}`"
|
||||||
|
alt="Feed Banner"
|
||||||
|
class="w-full object-cover"
|
||||||
|
:style="{ height: feedsHeightStyle }"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-1 md:col-span-2">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div v-for="(image, key) in fixed" :key="key" class="p-0">
|
||||||
|
<div v-if="image.link && image.link.startsWith('/')">
|
||||||
|
<router-link :to="image.link" class="block">
|
||||||
|
<img
|
||||||
|
:src="`${appStore.imageBaseUrl}${image.image.url}`"
|
||||||
|
alt="Fixed Banner"
|
||||||
|
class="w-full object-cover h-48"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<a :href="image.link" target="_blank" rel="noopener noreferrer" class="block">
|
||||||
|
<img
|
||||||
|
:src="`${appStore.imageBaseUrl}${image.image.url}`"
|
||||||
|
alt="Fixed Banner"
|
||||||
|
class="w-full object-cover h-48"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue';
|
||||||
|
import { useAppStore } from '@/stores/app';
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const fixed = ref([]);
|
||||||
|
const feeds = ref([]);
|
||||||
|
const fixedHeight = ref(188); // in px, as in original code
|
||||||
|
const feedsHeight = ref(188); // in px
|
||||||
|
|
||||||
|
const feedsHeightStyle = computed(() => {
|
||||||
|
return `${feedsHeight.value}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// ดึงข้อมูล feeds (carousel type)
|
||||||
|
const fetchedFeeds = await appStore.find('bottom-banners', 'banner_type=calousel&_limit=1');
|
||||||
|
feeds.value = fetchedFeeds;
|
||||||
|
|
||||||
|
// ดึงข้อมูล fixed banners
|
||||||
|
const fetchedFixed = await appStore.find('bottom-banners', 'banner_type=fixed&_limit=6');
|
||||||
|
fixed.value = fetchedFixed;
|
||||||
|
|
||||||
|
// คำนวณ feedsHeight ตาม fixed banners
|
||||||
|
const fixedNumber = fixed.value.length / 3; // Original logic
|
||||||
|
if (fixedNumber <= 1) {
|
||||||
|
feedsHeight.value = fixedHeight.value;
|
||||||
|
} else if (fixedNumber > 1 && fixedNumber <= 2) {
|
||||||
|
feedsHeight.value = fixedHeight.value * 2;
|
||||||
|
} else if (fixedNumber > 2 && fixedNumber <= 3) {
|
||||||
|
feedsHeight.value = fixedHeight.value * 3;
|
||||||
|
} else if (fixedNumber > 3 && fixedNumber <= 4) {
|
||||||
|
feedsHeight.value = fixedHeight.value * 4;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* No specific styles needed, using Tailwind classes */
|
||||||
|
</style>
|
||||||
135
src/components/Calousels.vue
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// src/components/Calousels.vue
|
||||||
|
<template>
|
||||||
|
<div class="relative w-full overflow-hidden">
|
||||||
|
<Carousel
|
||||||
|
:autoplay="5000"
|
||||||
|
:wrap-around="true"
|
||||||
|
:pause-autoplay-on-hover="true"
|
||||||
|
:navigation-enabled="true"
|
||||||
|
:pagination-enabled="true"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<Slide v-for="(item, i) in items" :key="i">
|
||||||
|
<a
|
||||||
|
v-if="item.type === 'image'"
|
||||||
|
:href="getImageLink(item)"
|
||||||
|
:target="item.link || item.File ? '_blank' : '_self'"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="block w-full carousel__slide-content"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="`${appStore.imageBaseUrl}${item.image?.url}`"
|
||||||
|
alt="Carousel Image"
|
||||||
|
class="w-full h-auto object-cover max-h-[600px] sm:max-h-[480px] md:max-h-[550px] lg:max-h-[600px] aspect-video"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div v-else-if="item.type === 'video' && item.video_url" class="w-full h-full carousel__slide-content">
|
||||||
|
<iframe
|
||||||
|
:src="getEmbedUrl(item.video_url)"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen
|
||||||
|
class="w-full h-full object-cover max-h-[600px] sm:max-h-[480px] md:max-h-[550px] lg:max-h-[600px] aspect-video"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="w-full h-[300px] bg-gray-200 flex items-center justify-center text-gray-500">
|
||||||
|
Content not available
|
||||||
|
</div>
|
||||||
|
</Slide>
|
||||||
|
|
||||||
|
<template #addons>
|
||||||
|
<Navigation />
|
||||||
|
<Pagination />
|
||||||
|
</template>
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from 'vue'; // เพิ่ม watch
|
||||||
|
import { Carousel, Slide, Pagination, Navigation } from 'vue3-carousel';
|
||||||
|
import { useAppStore } from '@/stores/app';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const items = ref([]);
|
||||||
|
|
||||||
|
const getImageLink = (item) => {
|
||||||
|
return item.File?.url ? `${appStore.imageBaseUrl}${item.File.url}` : item.link;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEmbedUrl = (url) => {
|
||||||
|
if (!url) return '';
|
||||||
|
|
||||||
|
const youtubeMatch = url.match(/(?:https?:\/\/)?(?:www\.)?(?:m\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|v\/|)([\w-]{11})(?:\S+)?/);
|
||||||
|
if (youtubeMatch && youtubeMatch[1]) {
|
||||||
|
return `https://www.youtube.com/embed/${youtubeMatch[1]}?autoplay=1&mute=1&controls=1&showinfo=0&rel=0`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vimeoMatch = url.match(/(?:https?:\/\/)?(?:www\.)?(?:player\.)?vimeo\.com\/(?:video\/)?(\d+)(?:\S+)?/);
|
||||||
|
if (vimeoMatch && vimeoMatch[1]) {
|
||||||
|
return `https://player.vimeo.com/video/${vimeoMatch[1]}?autoplay=1&muted=1`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- New function to fetch items ---
|
||||||
|
const fetchItems = async () => {
|
||||||
|
items.value = await appStore.find('calousels');
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Watch for language changes ---
|
||||||
|
watch(
|
||||||
|
() => appStore.checkLang.isTh, // สิ่งที่เราต้องการ watch คือสถานะภาษา
|
||||||
|
async (newLangStatus, oldLangStatus) => {
|
||||||
|
// ตรวจสอบว่าภาษามีการเปลี่ยนแปลงจริงหรือไม่
|
||||||
|
if (newLangStatus !== oldLangStatus) {
|
||||||
|
console.log('Language changed! Fetching new carousel items...');
|
||||||
|
await fetchItems(); // เรียก fetchItems อีกครั้ง
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true } // ให้เรียก fetchItems() ทันทีเมื่อ component ถูก mount ครั้งแรกด้วย
|
||||||
|
);
|
||||||
|
|
||||||
|
// ลบ onMounted hook เดิมออก ไม่จำเป็นต้องเรียก fetchItems() ซ้ำแล้ว
|
||||||
|
// onMounted(async () => {
|
||||||
|
// items.value = await appStore.find('calousels');
|
||||||
|
// });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Optional: Adjust max-height and ensure aspect ratio for video */
|
||||||
|
.carousel__slide-content {
|
||||||
|
display: flex; /* Helps to center content if not full width/height */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%; /* Ensure content fills the slide height */
|
||||||
|
overflow: hidden; /* Prevent overflow if aspect-ratio doesn't fit exactly */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For aspect-ratio if not supported by browser use padding-bottom trick */
|
||||||
|
.aspect-video {
|
||||||
|
aspect-ratio: 16 / 9; /* Modern way to set aspect ratio */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for older browsers if aspect-ratio is not fully supported */
|
||||||
|
/*
|
||||||
|
.video-responsive-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 56.25%; // 16:9 aspect ratio
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.video-responsive-container iframe,
|
||||||
|
.video-responsive-container video {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
</style>
|
||||||
54
src/components/Footer.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// src/components/Footer.vue
|
||||||
|
<template>
|
||||||
|
<footer class="bg-gray-700 text-white py-4 md:py-8">
|
||||||
|
<div class="container mx-auto text-center">
|
||||||
|
|
||||||
|
<p class="text-sm mb-4">
|
||||||
|
{{appStore.checkLang.isTh ? more : more_en}}
|
||||||
|
</p>
|
||||||
|
<div class="text-xs space-x-2">
|
||||||
|
<template v-for="(menu, key) in appStore.allFooterMenus" :key="key">
|
||||||
|
<span v-if="appStore.checkLang.isTh ? menu.active : menu.active_en">
|
||||||
|
<span v-if="key > 0">|</span> <a
|
||||||
|
v-if="menu.File && menu.File.url"
|
||||||
|
:href="`${appStore.imageBaseUrl}${menu.File.url}`"
|
||||||
|
target="_blank"
|
||||||
|
class="text-white hover:underline"
|
||||||
|
>
|
||||||
|
{{ appStore.checkLang.isTh ? menu.title : menu.title_en }}
|
||||||
|
</a>
|
||||||
|
<router-link
|
||||||
|
v-else-if="menu.link"
|
||||||
|
:to="menu.link"
|
||||||
|
class="text-white hover:underline"
|
||||||
|
>
|
||||||
|
{{ appStore.checkLang.isTh ? menu.title : menu.title_en }}
|
||||||
|
</router-link>
|
||||||
|
<span v-else>
|
||||||
|
{{ appStore.checkLang.isTh ? menu.title : menu.title_en }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|<br />
|
||||||
|
[ HumanTech Call Center: 0-1234-5678 ]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useAppStore } from '@/stores/app';
|
||||||
|
import { RouterLink } from 'vue-router'; // Import RouterLink for internal links
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const title = ref("ฮิวแมนเทคไทยแลนด์");
|
||||||
|
const title_en = ref("HumanTech");
|
||||||
|
const more = ref("ดำเนินการโดย ฮิวแมนเทคไทยแลนด์ แขวงสายไหม เขตสายไหม กรุงเทพฯ 10220 สงวนลิขสิทธิ พ.ศ.2564 ตามพระราชบัญญัติลิขสิทธิ์ พ.ศ.2537");
|
||||||
|
const more_en = ref("This website is administered by the HumanTech, Sai Mai, Bangkok 10220 Copyright ©2025 HumanTech all rights reserved.");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* สไตล์เฉพาะ Footer หากไม่ถูกครอบคลุมโดย Tailwind */
|
||||||
|
</style>
|
||||||
178
src/components/Header.vue
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
// src/components/Header.vue
|
||||||
|
<template>
|
||||||
|
<header
|
||||||
|
v-if="appStore.headerData"
|
||||||
|
:style="`background-image: url('${appStore.imageBaseUrl}${appStore.headerData.header_background.url}'); background-size: cover; background-position: center;`"
|
||||||
|
class="relative z-10"
|
||||||
|
>
|
||||||
|
<nav class="navbar p-2 md:p-4 shadow-md" :style="{ backgroundColor: appStore.headerData.mainColor }">
|
||||||
|
<div class="container mx-auto flex items-center justify-between w-full">
|
||||||
|
|
||||||
|
<div class="navbar-start w-auto flex-shrink-0">
|
||||||
|
<router-link to="/home" class="flex flex-col items-start leading-tight">
|
||||||
|
<span class="text-white text-lg font-bold">{{ appStore.checkLang.isTh ? 'ฮิวแมนเทคไทยแลนด์' : 'HumanTech' }}</span>
|
||||||
|
<span class="text-white text-xs opacity-80">{{ appStore.checkLang.isTh ? 'HumanTech' : 'ฮิวแมนเทคไทยแลนด์' }}</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-end hidden md:flex flex-1 items-center justify-end space-x-2 lg:space-x-4">
|
||||||
|
<ul class="menu menu-horizontal px-1 flex-nowrap">
|
||||||
|
<li v-for="menu in orderedMainMenus" :key="menu.id">
|
||||||
|
<template v-if="menu.sub_menus && menu.sub_menus.length > 0">
|
||||||
|
<details :open="activeMenuId === menu.id" @toggle="handleDetailsToggle(menu.id, $event)">
|
||||||
|
<summary class="text-white font-semibold text-base hover:bg-gray-700 hover:text-white">
|
||||||
|
{{ appStore.checkLang.isTh ? menu.main_menu : menu.main_menu_en }}
|
||||||
|
</summary>
|
||||||
|
<ul class="p-2 bg-base-100 rounded-box w-52 text-black">
|
||||||
|
<li v-for="subMenu in (appStore.checkLang.isTh ? orderedSubMenus(menu.sub_menus) : orderedSubMenusEng(menu.sub_menus))" :key="subMenu.id">
|
||||||
|
<router-link :to="subMenu.link" class="hover:bg-gray-200 hover:text-black">
|
||||||
|
{{ appStore.checkLang.isTh ? subMenu.title_th : subMenu.title_en }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<router-link
|
||||||
|
:to="menu.link"
|
||||||
|
class="text-white font-semibold text-base hover:bg-gray-700 hover:text-white"
|
||||||
|
@click="activeMenuId = null" >
|
||||||
|
{{ appStore.checkLang.isTh ? menu.main_menu : menu.main_menu_en }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="dropdown dropdown-end">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-ghost normal-case text-base text-white hover:bg-gray-700 hover:text-white">
|
||||||
|
{{ appStore.checkLang.isTh ? 'ภาษาไทย' : 'English' }}
|
||||||
|
</div>
|
||||||
|
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-30 text-black">
|
||||||
|
<li><a @click="switchLanguageToThai" class="hover:bg-gray-200 hover:text-black">ภาษาไทย</a></li>
|
||||||
|
<li><a @click="switchLanguageToEnglish" class="hover:bg-gray-200 hover:text-black">English</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-end md:hidden">
|
||||||
|
<div class="dropdown dropdown-end">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden text-white hover:bg-gray-700 hover:text-white">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /></svg>
|
||||||
|
</div>
|
||||||
|
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52 text-black">
|
||||||
|
<li v-for="menu in orderedMainMenus" :key="menu.id">
|
||||||
|
<template v-if="menu.sub_menus && menu.sub_menus.length > 0">
|
||||||
|
<details :open="activeMobileMenuId === menu.id" @toggle="handleMobileDetailsToggle(menu.id, $event)">
|
||||||
|
<summary class="hover:bg-gray-200 hover:text-black">{{ appStore.checkLang.isTh ? menu.main_menu : menu.main_menu_en }}</summary>
|
||||||
|
<ul class="p-2 bg-base-200">
|
||||||
|
<li v-for="subMenu in (appStore.checkLang.isTh ? orderedSubMenus(menu.sub_menus) : orderedSubMenusEng(menu.sub_menus))" :key="subMenu.id">
|
||||||
|
<router-link :to="subMenu.link" class="hover:bg-gray-200 hover:text-black">{{ appStore.checkLang.isTh ? subMenu.title_th : subMenu.title_en }}</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<router-link :to="menu.link" class="hover:bg-gray-200 hover:text-black">
|
||||||
|
{{ appStore.checkLang.isTh ? menu.main_menu : menu.main_menu_en }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<details :open="activeMobileMenuId === 'language'" @toggle="handleMobileDetailsToggle('language', $event)">
|
||||||
|
<summary class="hover:bg-gray-200 hover:text-black">{{ appStore.checkLang.isTh ? 'ภาษา' : 'Language' }}</summary>
|
||||||
|
<ul class="p-2 bg-base-200">
|
||||||
|
<li><a @click="switchLanguageToThai" class="hover:bg-gray-200 hover:text-black">ภาษาไทย</a></li>
|
||||||
|
<li><a @click="switchLanguageToEnglish" class="hover:bg-gray-200 hover:text-black">English</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useAppStore } from '@/stores/app';
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
// --- State to manage open menus ---
|
||||||
|
const activeMenuId = ref(null); // For desktop dropdowns
|
||||||
|
const activeMobileMenuId = ref(null); // For mobile dropdowns
|
||||||
|
|
||||||
|
// --- Handlers for desktop details toggles ---
|
||||||
|
const handleDetailsToggle = (menuId, event) => {
|
||||||
|
if (event.target.open) {
|
||||||
|
// If this details is being opened, set it as the active one
|
||||||
|
activeMenuId.value = menuId;
|
||||||
|
} else if (activeMenuId.value === menuId) {
|
||||||
|
// If this details is being closed, clear the active one
|
||||||
|
activeMenuId.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Handlers for mobile details toggles ---
|
||||||
|
const handleMobileDetailsToggle = (menuId, event) => {
|
||||||
|
if (event.target.open) {
|
||||||
|
activeMobileMenuId.value = menuId;
|
||||||
|
} else if (activeMobileMenuId.value === menuId) {
|
||||||
|
activeMobileMenuId.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Existing logic for menu ordering and language switching ---
|
||||||
|
const orderedMainMenus = computed(() => {
|
||||||
|
if (!appStore.allMainMenus) return [];
|
||||||
|
const activeMenus = appStore.allMainMenus.filter(menu =>
|
||||||
|
appStore.checkLang.isTh ? menu.active : menu.active_en
|
||||||
|
);
|
||||||
|
const homeMenu = activeMenus.find(menu => menu.id === 1);
|
||||||
|
const otherMenus = activeMenus.filter(menu => menu.id !== 1);
|
||||||
|
return homeMenu ? [homeMenu, ..._.orderBy(otherMenus, 'order')] : _.orderBy(activeMenus, 'order');
|
||||||
|
});
|
||||||
|
|
||||||
|
const orderedSubMenus = (subMenus) => {
|
||||||
|
if (!subMenus) return [];
|
||||||
|
const activeItems = subMenus.filter(f => f.active === true);
|
||||||
|
return _.orderBy(activeItems, 'order');
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderedSubMenusEng = (subMenus) => {
|
||||||
|
if (!subMenus) return [];
|
||||||
|
const activeItems = subMenus.filter(f => f.active_en === true);
|
||||||
|
return _.orderBy(activeItems, 'order');
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchLanguageToThai = () => {
|
||||||
|
appStore.toggleLanguage();
|
||||||
|
// Close mobile language dropdown after selection
|
||||||
|
activeMobileMenuId.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchLanguageToEnglish = () => {
|
||||||
|
appStore.toggleLanguage();
|
||||||
|
// Close mobile language dropdown after selection
|
||||||
|
activeMobileMenuId.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const savedLang = localStorage.getItem('lang');
|
||||||
|
if (savedLang === 'en') {
|
||||||
|
appStore.isTh = false;
|
||||||
|
} else {
|
||||||
|
appStore.isTh = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* No specific styles needed, using Tailwind/DaisyUI as before */
|
||||||
|
</style>
|
||||||
43
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
msg: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>{{ msg }}</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<button type="button" @click="count++">count is {{ count }}</button>
|
||||||
|
<p>
|
||||||
|
Edit
|
||||||
|
<code>components/HelloWorld.vue</code> to test HMR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Check out
|
||||||
|
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||||
|
>create-vue</a
|
||||||
|
>, the official Vue + Vite starter
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Learn more about IDE Support for Vue in the
|
||||||
|
<a
|
||||||
|
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||||
|
target="_blank"
|
||||||
|
>Vue Docs Scaling up Guide</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
103
src/components/News.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// src/components/News.vue
|
||||||
|
<template>
|
||||||
|
<div class="mt-6">
|
||||||
|
<div
|
||||||
|
class="p-4 bg-cover bg-center"
|
||||||
|
:style="`background-image: url('${appStore.imageBaseUrl}${appStore.headers.header_background.url}')`"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
class="inline-block text-white text-2xl md:text-3xl font-semibold px-6 py-2 bg-[#1b3872] rounded shadow-md"
|
||||||
|
>
|
||||||
|
{{ appStore.checkLang.isTh ? 'เรื่องราวดี ๆ ที่เราอยากบอกต่อ' : 'Our Inspiring Stories' }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="p-6" :style="`background-color:${appStore.headers.bgColor || '#ffffff'}`">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 ">
|
||||||
|
|
||||||
|
<div v-if="filteredFeatureNews.length > 0" class="flex flex-col">
|
||||||
|
<NewsItem :item="filteredFeatureNews[0]" :isFeature="true" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="h-full">
|
||||||
|
<div class="bg-gray-200 flex items-center justify-center rounded-lg text-gray-500 h-full">
|
||||||
|
{{ appStore.checkLang.isTh ? 'ไม่พบข่าวเด่น' : 'No feature news found' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(item, key) in filteredNews"
|
||||||
|
:key="key"
|
||||||
|
class="flex flex-col"
|
||||||
|
>
|
||||||
|
<NewsItem :item="item" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="md:col-span-2 flex justify-end mt-8">
|
||||||
|
<router-link
|
||||||
|
to="/news/HumanTechNews" class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md"
|
||||||
|
>
|
||||||
|
{{ appStore.checkLang.isTh ? more : more_en }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue';
|
||||||
|
// ไม่ต้อง import Carousel, Slide แล้ว
|
||||||
|
import NewsItem from './NewsItem.vue'; // Component ย่อย
|
||||||
|
import { useAppStore } from '@/stores/app';
|
||||||
|
import { RouterLink } from 'vue-router'; // Import RouterLink
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const news = ref([]);
|
||||||
|
const featureNews = ref([]); // ยังคงแยกเพื่อการ filter ที่ชัดเจน
|
||||||
|
const more = "อ่านข่าวต่อ";
|
||||||
|
const more_en = "Read More";
|
||||||
|
|
||||||
|
const filterNewsByDate = (items) => {
|
||||||
|
const today = new Date();
|
||||||
|
const currentDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); // Normalize to start of day
|
||||||
|
|
||||||
|
return items.filter(item => {
|
||||||
|
if (!item.release_date) return false;
|
||||||
|
const [month, day, year] = item.release_date.split('/');
|
||||||
|
const releaseDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||||
|
|
||||||
|
return releaseDate <= currentDate;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredFeatureNews = computed(() => {
|
||||||
|
const langFiltered = appStore.checkLang.isTh
|
||||||
|
? featureNews.value.filter(item => item.active && item.feature)
|
||||||
|
: featureNews.value.filter(item => item.active_en && item.feature);
|
||||||
|
// **สำคัญ:** จำกัดให้เหลือแค่ 1 ข่าวเด่นที่จะแสดง
|
||||||
|
return filterNewsByDate(langFiltered).slice(0, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredNews = computed(() => {
|
||||||
|
const langFiltered = appStore.checkLang.isTh
|
||||||
|
? news.value.filter(item => item.active && !item.feature)
|
||||||
|
: news.value.filter(item => item.active_en && !item.feature);
|
||||||
|
// Limit to 4 items as per original logic
|
||||||
|
return filterNewsByDate(langFiltered).slice(0, 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const allNews = await appStore.find('news', '');
|
||||||
|
news.value = allNews;
|
||||||
|
featureNews.value = allNews; // featureNews ก็ยังใช้ allNews เหมือนเดิม
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
86
src/components/NewsItem.vue
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// src/components/NewsItem.vue
|
||||||
|
<template>
|
||||||
|
<div class="bg-base-100 shadow-md rounded-lg overflow-hidden flex flex-col h-full hover:shadow-lg hover:-translate-y-1">
|
||||||
|
<div
|
||||||
|
class="w-full bg-gray-200 relative "
|
||||||
|
:class="{
|
||||||
|
'h-[266px]': isFeature, // คำนวณใหม่: (516px (total) - padding-top/bottom - content_height) / image_ratio
|
||||||
|
'h-[120px]': !isFeature // คำนวณใหม่: (250px (total) - padding-top/bottom - content_height) / image_ratio
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="`${appStore.imageBaseUrl}${item.image?.url ?? ''}`"
|
||||||
|
alt="News Thumbnail"
|
||||||
|
class="absolute inset-0 w-full h-full object-cover"
|
||||||
|
@error="e => e.target.style.display = 'none'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 flex flex-col flex-grow">
|
||||||
|
<router-link
|
||||||
|
:to="`/news-content/${item.id}`"
|
||||||
|
class="font-semibold text-gray-800 hover:underline mb-2"
|
||||||
|
:class="{
|
||||||
|
'text-xl md:text-2xl line-clamp-2': isFeature,
|
||||||
|
'text-lg line-clamp-2': !isFeature
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ appStore.checkLang.isTh ? item.title_th : item.title_en || 'No Title' }}
|
||||||
|
</router-link>
|
||||||
|
<p
|
||||||
|
class="text-sm text-gray-600 mb-3 flex-grow"
|
||||||
|
:class="{
|
||||||
|
'line-clamp-4': isFeature,
|
||||||
|
'line-clamp-3': !isFeature
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ truncateDetail(appStore.checkLang.isTh ? item.detail_th : item.detail_en || '', isFeature ? 200 : 120) }}
|
||||||
|
</p>
|
||||||
|
<div class="text-xs text-gray-400 mt-auto">
|
||||||
|
<span v-if="item.release_date">{{ formatDate(item.release_date) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
import { useAppStore } from '@/stores/app';
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isFeature: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const truncateDetail = (text, maxLength) => {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.length <= maxLength ? text : text.substring(0, maxLength) + '...';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
const parts = dateString.split('/');
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* No specific styles needed, using Tailwind classes */
|
||||||
|
</style>
|
||||||
158
src/components/TabNews.vue
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
// src/components/TabNews.vue
|
||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<!-- หัวข้อข่าว -->
|
||||||
|
<div class="mb-6 px-2 md:px-4">
|
||||||
|
<h4
|
||||||
|
class="inline-block text-white text-2xl md:text-3xl font-semibold px-6 py-2 bg-[#1b3872] rounded shadow-md"
|
||||||
|
>
|
||||||
|
{{ appStore.checkLang.isTh ? 'ข่าวสารอัพเดต' : 'Latest Updates' }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div role="tablist" class="flex flex-wrap space-x-2 border-b border-gray-300">
|
||||||
|
<button
|
||||||
|
v-for="(tab, index) in tabs"
|
||||||
|
:key="tab.category"
|
||||||
|
class="px-4 py-2 text-sm font-medium transition-all duration-200 border-b-2 whitespace-nowrap"
|
||||||
|
:class="{
|
||||||
|
'border-blue-700 text-blue-700': activeTab === tab.category,
|
||||||
|
'border-transparent text-gray-500 hover:text-blue-700 hover:border-blue-500': activeTab !== tab.category
|
||||||
|
}"
|
||||||
|
@click="changeTab(tab.category)"
|
||||||
|
>
|
||||||
|
{{ appStore.checkLang.isTh ? tab.title : tab.title_en }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<div
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.category"
|
||||||
|
v-show="activeTab === tab.category"
|
||||||
|
>
|
||||||
|
<div v-if="newsByTab[tab.category]?.data?.length > 0">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in newsByTab[tab.category].data"
|
||||||
|
:key="item.id"
|
||||||
|
class="card card-compact bg-base-100 shadow-md rounded-md overflow-hidden
|
||||||
|
transition-transform duration-200 hover:shadow-lg hover:-translate-y-1
|
||||||
|
flex flex-col md:flex-row"
|
||||||
|
>
|
||||||
|
<div class="crop w-full md:w-36 h-36 md:h-auto flex-shrink-0">
|
||||||
|
<img
|
||||||
|
:src="`${appStore.imageBaseUrl}${item.image?.url ?? ''}`"
|
||||||
|
alt="News Thumbnail"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
@error="e => e.target.style.display = 'none'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body p-4 flex flex-col justify-between flex-1">
|
||||||
|
<router-link
|
||||||
|
:to="`/content/${item.id}`"
|
||||||
|
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' }}
|
||||||
|
</router-link>
|
||||||
|
<p class="text-sm text-gray-600 line-clamp-2 flex-grow">
|
||||||
|
{{ truncateDetail(appStore.checkLang.isTh ? item.detail_th : item.detail_en || '', 80) }}
|
||||||
|
</p>
|
||||||
|
<div class="text-xs text-gray-400 mt-2">
|
||||||
|
<span v-if="item.release_date">{{ formatDate(item.release_date) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="text-center text-gray-500 py-8">
|
||||||
|
{{ appStore.checkLang.isTh ? 'ไม่พบข่าวสารในหมวดหมู่นี้' : 'No news found in this category' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-6">
|
||||||
|
<router-link
|
||||||
|
v-if="newsByTab[tab.category]?.total > 6"
|
||||||
|
:to="`/news/${tab.category}`"
|
||||||
|
class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md"
|
||||||
|
>
|
||||||
|
{{ appStore.checkLang.isTh ? more : more_en }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useAppStore } from '@/stores/app';
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const tabs = ref([
|
||||||
|
{ title: "ข่าวประชาสัมพันธ์หน่วยงาน", title_en: "RTAF News", category: "HumanTechNews" },
|
||||||
|
{ title: "ข่าวประชาสัมพันธ์ภายใน", title_en: "RTAF Organization News", category: "OrgNews" },
|
||||||
|
{ title: "ข่าวนวัตกรรม", title_en: "RTAF Service News", category: "InnovationNews" },
|
||||||
|
{ title: "Press Release", title_en: "RTAF Press Release", category: "GeneralPublic" },
|
||||||
|
{ title: "ข่าวบริการประชาชน", title_en: "Public Service News", category: "EventActivities" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const activeTab = ref(tabs.value[0].category);
|
||||||
|
const newsByTab = ref({});
|
||||||
|
const more = "อ่านข่าวต่อ";
|
||||||
|
const more_en = "Read More";
|
||||||
|
|
||||||
|
const truncateDetail = (text, maxLength) => {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.length <= maxLength ? text : text.substring(0, maxLength) + '...';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
const parts = dateString.split('/');
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchNewsForTab = async (category) => {
|
||||||
|
try {
|
||||||
|
const { data, total } = await appStore.fetchTabNews(category, 6);
|
||||||
|
newsByTab.value[category] = { data, total };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching news for ${category}:`, error);
|
||||||
|
newsByTab.value[category] = { data: [], total: 0 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeTab = async (category) => {
|
||||||
|
activeTab.value = category;
|
||||||
|
if (!newsByTab.value[category]) {
|
||||||
|
await fetchNewsForTab(category);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
for (const tab of tabs.value) {
|
||||||
|
await fetchNewsForTab(tab.category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.crop {
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
39
src/components/WebLinks.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// src/components/WebLinks.vue
|
||||||
|
<template>
|
||||||
|
<div class="mt-6 mb-6 p-4">
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||||
|
<div
|
||||||
|
class="text-center p-2 border border-blue-400 bg-blue-200 hover:bg-blue-300 transition-colors duration-200 rounded-md"
|
||||||
|
v-for="(image, key) in images"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
<a :href="image.link" target="_blank" rel="noopener noreferrer" class="block">
|
||||||
|
<img
|
||||||
|
:src="`${appStore.imageBaseUrl}${image.image[0].url}`"
|
||||||
|
alt="Web Link Icon"
|
||||||
|
class="mx-auto w-16 h-16 object-contain mb-2"
|
||||||
|
/>
|
||||||
|
<p class="text-sm font-medium text-gray-800 leading-tight">
|
||||||
|
{{ appStore.checkLang.isTh ? image.title : image.title_en }}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useAppStore } from '@/stores/app';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const images = ref([]);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
images.value = await appStore.find('bottom-links');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* No specific styles needed, using Tailwind classes */
|
||||||
|
</style>
|
||||||
25
src/layouts/DefaultLayout.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// src/layouts/DefaultLayout.vue
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex flex-col">
|
||||||
|
<Header /> <main class="flex-grow">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Header from '@/components/Header.vue'; // Import Header Component
|
||||||
|
import Footer from '@/components/Footer.vue'; // Import Footer Component
|
||||||
|
|
||||||
|
const imageBaseUrl = '';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* สไตล์พื้นหลังเฉพาะตัวของ layout นี้ */
|
||||||
|
body {
|
||||||
|
background-color: rgba(255, 255, 255, 0.8); /* สีพื้นหลัง default สำหรับ DefaultLayout */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
src/layouts/LandingLayout.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// src/views/LandingLayout.vue
|
||||||
|
<template>
|
||||||
|
<div class="landing-layout">
|
||||||
|
<main>
|
||||||
|
<router-view></router-view>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// ถ้ามี Background Image หรือ Logic อื่นๆ ที่เป็นของ Layout เท่านั้น ค่อยเพิ่มเข้ามา
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* สไตล์สำหรับ Landing Layout โดยรวม เช่น background */
|
||||||
|
.landing-layout {
|
||||||
|
min-height: 100vh; /* ทำให้ Layout สูงเต็มหน้าจอ */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center; /* จัดเนื้อหาตรงกลางแนวตั้ง */
|
||||||
|
align-items: center; /* จัดเนื้อหาตรงกลางแนวนอน */
|
||||||
|
/* ถ้าต้องการ Background Image สำหรับ Layout ทั้งหมด */
|
||||||
|
background-image: url('/images/moroccan-flower.png');
|
||||||
|
/* background-size: cover; */
|
||||||
|
/* background-position: center; */
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-grow: 1; /* ทำให้ main ขยายเต็มพื้นที่ที่เหลือ */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
16
src/main.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import './assets/main.css';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import router from "./router/index.js";
|
||||||
|
|
||||||
|
import '@fortawesome/fontawesome-free/css/all.min.css'; // หรือ all.css ถ้าต้องการ debug
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
const pinia = createPinia();
|
||||||
|
|
||||||
|
app.use(pinia); // ใช้ Pinia
|
||||||
|
app.use(router); // ใช้ router
|
||||||
|
|
||||||
|
app.mount('#app');
|
||||||
73
src/router/index.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// src/router/index.js
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
|
||||||
|
// 1. Import Layouts
|
||||||
|
import DefaultLayout from "@/layouts/DefaultLayout.vue";
|
||||||
|
import LandingLayout from "@/layouts/LandingLayout.vue";
|
||||||
|
|
||||||
|
// 2. Import Views (สำคัญ: ต้อง import ชื่อที่ถูกต้อง)
|
||||||
|
import LandingPageView from "@/views/LandingPageView.vue"; // View สำหรับหน้า Landing Page
|
||||||
|
import HomeView from "@/views/HomeView.vue"; // View สำหรับหน้า Home หลัก (ที่มี Calousels, News ฯลฯ)
|
||||||
|
|
||||||
|
import ContentView from "@/views/ContentView.vue"; // View สำหรับแสดงรายละเอียดข่าว
|
||||||
|
import NewsCategoryView from "@/views/NewsCategoryView.vue"; // View สำหรับแสดงรายการข่าวตามหมวดหมู่
|
||||||
|
import NotFoundView from '@/views/NotFoundView.vue';
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
// --- Route สำหรับหน้า Landing Page (ใช้ LandingLayout) ---
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: LandingLayout, // LandingLayout คือกรอบของหน้า Landing
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '', // นี่คือ path สำหรับเนื้อหาที่แสดงเมื่อเข้าถึง '/'
|
||||||
|
name: 'landing',
|
||||||
|
component: LandingPageView, // ชี้ไปที่ View ที่มีเนื้อหาเฉพาะ Landing
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Route สำหรับหน้า Home และหน้าอื่นๆ ที่ใช้ DefaultLayout ---
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: DefaultLayout, // DefaultLayout คือกรอบของหน้า Home หลัก
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'home', // HomeView จะถูกโหลดเมื่อเข้าถึง /home
|
||||||
|
name: 'home',
|
||||||
|
component: HomeView, // HomeView ที่มี Calousels, News, ฯลฯ
|
||||||
|
},
|
||||||
|
// !!! เพิ่ม Route สำหรับ ContentView !!!
|
||||||
|
{
|
||||||
|
path: 'news-content/:id', // Path สำหรับรายละเอียดข่าว เช่น /news-content/1, /news-content/2
|
||||||
|
name: 'ContentView',
|
||||||
|
component: ContentView,
|
||||||
|
props: true // ส่งค่า parameter (id) เป็น props ไปยัง component ได้
|
||||||
|
},
|
||||||
|
// !!! เพิ่ม Route สำหรับ NewsCategoryView !!!
|
||||||
|
{
|
||||||
|
path: 'news/:category', // Path สำหรับข่าวตามหมวดหมู่ เช่น /news/RTAFNews
|
||||||
|
name: 'NewsCategoryView',
|
||||||
|
component: NewsCategoryView,
|
||||||
|
props: true // ส่งค่า parameter (category) เป็น props ไปยัง component ได้
|
||||||
|
},
|
||||||
|
// ... ( routes อื่นๆ ที่อาจจะใช้ DefaultLayout )
|
||||||
|
|
||||||
|
// *** เส้นทาง 404 (ต้องอยู่สุดท้ายเสมอ!) ***
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*', // นี้คือ catch-all route ที่จะจับ URL ที่ไม่ตรงกับข้างบนทั้งหมด
|
||||||
|
name: 'NotFound',
|
||||||
|
component: NotFoundView
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// อย่าลืม export default router; ที่ท้ายไฟล์
|
||||||
|
export default router;
|
||||||
672
src/stores/app.js
Normal file
@ -0,0 +1,672 @@
|
|||||||
|
// src/stores/app.js
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
const IMAGE_BASE_URL = '';
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('app', {
|
||||||
|
state: () => ({
|
||||||
|
isTh: true,
|
||||||
|
tabs: [ // ตรวจสอบว่า tabs array ใน store ตรงกับ TabNews.vue
|
||||||
|
{ title: "ข่าวประชาสัมพันธ์หน่วยงาน", title_en: "RTAF News", category: "HumanTechNews" },
|
||||||
|
{ title: "ข่าวประชาสัมพันธ์ภายใน", title_en: "RTAF Organization News", category: "OrgNews" },
|
||||||
|
{ title: "ข่าวนวัตกรรม", title_en: "RTAF Service News", category: "InnovationNews" },
|
||||||
|
{ title: "Press Release", title_en: "RTAF Press Release", category: "GeneralPublic" },
|
||||||
|
{ title: "ข่าวบริการประชาชน", title_en: "Public Service News", category: "EventActivities" },
|
||||||
|
],
|
||||||
|
// *** ข้อมูลสำหรับ Header ***
|
||||||
|
headers: {
|
||||||
|
header_background: { url: '/images/news_header_bg_b815923058.png' },
|
||||||
|
logo: { url: '/images/Enter.png' }, // ใช้รูปโลโก้ตามที่ระบุ
|
||||||
|
mainColor: '#0A2647', // สีหลักของ Header/Navbar
|
||||||
|
main_menus: [
|
||||||
|
{ id: 1, main_menu: 'หน้าหลัก', main_menu_en: 'Home', link: '/home', order: 1, active: true, active_en: true, sub_menus: [] },
|
||||||
|
{
|
||||||
|
id: 2, main_menu: 'รู้จักหน่วยงาน', main_menu_en: 'About HumanTech', order: 2, active: true, active_en: true,
|
||||||
|
sub_menus: [
|
||||||
|
{ id: 21, title_th: 'ประวัติความเป็นมา', title_en: 'HumanTech History', link: '/about/history', order: 1, active: true, active_en: true },
|
||||||
|
{ id: 22, title_th: 'วิสัยทัศน์ พันธกิจ', title_en: 'Vision & Mission', link: '/about/vision-mission', order: 2, active: true, active_en: true },
|
||||||
|
{ id: 23, title_th: 'คณะผู้บริหารองค์กร', title_en: 'Commander-in-Chief', link: '/about/commander', order: 3, active: true, active_en: true },
|
||||||
|
{ id: 24, title_th: 'นโยบายองค์กร', title_en: 'C-in-C Policy', link: '/about/policy', order: 4, active: true, active_en: true },
|
||||||
|
{ id: 25, title_th: 'โครงสร้างองค์กร', title_en: 'Organization Structure', link: '/about/organization', order: 5, active: true, active_en: true },
|
||||||
|
{ id: 26, title_th: 'ทำเนียบผู้บริหารองค์กร', title_en: 'Commanders List', link: '/about/commanders-list', order: 6, active: true, active_en: true },
|
||||||
|
{ id: 27, title_th: 'ผู้บริหารเทคโนโลยีสารสนเทศระดับสูงองค์กร', title_en: 'HumanTech Senior IT Management', link: '/about/it-management', order: 7, active: true, active_en: true },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3, main_menu: 'ข้อมูลเผยแพร่', main_menu_en: 'Information', order: 3, active: true, active_en: true,
|
||||||
|
sub_menus: [
|
||||||
|
{ id: 31, title_th: 'เอกสารเผยแพร่', title_en: 'Publications', link: '/data/publications', order: 1, active: true, active_en: true },
|
||||||
|
{ id: 32, title_th: 'วารสารองค์กร', title_en: 'HumanTech Journal', link: '/data/journal', order: 2, active: true, active_en: true },
|
||||||
|
{ id: 33, title_th: 'คลังภาพ/สื่อผสม', title_en: 'Gallery/Media', link: '/data/gallery-media', order: 3, active: true, active_en: true },
|
||||||
|
{ id: 34, title_th: 'รายงานผลการดำเนินงาน', title_en: 'Performance Report', link: '/data/performance-report', order: 4, active: true, active_en: true },
|
||||||
|
{ id: 35, title_th: 'รายงานการเงินหน่วยงาน', title_en: 'HumanTech Financial Report', link: '/data/financial-report', order: 5, active: true, active_en: true },
|
||||||
|
{ id: 36, title_th: 'กฎหมายที่เกี่ยวข้อง', title_en: 'Related Laws', link: '/data/laws', order: 6, active: true, active_en: true },
|
||||||
|
{ id: 37, title_th: 'การเปิดเผยข้อมูลสาธารณะ', title_en: 'Public Information Disclosure', link: '/data/public-disclosure', order: 7, active: true, active_en: true },
|
||||||
|
{ id: 38, title_th: 'ปฏิทินกิจกรรม', title_en: 'Activity Calendar', link: '/data/calendar', order: 8, active: true, active_en: true },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ id: 4, main_menu: 'บริการของเรา', main_menu_en: 'HumanTech Services', link: '/services', order: 5, active: true, active_en: true, sub_menus: [] },
|
||||||
|
{
|
||||||
|
id: 5, main_menu: 'การติดต่อ', main_menu_en: 'Contact', order: 6, active: true, active_en: true,
|
||||||
|
sub_menus: [
|
||||||
|
{ id: 51, title_th: 'ติดต่อเรา', title_en: 'Contact Us', link: '/contact/us', order: 1, active: true, active_en: true },
|
||||||
|
{ id: 52, title_th: 'คำถามยอดฮิต', title_en: 'FAQ', link: '/contact/faq', order: 2, active: true, active_en: true },
|
||||||
|
{ id: 53, title_th: 'ร้องทุกข์ร้องเรียน', title_en: 'Complaints', link: '/contact/complaints', order: 3, active: true, active_en: true },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
// *** ข้อมูลสำหรับ Footer ***
|
||||||
|
footer_menus: {
|
||||||
|
menu: [
|
||||||
|
{ id: 1, title: 'ติดต่อเรา', title_en: 'Contact Us', link: '/contact', File: null, active: true, active_en: true },
|
||||||
|
{ id: 2, title: 'แผนผังเว็บไซต์', title_en: 'Sitemap', link: '/sitemap', File: null, active: true, active_en: true },
|
||||||
|
// เพิ่มเมนู Footer อื่นๆ
|
||||||
|
{ id: 3, title: 'เอกสารดาวน์โหลด', title_en: 'Downloads', link: null, File: { url: '/docs/document.pdf' }, active: true, active_en: true },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mock Data สำหรับ Calousels
|
||||||
|
calousels: [
|
||||||
|
{
|
||||||
|
id: 1, // รูปภาพปกติ, มีลิงก์ภายนอก
|
||||||
|
type: 'image', // ระบุประเภทเป็น 'image'
|
||||||
|
image: { url: '/uploads/main_banner_1.jpg' }, // URL รูปภาพ
|
||||||
|
link: 'https://www.google.com', // ลิงก์ภายนอก
|
||||||
|
File: null, // ไม่มีไฟล์ให้ดาวน์โหลด
|
||||||
|
video_url: null, // ไม่มีวิดีโอ URL
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
order: 1,
|
||||||
|
startDate: '01/01/2025',
|
||||||
|
endDate: '12/31/2025',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2, // รูปภาพ, มีไฟล์ให้ดาวน์โหลด
|
||||||
|
type: 'image',
|
||||||
|
image: { url: '/uploads/main_banner_2.jpg' },
|
||||||
|
link: null, // ไม่มีลิงก์ภายนอก
|
||||||
|
File: { url: '/uploads/document_2.pdf', name: 'document_2.pdf' }, // มีไฟล์ PDF ให้ดาวน์โหลด
|
||||||
|
video_url: null,
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
order: 2,
|
||||||
|
startDate: '01/01/2025',
|
||||||
|
endDate: '12/31/2025',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3, // วิดีโอ YouTube
|
||||||
|
type: 'video', // ระบุประเภทเป็น 'video'
|
||||||
|
image: { url: '/uploads/youtube-thumbnail-3.jpg' }, // Thumbnail สำหรับวิดีโอ (ถ้ามี)
|
||||||
|
link: null,
|
||||||
|
File: null,
|
||||||
|
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', // ตัวอย่าง YouTube URL (Rick Astley)
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
order: 3,
|
||||||
|
startDate: '01/01/2025',
|
||||||
|
endDate: '12/31/2025',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4, // วิดีโอ Vimeo
|
||||||
|
type: 'video',
|
||||||
|
image: { url: '/uploads/vimeo-thumbnail-4.jpg' }, // Thumbnail สำหรับวิดีโอ (ถ้ามี)
|
||||||
|
link: null,
|
||||||
|
File: null,
|
||||||
|
video_url: 'https://vimeo.com/76979871', // ตัวอย่าง Vimeo URL
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
order: 4,
|
||||||
|
startDate: '01/01/2025',
|
||||||
|
endDate: '12/31/2025',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5, // รูปภาพปกติ, ไม่มีลิงก์/ไฟล์
|
||||||
|
type: 'image',
|
||||||
|
image: { url: '/uploads/main_banner_3.jpg' },
|
||||||
|
link: null,
|
||||||
|
File: null,
|
||||||
|
video_url: null,
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
order: 5,
|
||||||
|
startDate: '01/01/2025',
|
||||||
|
endDate: '12/31/2025',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6, // รูปภาพ, ไม่มีลิงก์/ไฟล์, และ active เป็น false (ไม่ควรแสดง)
|
||||||
|
type: 'image',
|
||||||
|
image: { url: '/uploads/main_banner_4.jpg' },
|
||||||
|
link: null,
|
||||||
|
File: null,
|
||||||
|
video_url: null,
|
||||||
|
active: false, // จะไม่แสดงในภาษาไทย
|
||||||
|
active_en: true,
|
||||||
|
order: 6,
|
||||||
|
startDate: '01/01/2025',
|
||||||
|
endDate: '12/31/2025',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7, // วิดีโอ YouTube ที่ไม่ Active ในภาษาไทย
|
||||||
|
type: 'video',
|
||||||
|
image: { url: '/uploads/youtube-thumbnail-7.jpg' },
|
||||||
|
link: null,
|
||||||
|
File: null,
|
||||||
|
video_url: 'https://www.youtube.com/embed/n0NcsaOxMhM?si=6guI-XduDl87qMVC',
|
||||||
|
active: false, // จะไม่แสดงในภาษาไทย
|
||||||
|
active_en: true,
|
||||||
|
order: 7,
|
||||||
|
startDate: '01/01/2025',
|
||||||
|
endDate: '12/31/2025',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Mock Data สำหรับ News
|
||||||
|
mockNews: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title_th: 'โครงการบินเพื่อชีวิต: กองทัพอากาศช่วยเหลือผู้ป่วยฉุกเฉิน',
|
||||||
|
title_en: 'Flying for Life: RTAF Assists Emergency Patients',
|
||||||
|
detail_th: 'กองทัพอากาศได้ปฏิบัติภารกิจบินลำเลียงผู้ป่วยฉุกเฉินจากจังหวัดชายแดนสู่โรงพยาบาลกลางอย่างเร่งด่วน ภารกิจนี้ไม่เพียงแต่ช่วยชีวิต แต่ยังแสดงถึงบทบาทสำคัญของกองทัพในงานด้านมนุษยธรรม',
|
||||||
|
detail_en: 'The Royal Thai Air Force conducted an urgent airlift mission, transporting critical patients from border provinces to central hospitals, showcasing the humanitarian role of the military.',
|
||||||
|
image: { url: '/uploads/airlift_patient.jpg' },
|
||||||
|
release_date: '07/04/2025',
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
feature: true,
|
||||||
|
type: 'HumanTechNews'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2, title_th: 'ข่าวทั่วไป 1 (ไทย)', title_en: 'General News 1 (Eng)',
|
||||||
|
detail_th: 'รายละเอียดข่าวทั่วไป 1 ภาษาไทย...', detail_en: 'Detail of general news 1 in English...',
|
||||||
|
image: { url: '/uploads/news_2.jpg' }, release_date: '02/01/2025', active: true, active_en: true, feature: false, type: 'HumanTechNews'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3, title_th: 'ข่าวเด่น 2 (ไทย)', title_en: 'Hot News 2 (Eng)',
|
||||||
|
detail_th: 'รายละเอียดข่าวเด่น 2 ภาษาไทย...', detail_en: 'Detail of hot news 2 in English...',
|
||||||
|
image: { url: '/uploads/news_3.jpg' }, release_date: '03/01/2025', active: true, active_en: true, feature: false, type: 'OrgNews'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4, title_th: 'ข่าวทั่วไป 2 (ไทย)', title_en: 'General News 2 (Eng)',
|
||||||
|
detail_th: 'รายละเอียดข่าวทั่วไป 2 ภาษาไทย...', detail_en: 'Detail of general news 2 in English...',
|
||||||
|
image: { url: '/uploads/news_4.jpg' }, release_date: '04/01/2025', active: true, active_en: true, feature: false, type: 'OrgNews'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5, title_th: 'ข่าวประชาสัมพันธ์หน่วยงาน', title_en: 'RTAF News Sample',
|
||||||
|
detail_th: 'รายละเอียดข่าวประชาสัมพันธ์ ภาษาไทยที่มีความยาวมากเกินไปเพื่อทดสอบการตัดคำ: 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
|
||||||
|
detail_en: 'English detail of RTAF News Sample with excessive length to test 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
|
||||||
|
image: { url: '/uploads/news_5.jpg' }, release_date: '05/01/2025', active: true, active_en: true, feature: false, type: 'HumanTechNews'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6, title_th: 'ข่าวประชาสัมพันธ์หน่วยงานภายใน', title_en: 'RTAF Organization News Sample',
|
||||||
|
detail_th: 'รายละเอียดข่าวประชาสัมพันธ์หน่วยงานภายใน ภาษาไทย...', detail_en: 'Detail of RTAF Organization News Sample...',
|
||||||
|
image: { url: '/uploads/news_6.jpg' }, release_date: '06/01/2025', active: true, active_en: true, feature: false, type: 'OrgNews'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7, title_th: 'ข่าวบริการกำลังพล ทอ.', title_en: 'RTAF Service News Sample',
|
||||||
|
detail_th: 'รายละเอียดข่าวบริการกำลังพล ทอ. ภาษาไทย...', detail_en: 'Detail of RTAF Service News Sample...',
|
||||||
|
image: { url: '/uploads/news_7.jpg' }, release_date: '07/01/2025', active: true, active_en: true, feature: false, type: 'InnovationNews'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8, title_th: 'Press Release/โฆษก ทอ.', title_en: 'RTAF Press Release Sample',
|
||||||
|
detail_th: 'รายละเอียด Press Release/โฆษก ทอ. ภาษาไทย...', detail_en: 'Detail of RTAF Press Release Sample...',
|
||||||
|
image: { url: '/uploads/news_8.jpg' }, release_date: '08/01/2025', active: true, active_en: true, feature: false, type: 'GeneralPublic'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9, title_th: 'ข่าวบริการประชาชน', title_en: 'Public Service News Sample',
|
||||||
|
detail_th: 'รายละเอียดข่าวบริการประชาชน ภาษาไทย...', detail_en: 'Detail of Public Service News Sample...',
|
||||||
|
image: { url: '/uploads/news_9.jpg' }, release_date: '09/01/2025', active: true, active_en: true, feature: false, type: 'EventActivities'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10, title_th: 'ข่าวทั่วไป 3 (ไทย)', title_en: 'General News 3 (Eng)',
|
||||||
|
detail_th: 'รายละเอียดข่าวทั่วไป 3 ภาษาไทย...', detail_en: 'Detail of general news 3 in English...',
|
||||||
|
image: { url: '/uploads/news_10.jpg' }, release_date: '10/01/2025', active: true, active_en: true, feature: false, type: 'HumanTechNews'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Mock Data TabNews
|
||||||
|
mockNewsData : [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 1", // RTAF News
|
||||||
|
title_en: "RTAF News Item 1",
|
||||||
|
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 1 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for RTAF News Item 1 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_1.jpg" },
|
||||||
|
release_date: "07/01/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "HumanTechNews", // <-- ให้ตรงกับ category ใน TabNews.vue
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 2",
|
||||||
|
title_en: "RTAF News Item 2",
|
||||||
|
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_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.",
|
||||||
|
image: { url: "/uploads/news_2.jpg" },
|
||||||
|
release_date: "07/03/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "HumanTechNews", // <-- ให้ตรงกับ category ใน TabNews.vue
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 1", // RTAF Organization News
|
||||||
|
title_en: "RTAF Org News Item 1",
|
||||||
|
detail_th: "รายละเอียดข่าว นขต.ทอ. ชิ้นที่ 1 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for RTAF Org News Item 1 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_3.jpg" },
|
||||||
|
release_date: "06/15/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "OrgNews", // <-- ให้ตรงกับ category ใน TabNews.vue
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 2",
|
||||||
|
title_en: "RTAF Org News Item 2",
|
||||||
|
detail_th: "รายละเอียดข่าว นขต.ทอ. ชิ้นที่ 2 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for RTAF Org News Item 2 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_4.jpg" },
|
||||||
|
release_date: "07/02/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "OrgNews", // <-- ให้ตรงกับ category ใน TabNews.vue
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title_th: "ข่าวนวัตกรรม ชิ้นที่ 1", // RTAF Service News (หรือชื่อที่เหมาะสมกับ InnovationNews)
|
||||||
|
title_en: "RTAF Service News Item 1",
|
||||||
|
detail_th: "รายละเอียดข่าวนวัตกรรม ชิ้นที่ 1 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for RTAF Service News Item 1 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_5.jpg" },
|
||||||
|
release_date: "07/04/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "InnovationNews", // <-- ให้ตรงกับ category ใน TabNews.vue
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title_th: "ข่าวนวัตกรรม ชิ้นที่ 2",
|
||||||
|
title_en: "RTAF Service News Item 2",
|
||||||
|
detail_th: "รายละเอียดข่าวนวัตกรรม ชิ้นที่ 2 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for RTAF Service News Item 2 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_5.jpg" },
|
||||||
|
release_date: "07/05/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "InnovationNews", // <-- ให้ตรงกับ category ใน TabNews.vue
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
title_th: "Press Release ชิ้นที่ 1", // Press Release
|
||||||
|
title_en: "RTAF Press Release Item 1",
|
||||||
|
detail_th: "รายละเอียด Press Release ชิ้นที่ 1 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for RTAF Press Release Item 1 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_4.jpg" },
|
||||||
|
release_date: "06/20/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "GeneralPublic", // <-- ให้ตรงกับ category ใน TabNews.vue
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
title_th: "ข่าวบริการประชาชน ชิ้นที่ 1", // Public Service News
|
||||||
|
title_en: "Public Service News Item 1",
|
||||||
|
detail_th: "รายละเอียดข่าวบริการประชาชน ชิ้นที่ 1 เพื่อทดสอบการแสดงผลข้อมูล. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for Public Service News Item 1 for testing purposes. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_3.jpg" },
|
||||||
|
release_date: "07/06/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "EventActivities", // <-- ให้ตรงกับ category ใน TabNews.vue
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
// เพิ่มเติมเพื่อให้มีข้อมูลเพียงพอสำหรับแต่ละแท็บ (อย่างน้อย 6 ชิ้นในแต่ละประเภท เพื่อให้ปุ่ม Read More แสดง)
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 3",
|
||||||
|
title_en: "RTAF News Item 3",
|
||||||
|
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 3. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for RTAF News Item 3. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_2.jpg" },
|
||||||
|
release_date: "07/07/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "HumanTechNews",
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 4",
|
||||||
|
title_en: "RTAF News Item 4",
|
||||||
|
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 4. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for RTAF News Item 4. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_1.jpg" },
|
||||||
|
release_date: "07/07/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "HumanTechNews",
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 5",
|
||||||
|
title_en: "RTAF News Item 5",
|
||||||
|
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 5. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for RTAF News Item 5. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_1.jpg" },
|
||||||
|
release_date: "07/07/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "HumanTechNews",
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 6",
|
||||||
|
title_en: "RTAF News Item 6",
|
||||||
|
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 6. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for RTAF News Item 6. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_2.jpg" },
|
||||||
|
release_date: "07/07/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "HumanTechNews",
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 7 (สำหรับปุ่ม More)",
|
||||||
|
title_en: "RTAF News Item 7 (for More button)",
|
||||||
|
detail_th: "รายละเอียดข่าวประชาสัมพันธ์หน่วยงาน ชิ้นที่ 7. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
detail_en: "Details for RTAF News Item 7. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
image: { url: "/uploads/news_3.jpg" },
|
||||||
|
release_date: "07/07/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "HumanTechNews",
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
// เพิ่มข้อมูลสำหรับแต่ละ category ใน tabs ของคุณให้มีจำนวนเพียงพอที่จะแสดงผล
|
||||||
|
// ตัวอย่างเช่น สำหรับ OrgNews
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 3",
|
||||||
|
title_en: "RTAF Org News Item 3",
|
||||||
|
detail_th: "รายละเอียดข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 3.",
|
||||||
|
detail_en: "Details for RTAF Org News Item 3.",
|
||||||
|
image: { url: "/uploads/news_4.jpg" },
|
||||||
|
release_date: "07/07/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "OrgNews",
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 15,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 4",
|
||||||
|
title_en: "RTAF Org News Item 4",
|
||||||
|
detail_th: "รายละเอียดข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 4.",
|
||||||
|
detail_en: "Details for RTAF Org News Item 4.",
|
||||||
|
image: { url: "/uploads/news_5.jpg" },
|
||||||
|
release_date: "07/07/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "OrgNews",
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 16,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 5",
|
||||||
|
title_en: "RTAF Org News Item 5",
|
||||||
|
detail_th: "รายละเอียดข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 5.",
|
||||||
|
detail_en: "Details for RTAF Org News Item 5.",
|
||||||
|
image: { url: "/uploads/news_6.jpg" },
|
||||||
|
release_date: "07/07/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "OrgNews",
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 17,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 6",
|
||||||
|
title_en: "RTAF Org News Item 6",
|
||||||
|
detail_th: "รายละเอียดข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 6.",
|
||||||
|
detail_en: "Details for RTAF Org News Item 6.",
|
||||||
|
image: { url: "/uploads/news_1.jpg" },
|
||||||
|
release_date: "07/07/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "OrgNews",
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 18,
|
||||||
|
title_th: "ข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 7 (สำหรับปุ่ม More)",
|
||||||
|
title_en: "RTAF Org News Item 7 (for More button)",
|
||||||
|
detail_th: "รายละเอียดข่าวประชาสัมพันธ์ภายใน ชิ้นที่ 7.",
|
||||||
|
detail_en: "Details for RTAF Org News Item 7.",
|
||||||
|
image: { url: "/uploads/news_2.jpg" },
|
||||||
|
release_date: "07/07/2025",
|
||||||
|
active: true,
|
||||||
|
active_en: true,
|
||||||
|
type: "OrgNews",
|
||||||
|
feature: false,
|
||||||
|
},
|
||||||
|
// เพิ่มให้ครบทุก category เพื่อทดสอบ
|
||||||
|
],
|
||||||
|
|
||||||
|
mockBottomBanners: [
|
||||||
|
{ id: 1, banner_type: 'calousel', active: true, active_en: true, image: { url: '/uploads/slider_banner_1.jpg' }, link: 'www.google.co.th' },
|
||||||
|
],
|
||||||
|
mockFixedBanners: [
|
||||||
|
{ id: 1, banner_type: 'fixed', active: true, active_en: true, image: { url: '/uploads/news_1.jpg' }, link: '/some-internal-page' },
|
||||||
|
{ id: 2, banner_type: 'fixed', active: true, active_en: true, image: { url: '/uploads/news_2.jpg' }, link: '/weblink-4' },
|
||||||
|
{ id: 3, banner_type: 'fixed', active: true, active_en: true, image: { url: '/uploads/news_3.jpg' }, link: '/weblink-4' },
|
||||||
|
{ id: 4, banner_type: 'fixed', active: true, active_en: true, image: { url: '/uploads/news_4.jpg' }, link: '/weblink-4' },
|
||||||
|
{ id: 5, banner_type: 'fixed', active: true, active_en: true, image: { url: '/uploads/news_5.jpg' }, link: '/weblink-4' },
|
||||||
|
{ id: 6, banner_type: 'fixed', active: true, active_en: true, image: { url: '/uploads/news_6.jpg' }, link: '/weblink-4' },
|
||||||
|
{ id: 7, banner_type: 'fixed', active: true, active_en: true, image: { url: '/uploads/news_1.jpg' }, link: '/weblink-4' },
|
||||||
|
],
|
||||||
|
mockSliderBanners: [
|
||||||
|
{ id: 1, active: true, active_en: true, image: { url: '/uploads/slider_banner_1.jpg' }, link: '/weblink-4' },
|
||||||
|
{ id: 2, active: true, active_en: true, image: { url: '/uploads/slider_banner_2.jpg' }, link: '/weblink-4' },
|
||||||
|
{ id: 3, active: true, active_en: true, image: { url: '/uploads/slider_banner_3.jpg' }, link: '/weblink-4' },
|
||||||
|
{ id: 4, active: true, active_en: true, image: { url: '/uploads/slider_banner_4.jpg' }, link: '/weblink-4' },
|
||||||
|
{ id: 5, active: true, active_en: true, image: { url: '/uploads/slider_banner_5.jpg' }, link: '/weblink-4' },
|
||||||
|
],
|
||||||
|
mockBottomLinks: [
|
||||||
|
{ id: 1, active: true, active_en: true, image: [{ url: '/uploads/weblink-1.png' }], link: '/weblink-4', title: 'Print Magazine', title_en: 'Link 1' },
|
||||||
|
{ id: 2, active: true, active_en: true, image: [{ url: '/uploads/weblink-2.png' }], link: '/weblink-4', title: 'Rimberio', title_en: 'Link 2' },
|
||||||
|
{ id: 3, active: true, active_en: true, image: [{ url: '/uploads/weblink-3.png' }], link: '/weblink-4', title: 'Art Academy', title_en: 'Link 3' },
|
||||||
|
{ id: 4, active: true, active_en: true, image: [{ url: '/uploads/weblink-4.png' }], link: '/weblink-4', title: 'THE CREATIVE AGENCY', title_en: 'Link 4' },
|
||||||
|
{ id: 5, active: true, active_en: true, image: [{ url: '/uploads/weblink-5.png' }], link: '/weblink-5', title: 'Day Tech', title_en: 'Link 5' },
|
||||||
|
{ id: 6, active: true, active_en: true, image: [{ url: '/uploads/weblink-6.png' }], link: '/weblink-6', title: 'Artificial Intelligence', title_en: 'Link 6' },
|
||||||
|
],
|
||||||
|
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
// Getter สำหรับดึง Image Base URL
|
||||||
|
// หาก IMAGE_BASE_URL เป็น '', การรวมกับ /uploads/... จะได้ /uploads/... ซึ่ง Vite จะ serve จาก public folder
|
||||||
|
imageBaseUrl: (state) => IMAGE_BASE_URL,
|
||||||
|
// Getter สำหรับสถานะภาษา
|
||||||
|
checkLang: (state) => ({
|
||||||
|
isTh: state.isTh,
|
||||||
|
isEn: !state.isTh,
|
||||||
|
}),
|
||||||
|
// Getter สำหรับ Main Banner โดยเฉพาะ
|
||||||
|
mainBannerData: (state) => {
|
||||||
|
// ค้นหา Main Banner จาก calousels โดย id
|
||||||
|
return state.calousels.find(item => item.id === 1);
|
||||||
|
},
|
||||||
|
headerData: (state) => state.headers,
|
||||||
|
footerData: (state) => state.footer_menus,
|
||||||
|
allMainMenus: (state) => state.headers.main_menus,
|
||||||
|
allFooterMenus: (state) => state.footer_menus.menu,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// Action สำหรับเปลี่ยนภาษา
|
||||||
|
toggleLanguage() {
|
||||||
|
this.isTh = !this.isTh;
|
||||||
|
},
|
||||||
|
|
||||||
|
// *** Action ใหม่: สำหรับ TabNews โดยเฉพาะ (return { data, total }) ***
|
||||||
|
async fetchTabNews(category, limit = 6) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate API delay
|
||||||
|
|
||||||
|
const isTh = this.isTh;
|
||||||
|
// const currentDate = new Date(); // ไม่ต้องใช้แล้ว
|
||||||
|
// currentDate.setHours(0, 0, 0, 0); // ไม่ต้องใช้แล้ว
|
||||||
|
|
||||||
|
// ขั้นตอนที่ 1: กรองข้อมูลทั้งหมดที่เข้าเงื่อนไข (active, type) ก่อนที่จะ apply limit
|
||||||
|
const allMatchingNews = this.mockNewsData.filter(item => {
|
||||||
|
const isActive = isTh ? item.active : item.active_en;
|
||||||
|
const isCorrectType = item.type === category;
|
||||||
|
|
||||||
|
// ลบส่วนการตรวจสอบวันที่ออกไป
|
||||||
|
// const releaseDateParts = item.release_date ? item.release_date.split('/') : null;
|
||||||
|
// const itemReleaseDate = releaseDateParts ? new Date(parseInt(releaseDateParts[2]), parseInt(releaseDateParts[0]) - 1, parseInt(releaseDateParts[1])) : new Date(0);
|
||||||
|
// itemReleaseDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// return isActive && isCorrectType && (itemReleaseDate <= currentDate); // เงื่อนไขเก่า
|
||||||
|
return isActive && isCorrectType; // เงื่อนไขใหม่: ไม่สนใจวันที่
|
||||||
|
});
|
||||||
|
|
||||||
|
// เก็บจำนวนรวมทั้งหมดก่อนการจำกัด (total count)
|
||||||
|
const totalCount = allMatchingNews.length;
|
||||||
|
|
||||||
|
// ขั้นตอนที่ 2: เรียงลำดับข้อมูลที่กรองแล้ว (ยังคงเรียงตามวันที่จากใหม่ไปเก่า)
|
||||||
|
allMatchingNews.sort((a, b) => {
|
||||||
|
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 data = allMatchingNews.slice(0, limit);
|
||||||
|
|
||||||
|
return { data: data, total: totalCount }; // ส่ง Object กลับไปสำหรับ TabNews
|
||||||
|
},
|
||||||
|
|
||||||
|
// *** Action เดิม: find (ยังคง return เป็น Array เหมือนเดิมสำหรับ endpoint อื่นๆ) ***
|
||||||
|
async find(endpoint, queryParams = '') {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
let data = [];
|
||||||
|
const isTh = this.isTh;
|
||||||
|
|
||||||
|
switch (endpoint) {
|
||||||
|
case 'calousels':
|
||||||
|
data = this.calousels.filter(item => {
|
||||||
|
const isActive = isTh ? item.active : item.active_en;
|
||||||
|
|
||||||
|
// --- Logic การกรองตาม startDate และ endDate ---
|
||||||
|
let isWithinDateRange = true;
|
||||||
|
if (item.startDate && item.endDate) {
|
||||||
|
try {
|
||||||
|
const today = new Date();
|
||||||
|
// แปลงวันที่จาก MM/DD/YYYY เป็น Date Object
|
||||||
|
// ต้องแน่ใจว่า item.startDate/endDate เป็น MM/DD/YYYY
|
||||||
|
const [startMonth, startDay, startYear] = item.startDate.split('/');
|
||||||
|
const [endMonth, endDay, endYear] = item.endDate.split('/');
|
||||||
|
|
||||||
|
const startDate = new Date(parseInt(startYear), parseInt(startMonth) - 1, parseInt(startDay));
|
||||||
|
const endDate = new Date(parseInt(endYear), parseInt(endMonth) - 1, parseInt(endDay));
|
||||||
|
|
||||||
|
// เซ็ตเวลาของวันนี้ให้เป็น 00:00:00 เพื่อเปรียบเทียบกับ startDate/endDate ที่มักจะไม่มีเวลา
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
isWithinDateRange = today >= startDate && today <= endDate;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing date for calousel item:", item, e);
|
||||||
|
isWithinDateRange = false; // ถ้า parse ไม่ได้ ถือว่าไม่อยู่ในช่วง
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- จบ Logic การกรองตาม startDate และ endDate ---
|
||||||
|
|
||||||
|
return isActive && isWithinDateRange && item.order !== undefined; // ตรวจสอบ item.order เพื่อความปลอดภัย
|
||||||
|
}).sort((a, b) => a.order - b.order);
|
||||||
|
break;
|
||||||
|
case 'bottom-banners':
|
||||||
|
data = isTh ? this.mockBottomBanners.filter(b => b.active) : this.mockBottomBanners.filter(b => b.active_en);
|
||||||
|
break;
|
||||||
|
case 'slider-banners':
|
||||||
|
data = isTh ? this.mockSliderBanners.filter(b => b.active) : this.mockSliderBanners.filter(b => b.active_en);
|
||||||
|
break;
|
||||||
|
case 'bottom-links':
|
||||||
|
data = isTh ? this.mockBottomLinks.filter(l => l.active) : this.mockBottomLinks.filter(l => l.active_en);
|
||||||
|
break;
|
||||||
|
case 'contents':
|
||||||
|
case 'news':
|
||||||
|
data = this.mockNews.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 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+)/);
|
||||||
|
if (limit) {
|
||||||
|
data = data.slice(0, parseInt(limit[1]));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`Endpoint ${endpoint} not mocked or handled by specific action.`);
|
||||||
|
data = [];
|
||||||
|
}
|
||||||
|
return data; // สำหรับ find action ทั่วไป จะยังคง return เป็น Array
|
||||||
|
},
|
||||||
|
|
||||||
|
// ในอนาคตถ้าต้องการดึงข้อมูล Header/Footer จาก API
|
||||||
|
async fetchHeaderFooterData() {
|
||||||
|
// ตัวอย่าง: const response = await axios.get('/api/header-footer');
|
||||||
|
// this.headers = response.data.headers;
|
||||||
|
// this.footer_menus = response.data.footer;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
38
src/stores/landingPageView.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// src/stores/landingPageView.js
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
const IMAGE_BASE_URL = ''; // ให้เป็น string ว่างเปล่าเพื่อใช้ Path สัมพัทธ์กับ Public folder
|
||||||
|
|
||||||
|
export const useLandingPageViewStore = defineStore('landingPageView', {
|
||||||
|
state: () => ({
|
||||||
|
// ข้อมูล Mock สำหรับ Main Banner (จาก app.js เดิม)
|
||||||
|
mainBanner: {
|
||||||
|
url: '/images/MainBanner.jpg', // Path รูปภาพ Main Banner
|
||||||
|
link: 'www.google.com', // ลิงก์สำหรับรูป
|
||||||
|
startDate: '01/01/2025', // ต้องเป็น MM/DD/YYYY
|
||||||
|
endDate: '12/31/2025', // ต้องเป็น MM/DD/YYYY (กำหนดให้แสดงตลอดปี)
|
||||||
|
},
|
||||||
|
// ถ้ามีข้อมูลอื่นๆ ที่เกี่ยวข้องกับ Landing Page โดยเฉพาะ ก็สามารถเพิ่มที่นี่ได้
|
||||||
|
// เช่น backgroundImage, logoImage, text
|
||||||
|
backgroundImage: { url: '/images/moroccan-flower.png' }, // ตัวอย่าง
|
||||||
|
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
imageBaseUrl: (state) => IMAGE_BASE_URL,
|
||||||
|
// Getter สำหรับดึงข้อมูล Main Banner
|
||||||
|
mainBannerData: (state) => state.mainBanner,
|
||||||
|
|
||||||
|
// getters อื่นๆ สำหรับข้อมูลเฉพาะ Landing Page
|
||||||
|
getBackgroundImage: (state) => `${IMAGE_BASE_URL}${state.backgroundImage?.url || ''}`,
|
||||||
|
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// ถ้าในอนาคต Landing Page มีการดึงข้อมูลจาก API จริงๆ ค่อยมาใส่ใน Action นี้
|
||||||
|
async fetchLandingPageData() {
|
||||||
|
// จำลองการโหลดข้อมูล หรือดึงข้อมูลจาก API จริงๆ
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
console.log("Landing Page Data Loaded from dedicated store.");
|
||||||
|
// ในกรณีที่มี API จริง อาจจะมีการอัปเดต state ตรงนี้
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
2
src/style.css
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "daisyui";
|
||||||
114
src/views/ContentView.vue
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// src/views/ContentView.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 h-64 md:h-96">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<div v-else class="w-full h-full bg-gray-200 flex items-center justify-center text-gray-500">
|
||||||
|
No Image Available
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 md:p-8">
|
||||||
|
<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' }}:
|
||||||
|
{{ formatDate(newsItem.release_date) }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="prose max-w-none text-gray-800 leading-relaxed break-words whitespace-pre-line">
|
||||||
|
{{ appStore.checkLang.isTh ? newsItem.detail_th : newsItem.detail_en }}
|
||||||
|
</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 btn-primary">
|
||||||
|
{{ appStore.checkLang.isTh ? 'กลับหน้าหลัก' : 'Back to Home' }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router'; // สำหรับการเข้าถึง route parameters
|
||||||
|
import { useAppStore } from '@/stores/app.js';
|
||||||
|
import { RouterLink } from 'vue-router'; // ใช้สำหรับปุ่มกลับหน้าหลัก
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const route = useRoute(); // Instance ของ useRoute
|
||||||
|
const newsItem = ref(null); // ใช้เก็บข้อมูลข่าวที่ดึงมา
|
||||||
|
|
||||||
|
// Function to fetch news detail by ID
|
||||||
|
const fetchNewsDetail = async (id) => {
|
||||||
|
// ในสถานการณ์จริง คุณจะเรียก API เช่น await axios.get(`/api/news/${id}`);
|
||||||
|
// สำหรับตอนนี้ ใช้ mock data ใน store
|
||||||
|
const foundNews = appStore.mockNews.find(item => item.id == id); // ใช้ == เพื่อให้แปลง string/number ได้
|
||||||
|
if (foundNews) {
|
||||||
|
// ตรวจสอบ active และ active_en ตามภาษาปัจจุบัน
|
||||||
|
if (appStore.isTh && foundNews.active) {
|
||||||
|
newsItem.value = foundNews;
|
||||||
|
} else if (!appStore.isTh && foundNews.active_en) { // ใช้ !appStore.isTh สำหรับภาษาอังกฤษ
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ดึงข้อมูลเมื่อ Component ถูก mount
|
||||||
|
onMounted(() => {
|
||||||
|
fetchNewsDetail(route.params.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes in route.params.id (กรณีที่ผู้ใช้คลิก Link ไปยังข่าวอื่นบนหน้าเดิม)
|
||||||
|
watch(() => route.params.id, (newId) => {
|
||||||
|
fetchNewsDetail(newId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for language changes and re-fetch the news detail
|
||||||
|
watch(() => appStore.isTh, () => {
|
||||||
|
fetchNewsDetail(route.params.id);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* หากใช้ DaisyUI/TailwindCSS ก็ไม่จำเป็นต้องมี style scoped มากนัก */
|
||||||
|
.prose {
|
||||||
|
/* Tailwind CSS's @tailwindcss/typography plugin provides 'prose' class for nicely formatted text.
|
||||||
|
If you haven't installed it, these styles can be adjusted manually or by adding the plugin. */
|
||||||
|
max-width: 80ch; /* Limit line length for readability */
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.whitespace-pre-line {
|
||||||
|
white-space: pre-line; /* Keeps line breaks from your data */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
72
src/views/DashboardView.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-4 md:p-8">
|
||||||
|
<h1 class="text-4xl font-bold text-center mb-8">Dashboard Overview</h1>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||||
|
<div class="col-span-1">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">World News</h2>
|
||||||
|
<div class="card bg-base-100 shadow-xl p-4">
|
||||||
|
<p>This is where your World News panel would go, converted from NewsPanel.jsx.</p>
|
||||||
|
<img src="/images/Welcome_Page_680728.jpg" alt="News Video" class="w-full h-auto mt-4 rounded-lg">
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Example video thumbnail from World News section.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">World Statistics</h2>
|
||||||
|
<div class="card bg-base-100 shadow-xl p-4">
|
||||||
|
<p>This is where your Statistics panel would go, converted from StatsPanel.jsx.</p>
|
||||||
|
<ul class="list-disc list-inside mt-4">
|
||||||
|
<li>Market Review</li>
|
||||||
|
<li>Currencies</li>
|
||||||
|
<li>Population</li>
|
||||||
|
<li>Energy</li>
|
||||||
|
<li>Russian Economy</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<div class="col-span-1">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">Active Satellites</h2>
|
||||||
|
<div class="card bg-base-300 shadow-xl p-4 min-h-[400px] flex items-center justify-center">
|
||||||
|
<p class="text-lg text-gray-500">
|
||||||
|
[3D Globe Simulation Placeholder - Convert EarthBody.jsx, GlobeCanvas.jsx, etc. here]
|
||||||
|
</p>
|
||||||
|
<img src="/images/Welcome_Page_680728.jpg" alt="3D Globe" class="absolute w-full h-full object-cover opacity-70">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">Satellite Details</h2>
|
||||||
|
<div class="card bg-base-100 shadow-xl p-4">
|
||||||
|
<ul class="list-disc list-inside">
|
||||||
|
<li>ISS (1.05R, 51°)</li>
|
||||||
|
<li>Hubble Space Telescope (1.07R, 28.5°)</li>
|
||||||
|
<li>Starlink-123 (1.10R, 53.0°)</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-sm text-gray-600 mt-4">
|
||||||
|
Details for active satellites would be displayed here, possibly from PopUpDetail.jsx.
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 p-2 bg-base-200 rounded">
|
||||||
|
<p><strong>Hubble Space Telescope:</strong> </p>
|
||||||
|
<p class="text-sm">The Hubble Space Telescope is a space telescope that was launched into low Earth orbit in 1990 and remains in operation.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// import NewsPanel from '@/components/NewsPanel.vue';
|
||||||
|
// import StatsPanel from '@/components/StatsPanel.vue';
|
||||||
|
// import GlobeCanvas from '@/components/GlobeCanvas.vue'; // This and related components need careful conversion
|
||||||
|
|
||||||
|
// No specific data or logic needed for this mock view,
|
||||||
|
// as it primarily contains placeholders for other components.
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Specific styles for DashboardView if needed */
|
||||||
|
</style>
|
||||||
28
src/views/HomeView.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// src/views/HomeView.vue (สำหรับแสดงผลบน DefaultLayout)
|
||||||
|
<template>
|
||||||
|
<div class="home-page-container">
|
||||||
|
<Calousels />
|
||||||
|
<News />
|
||||||
|
<TabNews />
|
||||||
|
<BannerCalousels />
|
||||||
|
<WebLinks />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// Import Component ที่จำเป็นสำหรับ HomeView นี้
|
||||||
|
import Calousels from '@/components/Calousels.vue';
|
||||||
|
import News from '@/components/News.vue';
|
||||||
|
import TabNews from '@/components/TabNews.vue';
|
||||||
|
import BannerCalousels from '@/components/BannerCalousels.vue';
|
||||||
|
import WebLinks from '@/components/WebLinks.vue';
|
||||||
|
|
||||||
|
// เป็น Vue.js ปกติ ให้จัดการด้วย Navigation Guards ใน router/index.js
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-page-container {
|
||||||
|
/* เพิ่มสไตล์สำหรับ container ของหน้า Home (เช่น padding) */
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
100
src/views/LandingPageView.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// src/views/LandingPageView.vue
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto p-4 md:p-8">
|
||||||
|
<div class="flex flex-wrap -mx-4">
|
||||||
|
<div class="w-full px-4">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<template v-if="check === true">
|
||||||
|
<a
|
||||||
|
v-if="landingPageViewStore.mainBannerData && landingPageViewStore.mainBannerData.link"
|
||||||
|
:href="`//${landingPageViewStore.mainBannerData.link}`"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-block"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="mainBannerSrc"
|
||||||
|
alt="Main Banner (Clickable)"
|
||||||
|
class="max-w-[980px] max-h-[600px] w-full h-auto transition-transform duration-500 transform ease-in-out hover:scale-105"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<img
|
||||||
|
v-else-if="mainBannerSrc"
|
||||||
|
:src="mainBannerSrc"
|
||||||
|
alt="Main Banner"
|
||||||
|
class="max-w-[980px] max-h-[600px] w-full h-auto"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<router-link to="/home" class="btn btn-lg mt-4 normal-case
|
||||||
|
bg-yellow-500 text-white /* สีพื้นหลังเหลือง */
|
||||||
|
hover:bg-yellow-600 /* สีพื้นหลังเมื่อโฮเวอร์ */
|
||||||
|
active:bg-yellow-700 /* สีพื้นหลังเมื่อคลิก */
|
||||||
|
focus:ring-2 focus:ring-yellow-400 focus:ring-offset-2 /* Focus ring */
|
||||||
|
shadow-md /* เพิ่มเงาเล็กน้อย */
|
||||||
|
rounded-lg /* ทำให้โค้งมนขึ้นเล็กน้อย */
|
||||||
|
">
|
||||||
|
เข้าสู่เว็บไซต์
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
// เปลี่ยนมา import useLandingPageViewStore
|
||||||
|
import { useLandingPageViewStore } from '@/stores/landingPageView';
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
|
||||||
|
// เปลี่ยนมาใช้ useLandingPageViewStore
|
||||||
|
const landingPageViewStore = useLandingPageViewStore();
|
||||||
|
|
||||||
|
const check = ref(false);
|
||||||
|
|
||||||
|
const mainBannerSrc = computed(() => {
|
||||||
|
// ดึงข้อมูลจาก landingPageViewStore
|
||||||
|
const data = landingPageViewStore.mainBannerData;
|
||||||
|
if (data && data.url) { // ตรวจสอบ .url โดยตรงจาก mainBannerData
|
||||||
|
return `${landingPageViewStore.imageBaseUrl}${data.url}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => { // คง async ไว้ ถ้ามี fetchLandingPageData จริงๆ
|
||||||
|
// เรียก fetchLandingPageData ของ Store เฉพาะ
|
||||||
|
await landingPageViewStore.fetchLandingPageData();
|
||||||
|
|
||||||
|
const mainBannerConfig = landingPageViewStore.mainBannerData;
|
||||||
|
|
||||||
|
if (mainBannerConfig && mainBannerConfig.startDate && mainBannerConfig.endDate) {
|
||||||
|
const parseDateString = (dateString) => {
|
||||||
|
const parts = dateString.split('/');
|
||||||
|
return new Date(parseInt(parts[2]), parseInt(parts[0]) - 1, parseInt(parts[1]));
|
||||||
|
};
|
||||||
|
|
||||||
|
let from = parseDateString(mainBannerConfig.startDate);
|
||||||
|
let to = parseDateString(mainBannerConfig.endDate);
|
||||||
|
let today = new Date();
|
||||||
|
let dd = String(today.getDate()).padStart(2, '0');
|
||||||
|
let mm = String(today.getMonth() + 1).padStart(2, '0');
|
||||||
|
let yyyy = today.getFullYear();
|
||||||
|
let currentDate = `${mm}/${dd}/${yyyy}`;
|
||||||
|
let checkDate = parseDateString(currentDate);
|
||||||
|
|
||||||
|
check.value = checkDate >= from && checkDate <= to;
|
||||||
|
} else {
|
||||||
|
check.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugging logs:
|
||||||
|
console.log('Main Banner Data from LandingPageViewStore:', landingPageViewStore.mainBannerData);
|
||||||
|
console.log('Main Banner Image Source:', mainBannerSrc.value);
|
||||||
|
console.log('Check date result (check.value):', check.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* สไตล์ Tailwind CSS และ DaisyUI จะจัดการเรื่องความสวยงามเป็นหลัก */
|
||||||
|
</style>
|
||||||
133
src/views/NewsCategoryView.vue
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// src/views/NewsCategoryView.vue
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto p-4 md:p-8">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-gray-900 mb-6 border-b pb-4">
|
||||||
|
{{ appStore.checkLang.isTh ? `ข่าวสารหมวดหมู่: ${categoryTitleTh}` : `News Category: ${categoryTitleEn}` }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div v-if="newsList.length > 0" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<div
|
||||||
|
v-for="item in newsList"
|
||||||
|
:key="item.id"
|
||||||
|
class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<router-link :to="`/content/${item.id}`" class="block">
|
||||||
|
<div class="w-full h-48 overflow-hidden">
|
||||||
|
<img
|
||||||
|
v-if="item.image && item.image.url"
|
||||||
|
class="w-full h-full object-cover transition-transform duration-300 hover:scale-105"
|
||||||
|
:src="`${appStore.imageBaseUrl}${item.image.url}`"
|
||||||
|
: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">
|
||||||
|
No Image
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<h3 class="font-semibold text-lg text-gray-800 hover:text-primary-focus leading-tight mb-2">
|
||||||
|
{{ appStore.checkLang.isTh ? item.title_th : item.title_en }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 leading-snug mb-3">
|
||||||
|
{{ truncateDetail(appStore.checkLang.isTh ? item.detail_th : item.detail_en, 120) }}
|
||||||
|
</p>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
<span v-if="item.release_date">{{ formatDate(item.release_date) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center text-gray-600 py-16">
|
||||||
|
<p class="text-2xl font-semibold mb-4">
|
||||||
|
{{ appStore.checkLang.isTh ? 'ไม่พบข่าวสารในหมวดหมู่นี้ หรือข่าวสารนี้ไม่พร้อมใช้งานในภาษาปัจจุบัน' : 'No news found in this category 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 newsList = ref([]);
|
||||||
|
|
||||||
|
// Computed property เพื่อแสดงชื่อหมวดหมู่ตามภาษา
|
||||||
|
const categoryTitleTh = computed(() => {
|
||||||
|
// เพิ่มการตรวจสอบว่า appStore.tabs มีอยู่จริงหรือไม่ และเป็น Array หรือไม่
|
||||||
|
if (appStore.tabs && Array.isArray(appStore.tabs) && appStore.tabs.length > 0) {
|
||||||
|
const tab = appStore.tabs.find(t => t.category === route.params.category);
|
||||||
|
return tab ? tab.title : 'ไม่ระบุหมวดหมู่';
|
||||||
|
}
|
||||||
|
return 'กำลังโหลด...';
|
||||||
|
});
|
||||||
|
|
||||||
|
const categoryTitleEn = computed(() => {
|
||||||
|
if (appStore.tabs && Array.isArray(appStore.tabs) && appStore.tabs.length > 0) {
|
||||||
|
const tab = appStore.tabs.find(t => t.category === route.params.category);
|
||||||
|
return tab ? tab.title_en : 'Unspecified Category';
|
||||||
|
}
|
||||||
|
return 'Loading...';
|
||||||
|
});
|
||||||
|
|
||||||
|
const truncateDetail = (text, maxLength) => {
|
||||||
|
if (!text) return '';
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return text.substring(0, maxLength) + '...';
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchNewsForCategory = async (category) => {
|
||||||
|
try {
|
||||||
|
// เพราะ appStore.find ควรจะมีการกรอง active, active_en, release_date และ feature ให้เรียบร้อยแล้ว
|
||||||
|
const allNews = await appStore.find('tabNews', ''); // ดึงข่าวทั้งหมดก่อน เพื่อให้ filter ได้ถูกต้อง
|
||||||
|
|
||||||
|
const filteredNews = allNews.filter(item => {
|
||||||
|
return item.type === category;
|
||||||
|
});
|
||||||
|
|
||||||
|
newsList.value = filteredNews;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching news for category ${category}:`, error);
|
||||||
|
newsList.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchNewsForCategory(route.params.category);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => route.params.category, (newCategory) => {
|
||||||
|
fetchNewsForCategory(newCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for language changes and re-fetch the news list
|
||||||
|
watch(() => appStore.checkLang.isTh, () => { // **แก้ไขตรงนี้**
|
||||||
|
fetchNewsForCategory(route.params.category);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
60
src/views/NotFoundView.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// src/views/NotFoundView.vue
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full space-y-8 text-center bg-white dark:bg-gray-800 p-8 rounded-lg shadow-xl">
|
||||||
|
<div class="text-9xl font-extrabold text-blue-600 dark:text-blue-400 animate-bounce">404</div>
|
||||||
|
<h1 class="mt-6 text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||||
|
{{ appStore.checkLang.isTh ? 'ไม่พบหน้าดังกล่าว' : 'Page Not Found' }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-lg text-gray-600 dark:text-gray-300">
|
||||||
|
{{ appStore.checkLang.isTh ? 'ขออภัย, เราไม่พบหน้าที่คุณกำลังมองหา' : 'Sorry, we couldn\'t find the page you\'re looking for.' }}
|
||||||
|
</p>
|
||||||
|
<div class="mt-6">
|
||||||
|
<router-link
|
||||||
|
to="/"
|
||||||
|
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-full shadow-sm text-white bg-blue-600 hover:bg-blue-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"></path>
|
||||||
|
</svg>
|
||||||
|
{{ appStore.checkLang.isTh ? 'กลับหน้าหลัก' : 'Go to Homepage' }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ appStore.checkLang.isTh ? 'หากคุณคิดว่านี่เป็นข้อผิดพลาด โปรดติดต่อผู้ดูแลระบบ' : 'If you believe this is an error, please contact the administrator.' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAppStore } from '@/stores/app'; // Assuming you have appStore for language
|
||||||
|
import { RouterLink } from 'vue-router'; // Required for <router-link>
|
||||||
|
|
||||||
|
const appStore = useAppStore(); // Initialize appStore
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Optional: Add custom keyframes for bounce animation if not using utility classes directly */
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(-25%);
|
||||||
|
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(0);
|
||||||
|
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animate-bounce {
|
||||||
|
animation: bounce 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: Dark mode adjustments */
|
||||||
|
.dark\:bg-gray-900 { background-color: #1a202c; }
|
||||||
|
.dark\:bg-gray-800 { background-color: #2d3748; }
|
||||||
|
.dark\:text-blue-400 { color: #63b3ed; }
|
||||||
|
.dark\:text-white { color: #ffffff; }
|
||||||
|
.dark\:text-gray-300 { color: #cbd5e0; }
|
||||||
|
.dark\:text-gray-400 { color: #a0aec0; }
|
||||||
|
</style>
|
||||||
17
vite.config.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// vite.config.js
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import { fileURLToPath, URL } from 'node:url'; // <<< ต้องเพิ่มบรรทัดนี้
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
tailwindcss(), // ตรวจสอบว่า @tailwindcss/vite ถูกติดตั้งและใช้ถูกต้อง
|
||||||
|
vue()
|
||||||
|
],
|
||||||
|
resolve: { // จะได้ใช้ import ... from '@/...'; ได้
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||