feat:移除了弹窗,服务器添加sls
This commit is contained in:
343
apps/app-frontend/src/components/ui/Breadcrumbs.vue
Normal file
343
apps/app-frontend/src/components/ui/Breadcrumbs.vue
Normal file
@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<div
|
||||
ref="outerRef"
|
||||
data-tauri-drag-region
|
||||
class="min-w-0 overflow-hidden pl-3"
|
||||
:class="{ 'breadcrumb-fade-mask': isOverflowing }"
|
||||
:style="isOverflowing ? { '--scroll-distance': `-${overflowAmount}px` } : undefined"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
>
|
||||
<div
|
||||
ref="innerRef"
|
||||
data-tauri-drag-region
|
||||
class="flex w-fit items-center gap-1"
|
||||
:class="{ 'breadcrumbs-scroll': isAnimating }"
|
||||
@animationiteration="onAnimationIteration"
|
||||
>
|
||||
<template v-for="(breadcrumb, index) in breadcrumbs" :key="breadcrumb.name">
|
||||
<router-link
|
||||
v-if="breadcrumb.link"
|
||||
:to="{
|
||||
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id as string)),
|
||||
query: breadcrumb.query,
|
||||
}"
|
||||
class="flex shrink-0 items-center gap-1 whitespace-nowrap text-primary"
|
||||
>
|
||||
<Avatar
|
||||
v-if="resolveIconUrl(breadcrumb)"
|
||||
:src="resolveIconUrl(breadcrumb)"
|
||||
:alt="resolveLabel(breadcrumb.name)"
|
||||
size="20px"
|
||||
no-shadow
|
||||
raised
|
||||
class="shrink-0 !rounded-md"
|
||||
/>
|
||||
<component
|
||||
:is="resolveIcon(breadcrumb)"
|
||||
v-else-if="resolveIcon(breadcrumb)"
|
||||
class="size-5 shrink-0 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ resolveLabel(breadcrumb.name) }}
|
||||
</router-link>
|
||||
<span
|
||||
v-else
|
||||
data-tauri-drag-region
|
||||
class="flex shrink-0 items-center gap-1 whitespace-nowrap text-contrast font-semibold cursor-default select-none"
|
||||
>
|
||||
<Avatar
|
||||
v-if="resolveIconUrl(breadcrumb)"
|
||||
:src="resolveIconUrl(breadcrumb)"
|
||||
:alt="resolveLabel(breadcrumb.name)"
|
||||
size="20px"
|
||||
no-shadow
|
||||
raised
|
||||
class="shrink-0 !rounded-md"
|
||||
/>
|
||||
<component
|
||||
:is="resolveIcon(breadcrumb)"
|
||||
v-else-if="resolveIcon(breadcrumb)"
|
||||
class="size-5 shrink-0 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ resolveLabel(breadcrumb.name) }}
|
||||
</span>
|
||||
<ChevronRightIcon
|
||||
v-if="index < breadcrumbs.length - 1"
|
||||
data-tauri-drag-region
|
||||
class="w-5 h-5 shrink-0"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowBigUpDashIcon,
|
||||
ChangeSkinIcon,
|
||||
ChevronRightIcon,
|
||||
CodeIcon,
|
||||
CompassIcon,
|
||||
DownloadIcon,
|
||||
FileTextIcon,
|
||||
FlaskConicalIcon,
|
||||
FolderIcon,
|
||||
GlobeIcon,
|
||||
HeartIcon,
|
||||
HomeIcon,
|
||||
ImagesIcon,
|
||||
LibraryIcon,
|
||||
MapIcon,
|
||||
PackageIcon,
|
||||
PencilIcon,
|
||||
ServerIcon,
|
||||
SettingsIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { type Component, computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { resolveBreadcrumbLabel } from '@/helpers/breadcrumb-label'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
|
||||
interface Breadcrumb {
|
||||
name: string
|
||||
link?: string
|
||||
query?: Record<string, string>
|
||||
iconUrl?: string | null
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbData = useBreadcrumbs()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
home: { id: 'app.navigation.home', defaultMessage: 'Home' },
|
||||
worlds: { id: 'app.navigation.worlds', defaultMessage: 'Worlds' },
|
||||
discoverContent: {
|
||||
id: 'app.navigation.discover-content',
|
||||
defaultMessage: 'Discover content',
|
||||
},
|
||||
skinSelector: { id: 'app.navigation.skin-selector', defaultMessage: 'Skin selector' },
|
||||
multiplayer: { id: 'app.navigation.multiplayer', defaultMessage: 'Multiplayer' },
|
||||
library: { id: 'app.navigation.library', defaultMessage: 'Library' },
|
||||
downloads: { id: 'app.navigation.downloads', defaultMessage: 'Downloads' },
|
||||
lab: { id: 'app.navigation.lab', defaultMessage: 'Lab' },
|
||||
gradientText: {
|
||||
id: 'app.lab.gradient-text.title',
|
||||
defaultMessage: 'Gradient text generator',
|
||||
},
|
||||
seedMap: { id: 'app.lab.seed-map.title', defaultMessage: 'Seed map' },
|
||||
schematicWorkshop: {
|
||||
id: 'app.lab.schematic-preview.title',
|
||||
defaultMessage: 'Schematic workshop',
|
||||
},
|
||||
modTranslation: {
|
||||
id: 'app.lab.mod-translation.title',
|
||||
defaultMessage: 'Mod translation',
|
||||
},
|
||||
skinEditor: { id: 'app.lab.skin-editor.title', defaultMessage: 'Skin editor' },
|
||||
content: { id: 'app.instance.tabs.content', defaultMessage: 'Content' },
|
||||
files: { id: 'app.instance.tabs.files', defaultMessage: 'Files' },
|
||||
studio: { id: 'instance.files.studio.title', defaultMessage: 'Studio' },
|
||||
logs: { id: 'app.instance.tabs.logs', defaultMessage: 'Logs' },
|
||||
editWorld: { id: 'app.navigation.edit-world', defaultMessage: 'Edit world' },
|
||||
upgradeInstance: { id: 'app.instance.upgrade-instance', defaultMessage: 'Upgrade instance' },
|
||||
})
|
||||
|
||||
const staticLabels = {
|
||||
Home: messages.home,
|
||||
Worlds: messages.worlds,
|
||||
'Discover content': messages.discoverContent,
|
||||
'Skin selector': messages.skinSelector,
|
||||
Multiplayer: messages.multiplayer,
|
||||
Library: messages.library,
|
||||
Downloads: messages.downloads,
|
||||
Settings: commonMessages.settingsLabel,
|
||||
Lab: messages.lab,
|
||||
'Gradient text generator': messages.gradientText,
|
||||
'Seed map': messages.seedMap,
|
||||
'Schematic workshop': messages.schematicWorkshop,
|
||||
'Mod translation': messages.modTranslation,
|
||||
'Skin editor': messages.skinEditor,
|
||||
Content: messages.content,
|
||||
Files: messages.files,
|
||||
Studio: messages.studio,
|
||||
Logs: messages.logs,
|
||||
'Edit world': messages.editWorld,
|
||||
Upgrade: messages.upgradeInstance,
|
||||
}
|
||||
|
||||
const staticIcons: Record<string, Component> = {
|
||||
Home: HomeIcon,
|
||||
Worlds: GlobeIcon,
|
||||
'Discover content': CompassIcon,
|
||||
'Skin selector': ChangeSkinIcon,
|
||||
Multiplayer: ServerIcon,
|
||||
Library: LibraryIcon,
|
||||
Downloads: DownloadIcon,
|
||||
Settings: SettingsIcon,
|
||||
Lab: FlaskConicalIcon,
|
||||
'Gradient text generator': FlaskConicalIcon,
|
||||
'Seed map': MapIcon,
|
||||
'Schematic workshop': CodeIcon,
|
||||
'Mod translation': CodeIcon,
|
||||
'Skin editor': PencilIcon,
|
||||
Content: PackageIcon,
|
||||
Files: FolderIcon,
|
||||
Studio: CodeIcon,
|
||||
Logs: FileTextIcon,
|
||||
'Edit world': PencilIcon,
|
||||
Upgrade: ArrowBigUpDashIcon,
|
||||
Favorites: HeartIcon,
|
||||
Versions: PackageIcon,
|
||||
Gallery: ImagesIcon,
|
||||
Screenshots: ImagesIcon,
|
||||
'Drop help': FileTextIcon,
|
||||
'Recipe generator': FlaskConicalIcon,
|
||||
Downloaded: DownloadIcon,
|
||||
Modpacks: PackageIcon,
|
||||
LibraryServers: ServerIcon,
|
||||
Custom: PackageIcon,
|
||||
Shared: PackageIcon,
|
||||
Saved: HeartIcon,
|
||||
}
|
||||
|
||||
const breadcrumbs = computed<Breadcrumb[]>(() => {
|
||||
const additionalContext =
|
||||
route.meta.useContext === true
|
||||
? breadcrumbData.context
|
||||
: route.meta.useRootContext === true
|
||||
? breadcrumbData.rootContext
|
||||
: null
|
||||
const crumbs = (route.meta.breadcrumb ?? []) as Breadcrumb[]
|
||||
if (
|
||||
additionalContext?.name.startsWith('?') &&
|
||||
crumbs.some((crumb) => crumb.name === additionalContext.name)
|
||||
) {
|
||||
return crumbs
|
||||
}
|
||||
return additionalContext ? [additionalContext as Breadcrumb, ...crumbs] : crumbs
|
||||
})
|
||||
|
||||
function resolveLabel(name: string): string {
|
||||
return resolveBreadcrumbLabel(
|
||||
name,
|
||||
(key) => breadcrumbData.getName(key),
|
||||
staticLabels,
|
||||
(message) => formatMessage(message),
|
||||
)
|
||||
}
|
||||
|
||||
function resolveIcon(breadcrumb: Breadcrumb): Component | undefined {
|
||||
if (breadcrumb.iconUrl || breadcrumbData.getIcon(breadcrumb.name.slice(1))) return undefined
|
||||
const dynamicIcons: Record<string, Component> = {
|
||||
'?Project': PackageIcon,
|
||||
'?Version': PackageIcon,
|
||||
'?BrowseTitle': CompassIcon,
|
||||
'?FavoritesTitle': HeartIcon,
|
||||
}
|
||||
if (dynamicIcons[breadcrumb.name]) return dynamicIcons[breadcrumb.name]
|
||||
const key = breadcrumb.name.startsWith('?') ? resolveLabel(breadcrumb.name) : breadcrumb.name
|
||||
return staticIcons[key]
|
||||
}
|
||||
|
||||
function resolveIconUrl(breadcrumb: Breadcrumb): string | null {
|
||||
return (
|
||||
breadcrumb.iconUrl ??
|
||||
(breadcrumb.name.startsWith('?') ? breadcrumbData.getIcon(breadcrumb.name.slice(1)) : null)
|
||||
)
|
||||
}
|
||||
|
||||
// Overflow detection
|
||||
const outerRef = ref<HTMLDivElement | null>(null)
|
||||
const innerRef = ref<HTMLDivElement | null>(null)
|
||||
const isOverflowing = ref(false)
|
||||
const isAnimating = ref(false)
|
||||
const overflowAmount = ref(0)
|
||||
|
||||
let hovered = false
|
||||
let stopping = false
|
||||
|
||||
function checkOverflow() {
|
||||
if (!outerRef.value || !innerRef.value) return
|
||||
const overflow = innerRef.value.scrollWidth - outerRef.value.clientWidth
|
||||
isOverflowing.value = overflow > 0
|
||||
overflowAmount.value = overflow + 12
|
||||
}
|
||||
|
||||
function onMouseEnter() {
|
||||
hovered = true
|
||||
stopping = false
|
||||
if (isOverflowing.value) {
|
||||
isAnimating.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
hovered = false
|
||||
if (isAnimating.value) {
|
||||
stopping = true
|
||||
}
|
||||
}
|
||||
|
||||
function onAnimationIteration() {
|
||||
if (stopping && !hovered) {
|
||||
isAnimating.value = false
|
||||
stopping = false
|
||||
}
|
||||
}
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
onMounted(() => {
|
||||
checkOverflow()
|
||||
resizeObserver = new ResizeObserver(checkOverflow)
|
||||
if (outerRef.value) resizeObserver.observe(outerRef.value)
|
||||
if (innerRef.value) resizeObserver.observe(innerRef.value)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect()
|
||||
})
|
||||
|
||||
watch(
|
||||
breadcrumbs,
|
||||
() => {
|
||||
breadcrumbData.resetToNames(breadcrumbs.value)
|
||||
requestAnimationFrame(checkOverflow)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.breadcrumb-fade-mask {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black 12px,
|
||||
black calc(100% - 12px),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.breadcrumbs-scroll {
|
||||
animation: breadcrumb-scroll 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes breadcrumb-scroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
35%,
|
||||
65% {
|
||||
transform: translateX(var(--scroll-distance));
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user