263 lines
6.1 KiB
Vue
263 lines
6.1 KiB
Vue
<template>
|
||
<view class="center-modal" v-if="visible">
|
||
<!-- 遮罩层:点击关闭。mask 与 dialog-wrap 是兄弟节点,点击不会相互干扰 -->
|
||
<view class="cm-mask" @click="handleMaskClick" hover-stop-propagation></view>
|
||
<!-- 弹窗容器,绝对定位居中 -->
|
||
<view class="cm-dialog-wrap">
|
||
<!-- 弹窗主体 -->
|
||
<view class="cm-dialog" :style="{ backgroundColor: themeConfig.cardBgColor }">
|
||
<view class="cm-header" v-if="title || $slots.header">
|
||
<slot name="header">
|
||
<text class="cm-title" :style="{ color: themeConfig.textPrimary }">{{ title }}</text>
|
||
</slot>
|
||
</view>
|
||
<view class="cm-body" :style="{ maxHeight: maxBodyHeight }">
|
||
<slot></slot>
|
||
</view>
|
||
<view class="cm-footer" v-if="showCancel || showConfirm || $slots.footer">
|
||
<slot name="footer">
|
||
<view
|
||
v-if="showCancel"
|
||
class="cm-btn cm-btn--cancel"
|
||
:style="{ background: cancelBg, color: themeConfig.textPrimary }"
|
||
hover-class="cm-btn--cancel--active"
|
||
:hover-stay-time="100"
|
||
@click="handleCancel"
|
||
>
|
||
<text class="cm-btn__text">{{ cancelText }}</text>
|
||
</view>
|
||
<view
|
||
v-if="showConfirm"
|
||
class="cm-btn cm-btn--confirm"
|
||
:class="{ 'cm-btn--disabled': confirmDisabled }"
|
||
:style="confirmBtnStyle"
|
||
hover-class="cm-btn--confirm--active"
|
||
:hover-stay-time="100"
|
||
@click="handleConfirm"
|
||
>
|
||
<text class="cm-btn__text">{{ confirmText }}</text>
|
||
</view>
|
||
</slot>
|
||
</view>
|
||
</view>
|
||
<!-- 关闭按钮:放在弹窗容器内,与 dialog 平级,避免被 dialog 拦截事件 -->
|
||
<view
|
||
v-if="showClose"
|
||
class="cm-close"
|
||
:style="{ background: themeConfig.cardBgColor, color: themeConfig.textTertiary, boxShadow: `0 6rpx 20rpx ${themeConfig.shadowColor || 'rgba(0,0,0,0.15)'}` }"
|
||
hover-class="cm-close--active"
|
||
:hover-stay-time="100"
|
||
@click="handleClose"
|
||
>
|
||
<FaIcon icon="xmark" :size="22" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import FaIcon from './FaIcon.vue'
|
||
import themeMixin from '@/mixins/themeMixin.js'
|
||
|
||
export default {
|
||
name: 'CenterModal',
|
||
components: { FaIcon },
|
||
mixins: [themeMixin],
|
||
props: {
|
||
visible: { type: Boolean, default: false },
|
||
title: { type: String, default: '' },
|
||
showClose: { type: Boolean, default: true },
|
||
showCancel: { type: Boolean, default: true },
|
||
showConfirm: { type: Boolean, default: true },
|
||
maskClosable: { type: Boolean, default: true },
|
||
cancelText: { type: String, default: '取消' },
|
||
confirmText: { type: String, default: '确定' },
|
||
confirmColor: { type: String, default: '' },
|
||
confirmDisabled: { type: Boolean, default: false },
|
||
maxBodyHeight: { type: String, default: '70vh' }
|
||
},
|
||
computed: {
|
||
/**
|
||
* 确认按钮样式:直接应用主题渐变色(不依赖 CSS 变量),确保主题切换立即生效
|
||
*/
|
||
confirmBtnStyle() {
|
||
const bg = this.confirmColor || this.themeConfig.gradientSoft
|
||
return {
|
||
background: bg,
|
||
color: '#ffffff',
|
||
boxShadow: `0 6rpx 18rpx ${this.themeConfig.shadowColor || 'rgba(198, 142, 63, 0.35)'}`
|
||
}
|
||
},
|
||
cancelBg() {
|
||
// 跟随主题色:使用 inputBgColor 创造与主题协调的次按钮背景
|
||
return this.themeConfig.inputBgColor || 'rgba(0, 0, 0, 0.06)'
|
||
}
|
||
},
|
||
methods: {
|
||
handleMaskClick() {
|
||
if (!this.maskClosable) return
|
||
this.handleClose()
|
||
},
|
||
handleClose() {
|
||
if (!this.visible) return
|
||
this.$emit('update:visible', false)
|
||
this.$emit('close')
|
||
},
|
||
handleCancel() {
|
||
if (!this.visible) return
|
||
this.$emit('update:visible', false)
|
||
this.$emit('close')
|
||
this.$emit('cancel')
|
||
},
|
||
handleConfirm() {
|
||
if (!this.visible) return
|
||
if (this.confirmDisabled) return
|
||
this.$emit('confirm')
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.center-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 99999;
|
||
}
|
||
|
||
.cm-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
animation: cm-fadeIn 0.2s ease;
|
||
}
|
||
|
||
@keyframes cm-fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
/* 弹窗容器:用绝对定位 + transform 居中,简单可靠 */
|
||
.cm-dialog-wrap {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
animation: cm-zoomIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
@keyframes cm-zoomIn {
|
||
from { transform: translate(-50%, -50%) scale(0.92); opacity: 0; }
|
||
to { transform: translate(-50%, -50%) scale(1); opacity: 1; }
|
||
}
|
||
|
||
.cm-dialog {
|
||
position: relative;
|
||
width: 80vw;
|
||
max-width: 600rpx;
|
||
max-height: 85vh;
|
||
border-radius: 24rpx;
|
||
padding: 56rpx 40rpx 40rpx;
|
||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.18);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.cm-header {
|
||
text-align: center;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.cm-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.cm-body {
|
||
font-size: 28rpx;
|
||
line-height: 1.6;
|
||
min-height: 60rpx;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
|
||
.cm-footer {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 24rpx;
|
||
margin-top: 40rpx;
|
||
}
|
||
|
||
/* 关闭按钮:紧贴弹窗右上角外侧 */
|
||
.cm-close {
|
||
position: absolute;
|
||
top: -28rpx;
|
||
right: -28rpx;
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 50%;
|
||
z-index: 10;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 2rpx solid rgba(0, 0, 0, 0.06);
|
||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
.cm-close--active {
|
||
transform: scale(0.9);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* 按钮:默认中等大小,不依赖 .btn 统一样式,避免被外部样式干扰 */
|
||
.cm-btn {
|
||
flex: 1;
|
||
min-height: 80rpx;
|
||
padding: 0 40rpx;
|
||
border-radius: 14rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
border: none;
|
||
outline: none;
|
||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
.cm-btn__text {
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.cm-btn--cancel--active {
|
||
opacity: 0.75;
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.cm-btn--confirm--active {
|
||
opacity: 0.92;
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.cm-btn--disabled {
|
||
opacity: 0.5;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.cm-btn:only-child {
|
||
flex: 1;
|
||
}
|
||
</style>
|