Browse Source

feat(component): add data display components

main
Mohan 1 month ago
parent
commit
cab79da612
  1. 50
      src/components/SongBadge.vue
  2. 107
      src/components/SongModal.vue
  3. 87
      src/components/SongProgress.vue
  4. 59
      src/components/SongTable.vue

50
src/components/SongBadge.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>

107
src/components/SongModal.vue

@ -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>

87
src/components/SongProgress.vue

@ -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>

59
src/components/SongTable.vue

@ -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>
Loading…
Cancel
Save