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