feat: 面包屑导航

This commit is contained in:
2026-04-29 15:21:50 +08:00
parent 7eff1b0506
commit d6a07e7fc7
8 changed files with 238 additions and 80 deletions

View File

@@ -1,8 +1,37 @@
import { createRouter, createWebHistory, RouteRecordRaw, createWebHashHistory } from "vue-router"
import type { RouteLocationNormalizedLoaded, RouteLocationRaw } from "vue-router"
import store from "@/store"
import { Https } from "@/tool/https"
import { getCookie, setCookie } from "@/tool/cookie"
type SellerRouteMetaValue<T> = T | ((route: RouteLocationNormalizedLoaded) => T)
type SellerBreadcrumbItem = {
title?: SellerRouteMetaValue<string>
titleKey?: SellerRouteMetaValue<string>
to?: SellerRouteMetaValue<RouteLocationRaw>
}
const myListingsBreadcrumb: SellerBreadcrumbItem = {
title: "My Listings",
to: { name: "myListingsIndex" }
}
const selectCollectionBreadcrumb: SellerBreadcrumbItem = {
title: "Select Collection",
to: { name: "myListingsSelect" }
}
const selectSketchBreadcrumb: SellerBreadcrumbItem = {
title: "Select Sketch"
}
const editListingBreadcrumb: SellerBreadcrumbItem = {
title: "Edit Listing Details"
}
const statusBreadcrumb: SellerBreadcrumbItem = {
titleKey: (route) =>
route.params.status === "publish"
? "SellerListEdit.listingLive"
: "SellerListEdit.draftSaved"
}
const routes: Array<RouteRecordRaw> = [
{
path: "/",
@@ -167,7 +196,11 @@ const routes: Array<RouteRecordRaw> = [
{
path: "becomeSeller",
name: "becomeSeller",
meta: { enter: "all" },
meta: {
enter: "all",
sellerHeaderTitle: "Apply to Become a Seller",
sellerHeaderTip: "Join the Stylish Parade and start selling your design work"
},
component: () => import("@/views/SellerDashboard/BecomeSeller/index.vue")
},
{
@@ -185,7 +218,7 @@ const routes: Array<RouteRecordRaw> = [
{
path: "myListings",
name: "myListings",
meta: { enter: "all" },
meta: { enter: "all", sellerBreadcrumb: myListingsBreadcrumb },
children: [
{
path: "",
@@ -196,35 +229,74 @@ const routes: Array<RouteRecordRaw> = [
{
path: "index",
name: "myListingsIndex",
meta: { enter: "all" },
meta: {
enter: "all",
sellerHeaderTitle: "My Listings",
sellerHeaderTip: "Active listings and unpublished inventory.",
sellerBreadcrumbs: [myListingsBreadcrumb]
},
component: () =>
import("@/views/SellerDashboard/MyListings/main/index.vue")
},
{
path: "select",
name: "myListingsSelect",
meta: { enter: "all" },
meta: {
enter: "all",
sellerHeaderTitle: "Select Collection",
sellerBreadcrumbs: [
myListingsBreadcrumb,
selectCollectionBreadcrumb
]
},
component: () =>
import("@/views/SellerDashboard/MyListings/createSelect/index.vue")
},
{
path: "select/:collectionId",
name: "myListingsSelectItem",
meta: { enter: "all" },
meta: {
enter: "all",
sellerHeaderTitle: "Select Collection",
sellerBreadcrumbs: [
myListingsBreadcrumb,
selectCollectionBreadcrumb,
selectSketchBreadcrumb
]
},
component: () =>
import("@/views/SellerDashboard/MyListings/createSelectItem/index.vue")
},
{
path: "edit",
name: "EditDetail",
meta: { enter: "all" },
meta: {
enter: "all",
sellerHeaderTitle: "Edit Listing Details",
sellerBreadcrumbs: [
myListingsBreadcrumb,
selectCollectionBreadcrumb,
selectSketchBreadcrumb,
editListingBreadcrumb
]
},
component: () =>
import("@/views/SellerDashboard/MyListings/EditDetail/index.vue")
},
{
path:'edit/status/:status',
name:'Status',
meta:{enter:'all'},
path: "edit/status/:status",
name: "Status",
meta: {
enter: "all",
sellerHeaderTitle: "Edit Listing Details",
sellerBreadcrumbs: [
myListingsBreadcrumb,
selectCollectionBreadcrumb,
selectSketchBreadcrumb,
editListingBreadcrumb,
statusBreadcrumb
]
},
component: () =>
import("@/views/SellerDashboard/MyListings/EditDetail/Status.vue")
}

View File

@@ -1,9 +1,6 @@
<template>
<div class="become-seller">
<seller-header
title="Apply to Become a Seller"
tip="Join the Stylish Parade and start selling your design work"
/>
<seller-header />
<div class="content">
<seller-apply v-if="applyStatus === null" @submit="onSubmit" />
<seller-review v-else />

View File

@@ -2,14 +2,6 @@
<div class="status-wrapper flex flex-col flex-1">
<seller-header
class="edit-detail-header"
title="Edit Listing Details"
:breadcrumbs="[
{ title: 'My Listings', name: 'myListingsIndex' },
{ title: 'Select Collection', name: 'myListingsSelect' },
{ title: 'Select Sketch', name: 'myListingsSelectItem' },
{ title: 'Edit Listing Details', name: 'EditDetail' },
{ title: $t(title), name: 'Status' }
]"
/>
<div class="status-container flex flex-col flex-1 flex-center">
<img src="@/assets/images/seller/success-0.png" class="icon" alt="" />

View File

@@ -2,13 +2,6 @@
<div class="edit-detail-wrapper flex-1">
<seller-header
class="edit-detail-header"
title="Edit Listing Details"
:breadcrumbs="[
{ title: 'My Listings', name: 'myListingsIndex' },
{ title: 'Select Collection', name: 'myListingsSelect' },
{ title: 'Select Sketch', name: 'myListingsSelectItem' },
{ title: 'Edit Listing Details', name: 'EditDetail' }
]"
>
<template #right>
<div class="operate-menu flex">

View File

@@ -40,14 +40,7 @@ defineExpose({})
</script>
<template>
<div class="create-select">
<seller-header
title="Select Collection"
:breadcrumbs="[
{title:'My Listings', name:'myListingsIndex'},
{title:'Select Collection', name: 'myListingsSelect' }
]"
>
</seller-header>
<seller-header />
<div class="content">
<div class="title">
<div class="left">
@@ -152,4 +145,4 @@ defineExpose({})
}
}
}
</style>
</style>

View File

@@ -149,14 +149,7 @@ const {} = toRefs(data);
</script>
<template>
<div class="create-select-item">
<seller-header
title="Select Collection"
:breadcrumbs="[
{title:'My Listings', name:'myListingsIndex'},
{title:'Select Collection', name: 'myListingsSelect' },
{title:'Select Sketch', name: 'myListingsSelectItem' }
]"
>
<seller-header>
<template #right>
<div class="header-right">
<div class="chooseNum">
@@ -383,4 +376,4 @@ const {} = toRefs(data);
}
}
}
</style>
</style>

