feat: connect portal content and banner carousel

This commit is contained in:
richjohnrambo
2026-10-16 18:59:16 +08:00
parent a86db10342
commit bce5edfaf5
10 changed files with 332 additions and 85 deletions
+66 -4
View File
@@ -6,6 +6,13 @@
@read="markPopupRead"
/>
<div class="m-home-hero" :style="bannerStyle">
<button
v-if="currentBanner && currentBanner.target_type !== 'none'"
class="m-home-banner-link"
type="button"
:aria-label="currentBanner.alt_text || '打开 Banner 链接'"
@click="openBanner(currentBanner)"
></button>
<label class="m-search">
<n-icon :component="SearchOutline" />
<input readonly placeholder="搜索活动、商品、问题等" @click="router.push('/app/mall')" />
@@ -21,6 +28,16 @@
>
<div class="m-coin" aria-hidden="true">¥</div>
</div>
<div v-if="(home?.banners?.length || 0) > 1" class="m-home-banner-dots" aria-label="Banner 切换">
<button
v-for="(_, index) in home?.banners"
:key="index"
type="button"
:class="{ active: index === activeBannerIndex }"
:aria-label="`显示第 ${index + 1} 张 Banner`"
@click="selectBanner(index)"
></button>
</div>
</div>
<div class="m-home-body">
@@ -82,7 +99,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { SearchOutline, MegaphoneOutline } from '@vicons/ionicons5';
import AsyncState from '../components/AsyncState.vue';
@@ -103,7 +120,7 @@
profile: { username: string; avatar_url: string; level_name: string };
wallet: { available_amount: number; frozen_amount: number; currency: string };
features: Record<string, boolean>;
banners: Array<{ image_url?: string; title?: string }>;
banners: BannerData[];
notices: Array<{ title?: string; content?: string }>;
task_summary: {
today_count: number;
@@ -111,6 +128,13 @@
yesterday_commission_amount: number;
};
}
interface BannerData {
id: number;
image_url?: string;
alt_text?: string;
target_type: 'none' | 'internal_route' | 'external_url';
target_value?: string;
}
interface LeaderboardEntry {
rank: number;
username: string;
@@ -124,9 +148,12 @@
const error = ref('');
const leaderboard = ref<LeaderboardEntry[]>([]);
const popupNotice = ref<AnnouncementNotice | null>(null);
const activeBannerIndex = ref(0);
let bannerTimer: number | undefined;
const formatCount = (value: number) => new Intl.NumberFormat('zh-CN').format(value || 0);
const currentBanner = computed(() => home.value?.banners?.[activeBannerIndex.value]);
const bannerStyle = computed(() => {
const url = safeImage(home.value?.banners?.[0]?.image_url);
const url = safeImage(currentBanner.value?.image_url);
return {
backgroundImage: url
? `linear-gradient(180deg, rgba(16,13,8,.18), rgba(16,13,8,.54)), url(${url})`
@@ -178,6 +205,32 @@
if (item.enabled) router.push(item.path);
else window.$message?.warning('该功能暂未开放');
}
function selectBanner(index: number) {
activeBannerIndex.value = index;
restartBannerTimer();
}
function openBanner(banner: BannerData) {
const target = String(banner.target_value || '').trim();
if (!target) return;
if (banner.target_type === 'internal_route' && target.startsWith('/')) {
void router.push(target);
} else if (banner.target_type === 'external_url' && /^https:\/\//i.test(target)) {
window.open(target, '_blank', 'noopener,noreferrer');
}
}
function restartBannerTimer() {
if (bannerTimer) window.clearInterval(bannerTimer);
bannerTimer = undefined;
const count = home.value?.banners?.length || 0;
if (count > 1) {
bannerTimer = window.setInterval(() => {
activeBannerIndex.value = (activeBannerIndex.value + 1) % count;
}, 4500);
}
}
async function load() {
loading.value = true;
error.value = '';
@@ -205,7 +258,16 @@
const noticeId = popupNotice.value.id;
popupNotice.value = null;
completeLoginNotice();
request<void>({ url: `/v1/notices/${noticeId}/read`, method: 'POST' }).catch(() => undefined);
if (noticeId > 0) {
request<void>({ url: `/v1/notices/${noticeId}/read`, method: 'POST' }).catch(() => undefined);
}
}
watch(() => home.value?.banners?.length, () => {
activeBannerIndex.value = 0;
restartBannerTimer();
});
onMounted(load);
onBeforeUnmount(() => {
if (bannerTimer) window.clearInterval(bannerTimer);
});
</script>