Browse Source

feat(component): add toast layer and theme composable

main
Mohan 1 month ago
parent
commit
e92c81fcdc
  1. 84
      src/components/SongMessageContainer.vue
  2. 55
      src/components/index.js
  3. 38
      src/composables/useMessage.js
  4. 34
      src/composables/useSongTheme.js

84
src/components/SongMessageContainer.vue

@ -0,0 +1,84 @@
<script setup>
/* SongMessageContainer — 提示框承载层(雾隐淡入,墨线 + 小章意象) */
defineProps({
list: { type: Array, required: true }
})
</script>
<template>
<div class="song-messages">
<TransitionGroup name="song-toast">
<div v-for="t in list" :key="t.id" class="song-toast" :class="`song-toast--${t.type}`">
<span class="song-toast__stamp" aria-hidden="true">
<template v-if="t.type === 'success'"></template>
<template v-else-if="t.type === 'danger'"></template>
<template v-else-if="t.type === 'warning'">!</template>
<template v-else></template>
</span>
<span class="song-toast__text">{{ t.text }}</span>
</div>
</TransitionGroup>
</div>
</template>
<style scoped>
.song-messages {
position: fixed;
top: 24px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
z-index: 1000;
pointer-events: none;
}
.song-toast {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 18px;
background: var(--song-bg-elevated);
border: 1px solid var(--song-border);
border-left: 3px solid var(--song-text-secondary);
border-radius: var(--song-radius-s);
box-shadow: var(--song-shadow-2);
color: var(--song-text);
font-size: 13px;
pointer-events: auto;
}
.song-toast__stamp {
display: grid;
place-items: center;
width: 18px;
height: 18px;
flex-shrink: 0;
border: 1px solid currentColor;
border-radius: 2px;
font-size: 11px;
line-height: 1;
font-weight: 700;
color: var(--song-text-secondary);
}
.song-toast.song-toast--success { border-left-color: var(--song-success); }
.song-toast.song-toast--success .song-toast__stamp { color: var(--song-success); }
.song-toast.song-toast--danger { border-left-color: var(--song-danger); }
.song-toast.song-toast--danger .song-toast__stamp { color: var(--song-danger); }
.song-toast.song-toast--warning { border-left-color: var(--song-warning); }
.song-toast.song-toast--warning .song-toast__stamp { color: var(--song-warning); }
.song-toast.song-toast--info { border-left-color: var(--song-primary); }
.song-toast.song-toast--info .song-toast__stamp { color: var(--song-primary); }
/* 雾隐淡入 §7 */
.song-toast-enter-active,
.song-toast-leave-active {
transition: opacity var(--song-dur-2) var(--song-ease),
transform var(--song-dur-2) var(--song-ease);
}
.song-toast-enter-from,
.song-toast-leave-to {
opacity: 0;
transform: translateY(-10px);
}
</style>

55
src/components/index.js

@ -0,0 +1,55 @@
import SongButton from './SongButton.vue'
import SongCard from './SongCard.vue'
import SongTag from './SongTag.vue'
import SongInput from './SongInput.vue'
import SongNav from './SongNav.vue'
import SongTable from './SongTable.vue'
import SongBadge from './SongBadge.vue'
import SongProgress from './SongProgress.vue'
import SongModal from './SongModal.vue'
import SongScrim from './SongScrim.vue'
import SongLogo from './SongLogo.vue'
import SongDisclaimer from './SongDisclaimer.vue'
import SongLoading from './SongLoading.vue'
import SongSeal from './SongSeal.vue'
export {
SongButton,
SongCard,
SongTag,
SongInput,
SongNav,
SongTable,
SongBadge,
SongProgress,
SongModal,
SongScrim,
SongLogo,
SongDisclaimer,
SongLoading,
SongSeal
}
export const SongComponents = {
install(app) {
const map = {
SongButton,
SongCard,
SongTag,
SongInput,
SongNav,
SongTable,
SongBadge,
SongProgress,
SongModal,
SongScrim,
SongLogo,
SongDisclaimer,
SongLoading,
SongSeal
}
for (const [name, c] of Object.entries(map)) {
app.component(name, c)
}
}
}

38
src/composables/useMessage.js

@ -0,0 +1,38 @@
import { reactive, createVNode, render } from 'vue'
import SongMessageContainer from '../components/SongMessageContainer.vue'
const list = reactive([])
let seed = 0
let hostEl = null
function push(type, text, duration = 3400) {
const id = ++seed
list.push({ id, type, text })
setTimeout(() => remove(id), duration)
}
function remove(id) {
const i = list.findIndex(t => t.id === id)
if (i > -1) list.splice(i, 1)
}
function ensureHost() {
if (!hostEl) {
hostEl = document.createElement('div')
hostEl.className = 'song-message-host'
document.body.appendChild(hostEl)
const vnode = createVNode(SongMessageContainer, { list })
render(vnode, hostEl)
}
}
export function useMessage() {
ensureHost()
return {
list,
success: text => push('success', text),
danger: text => push('danger', text),
warning: text => push('warning', text),
info: text => push('info', text)
}
}

34
src/composables/useSongTheme.js

@ -0,0 +1,34 @@
import { ref, reactive } from 'vue'
const STORAGE_KEY = 'song-theme'
const isDark = ref(false)
let initialized = false
function apply(v) {
isDark.value = v
document.documentElement.setAttribute('data-theme', v ? 'dark' : 'light')
try {
localStorage.setItem(STORAGE_KEY, v ? 'dark' : 'light')
} catch { /* ignore */ }
}
const theme = reactive({
isDark,
toggle() {
apply(!isDark.value)
}
})
export function useSongTheme() {
if (!initialized) {
initialized = true
let stored = null
try { stored = localStorage.getItem(STORAGE_KEY) } catch { /* ignore */ }
const prefersDark = typeof window !== 'undefined' &&
window.matchMedia('(prefers-color-scheme: dark)').matches
apply(stored ? stored === 'dark' : prefersDark)
}
return theme
}
export const initSongTheme = useSongTheme
Loading…
Cancel
Save