feat: add shared components for donation, facility, feature bento, filter panel, join wizard, leaderboard, player, playstyle, and town cards

- Implemented DonationCard.vue for displaying donation details.
- Created FacilityCard.vue to showcase facility information with status badges.
- Developed FeatureBentoCard.vue and FeatureBentoGrid.vue for feature display in a grid layout.
- Added FilterPanel.vue for filtering content with search and tag options.
- Introduced JoinWizard.vue for a step-by-step joining process with device and playstyle selection.
- Created LeaderboardCard.vue to display leaderboard information.
- Implemented PlayerCard.vue for showcasing player profiles and stats.
- Developed PlaystyleCard.vue for selecting playstyle options.
- Added TownCard.vue to present town details with badges and images.
- Included demo data in demoData.js for testing and development purposes.
This commit is contained in:
zhangyuheng
2026-03-18 10:50:23 +08:00
parent 9db782ae4b
commit d254ec86df
35 changed files with 3782 additions and 79 deletions

View File

@@ -0,0 +1,144 @@
<script setup>
defineProps({
open: {
type: Boolean,
default: false,
},
items: {
type: Array,
default: () => [],
},
ctaLabel: {
type: String,
default: '加入游戏',
},
ctaHref: {
type: String,
default: '/join',
},
});
const emit = defineEmits(['close']);
</script>
<template>
<transition name="drawer-fade">
<div v-if="open" class="mobile-drawer-mask" @click="emit('close')">
<aside class="mobile-drawer" @click.stop>
<div class="mobile-drawer__header">
<p>站点导航</p>
<button type="button" class="mobile-drawer__close" aria-label="关闭菜单" @click="emit('close')">
×
</button>
</div>
<nav class="mobile-drawer__links" aria-label="移动端导航">
<a
v-for="item in items"
:key="item.href"
class="mobile-drawer__link"
:href="item.href"
@click="emit('close')"
>
<span>{{ item.label }}</span>
<small v-if="item.description">{{ item.description }}</small>
</a>
</nav>
<a class="mobile-drawer__cta" :href="ctaHref" @click="emit('close')">{{ ctaLabel }}</a>
</aside>
</div>
</transition>
</template>
<style scoped>
.drawer-fade-enter-active,
.drawer-fade-leave-active {
transition: opacity 0.25s ease;
}
.drawer-fade-enter-from,
.drawer-fade-leave-to {
opacity: 0;
}
.mobile-drawer-mask {
position: fixed;
inset: 0;
z-index: 1200;
display: flex;
justify-content: flex-end;
background: rgba(15, 23, 42, 0.28);
backdrop-filter: blur(12px);
}
.mobile-drawer {
width: min(360px, 100%);
height: 100%;
padding: 24px 20px 28px;
background: rgba(255, 255, 255, 0.96);
box-shadow: -20px 0 60px rgba(15, 23, 42, 0.16);
display: flex;
flex-direction: column;
}
.mobile-drawer__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.mobile-drawer__header p {
margin: 0;
font-size: 0.9rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--bl-text-tertiary);
}
.mobile-drawer__close {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bl-surface-muted);
font-size: 1.4rem;
cursor: pointer;
}
.mobile-drawer__links {
display: grid;
gap: 10px;
}
.mobile-drawer__link {
display: flex;
flex-direction: column;
gap: 2px;
padding: 14px 16px;
border-radius: var(--bl-radius-md);
background: #fff;
text-decoration: none;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.03);
}
.mobile-drawer__link span {
font-weight: 600;
}
.mobile-drawer__link small {
color: var(--bl-text-secondary);
}
.mobile-drawer__cta {
margin-top: auto;
display: inline-flex;
justify-content: center;
align-items: center;
min-height: 48px;
border-radius: 999px;
background: var(--bl-accent);
color: #fff;
text-decoration: none;
font-weight: 700;
}
</style>