4 changed files with 303 additions and 0 deletions
-
50src/components/SongBadge.vue
-
107src/components/SongModal.vue
-
87src/components/SongProgress.vue
-
59src/components/SongTable.vue
@ -0,0 +1,50 @@ |
|||||
|
<script setup> |
||||
|
import { computed } from 'vue' |
||||
|
const props = defineProps({ |
||||
|
count: { type: [Number, String], default: 0 }, |
||||
|
type: { type: String, default: 'default' }, |
||||
|
dot: { type: Boolean, default: false } |
||||
|
}) |
||||
|
defineOptions({ name: 'SongBadge' }) |
||||
|
const show = computed(() => !props.dot && props.count) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<span class="song-badge" :class="[`song-badge--${type}`, { 'song-badge--dot': dot }]"> |
||||
|
<slot /> |
||||
|
<span v-if="dot || show" class="song-badge__chip" :class="{ 'song-badge__chip--dot': dot }"> |
||||
|
{{ dot ? '' : count }} |
||||
|
</span> |
||||
|
</span> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.song-badge { |
||||
|
position: relative; |
||||
|
display: inline-flex; |
||||
|
} |
||||
|
.song-badge__chip { |
||||
|
position: absolute; |
||||
|
top: -8px; |
||||
|
right: -12px; |
||||
|
min-width: 16px; |
||||
|
height: 16px; |
||||
|
padding: 0 5px; |
||||
|
border-radius: 8px; |
||||
|
background: var(--song-danger); |
||||
|
color: #fff; |
||||
|
font-size: 11px; |
||||
|
line-height: 16px; |
||||
|
text-align: center; |
||||
|
font-variant-numeric: tabular-nums; |
||||
|
} |
||||
|
.song-badge__chip--dot { |
||||
|
min-width: 8px; |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
padding: 0; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
.song-badge--primary .song-badge__chip { background: var(--song-primary); } |
||||
|
.song-badge--success .song-badge__chip { background: var(--song-success); } |
||||
|
</style> |
||||
@ -0,0 +1,107 @@ |
|||||
|
<script setup> |
||||
|
import { ref, watch, onBeforeUnmount } from 'vue' |
||||
|
const props = defineProps({ |
||||
|
modelValue: { type: Boolean, default: false }, |
||||
|
title: { type: String, default: '' }, |
||||
|
width: { type: String, default: '440px' } |
||||
|
}) |
||||
|
const emit = defineEmits(['update:modelValue', 'close']) |
||||
|
defineOptions({ name: 'SongModal' }) |
||||
|
|
||||
|
const open = ref(props.modelValue) |
||||
|
watch(() => props.modelValue, v => { |
||||
|
open.value = v |
||||
|
document.body.style.overflow = v ? 'hidden' : '' |
||||
|
}) |
||||
|
|
||||
|
function close() { |
||||
|
emit('update:modelValue', false) |
||||
|
emit('close') |
||||
|
} |
||||
|
onBeforeUnmount(() => { document.body.style.overflow = '' }) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Teleport to="body"> |
||||
|
<Transition name="song-modal"> |
||||
|
<div v-if="open" class="song-modal" @click.self="close"> |
||||
|
<div class="song-modal__panel" :style="{ width }"> |
||||
|
<div class="song-modal__head"> |
||||
|
<span class="h3">{{ title }}</span> |
||||
|
<button class="song-modal__close" @click="close" aria-label="关闭">×</button> |
||||
|
</div> |
||||
|
<div class="song-modal__body"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
<div class="song-modal__foot"> |
||||
|
<slot name="footer" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</Transition> |
||||
|
</Teleport> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.song-modal { |
||||
|
position: fixed; |
||||
|
inset: 0; |
||||
|
background: rgba(20, 24, 27, 0.45); |
||||
|
display: grid; |
||||
|
place-items: center; |
||||
|
z-index: 900; |
||||
|
} |
||||
|
.song-modal__panel { |
||||
|
background: var(--song-bg-elevated); |
||||
|
border-radius: var(--song-radius-l); |
||||
|
border: 1px solid var(--song-border); |
||||
|
box-shadow: var(--song-shadow-3); |
||||
|
max-width: calc(100vw - 48px); |
||||
|
} |
||||
|
.song-modal__head { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
padding: var(--song-space-4) var(--song-space-5); |
||||
|
border-bottom: 1px solid var(--song-border); |
||||
|
} |
||||
|
.song-modal__close { |
||||
|
border: none; |
||||
|
background: transparent; |
||||
|
font-size: 20px; |
||||
|
color: var(--song-text-secondary); |
||||
|
cursor: pointer; |
||||
|
padding: 0 4px; |
||||
|
} |
||||
|
.song-modal__close:hover { color: var(--song-danger); } |
||||
|
.song-modal__body { |
||||
|
padding: var(--song-space-5); |
||||
|
color: var(--song-text-secondary); |
||||
|
line-height: 1.8; |
||||
|
} |
||||
|
.song-modal__foot { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
gap: 12px; |
||||
|
padding: var(--song-space-4) var(--song-space-5); |
||||
|
border-top: 1px solid var(--song-border); |
||||
|
} |
||||
|
|
||||
|
.song-modal-enter-active, |
||||
|
.song-modal-leave-active { |
||||
|
transition: opacity var(--song-dur-2) var(--song-ease); |
||||
|
} |
||||
|
.song-modal-enter-active .song-modal__panel, |
||||
|
.song-modal-leave-active .song-modal__panel { |
||||
|
transition: transform var(--song-dur-2) var(--song-ease), opacity var(--song-dur-2) var(--song-ease); |
||||
|
} |
||||
|
.song-modal-enter-from, |
||||
|
.song-modal-leave-to { |
||||
|
opacity: 0; |
||||
|
} |
||||
|
.song-modal-enter-from .song-modal__panel, |
||||
|
.song-modal-leave-to .song-modal__panel { |
||||
|
transform: translateY(8px); |
||||
|
opacity: 0; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,87 @@ |
|||||
|
<script setup> |
||||
|
import { computed } from 'vue' |
||||
|
const props = defineProps({ |
||||
|
percent: { type: Number, default: 0 }, |
||||
|
type: { type: String, default: 'primary' }, |
||||
|
bar: { type: Boolean, default: true } |
||||
|
}) |
||||
|
defineOptions({ name: 'SongProgress' }) |
||||
|
|
||||
|
const deg = computed(() => `${props.percent * 3.6}deg`) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="song-progress" :class="{ 'song-progress--ring': !bar }"> |
||||
|
<template v-if="bar"> |
||||
|
<div class="song-progress__track"> |
||||
|
<div |
||||
|
class="song-progress__fill" |
||||
|
:class="`song-progress__fill--${type}`" |
||||
|
:style="{ width: percent + '%' }" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="song-progress__mark tnum">{{ percent }}%</div> |
||||
|
</template> |
||||
|
|
||||
|
<template v-else> |
||||
|
<div |
||||
|
class="song-progress__ring" |
||||
|
:class="`song-progress__ring--${type}`" |
||||
|
:style="{ background: `conic-gradient(var(--ring-color) ${deg}, transparent 0)` }" |
||||
|
> |
||||
|
<div class="song-progress__ring-inner tnum">{{ percent }}%</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.song-progress { |
||||
|
--ring-color: var(--song-primary); |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
.song-progress__track { |
||||
|
width: 160px; |
||||
|
height: 5px; |
||||
|
border-radius: 3px; |
||||
|
background: var(--song-surface-3); |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
.song-progress__fill { |
||||
|
height: 100%; |
||||
|
border-radius: 3px; |
||||
|
} |
||||
|
.song-progress__fill--primary { background: var(--song-primary); } |
||||
|
.song-progress__fill--success { background: var(--song-success); } |
||||
|
.song-progress__fill--warning { background: var(--song-warning); } |
||||
|
.song-progress__fill--danger { background: var(--song-danger); } |
||||
|
.song-progress__mark { |
||||
|
font-size: 12px; |
||||
|
color: var(--song-text-secondary); |
||||
|
} |
||||
|
|
||||
|
/* 环形进度 — 缠枝墨线描边意象 §1.2 */ |
||||
|
.song-progress--ring .song-progress__ring { |
||||
|
--ring-color: var(--song-primary); |
||||
|
width: 88px; |
||||
|
height: 88px; |
||||
|
border-radius: 50%; |
||||
|
display: grid; |
||||
|
place-items: center; |
||||
|
} |
||||
|
.song-progress__ring-inner { |
||||
|
width: 74px; |
||||
|
height: 74px; |
||||
|
border-radius: 50%; |
||||
|
background: var(--song-bg-elevated); |
||||
|
display: grid; |
||||
|
place-items: center; |
||||
|
font-size: 16px; |
||||
|
border: 1px solid var(--song-border); |
||||
|
} |
||||
|
.song-progress__ring--success { --ring-color: var(--song-success); } |
||||
|
.song-progress__ring--warning { --ring-color: var(--song-warning); } |
||||
|
.song-progress__ring--danger { --ring-color: var(--song-danger); } |
||||
|
</style> |
||||
@ -0,0 +1,59 @@ |
|||||
|
<script setup> |
||||
|
defineProps({ |
||||
|
columns: { type: Array, required: true }, |
||||
|
data: { type: Array, required: true }, |
||||
|
striping: { type: Boolean, default: true } |
||||
|
}) |
||||
|
defineOptions({ name: 'SongTable' }) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<table class="song-table"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th v-for="c in columns" :key="c.key" :style="c.width ? { width: c.width } : {}"> |
||||
|
{{ c.title }} |
||||
|
</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<tr v-for="(row, ri) in data" :key="ri" :class="{ 'song-table__alt': striping && ri % 2 === 1 }"> |
||||
|
<td v-for="c in columns" :key="c.key"> |
||||
|
<slot :name="c.key" :row="row">{{ row[c.key] }}</slot> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.song-table { |
||||
|
width: 100%; |
||||
|
border-collapse: collapse; |
||||
|
font-size: 13px; |
||||
|
} |
||||
|
.song-table th { |
||||
|
text-align: left; |
||||
|
padding: 12px var(--song-space-4); |
||||
|
font-weight: 500; |
||||
|
font-size: 12px; |
||||
|
color: var(--song-text-secondary); |
||||
|
border-bottom: 1px solid var(--song-border-strong); |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
.song-table td { |
||||
|
padding: 12px var(--song-space-4); |
||||
|
border-bottom: 1px solid var(--song-border); |
||||
|
color: var(--song-text); |
||||
|
font-variant-numeric: tabular-nums; |
||||
|
} |
||||
|
.song-table tbody tr { |
||||
|
transition: background-color var(--song-dur-1) var(--song-ease); |
||||
|
} |
||||
|
.song-table tbody tr:hover { |
||||
|
background: var(--song-hover); |
||||
|
} |
||||
|
.song-table__alt { |
||||
|
background: rgba(58, 74, 82, 0.025); |
||||
|
} |
||||
|
</style> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue