init
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { RouterView, useRouter, useRoute } from 'vue-router'
|
||||
import { Anchor, ShieldAlert, HeartHandshake, Menu, X } from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const mobileMenuOpen = ref(false)
|
||||
|
||||
const navItems = [
|
||||
{ name: '메인 홈', path: '/', icon: Anchor },
|
||||
{ name: '캠페인 소개', path: '/about', icon: ShieldAlert },
|
||||
{ name: '희망 기부', path: '/donation', icon: HeartHandshake }
|
||||
]
|
||||
|
||||
const navigateTo = (path: string) => {
|
||||
router.push(path)
|
||||
mobileMenuOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<!-- Header -->
|
||||
<header class="app-header">
|
||||
<div class="header-logo" @click="navigateTo('/')">
|
||||
<div class="logo-icon-wrapper">
|
||||
<Anchor class="logo-icon" />
|
||||
</div>
|
||||
<div class="logo-text">
|
||||
<span class="logo-title">희망의 바다</span>
|
||||
<span class="logo-subtitle">수협중앙회</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Nav -->
|
||||
<nav class="desktop-nav">
|
||||
<button
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:class="['nav-link', { active: route.path === item.path }]"
|
||||
@click="navigateTo(item.path)"
|
||||
>
|
||||
<component :is="item.icon" class="nav-icon" />
|
||||
<span>{{ item.name }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Mobile Nav Toggle -->
|
||||
<button class="mobile-nav-toggle" @click="mobileMenuOpen = !mobileMenuOpen">
|
||||
<Menu v-if="!mobileMenuOpen" />
|
||||
<X v-else />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Mobile Nav Menu -->
|
||||
<transition name="slide-fade">
|
||||
<nav v-if="mobileMenuOpen" class="mobile-nav">
|
||||
<button
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:class="['mobile-nav-link', { active: route.path === item.path }]"
|
||||
@click="navigateTo(item.path)"
|
||||
>
|
||||
<component :is="item.icon" class="nav-icon" />
|
||||
<span>{{ item.name }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</transition>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="main-content">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</RouterView>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="app-footer">
|
||||
<div class="footer-content">
|
||||
<p class="footer-title">수협중앙회 희망의 바다 만들기 운동본부</p>
|
||||
<p class="footer-text">© 2026 수협중앙회. All rights reserved. 깨끗하고 풍요로운 바다, 함께 만들어갑니다.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 전역 스코프 및 공통 레이아웃 스타일 정의 */
|
||||
:root {
|
||||
--primary-color: #00d2ff;
|
||||
--primary-rgb: 0, 210, 255;
|
||||
--secondary-color: #0066ff;
|
||||
--accent-color: #ff6b6b;
|
||||
--dark-bg: #030814;
|
||||
--glass-bg: rgba(6, 17, 38, 0.65);
|
||||
--glass-border: rgba(0, 210, 255, 0.15);
|
||||
--text-color: #e0f2fe;
|
||||
}
|
||||
|
||||
body {
|
||||
background: radial-gradient(circle at center, #061530 0%, #030814 100%) !important;
|
||||
color: var(--text-color);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0 !important; /* Wails 기본 padding 덮어쓰기 */
|
||||
}
|
||||
|
||||
/* Wails default background override */
|
||||
.bg {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 70px;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 40px;
|
||||
z-index: 100;
|
||||
--wails-draggable: drag;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
--wails-draggable: no-drag;
|
||||
}
|
||||
|
||||
.logo-icon-wrapper {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 15px rgba(0, 210, 255, 0.4);
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
color: white;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-3px); }
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.5px;
|
||||
background: linear-gradient(to right, #ffffff, var(--primary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.logo-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.desktop-nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
--wails-draggable: no-drag;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--primary-color);
|
||||
background: rgba(0, 210, 255, 0.08);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: white;
|
||||
background: linear-gradient(135deg, rgba(0, 210, 255, 0.2), rgba(0, 102, 255, 0.2));
|
||||
border: 1px solid var(--primary-color);
|
||||
box-shadow: 0 0 15px rgba(0, 210, 255, 0.25);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.mobile-nav-toggle {
|
||||
display: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
--wails-draggable: no-drag;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-top: 70px;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
background: #020611;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding: 30px 40px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.footer-title {
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade-enter-active,
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.desktop-nav {
|
||||
display: none;
|
||||
}
|
||||
.mobile-nav-toggle {
|
||||
display: block;
|
||||
}
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
}
|
||||
.mobile-nav {
|
||||
position: fixed;
|
||||
top: 70px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
gap: 8px;
|
||||
z-index: 99;
|
||||
}
|
||||
.mobile-nav-link {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.mobile-nav-link.active {
|
||||
color: white;
|
||||
background: rgba(0, 210, 255, 0.15);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, useTemplateRef} from 'vue'
|
||||
import {Events, WML} from "@wailsio/runtime";
|
||||
import {GreetService} from "../../bindings/changeme";
|
||||
|
||||
// Show the actual Wails version this project was generated against.
|
||||
const wailsVersion = "v3.0.0-alpha2.106";
|
||||
|
||||
const name = ref('');
|
||||
const time = ref('Listening for Time event...');
|
||||
|
||||
const titleNameRef = useTemplateRef<HTMLElement>('titleNameRef');
|
||||
const toastRef = useTemplateRef<HTMLElement>('toastRef');
|
||||
const resultRef = useTemplateRef<HTMLElement>('resultRef');
|
||||
let toastTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
// Crossfade the framework word in the heading ("Wails + Vue") to the name the
|
||||
// user entered ("Wails + <name>"): the old word fades out while the new one
|
||||
// fades in over the same spot.
|
||||
function swapTitleName(newName: string) {
|
||||
const titleNameElement = titleNameRef.value;
|
||||
if (!titleNameElement) {
|
||||
return;
|
||||
}
|
||||
const current = titleNameElement.querySelector('.title-name-text:not(.is-outgoing)');
|
||||
if (!current || current.textContent === newName) {
|
||||
return;
|
||||
}
|
||||
const incoming = document.createElement('span');
|
||||
incoming.className = 'title-name-text is-entering';
|
||||
incoming.textContent = newName;
|
||||
current.classList.add('is-outgoing');
|
||||
titleNameElement.appendChild(incoming);
|
||||
// Force a reflow so the transitions run from the starting state.
|
||||
void incoming.offsetWidth;
|
||||
incoming.classList.remove('is-entering');
|
||||
current.classList.add('is-leaving');
|
||||
current.addEventListener('transitionend', () => current.remove(), {once: true});
|
||||
}
|
||||
|
||||
// Pop the toast with the message Go returned, then auto-dismiss it.
|
||||
function showToast(message: string) {
|
||||
if (!resultRef.value || !toastRef.value) {
|
||||
return;
|
||||
}
|
||||
resultRef.value.innerText = message;
|
||||
toastRef.value.classList.add('is-visible');
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => toastRef.value?.classList.remove('is-visible'), 4000);
|
||||
}
|
||||
|
||||
const doGreet = () => {
|
||||
let n = name.value || 'anonymous';
|
||||
swapTitleName(n);
|
||||
GreetService.Greet(n).then(showToast).catch(console.error);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
Events.On('time', (timeValue: { data: string }) => {
|
||||
// On a narrow screen the full RFC1123 stamp is too wide for the footer, so
|
||||
// show just the clock time there (matching the CSS breakpoint).
|
||||
const full = timeValue.data;
|
||||
const compact = (full.match(/\d{1,2}:\d{2}:\d{2}/) || [full])[0];
|
||||
time.value = window.matchMedia('(max-width: 640px)').matches ? compact : full;
|
||||
});
|
||||
// Wire up data-wml-openURL links (logos + footer "Docs" link).
|
||||
WML.Reload();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="container">
|
||||
<header class="brand">
|
||||
<a class="brand-mark" data-wml-openURL="https://v3.wails.io" aria-label="Wails website">
|
||||
<img src="/wails.png" class="brand-logo" alt="Wails logo"/>
|
||||
</a>
|
||||
<a class="brand-badge" data-wml-openURL="https://vuejs.org/" aria-label="Vue">
|
||||
<img src="/vue.svg" alt="Vue logo"/>
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<h1 class="title"><span class="title-accent">Wails +</span> <span class="title-name" ref="titleNameRef"><span class="title-name-text">Vue</span></span></h1>
|
||||
<p class="subtitle">Build beautiful cross-platform apps with Go and Vue.</p>
|
||||
|
||||
<div class="greet">
|
||||
<div class="input-box" id="input">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<input aria-label="input" class="input" id="name" v-model="name" type="text" placeholder="Your name" autocomplete="off"/>
|
||||
<button aria-label="greet-btn" class="btn" @click="doGreet">Greet
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<hr class="footer-divider"/>
|
||||
<footer class="footer">
|
||||
<span class="footer-version">{{ wailsVersion }}</span>
|
||||
<span class="footer-time">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<span id="time">{{ time }}</span>
|
||||
</span>
|
||||
<a class="footer-docs" data-wml-openURL="https://v3.wails.io" aria-label="Wails documentation">Docs
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/></svg>
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
<!-- Toast: shows the greeting returned by the Go backend (overlay, so it never
|
||||
reflows the layout). -->
|
||||
<div class="toast" id="toast" ref="toastRef" role="status" aria-live="polite">
|
||||
<span class="toast-label">From Go</span>
|
||||
<span aria-label="result" class="toast-msg" id="result" ref="resultRef"></span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import Home from '../views/Home.vue';
|
||||
import About from '../views/About.vue';
|
||||
import Donation from '../views/Donation.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: About
|
||||
},
|
||||
{
|
||||
path: '/donation',
|
||||
name: 'Donation',
|
||||
component: Donation
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,563 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowRight, Compass, Shield, Award, Users } from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const activeTab = ref('seed')
|
||||
|
||||
const activities = {
|
||||
seed: {
|
||||
title: '수산종자 방류 사업',
|
||||
subtitle: '수산자원의 회복과 복원',
|
||||
desc: '고갈되어 가는 우리 바다의 어족 자원을 늘리고 어업인의 소득을 보장하기 위해, 지역 해역 특성에 맞는 우수한 품종의 치어를 방류합니다.',
|
||||
points: [
|
||||
'방류 효과 분석을 바탕으로 한 경제성 높은 어종 선정',
|
||||
'전국 연안 지자체 및 회원조합과 공동으로 매년 추진',
|
||||
'감성돔, 조피볼락, 꽃게 등 대표 고부가가치 품종 방류',
|
||||
'어업인의 수산 소득 증대 및 미래 생태 식량 확보'
|
||||
],
|
||||
infographic: [
|
||||
{ step: '01', title: '우량 종자 확보', desc: '질병 검사를 마친 건강한 수산 치어 선별' },
|
||||
{ step: '02', title: '해역 적합도 평가', desc: '지역별 염도 및 온도에 알맞은 어종 연구' },
|
||||
{ step: '03', title: '체계적 방류', desc: '수산 자원 고갈 지점에서 집중 방류 실시' },
|
||||
{ step: '04', title: '추적 및 관리', desc: '방류된 치어의 생존율 및 자원조성 효과 모니터링' }
|
||||
]
|
||||
},
|
||||
cleanup: {
|
||||
title: '해양 쓰레기 수거 사업',
|
||||
subtitle: '조업 중 쓰레기 수매 및 수거 지원',
|
||||
desc: '바다 밑에 침적된 폐어구, 플라스틱 등 어업 활동을 방해하고 해양 생태계를 오염시키는 해양 쓰레기를 신속히 수거하여 깨끗한 환경을 만듭니다.',
|
||||
points: [
|
||||
'어업인 참여 유도를 위한 조업 중 인양 쓰레기 수매 사업 진행',
|
||||
'시민단체 및 연안 해안가 집중 정화 운동 전개',
|
||||
'침적 폐그물 수거를 통해 해양 동물의 유령어업(Ghost Fishing) 방지',
|
||||
'해양 플라스틱 오염 방지를 통한 미세 플라스틱 먹이사슬 차단'
|
||||
],
|
||||
infographic: [
|
||||
{ step: '01', title: '인양 및 수집', desc: '조업 중 수거한 쓰레기를 항구로 반입' },
|
||||
{ step: '02', title: '수협 수매 시스템', desc: '수협이 매입하여 쓰레기 회수율 극대화' },
|
||||
{ step: '03', title: '친환경 처리', desc: '재활용 가능 플라스틱과 폐어구 분류 처리' },
|
||||
{ step: '04', title: '바다 청정화', desc: '어장 환경의 개선과 선박 안전 운항 확보' }
|
||||
]
|
||||
},
|
||||
restoration: {
|
||||
title: '바닷속 환경 정화 사업',
|
||||
subtitle: '갯녹음 치유 및 생태계 복원',
|
||||
desc: '지구 온난화와 환경 오염으로 바다가 하얗게 사막화되는 갯녹음 현상(백화현상)을 방지하고, 해조류가 자라는 풍요로운 바다 숲을 만듭니다.',
|
||||
points: [
|
||||
'갯녹음 해역의 해적생물(성게 등) 집중 구제 및 유해 유기물 제거',
|
||||
'어장 환경 개선을 위한 바닥갈이(경운) 및 물갈이 사업 지원',
|
||||
'미생물 및 친환경 공법을 활용한 바다 해저 토양 정화',
|
||||
'바다숲 조성을 통해 수산 생물의 산란장 및 서식지 제공'
|
||||
],
|
||||
infographic: [
|
||||
{ step: '01', title: '현황 정밀 조사', desc: '갯녹음 및 바다 바닥 황폐화 상태 과학적 진단' },
|
||||
{ step: '02', title: '어장 바닥갈이', desc: '경운 작업을 통해 암반의 퇴적물과 유해물질 세척' },
|
||||
{ step: '03', title: '해조류 이식 및 관리', desc: '다시마, 감태 등 해조류를 안착시켜 바다숲 형성' },
|
||||
{ step: '04', title: '풍요로운 어장 복원', desc: '어패류가 산란하고 성장할 수 있는 보금자리 완성' }
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="about-container">
|
||||
<!-- Intro Hero -->
|
||||
<section class="about-hero">
|
||||
<div class="glow-bg"></div>
|
||||
<div class="hero-content">
|
||||
<h1 class="about-title">바다를 살리는 3가지 약속</h1>
|
||||
<p class="about-subtitle-text">
|
||||
수협중앙회는 2007년부터 전국의 어업인들과 힘을 합쳐,<br />
|
||||
바다의 수산 자원을 회복하고 해양 환경을 맑게 가꾸는 공익적 실천을 이어가고 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabbed Activities Grid -->
|
||||
<section class="activities-section">
|
||||
<div class="tab-buttons">
|
||||
<button
|
||||
:class="['tab-btn', { active: activeTab === 'seed' }]"
|
||||
@click="activeTab = 'seed'"
|
||||
>
|
||||
<Compass class="tab-icon" />
|
||||
<span>수산종자 방류</span>
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-btn', { active: activeTab === 'cleanup' }]"
|
||||
@click="activeTab = 'cleanup'"
|
||||
>
|
||||
<Shield class="tab-icon" />
|
||||
<span>쓰레기 수거</span>
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-btn', { active: activeTab === 'restoration' }]"
|
||||
@click="activeTab = 'restoration'"
|
||||
>
|
||||
<Award class="tab-icon" />
|
||||
<span>바닷속 환경 정화</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content Area -->
|
||||
<transition name="fade" mode="out-in">
|
||||
<div :key="activeTab" class="tab-content">
|
||||
<div class="content-left">
|
||||
<h2 class="content-title">{{ activities[activeTab].title }}</h2>
|
||||
<h3 class="content-subtitle">{{ activities[activeTab].subtitle }}</h3>
|
||||
<p class="content-desc">{{ activities[activeTab].desc }}</p>
|
||||
<ul class="point-list">
|
||||
<li v-for="(point, idx) in activities[activeTab].points" :key="idx" class="point-item">
|
||||
<span class="bullet"></span>
|
||||
<span>{{ point }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="content-right">
|
||||
<h4 class="right-title">추진 프로세스</h4>
|
||||
<div class="infographic-flow">
|
||||
<div
|
||||
v-for="step in activities[activeTab].infographic"
|
||||
:key="step.step"
|
||||
class="flow-step"
|
||||
>
|
||||
<div class="step-num">{{ step.step }}</div>
|
||||
<div class="step-info">
|
||||
<h5 class="step-title">{{ step.title }}</h5>
|
||||
<p class="step-desc">{{ step.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</section>
|
||||
|
||||
<!-- Citizen Campaign Info -->
|
||||
<section class="citizen-section">
|
||||
<div class="citizen-card">
|
||||
<div class="citizen-icon-wrapper">
|
||||
<Users class="citizen-icon" />
|
||||
</div>
|
||||
<div class="citizen-info">
|
||||
<span class="citizen-badge">국민 참여 프로그램</span>
|
||||
<h2 class="citizen-title">모두의 바다, 함께海 캠페인</h2>
|
||||
<p class="citizen-desc">
|
||||
해양 환경 정화에 관심이 있는 학생, 일반 시민 모임, 동호회가 바닷가 정화 활동을 실천할 수 있도록 수협중앙회가 활동 경비(최대 100만 원)를 지원합니다.
|
||||
</p>
|
||||
<div class="citizen-benefits">
|
||||
<div class="benefit-item">
|
||||
<div class="benefit-dot"></div>
|
||||
<span>전국 항·포구 및 해안가 정화 단체 지원</span>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<div class="benefit-dot"></div>
|
||||
<span>청소년 교육 및 환경 인식 제고</span>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<div class="benefit-dot"></div>
|
||||
<span>기금 기부를 통한 가상의 바다 정화 동참</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="router.push('/donation')">
|
||||
<span>참여하고 바다 가꾸기</span>
|
||||
<ArrowRight class="btn-icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.about-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 80px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* About Hero */
|
||||
.about-hero {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
border-radius: 24px;
|
||||
background: rgba(6, 17, 38, 0.4);
|
||||
border: 1px solid rgba(0, 210, 255, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glow-bg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(0, 210, 255, 0.15) 0%, transparent 70%);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.about-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 16px 0;
|
||||
background: linear-gradient(to right, #ffffff, var(--primary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.about-subtitle-text {
|
||||
font-size: 1.1rem;
|
||||
color: #94a3b8;
|
||||
line-height: 1.6;
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Activities Section */
|
||||
.activities-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.tab-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: rgba(6, 17, 38, 0.6);
|
||||
border: 1px solid rgba(0, 210, 255, 0.1);
|
||||
padding: 16px 30px;
|
||||
border-radius: 16px;
|
||||
color: #94a3b8;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
border-color: rgba(0, 210, 255, 0.3);
|
||||
color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: linear-gradient(135deg, rgba(0, 210, 255, 0.15), rgba(0, 102, 255, 0.15));
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
box-shadow: 0 5px 20px rgba(0, 210, 255, 0.15);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
gap: 40px;
|
||||
background: rgba(6, 17, 38, 0.4);
|
||||
border: 1px solid rgba(0, 210, 255, 0.08);
|
||||
border-radius: 24px;
|
||||
padding: 40px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.content-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.content-subtitle {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.content-desc {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.point-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.point-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
color: #cbd5e1;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bullet {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
margin-top: 8px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 8px var(--primary-color);
|
||||
}
|
||||
|
||||
/* Right Column Infographic Flow */
|
||||
.content-right {
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding-left: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.right-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
margin: 0 0 20px 0;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.infographic-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flow-step:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 17px;
|
||||
width: 2px;
|
||||
height: calc(100% - 10px);
|
||||
background: linear-gradient(to bottom, var(--primary-color), rgba(0, 102, 255, 0.1));
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 800;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 10px rgba(0, 210, 255, 0.3);
|
||||
}
|
||||
|
||||
.step-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Citizen Campaign Info */
|
||||
.citizen-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.citizen-card {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 40px;
|
||||
background: radial-gradient(circle at bottom right, rgba(0, 102, 255, 0.1) 0%, rgba(3, 8, 20, 0.8) 80%);
|
||||
border: 1px solid rgba(0, 210, 255, 0.15);
|
||||
border-radius: 24px;
|
||||
padding: 50px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.citizen-icon-wrapper {
|
||||
background: linear-gradient(135deg, rgba(0, 210, 255, 0.1), rgba(0, 102, 255, 0.1));
|
||||
border: 1px solid rgba(0, 210, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.citizen-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: var(--primary-color);
|
||||
animation: pulse 2s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); filter: drop-shadow(0 0 10px rgba(0, 210, 255, 0.2)); }
|
||||
100% { transform: scale(1.05); filter: drop-shadow(0 0 20px rgba(0, 210, 255, 0.5)); }
|
||||
}
|
||||
|
||||
.citizen-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.citizen-badge {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #ff6b6b;
|
||||
background: rgba(255, 107, 107, 0.12);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.citizen-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.citizen-desc {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.citizen-benefits {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.benefit-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #cbd5e1;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.benefit-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #ff6b6b;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 992px) {
|
||||
.tab-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
.content-right {
|
||||
border-left: none;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding-left: 0;
|
||||
padding-top: 30px;
|
||||
}
|
||||
.citizen-card {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
padding: 30px;
|
||||
}
|
||||
.citizen-icon-wrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
.citizen-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tab-buttons {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.tab-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,784 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { Heart, Send, CheckCircle2, MessageSquare, Shield, Fish, Trash2, HelpCircle } from 'lucide-vue-next'
|
||||
|
||||
const stats = ref({
|
||||
totalDonors: 12543,
|
||||
totalAmount: 627150000,
|
||||
releasedFish: 4200000,
|
||||
collectedWaste: 185000,
|
||||
purifiedArea: 95000
|
||||
})
|
||||
|
||||
const recentDonations = ref<any[]>([])
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
amount: 30000,
|
||||
message: ''
|
||||
})
|
||||
|
||||
const showSuccess = ref(false)
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const predefinedAmounts = [10000, 30000, 50000, 100000, 300000]
|
||||
|
||||
// Real-time impact simulation computed values
|
||||
const simulatedImpact = computed(() => {
|
||||
const amount = form.value.amount || 0
|
||||
const multiplier = amount / 10000
|
||||
return {
|
||||
fish: Math.floor(multiplier * 2),
|
||||
waste: (multiplier * 0.5).toFixed(1),
|
||||
area: (multiplier * 0.2).toFixed(2)
|
||||
}
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const CampaignService = await import('../../bindings/changeme/campaignservice')
|
||||
const currentStats = await CampaignService.GetStats()
|
||||
if (currentStats) stats.value = currentStats
|
||||
const list = await CampaignService.GetRecentDonations()
|
||||
if (list) {
|
||||
// Reverse list to show newest first
|
||||
recentDonations.value = [...list].reverse()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Wails bindings not available, using mock local data.', e)
|
||||
// Local mock data fallback
|
||||
recentDonations.value = [
|
||||
{ name: '김어부', amount: 50000, message: '바다가 다시 풍요로워지길 바랍니다.', timestamp: new Date(Date.now() - 10 * 60000).toISOString() },
|
||||
{ name: '이바다', amount: 30000, message: '깨끗한 바다를 우리 아이들에게!', timestamp: new Date(Date.now() - 60 * 60000).toISOString() },
|
||||
{ name: '박수협', amount: 100000, message: '어업인 여러분 힘내세요!', timestamp: new Date(Date.now() - 180 * 60000).toISOString() }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
const handleDonate = async () => {
|
||||
if (form.value.amount <= 0) return
|
||||
isSubmitting.value = true
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
const CampaignService = await import('../../bindings/changeme/campaignservice')
|
||||
const newStats = await CampaignService.Donate(form.value.name, form.value.amount, form.value.message)
|
||||
if (newStats) stats.value = newStats
|
||||
|
||||
// Refresh donation list
|
||||
const list = await CampaignService.GetRecentDonations()
|
||||
if (list) recentDonations.value = [...list].reverse()
|
||||
|
||||
showSuccessModal()
|
||||
} catch (e) {
|
||||
console.warn('Wails backend Donate call skipped. Simulating successfully on frontend.', e)
|
||||
// Simulating locally if not running in Wails
|
||||
stats.value.totalDonors += 1
|
||||
stats.value.totalAmount += form.value.amount
|
||||
|
||||
const multiplier = form.value.amount / 10000
|
||||
stats.value.releasedFish += Math.floor(multiplier * 2)
|
||||
stats.value.collectedWaste += Math.floor(multiplier * 0.5)
|
||||
stats.value.purifiedArea += Math.floor(multiplier * 0.2)
|
||||
|
||||
recentDonations.value.unshift({
|
||||
name: form.value.name || '익명의 기부자',
|
||||
amount: form.value.amount,
|
||||
message: form.value.message || '푸른 바다를 함께 응원합니다!',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
showSuccessModal()
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const showSuccessModal = () => {
|
||||
showSuccess.value = true
|
||||
// Reset form
|
||||
form.value.name = ''
|
||||
form.value.message = ''
|
||||
}
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(num)
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const d = new Date(dateStr)
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="donation-container">
|
||||
<!-- Hero Header -->
|
||||
<div class="donation-hero">
|
||||
<h1 class="page-title">희망의 바다 기금 기부</h1>
|
||||
<p class="page-subtitle">수협중앙회와 함께 더 맑은 바다 생태계를 가꾸는 가장 가치 있는 첫걸음입니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Grid layout -->
|
||||
<div class="donation-grid">
|
||||
<!-- Left Column: Form and Impact simulation -->
|
||||
<div class="column-left">
|
||||
<!-- Interactive Impact Simulator Card -->
|
||||
<div class="card simulator-card">
|
||||
<h3 class="card-title"><Fish class="card-icon" /> 실시간 기부 효과 시뮬레이션</h3>
|
||||
<p class="card-desc">기부하시는 금액에 따라 우리 바다에 생겨날 놀라운 변화를 확인해보세요.</p>
|
||||
|
||||
<div class="simulator-graphic">
|
||||
<!-- Ocean environment visual feedback -->
|
||||
<div class="mini-ocean">
|
||||
<div class="sim-bubbles">
|
||||
<div v-for="n in 6" :key="n" class="sim-bubble" :style="`--i:${n}`"></div>
|
||||
</div>
|
||||
|
||||
<!-- Floating simulated fish -->
|
||||
<div class="fish-pond">
|
||||
<div
|
||||
v-for="n in Math.min(simulatedImpact.fish, 10)"
|
||||
:key="n"
|
||||
class="sim-fish"
|
||||
:style="`--speed:${1.5 + Math.random() * 2}s; --delay:${Math.random() * 2}s; --y:${15 + (n * 8)}%`"
|
||||
>
|
||||
🐟
|
||||
</div>
|
||||
<div v-if="simulatedImpact.fish === 0" class="no-fish-text">치어가 방류 대기 중입니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Impact Metrics Grid -->
|
||||
<div class="impact-metrics">
|
||||
<div class="metric-box">
|
||||
<span class="m-icon"><Fish /></span>
|
||||
<span class="m-val">{{ simulatedImpact.fish }} 마리</span>
|
||||
<span class="m-lbl">수산 치어 방류</span>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<span class="m-icon"><Trash2 /></span>
|
||||
<span class="m-val">{{ simulatedImpact.waste }} kg</span>
|
||||
<span class="m-lbl">해양 쓰레기 수거</span>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<span class="m-icon"><Shield /></span>
|
||||
<span class="m-val">{{ simulatedImpact.area }} m²</span>
|
||||
<span class="m-lbl">바닷속 갯녹음 정화</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Donation Form Card -->
|
||||
<div class="card form-card">
|
||||
<h3 class="card-title"><Heart class="card-icon form-icon" /> 가상 후원 참여 폼</h3>
|
||||
<p class="card-desc">후원 금액과 응원 메시지는 실시간 캠페인 대시보드에 반영됩니다.</p>
|
||||
|
||||
<form @submit.prevent="handleDonate" class="donation-form">
|
||||
<!-- Name Input -->
|
||||
<div class="form-group">
|
||||
<label for="donor-name" class="form-label">후원자명 (또는 단체명)</label>
|
||||
<input
|
||||
id="donor-name"
|
||||
type="text"
|
||||
v-model="form.name"
|
||||
placeholder="익명의 기부자"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Amount Input & Preset Buttons -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">후원 금액 선택</label>
|
||||
<div class="preset-buttons">
|
||||
<button
|
||||
type="button"
|
||||
v-for="amt in predefinedAmounts"
|
||||
:key="amt"
|
||||
:class="['preset-btn', { active: form.amount === amt }]"
|
||||
@click="form.amount = amt"
|
||||
>
|
||||
{{ formatNumber(amt / 10000) }}만 원
|
||||
</button>
|
||||
</div>
|
||||
<div class="custom-amount-input">
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="form.amount"
|
||||
placeholder="직접 입력 (원)"
|
||||
min="1000"
|
||||
step="1000"
|
||||
class="form-input custom-input"
|
||||
/>
|
||||
<span class="currency-label">원</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message Input -->
|
||||
<div class="form-group">
|
||||
<label for="donor-msg" class="form-label">바다를 위한 응원 메시지</label>
|
||||
<textarea
|
||||
id="donor-msg"
|
||||
v-model="form.message"
|
||||
placeholder="깨끗한 바다를 향한 어업인과 수협에 전할 격려의 한마디를 작성해주세요."
|
||||
class="form-textarea"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-submit" :disabled="isSubmitting || form.amount < 1000">
|
||||
<Heart class="submit-icon" />
|
||||
<span>{{ isSubmitting ? '전송 중...' : '기금 후원하기' }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Live dashboard stats and recent list -->
|
||||
<div class="column-right">
|
||||
<!-- Campaign Stats Box -->
|
||||
<div class="card stats-box-card">
|
||||
<h3 class="card-title">실시간 희망 바다 현황</h3>
|
||||
<div class="simple-stat-row">
|
||||
<span class="simple-lbl">누적 기금 조성액</span>
|
||||
<span class="simple-val accent-text">{{ formatNumber(stats.totalAmount) }} 원</span>
|
||||
</div>
|
||||
<div class="simple-stat-row">
|
||||
<span class="simple-lbl">누적 동참자</span>
|
||||
<span class="simple-val">{{ formatNumber(stats.totalDonors) }} 명</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Donations Feed -->
|
||||
<div class="card feed-card">
|
||||
<h3 class="card-title"><MessageSquare class="card-icon" /> 실시간 참여 피드</h3>
|
||||
<div class="donation-feed">
|
||||
<transition-group name="list">
|
||||
<div v-for="(item, idx) in recentDonations" :key="idx" class="feed-item">
|
||||
<div class="feed-header">
|
||||
<span class="feed-name">{{ item.name }} 님</span>
|
||||
<span class="feed-amt">{{ formatNumber(item.amount) }}원 후원</span>
|
||||
</div>
|
||||
<p class="feed-msg">"{{ item.message || '풍요로운 바다의 미래를 힘차게 응원합니다!' }}"</p>
|
||||
<span class="feed-time">{{ formatDate(item.timestamp) }}</span>
|
||||
</div>
|
||||
</transition-group>
|
||||
|
||||
<div v-if="recentDonations.length === 0" class="empty-feed">
|
||||
첫 번째 기부 참여를 기다리고 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Modal popup -->
|
||||
<div v-if="showSuccess" class="modal-overlay" @click.self="showSuccess = false">
|
||||
<div class="modal-content">
|
||||
<CheckCircle2 class="success-big-icon" />
|
||||
<h2 class="modal-title">후원이 완료되었습니다!</h2>
|
||||
<p class="modal-desc">
|
||||
보내주신 따뜻한 희망의 씨앗이 건강한 바다 생태계 복원과 우리 어업인 지원 기금에 소중하게 사용됩니다. 감사합니다!
|
||||
</p>
|
||||
<button class="btn btn-primary btn-modal-close" @click="showSuccess = false">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.donation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.donation-hero {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 10px 0;
|
||||
background: linear-gradient(to right, #ffffff, var(--primary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.donation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.column-left, .column-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
/* Card Common Base */
|
||||
.card {
|
||||
background: rgba(6, 17, 38, 0.4);
|
||||
border: 1px solid rgba(0, 210, 255, 0.08);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0 0 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 0.9rem;
|
||||
color: #64748b;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
/* Live Simulator Visual Graphic */
|
||||
.simulator-card {
|
||||
border-color: rgba(0, 210, 255, 0.15);
|
||||
}
|
||||
|
||||
.simulator-graphic {
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mini-ocean {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
background: linear-gradient(to bottom, #072a56 0%, #03152d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 210, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sim-bubbles {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.sim-bubble {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
background: rgba(0, 210, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
left: calc(var(--i) * 15%);
|
||||
animation: sim-bubble-up 3s linear infinite;
|
||||
animation-delay: calc(var(--i) * 0.4s);
|
||||
}
|
||||
|
||||
@keyframes sim-bubble-up {
|
||||
0% { transform: translateY(0) scale(1); opacity: 0; }
|
||||
20% { opacity: 0.6; }
|
||||
90% { opacity: 0.6; }
|
||||
100% { transform: translateY(-200px) scale(1.3); opacity: 0; }
|
||||
}
|
||||
|
||||
.fish-pond {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.sim-fish {
|
||||
position: absolute;
|
||||
font-size: 1.3rem;
|
||||
animation: swim var(--speed) linear infinite alternate;
|
||||
animation-delay: var(--delay);
|
||||
left: -20px;
|
||||
top: var(--y);
|
||||
}
|
||||
|
||||
@keyframes swim {
|
||||
0% { left: -20px; transform: scaleX(1); }
|
||||
45% { transform: scaleX(1); }
|
||||
50% { transform: scaleX(-1); }
|
||||
95% { transform: scaleX(-1); }
|
||||
100% { left: calc(100% - 10px); transform: scaleX(1); }
|
||||
}
|
||||
|
||||
.no-fish-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Impact Metrics */
|
||||
.impact-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.metric-box {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(0, 210, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
padding: 15px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.m-icon {
|
||||
color: var(--primary-color);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.m-val {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.m-lbl {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Donation Form */
|
||||
.donation-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.form-input, .form-textarea {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
color: white;
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 10px rgba(0, 210, 255, 0.2);
|
||||
}
|
||||
|
||||
.preset-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
color: #94a3b8;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.preset-btn:hover {
|
||||
background: rgba(0, 210, 255, 0.08);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.preset-btn.active {
|
||||
background: rgba(0, 210, 255, 0.15);
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.custom-amount-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
width: 100%;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.currency-label {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
color: #64748b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff4747);
|
||||
color: white;
|
||||
justify-content: center;
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 5px 15px rgba(255, 107, 107, 0.3);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-submit:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(255, 107, 107, 0.5);
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
background: #1e293b;
|
||||
color: #475569;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.submit-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Campaign Stats Box */
|
||||
.stats-box-card {
|
||||
background: linear-gradient(135deg, rgba(6, 17, 38, 0.7), rgba(3, 8, 20, 0.7));
|
||||
border-color: rgba(0, 210, 255, 0.15);
|
||||
}
|
||||
|
||||
.simple-stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.simple-stat-row:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.simple-lbl {
|
||||
font-size: 0.9rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.simple-val {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.accent-text {
|
||||
color: var(--primary-color);
|
||||
text-shadow: 0 0 10px rgba(0, 210, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Feed Card */
|
||||
.feed-card {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.donation-feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
max-height: 380px;
|
||||
overflow-y: auto;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.donation-feed::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.donation-feed::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 210, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.feed-item {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.03);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.feed-item:hover {
|
||||
border-color: rgba(0, 210, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.feed-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.feed-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.feed-amt {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.feed-msg {
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.feed-time {
|
||||
font-size: 0.75rem;
|
||||
color: #475569;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.empty-feed {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #475569;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Success Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(2, 6, 17, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 110;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: radial-gradient(circle at center, #061530 0%, #030814 100%);
|
||||
border: 1px solid rgba(0, 210, 255, 0.25);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
border-radius: 24px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 20px 50px rgba(0, 210, 255, 0.3);
|
||||
}
|
||||
|
||||
.success-big-icon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 20px;
|
||||
animation: scale-up-bounce 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
@keyframes scale-up-bounce {
|
||||
0% { transform: scale(0.3); opacity: 0; }
|
||||
70% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.modal-desc {
|
||||
font-size: 0.95rem;
|
||||
color: #94a3b8;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 30px 0;
|
||||
}
|
||||
|
||||
.btn-modal-close {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 992px) {
|
||||
.donation-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,585 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Anchor, Users, Fish, Trash2, HelpCircle, ChevronRight, Waves } from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const stats = ref({
|
||||
totalDonors: 12543,
|
||||
totalAmount: 627150000,
|
||||
releasedFish: 4200000,
|
||||
collectedWaste: 185000,
|
||||
purifiedArea: 95000
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Dynamic import to handle non-Wails preview mode gracefully
|
||||
// @ts-ignore
|
||||
const CampaignService = await import('../../bindings/changeme/campaignservice')
|
||||
const res = await CampaignService.GetStats()
|
||||
if (res) {
|
||||
stats.value = res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Wails bindings not available, using mock statistics.', e)
|
||||
}
|
||||
})
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(num)
|
||||
}
|
||||
|
||||
const formatKoreanAmount = (num: number) => {
|
||||
if (num >= 100000000) {
|
||||
const eok = Math.floor(num / 100000000)
|
||||
const man = Math.floor((num % 100000000) / 10000)
|
||||
return `${eok}억 ${man > 0 ? man + '만' : ''}원`
|
||||
}
|
||||
return formatNumber(num) + '원'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section">
|
||||
<div class="ocean-bg">
|
||||
<div class="wave wave1"></div>
|
||||
<div class="wave wave2"></div>
|
||||
<div class="wave wave3"></div>
|
||||
<div class="light-ray"></div>
|
||||
<div class="bubbles">
|
||||
<div v-for="n in 15" :key="n" class="bubble" :style="`--i:${n}; --s:${Math.random() * 1.5 + 0.5}`"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-content">
|
||||
<span class="hero-badge"><Waves class="badge-icon" /> 수협중앙회 공익 캠페인</span>
|
||||
<h1 class="hero-title">
|
||||
생명의 바다,<br />
|
||||
<span class="highlight">희망을 품다</span>
|
||||
</h1>
|
||||
<p class="hero-desc">
|
||||
바다는 어업인의 일터이자 인류 생명의 젖줄입니다.<br />
|
||||
수협중앙회 '희망의 바다 만들기 운동'과 함께 깨끗하고 풍요로운 바다 생태계를 미래 세대에게 물려주세요.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<button class="btn btn-primary" @click="router.push('/donation')">
|
||||
<span>캠페인 동참하기</span>
|
||||
<HeartHandshake class="btn-icon" />
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="router.push('/about')">
|
||||
<span>자세히 알아보기</span>
|
||||
<ChevronRight class="btn-icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Dashboard Section -->
|
||||
<section class="stats-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">희망의 바다 누적 성과</h2>
|
||||
<p class="section-subtitle">2007년부터 수협중앙회와 전국의 어업인이 땀 흘려 가꿔온 생명의 기록입니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<!-- Card 1: Donors -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper donor-icon">
|
||||
<Users class="stat-icon" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">누적 참여 후원자</span>
|
||||
<span class="stat-number">{{ formatNumber(stats.totalDonors) }} <span class="stat-unit">명</span></span>
|
||||
</div>
|
||||
<div class="stat-progress">
|
||||
<div class="progress-bar" style="width: 82%"></div>
|
||||
</div>
|
||||
<p class="stat-desc">많은 시민과 어업인이 한마음으로 동참하고 있습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Released Fish -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper fish-icon">
|
||||
<Fish class="stat-icon" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">방류된 수산종자</span>
|
||||
<span class="stat-number">{{ formatNumber(stats.releasedFish) }} <span class="stat-unit">마리</span></span>
|
||||
</div>
|
||||
<div class="stat-progress">
|
||||
<div class="progress-bar" style="width: 75%"></div>
|
||||
</div>
|
||||
<p class="stat-desc">조피볼락, 감성돔 등 우수 수산종자를 바다로 돌려보냈습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Waste -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper waste-icon">
|
||||
<Trash2 class="stat-icon" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">수거된 해양 쓰레기</span>
|
||||
<span class="stat-number">{{ formatNumber(stats.collectedWaste) }} <span class="stat-unit">kg</span></span>
|
||||
</div>
|
||||
<div class="stat-progress">
|
||||
<div class="progress-bar" style="width: 90%"></div>
|
||||
</div>
|
||||
<p class="stat-desc">조업 중 인양 폐기물과 침적 폐어구를 집중 정화했습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 4: Purified Area -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper area-icon">
|
||||
<Anchor class="stat-icon" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">정화된 해역 면적</span>
|
||||
<span class="stat-number">{{ formatNumber(stats.purifiedArea) }} <span class="stat-unit">m²</span></span>
|
||||
</div>
|
||||
<div class="stat-progress">
|
||||
<div class="progress-bar" style="width: 68%"></div>
|
||||
</div>
|
||||
<p class="stat-desc">갯녹음 해역 복원 및 바닥갈이 사업을 진행했습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General Info Alert -->
|
||||
<div class="total-donation-badge">
|
||||
<span class="badge-label">희망의 바다 조성 기금 누적액</span>
|
||||
<span class="badge-amount">{{ formatKoreanAmount(stats.totalAmount) }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Call to action Section -->
|
||||
<section class="cta-section">
|
||||
<div class="cta-card">
|
||||
<h3 class="cta-title">어업인과 국민이 함께 해양 생태계를 살립니다</h3>
|
||||
<p class="cta-desc">
|
||||
풍요로운 어장 복원과 맑은 바다 환경 조성은 우리 모두의 의무입니다. <br />
|
||||
당신의 작은 실천이 푸른 바다의 희망찬 미래를 만들어 냅니다.
|
||||
</p>
|
||||
<button class="btn btn-primary" @click="router.push('/donation')">지금 바로 후원하기</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 80px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 550px;
|
||||
padding: 40px;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.ocean-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, #021a3a 0%, #030814 100%);
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Waves animation */
|
||||
.wave {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 200%;
|
||||
height: 100px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath d='M321.39,56.44c58-10.79,114.16-30.13,172-41.86,82.39-16.72,168.19-17.73,250.45-.39C823.78,31,906.67,72,985.66,92.83c70.05,18.48,146.53,26.09,214.34,3V120H0V0C26.9,8.75,55.05,18.06,83.4,26.8,145.42,46,212.41,60.11,321.39,56.44Z' fill='%2300d2ff' fill-opacity='0.08'/%3E%3C/svg%3E") repeat-x;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.wave1 {
|
||||
animation: wave 20s linear infinite;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.wave2 {
|
||||
animation: wave 12s linear infinite reverse;
|
||||
opacity: 0.5;
|
||||
z-index: 3;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.wave3 {
|
||||
animation: wave 8s linear infinite;
|
||||
opacity: 0.2;
|
||||
z-index: 4;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
/* Underwater Light Ray */
|
||||
.light-ray {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 30%;
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, rgba(0, 210, 255, 0.15) 0%, transparent 70%);
|
||||
transform: skewX(-25deg);
|
||||
filter: blur(40px);
|
||||
pointer-events: none;
|
||||
animation: shimmer 6s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { opacity: 0.5; transform: skewX(-20deg) translateX(-10px); }
|
||||
100% { opacity: 1; transform: skewX(-30deg) translateX(10px); }
|
||||
}
|
||||
|
||||
/* Bubbles */
|
||||
.bubble {
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
background: rgba(0, 210, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 50%;
|
||||
width: calc(var(--s) * 15px);
|
||||
height: calc(var(--s) * 15px);
|
||||
left: calc(var(--i) * 6.5%);
|
||||
animation: bubble-up calc(10s / var(--s)) linear infinite;
|
||||
animation-delay: calc(var(--i) * 0.4s);
|
||||
}
|
||||
|
||||
@keyframes bubble-up {
|
||||
0% {
|
||||
transform: translateY(0) scale(1) translateX(0);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
90% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-600px) scale(1.2) translateX(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
max-width: 600px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
background: rgba(0, 210, 255, 0.12);
|
||||
border: 1px solid rgba(0, 210, 255, 0.3);
|
||||
border-radius: 99px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(2.2rem, 5vw, 3.4rem);
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.hero-title .highlight {
|
||||
background: linear-gradient(to right, #00d2ff, #0066ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.6;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 28px;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
box-shadow: 0 5px 15px rgba(0, 210, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 210, 255, 0.5);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Stats Section */
|
||||
.stats-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 10px 0;
|
||||
background: linear-gradient(to right, #ffffff, #94a3b8);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: rgba(6, 17, 38, 0.4);
|
||||
border: 1px solid rgba(0, 210, 255, 0.08);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: rgba(0, 210, 255, 0.25);
|
||||
box-shadow: 0 10px 30px rgba(0, 210, 255, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon-wrapper {
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.donor-icon {
|
||||
background: rgba(0, 210, 255, 0.15);
|
||||
}
|
||||
.fish-icon {
|
||||
background: rgba(0, 102, 255, 0.15);
|
||||
}
|
||||
.waste-icon {
|
||||
background: rgba(255, 107, 107, 0.15);
|
||||
}
|
||||
.area-icon {
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.stat-unit {
|
||||
font-size: 1rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-progress {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.stat-desc {
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.total-donation-badge {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 16px 30px;
|
||||
background: linear-gradient(135deg, rgba(6, 17, 38, 0.8), rgba(3, 8, 20, 0.8));
|
||||
border: 1px solid rgba(0, 210, 255, 0.2);
|
||||
border-radius: 99px;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.badge-label {
|
||||
font-size: 0.95rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-amount {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 800;
|
||||
color: var(--primary-color);
|
||||
text-shadow: 0 0 10px rgba(0, 210, 255, 0.4);
|
||||
}
|
||||
|
||||
/* CTA Section */
|
||||
.cta-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cta-card {
|
||||
background: radial-gradient(circle at top right, rgba(0, 210, 255, 0.15) 0%, rgba(3, 8, 20, 0.8) 70%);
|
||||
border: 1px solid rgba(0, 210, 255, 0.15);
|
||||
border-radius: 24px;
|
||||
padding: 50px 40px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cta-desc {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.6;
|
||||
color: #94a3b8;
|
||||
max-width: 700px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
padding: 30px 20px;
|
||||
min-height: auto;
|
||||
}
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.total-donation-badge {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
border-radius: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.cta-card {
|
||||
padding: 35px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user