feat: connect portal content and banner carousel
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user