4 changed files with 211 additions and 0 deletions
-
84src/components/SongMessageContainer.vue
-
55src/components/index.js
-
38src/composables/useMessage.js
-
34src/composables/useSongTheme.js
@ -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> |
|||
@ -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) |
|||
} |
|||
} |
|||
} |
|||
@ -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) |
|||
} |
|||
} |
|||
@ -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 |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue