feat(h5): 新增通用组件 NavBar/Toast/Chip/StarLevel/Skeleton

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-04-24 16:15:54 +08:00
parent f410a95a3d
commit 82490bc786
7 changed files with 79 additions and 0 deletions

View File

@ -1,3 +1,6 @@
<script setup>
import Toast from '@/components/Toast.vue'
</script>
<template>
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
@ -6,6 +9,7 @@
</keep-alive>
</transition>
</router-view>
<Toast />
</template>
<style>

View File

@ -0,0 +1,14 @@
<script setup>
defineProps({ tone: { type: String, default: 'neutral' } })
const map = {
brand: 'bg-brand/10 text-brand',
danger: 'bg-danger/10 text-danger',
info: 'bg-info/10 text-info',
neutral: 'bg-neutral-100 text-neutral-600',
}
</script>
<template>
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded-full whitespace-nowrap" :class="map[tone]">
<slot />
</span>
</template>

View File

@ -0,0 +1,15 @@
<script setup>
import { useRouter } from 'vue-router'
defineProps({ title: String, showBack: { type: Boolean, default: true } })
const router = useRouter()
</script>
<template>
<header class="sticky top-0 z-20 h-12 flex items-center bg-white/90 backdrop-blur border-b border-line">
<button v-if="showBack" class="w-12 h-12 flex items-center justify-center active:bg-line" @click="router.back()">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M13 4l-6 6 6 6" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div v-else class="w-12 h-12"></div>
<h1 class="flex-1 text-center text-base font-semibold truncate px-4">{{ title }}</h1>
<div class="w-12 h-12"><slot name="right" /></div>
</header>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div class="animate-pulse bg-neutral-200/60 rounded" />
</template>

View File

@ -0,0 +1,11 @@
<script setup>
defineProps({ value: { type: Number, default: 0 }, max: { type: Number, default: 3 } })
</script>
<template>
<span class="inline-flex items-center gap-0.5">
<svg v-for="i in max" :key="i" width="14" height="14" viewBox="0 0 20 20"
:fill="i <= value ? '#2F4F3F' : '#E5E5E5'">
<path d="M10 1.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L10 14.9 4.8 17.6l1-5.8L1.5 7.7l5.9-.9z"/>
</svg>
</span>
</template>

View File

@ -0,0 +1,21 @@
<script setup>
import { useToast } from '@/composables/useToast'
const { state } = useToast()
</script>
<template>
<teleport to="body">
<div class="fixed top-2 left-0 right-0 z-50 flex flex-col items-center gap-2 pointer-events-none">
<transition-group name="toast">
<div v-for="t in state.list" :key="t.id"
class="px-4 py-2 rounded-full text-sm shadow-card"
:class="t.type==='error' ? 'bg-danger text-white' : 'bg-neutral-800/90 text-white'">
{{ t.msg }}
</div>
</transition-group>
</div>
</teleport>
</template>
<style>
.toast-enter-active,.toast-leave-active{transition:all .25s ease}
.toast-enter-from,.toast-leave-to{opacity:0;transform:translateY(-8px)}
</style>

View File

@ -0,0 +1,11 @@
import { reactive } from 'vue'
const state = reactive({ list: [] })
let seq = 0
export function useToast() {
const show = (msg, type = 'info', duration = 2000) => {
const id = ++seq
state.list.push({ id, msg, type })
setTimeout(() => { state.list = state.list.filter(x => x.id !== id) }, duration)
}
return { state, show }
}