Home hero becomes a 450px flex row with a pinned 240x450 category rail (ShellCategoryMenu `pinned` mode) left of the 960px center-cropped carousel; hovering a top-level category expands the mega-menu over the carousel. The header no longer collapses on scroll, and its category dropdown is now hover-only on non-home pages. OpenSpec change: openspec/changes/pin-home-category-menu BREAKING CHANGE: the header logo/search/cart bar no longer auto-hides past 200px of scroll, and the home page no longer shows the header category dropdown (it is pinned in the hero row instead).
219 lines
5.4 KiB
Vue
219 lines
5.4 KiB
Vue
<script setup lang="ts">
|
|
const { t, locale, locales, setLocale } = useI18n();
|
|
const { currency } = usePrefs();
|
|
const cart = useCartStore();
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
const keywords = ref(typeof route.query.q === "string" ? route.query.q : "");
|
|
watch(
|
|
() => route.query.q,
|
|
(q) => {
|
|
keywords.value = typeof q === "string" ? q : "";
|
|
},
|
|
);
|
|
|
|
function search(): void {
|
|
const q = keywords.value.trim();
|
|
void router.push({ path: "/search", query: q ? { q } : {} });
|
|
}
|
|
|
|
const currencies = ref<string[]>(["USD"]);
|
|
const { $api } = useNuxtApp();
|
|
onMounted(async () => {
|
|
try {
|
|
const list = await $api.listCurrencies();
|
|
if (list.length > 0) currencies.value = list.filter((c) => c.enabled).map((c) => c.code);
|
|
} catch {
|
|
/* keep default */
|
|
}
|
|
void cart.refresh();
|
|
});
|
|
|
|
const menuOpen = ref(false);
|
|
const isHome = computed(() => route.path === "/");
|
|
// The home page pins the category menu in the hero row instead, so the header
|
|
// dropdown is hover-only and only on non-home pages.
|
|
const showMenu = computed(() => menuOpen.value && !isHome.value);
|
|
</script>
|
|
|
|
<template>
|
|
<header class="hdr">
|
|
<div class="w1200 hdr-top">
|
|
<NuxtLink to="/" class="logo">VMall</NuxtLink>
|
|
<div class="search-box">
|
|
<input
|
|
v-model="keywords"
|
|
class="search-input"
|
|
type="text"
|
|
:placeholder="t('shell.searchPlaceholder')"
|
|
@keyup.enter="search"
|
|
/>
|
|
<button class="search-btn" type="button" :aria-label="t('common.search')" @click="search">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
|
<path d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 10-.7.7l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0A4.5 4.5 0 1114 9.5 4.5 4.5 0 019.5 14z" />
|
|
</svg>
|
|
</button>
|
|
<div class="cart-links">
|
|
<NuxtLink to="/user" class="cart-link">{{ t("shell.myMall") }}</NuxtLink>
|
|
<NuxtLink to="/cart" class="cart-link">
|
|
{{ t("shell.myCart") }}
|
|
<span v-if="cart.count > 0" class="dot">{{ cart.count }}</span>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
<div class="prefs">
|
|
<select :value="currency" :aria-label="t('common.currency')" @change="currency = ($event.target as HTMLSelectElement).value">
|
|
<option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
|
|
</select>
|
|
<select :value="locale" :aria-label="t('common.language')" @change="setLocale(($event.target as HTMLSelectElement).value as 'en' | 'zh')">
|
|
<option v-for="l in locales" :key="l.code" :value="l.code">{{ l.name }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<nav class="nav">
|
|
<div class="w1200 nav-in">
|
|
<div
|
|
class="nav-cats"
|
|
@mouseenter="menuOpen = true"
|
|
@mouseleave="menuOpen = false"
|
|
>
|
|
{{ t("shell.allCategories") }}
|
|
<ShellCategoryMenu v-show="showMenu" />
|
|
</div>
|
|
<ul class="nav-links">
|
|
<li><NuxtLink to="/">{{ t("shell.nav.home") }}</NuxtLink></li>
|
|
<li><NuxtLink to="/stores">{{ t("shell.nav.stores") }}</NuxtLink></li>
|
|
<li><NuxtLink to="/seckill">{{ t("shell.nav.seckill") }}</NuxtLink></li>
|
|
<li><NuxtLink to="/collective">{{ t("shell.nav.collective") }}</NuxtLink></li>
|
|
<li><NuxtLink to="/integral">{{ t("shell.nav.integral") }}</NuxtLink></li>
|
|
<li><NuxtLink to="/user">{{ t("shell.nav.help") }}</NuxtLink></li>
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.hdr {
|
|
background: #fff;
|
|
}
|
|
.hdr-top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 40px;
|
|
padding: 24px 0;
|
|
}
|
|
.logo {
|
|
font-size: 34px;
|
|
font-weight: 800;
|
|
color: var(--mall-red);
|
|
text-decoration: none;
|
|
letter-spacing: 1px;
|
|
}
|
|
.search-box {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.search-input {
|
|
width: 434px;
|
|
height: 38px;
|
|
padding: 10px;
|
|
outline: 0;
|
|
border: 1px solid var(--mall-line);
|
|
border-right: none;
|
|
box-sizing: border-box;
|
|
font-size: 12px;
|
|
}
|
|
.search-btn {
|
|
border: 1px solid var(--mall-line);
|
|
width: 38px;
|
|
height: 38px;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.search-btn:hover {
|
|
color: #fff;
|
|
background: var(--mall-nav);
|
|
}
|
|
.cart-links {
|
|
display: flex;
|
|
height: 38px;
|
|
border: 1px solid var(--mall-line);
|
|
margin-left: 30px;
|
|
box-sizing: border-box;
|
|
font-size: 12px;
|
|
line-height: 38px;
|
|
padding: 0 20px;
|
|
gap: 20px;
|
|
}
|
|
.cart-link {
|
|
position: relative;
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
.cart-link:hover {
|
|
color: var(--mall-red);
|
|
}
|
|
.dot {
|
|
position: absolute;
|
|
top: -12px;
|
|
right: -14px;
|
|
line-height: 16px;
|
|
background: var(--mall-red);
|
|
padding: 0 4px;
|
|
color: #fff;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
}
|
|
.prefs {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
.prefs select {
|
|
height: 30px;
|
|
min-width: 76px;
|
|
padding: 0 6px;
|
|
border: 1px solid var(--mall-line);
|
|
background: #fff;
|
|
font-size: 12px;
|
|
}
|
|
.nav {
|
|
height: 40px;
|
|
background: var(--mall-nav);
|
|
font-size: 14px;
|
|
line-height: 40px;
|
|
color: #fff;
|
|
}
|
|
.nav-in {
|
|
display: flex;
|
|
}
|
|
.nav-cats {
|
|
position: relative;
|
|
cursor: pointer;
|
|
width: 240px;
|
|
background: var(--mall-red);
|
|
box-sizing: border-box;
|
|
padding-left: 15px;
|
|
}
|
|
.nav-links {
|
|
display: flex;
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0 0 0 20px;
|
|
}
|
|
.nav-links a {
|
|
line-height: 40px;
|
|
padding: 0 30px;
|
|
color: #fff;
|
|
display: block;
|
|
text-decoration: none;
|
|
}
|
|
.nav-links a:hover {
|
|
background: #000;
|
|
}
|
|
</style>
|