feat: 添加横幅轮播和公告功能,支持自动切换和点击事件

This commit is contained in:
2026-01-18 16:20:47 +07:00
parent 934ee073cb
commit 2e2386f437
2 changed files with 128 additions and 112 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@@ -3,8 +3,10 @@ import type { Treaty } from "@elysiajs/eden";
import type { InfiniteScrollCustomEvent } from "@ionic/vue"; import type { InfiniteScrollCustomEvent } from "@ionic/vue";
import type { Action } from "./"; import type { Action } from "./";
import type { TreatyQuery } from "@/api/types"; import type { TreatyQuery } from "@/api/types";
import { chevronForwardOutline, eyeOutline, timeOutline } from "ionicons/icons"; import { chevronForwardOutline, eyeOutline, megaphoneOutline, timeOutline } from "ionicons/icons";
import { client, safeClient } from "@/api"; import { client, safeClient } from "@/api";
import banner2 from "@/assets/images/home-banner2.jpg?url";
import banner1 from "@/assets/images/home-banner.jpg?url";
import { actions } from "./"; import { actions } from "./";
type NewsItem = Treaty.Data<typeof client.api.news.get>["data"][number]; type NewsItem = Treaty.Data<typeof client.api.news.get>["data"][number];
@@ -18,11 +20,58 @@ const [query] = useResetRef<NewsQuery>({
const data = ref<NewsItem[]>([]); const data = ref<NewsItem[]>([]);
const isFinished = ref(false); const isFinished = ref(false);
const banners = ref([
{ id: 1, image: banner1, title: "横幅1" },
{ id: 2, image: banner2, title: "横幅2" },
]);
const currentBannerIndex = ref(0);
// 公告数据
interface Announcement {
id: number;
title: string;
onClick?: () => void;
}
const announcements = ref<Announcement[]>([
{
id: 1,
title: "欢迎使用我们的服务平台,祝您投资顺利!",
onClick: () => {
console.log("点击了第一条公告");
},
},
]);
// 横幅自动切换
let bannerTimer: ReturnType<typeof setInterval> | null = null;
function startBannerCarousel() {
bannerTimer = setInterval(() => {
currentBannerIndex.value = (currentBannerIndex.value + 1) % banners.value.length;
}, 3000);
}
function stopBannerCarousel() {
if (bannerTimer) {
clearInterval(bannerTimer);
bannerTimer = null;
}
}
onMounted(() => {
startBannerCarousel();
});
onUnmounted(() => {
stopBannerCarousel();
});
async function fetchNews() { async function fetchNews() {
const { data: responseData } = await safeClient(client.api.news.get({ query: { ...query.value } })); const { data: responseData } = await safeClient(client.api.news.get({ query: { ...query.value } }));
data.value.push(...(responseData.value?.data || [])); data.value.push(...(responseData.value?.data || []));
isFinished.value = responseData.value?.pagination.hasNextPage === false; isFinished.value = responseData.value?.pagination.hasNextPage === false;
} }
async function handleInfinite(event: InfiniteScrollCustomEvent) { async function handleInfinite(event: InfiniteScrollCustomEvent) {
if (isFinished.value) { if (isFinished.value) {
event.target.complete(); event.target.complete();
@@ -35,6 +84,7 @@ async function handleInfinite(event: InfiniteScrollCustomEvent) {
event.target.complete(); event.target.complete();
}, 500); }, 500);
} }
function handleQuickAction(action: Action) { function handleQuickAction(action: Action) {
switch (action.id) { switch (action.id) {
case "check_in": case "check_in":
@@ -67,8 +117,50 @@ function handleNewsClick(news: NewsItem) {
</ion-header> </ion-header>
<ion-content :fullscreen="true" class="home-page"> <ion-content :fullscreen="true" class="home-page">
<div class="ion-padding-horizontal"> <div class="ion-padding-horizontal">
<!-- 横幅轮播 -->
<div class="rounded-2xl overflow-hidden relative mt-4 shadow-md"> <div class="rounded-2xl overflow-hidden relative mt-4 shadow-md">
<img src="@/assets/images/home-banner.jpg" class="h-50 w-full object-cover" alt="首页横幅"> <div class="relative h-50 w-full">
<transition-group name="banner-fade">
<img
v-for="(banner, index) in banners"
v-show="index === currentBannerIndex"
:key="banner.id"
:src="banner.image"
class="absolute inset-0 h-full w-full object-cover"
:alt="banner.title"
>
</transition-group>
</div>
<!-- 指示点 -->
<div class="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2 z-10">
<div
v-for="(banner, index) in banners"
:key="banner.id"
class="w-2 h-2 rounded-full transition-all duration-300 cursor-pointer"
:class="index === currentBannerIndex ? 'bg-white w-4' : 'bg-white/50'"
@click="currentBannerIndex = index"
/>
</div>
</div>
<!-- 公告栏 -->
<div
v-for="announcement in announcements"
:key="announcement.id"
class="mt-3 bg-linear-to-r from-[#fff7e6] to-[#fffbe6] rounded-xl px-4 py-2.5 flex items-center gap-2 overflow-hidden"
>
<ion-icon :icon="megaphoneOutline" class="text-lg text-[#fa8c16] shrink-0" />
<div class="flex-1 overflow-hidden">
<div class="announcement-scroll">
<div
class="announcement-item text-sm text-[#d48806] cursor-pointer hover:text-[#fa8c16] transition-colors"
@click="announcement.onClick?.()"
>
{{ announcement.title }}
</div>
</div>
</div>
</div> </div>
<section class="my-5 grid grid-cols-4 gap-4"> <section class="my-5 grid grid-cols-4 gap-4">
@@ -149,128 +241,52 @@ function handleNewsClick(news: NewsItem) {
</template> </template>
<style lang='css' scoped> <style lang='css' scoped>
/* 中国风图案 */ /* 新闻标签渐变 */
.chinese-pattern {
position: absolute;
width: 100%;
height: 100%;
opacity: 0.8;
}
.pattern-svg {
position: absolute;
animation: rotate-pattern 30s linear infinite;
}
.pattern-1 {
width: 150px;
height: 150px;
top: 20px;
right: 30px;
}
.pattern-2 {
width: 120px;
height: 120px;
top: 100px;
left: 40px;
animation-direction: reverse;
animation-duration: 40s;
}
@keyframes rotate-pattern {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 五角星装饰 */
.stars-decoration {
position: absolute;
width: 100%;
height: 100%;
}
.star {
position: absolute;
color: rgba(255, 215, 0, 0.3);
font-size: 24px;
animation: twinkle 3s ease-in-out infinite;
}
.star-1 {
top: 40px;
left: 25%;
animation-delay: 0s;
}
.star-2 {
top: 80px;
right: 30%;
font-size: 18px;
animation-delay: 1s;
}
.star-3 {
top: 130px;
left: 15%;
font-size: 20px;
animation-delay: 2s;
}
@keyframes twinkle {
0%,
100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.2);
}
}
/* 底部波浪 */
.wave-bottom {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 120px;
opacity: 0.6;
}
/* 新闻标签渐变 - 使用 @apply 不支持渐变,需要保留 */
.news-badge { .news-badge {
box-shadow: 0 2px 8px rgba(196, 30, 58, 0.3); box-shadow: 0 2px 8px rgba(196, 30, 58, 0.3);
} }
/* 走马灯动画 */ /* 横幅轮播动画 */
.announcement-carousel { .banner-fade-enter-active,
transition: height 0.3s ease; .banner-fade-leave-active {
transition: opacity 0.8s ease;
} }
.slide-enter-active, .banner-fade-enter-from {
.slide-leave-active {
transition: all 0.5s ease;
}
.slide-enter-from {
transform: translateX(100%);
opacity: 0; opacity: 0;
} }
.slide-leave-to { .banner-fade-leave-to {
transform: translateX(-100%);
opacity: 0; opacity: 0;
} }
.slide-enter-to, .banner-fade-enter-to,
.slide-leave-from { .banner-fade-leave-from {
transform: translateX(0);
opacity: 1; opacity: 1;
} }
/* 公告滚动动画 */
.announcement-scroll {
display: flex;
animation: scroll-announcement 5s linear infinite;
white-space: nowrap;
}
.announcement-item {
padding-right: 100px;
display: inline-block;
}
@keyframes scroll-announcement {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-33.333%);
}
}
.announcement-scroll:hover {
animation-play-state: paused;
}
</style> </style>