ahutchen's picture
feat(component): 新增歌单相关组件和功能
021ee94
<template>
<Teleport to="body">
<Transition name="modal" appear>
<div v-if="visible" class="modal-overlay" @click="handleOverlayClick">
<div
class="modal"
:class="size"
@click.stop
>
<!-- 头部 -->
<div class="modal-header" v-if="title || closable">
<h3 class="modal-title" v-if="title">{{ title }}</h3>
<button
v-if="closable"
class="modal-close-btn"
@click="handleClose"
>
<i class="fas fa-times"></i>
</button>
</div>
<!-- 内容 -->
<div class="modal-body">
<slot></slot>
</div>
<!-- 底部 -->
<div class="modal-footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const props = defineProps({
// 标题
title: {
type: String,
default: ''
},
// 大小:small, medium, large
size: {
type: String,
default: 'medium',
validator: (value) => ['small', 'medium', 'large'].includes(value)
},
// 是否可关闭
closable: {
type: Boolean,
default: true
},
// 点击遮罩是否关闭
maskClosable: {
type: Boolean,
default: true
},
// 是否锁定滚动
lockScroll: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['close', 'open'])
const visible = ref(false)
// 处理遮罩点击
const handleOverlayClick = () => {
if (props.maskClosable) {
handleClose()
}
}
// 处理关闭
const handleClose = () => {
close()
}
// 处理键盘事件
const handleKeyDown = (event) => {
if (event.key === 'Escape' && props.closable) {
handleClose()
}
}
// 锁定/解锁滚动
const toggleScrollLock = (lock) => {
if (!props.lockScroll) return
if (lock) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
}
// 显示模态框
const open = () => {
visible.value = true
toggleScrollLock(true)
document.addEventListener('keydown', handleKeyDown)
emit('open')
}
// 关闭模态框
const close = () => {
visible.value = false
toggleScrollLock(false)
document.removeEventListener('keydown', handleKeyDown)
emit('close')
}
// 生命周期
onMounted(() => {
// 移除自动打开逻辑,改为由父组件控制
})
onUnmounted(() => {
toggleScrollLock(false)
document.removeEventListener('keydown', handleKeyDown)
})
// 暴露方法
defineExpose({
open,
close
})
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal {
background: var(--bg-secondary);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
max-height: calc(100vh - 40px);
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal.small {
max-width: 400px;
width: 90%;
}
.modal.medium {
max-width: 600px;
width: 90%;
}
.modal.large {
max-width: 800px;
width: 95%;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.modal-close-btn {
width: 32px;
height: 32px;
border: none;
background: rgba(255, 255, 255, 0.1);
color: var(--text-secondary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition-fast);
}
.modal-close-btn:hover {
background: rgba(255, 255, 255, 0.2);
color: var(--text-primary);
}
.modal-body {
flex: 1;
padding: 20px 24px;
overflow-y: auto;
}
.modal-footer {
padding: 16px 24px 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
/* 动画 */
.modal-enter-active,
.modal-leave-active {
transition: all 0.3s ease-out;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal,
.modal-leave-to .modal {
transform: scale(0.9) translateY(-20px);
}
/* 响应式 */
@media (max-width: 375px) {
.modal-overlay {
padding: 16px;
}
.modal {
border-radius: 12px;
}
.modal.small,
.modal.medium,
.modal.large {
width: 100%;
max-width: none;
}
.modal-header {
padding: 16px 20px 12px;
}
.modal-title {
font-size: 16px;
}
.modal-close-btn {
width: 28px;
height: 28px;
}
.modal-body {
padding: 16px 20px;
}
.modal-footer {
padding: 12px 20px 16px;
}
}
/* 滚动条样式 */
.modal-body::-webkit-scrollbar {
width: 6px;
}
.modal-body::-webkit-scrollbar-track {
background: transparent;
}
.modal-body::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.modal-body::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>