feat:實作 Member Portal MVP 前端與後端整合
後端: - 新增 DivingOffer Model / DivingOfferController(列表+詳情 API,支援搜尋/篩選/分頁) - 修正 Google OAuth callback 改為 redirect 至前端(SocialAuthController) - 新增 config/cors.php 允許前端 origin - .gitignore 新增 frontend/ 排除規則 前端(frontend/): - Vue 3 + Vite + Tailwind CSS + Pinia + Vue Router - 頁面:首頁、課程列表、課程詳情、登入、註冊、個人資料、OAuth callback - 整合至 Docker(multi-stage build,nginx 靜態服務於 port 5173) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import api from '../api/axios'
|
||||
import CourseCard from '../components/CourseCard.vue'
|
||||
|
||||
const offers = ref([])
|
||||
const meta = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const search = ref('')
|
||||
const region = ref('')
|
||||
|
||||
const REGIONS = ['北部', '中部', '南部', '東部', '離島']
|
||||
|
||||
async function fetchOffers(page = 1) {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const params = { page, per_page: 12 }
|
||||
if (search.value) params.q = search.value
|
||||
if (region.value) params.region = region.value
|
||||
|
||||
const res = await api.get('/diving-offers', { params })
|
||||
offers.value = res.data.data
|
||||
meta.value = res.data.meta
|
||||
} catch {
|
||||
error.value = '無法載入課程,請稍後再試。'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() { fetchOffers(1) }
|
||||
function onRegion() { fetchOffers(1) }
|
||||
|
||||
onMounted(() => fetchOffers())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="max-w-6xl mx-auto px-4 py-10">
|
||||
<h1 class="text-3xl font-bold text-gray-800 mb-6">探索潛水課程</h1>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-3 mb-8">
|
||||
<input
|
||||
v-model="search"
|
||||
@keyup.enter="onSearch"
|
||||
type="text"
|
||||
placeholder="搜尋課程名稱、地點..."
|
||||
class="flex-1 border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-ocean-400"
|
||||
/>
|
||||
<button
|
||||
@click="onSearch"
|
||||
class="bg-ocean-700 text-white px-6 py-2 rounded-lg hover:bg-ocean-600 transition"
|
||||
>
|
||||
搜尋
|
||||
</button>
|
||||
<select
|
||||
v-model="region"
|
||||
@change="onRegion"
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ocean-400"
|
||||
>
|
||||
<option value="">所有地區</option>
|
||||
<option v-for="r in REGIONS" :key="r" :value="r">{{ r }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="text-center text-gray-400 py-20">載入中...</div>
|
||||
|
||||
<div v-else-if="error" class="text-center text-red-500 py-20">{{ error }}</div>
|
||||
|
||||
<div v-else-if="offers.length === 0" class="text-center text-gray-400 py-20">
|
||||
😢 找不到符合的課程,試試其他關鍵字
|
||||
</div>
|
||||
|
||||
<div v-else class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<CourseCard v-for="offer in offers" :key="offer.id" :offer="offer" />
|
||||
</div>
|
||||
|
||||
<div v-if="meta && meta.last_page > 1" class="flex justify-center gap-2 mt-10">
|
||||
<button
|
||||
v-for="p in meta.last_page"
|
||||
:key="p"
|
||||
@click="fetchOffers(p)"
|
||||
:class="[
|
||||
'px-3 py-1 rounded-lg border transition',
|
||||
p === meta.current_page
|
||||
? 'bg-ocean-700 text-white border-ocean-700'
|
||||
: 'border-gray-300 text-gray-600 hover:bg-gray-100'
|
||||
]"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
Reference in New Issue
Block a user