View File

@@ -41,10 +41,7 @@ const {} = toRefs(data);
</script>
<template>
<div class="myListings-seller">
<seller-header
title="My Listings"
tip="Active listings and unpublished inventory."
>
<seller-header>
<template #right>
<div class="button" @click="newListing">
<span>New Listing</span>
@@ -99,4 +96,4 @@ const {} = toRefs(data);
position: relative;
}
}
</style>
</style>

View File

@@ -4,24 +4,20 @@
<svg-icon name="seller-back" size="24" />
</div>
<div class="content">
<span class="title" v-show="title">{{ title }}</span>
<span class="tip" v-show="tip">{{ tip }}</span>
<div class="breadcrumbs" v-show="breadcrumbs.length > 0">
<template v-for="(v, i) in breadcrumbs" :key="i">
<span class="title" v-show="displayTitle">{{ displayTitle }}</span>
<span class="tip" v-show="displayTip">{{ displayTip }}</span>
<div class="breadcrumbs" v-show="breadcrumbList.length > 1">
<template v-for="(v, i) in breadcrumbList" :key="`${v.title}-${i}`">
<span
class="title"
:class="{
last: i === breadcrumbs.length - 1
last: i === breadcrumbList.length - 1,
clickable: i < breadcrumbList.length - 1
}"
@click="
() => {
const index = -(breadcrumbs.length - i - 1)
if (index < 0) router.go(index)
}
"
@click="onBreadcrumbClick(v, i)"
>{{ v.title }}</span
>
<span class="icon" v-show="i < breadcrumbs.length - 1">
<span class="icon" v-show="i < breadcrumbList.length - 1">
<svg-icon name="seller-arrow_right_solid" size="10" />
</span>
</template>
@@ -33,25 +29,150 @@
</div>
</template>
<script setup>
import { ref, computed } from "vue"
<script setup lang="ts">
import { computed } from "vue"
import { useI18n } from "vue-i18n"
import { useRoute, useRouter } from "vue-router"
const props = defineProps({
title: {
type: String,
default: ""
},
tip: {
type: String,
default: ""
},
breadcrumbs: {
type: Array, // { title: string, name: string }
default: () => []
import type { RouteLocationNormalizedLoaded, RouteLocationRaw } from "vue-router"
type RouteMetaValue<T> = T | ((route: RouteLocationNormalizedLoaded) => T)
type SellerBreadcrumbSource =
| string
| {
title?: RouteMetaValue<string>
titleKey?: RouteMetaValue<string>
to?: RouteMetaValue<RouteLocationRaw>
name?: string
path?: string
}
type SellerBreadcrumbItem = {
title: string
to?: RouteLocationRaw
}
const props = withDefaults(
defineProps<{
title?: string
tip?: string
breadcrumbs?: SellerBreadcrumbSource[]
}>(),
{
title: "",
tip: "",
breadcrumbs: () => []
}
})
)
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const resolveMetaValue = <T,>(value?: RouteMetaValue<T>) => {
if (typeof value === "function") {
return (value as (route: RouteLocationNormalizedLoaded) => T)(route)
}
return value
}
const resolveRouteLocation = (to?: RouteLocationRaw) => {
if (!to || typeof to === "string") return to
const location = to as any
if (location.name) {
return {
...location,
params: {
...route.params,
...location.params
}
}
}
return to
}
const resolveBreadcrumb = (source: SellerBreadcrumbSource) => {
if (typeof source === "string") {
return {
title: source
}
}
const titleKey = resolveMetaValue(source.titleKey)
const title = titleKey ? t(titleKey) : resolveMetaValue(source.title)
const to = resolveRouteLocation(resolveMetaValue(source.to))
const fallbackTo = source.name ? { name: source.name } : source.path
return {
title: title || "",
to: resolveRouteLocation(to || fallbackTo)
}
}
const resolveTitle = (title?: RouteMetaValue<string>, titleKey?: RouteMetaValue<string>) => {
const key = resolveMetaValue(titleKey)
return key ? t(key) : resolveMetaValue(title) || ""
}
const autoBreadcrumbs = computed(() => {
const currentRecord = route.matched[route.matched.length - 1]
const customBreadcrumbs = currentRecord?.meta?.sellerBreadcrumbs
if (Array.isArray(customBreadcrumbs)) {
return customBreadcrumbs
.map((breadcrumb) => resolveBreadcrumb(breadcrumb as SellerBreadcrumbSource))
.filter((breadcrumb) => breadcrumb.title)
}
return route.matched
.map((record) => record.meta?.sellerBreadcrumb)
.filter(
(breadcrumb): breadcrumb is SellerBreadcrumbSource =>
typeof breadcrumb === "string" ||
(typeof breadcrumb === "object" && breadcrumb !== null)
)
.map(resolveBreadcrumb)
.filter((breadcrumb) => breadcrumb.title)
})
const breadcrumbList = computed<SellerBreadcrumbItem[]>(() => {
if (props.breadcrumbs.length) {
return props.breadcrumbs
.map(resolveBreadcrumb)
.filter((breadcrumb) => breadcrumb.title)
}
return autoBreadcrumbs.value
})
const displayTitle = computed(() => {
return (
props.title ||
resolveTitle(
route.meta.sellerHeaderTitle as RouteMetaValue<string> | undefined,
route.meta.sellerHeaderTitleKey as RouteMetaValue<string> | undefined
) ||
breadcrumbList.value[breadcrumbList.value.length - 1]?.title ||
""
)
})
const displayTip = computed(() => {
return (
props.tip ||
resolveTitle(
route.meta.sellerHeaderTip as RouteMetaValue<string> | undefined,
route.meta.sellerHeaderTipKey as RouteMetaValue<string> | undefined
)
)
})
const onBreadcrumbClick = (breadcrumb: SellerBreadcrumbItem, index: number) => {
if (index >= breadcrumbList.value.length - 1) return
if (breadcrumb.to) {
router.push(breadcrumb.to)
return
}
const historyIndex = -(breadcrumbList.value.length - index - 1)
if (historyIndex < 0) router.go(historyIndex)
}
</script>
<style scoped lang="less">
.seller-header {
@@ -90,8 +211,8 @@
font-family: "pingfang_regular";
color: #999;
font-size: 1.4rem;
cursor: pointer;
&:not(.last) {
&.clickable {
cursor: pointer;
text-decoration: underline;
}
}