128 lines
4.0 KiB
Vue
128 lines
4.0 KiB
Vue
<template>
|
||
<!-- 自建确认弹层:替代 window.confirm(后者在 Tauri webview 带 localhost:port 来源信息,无法去除)。
|
||
组件自包含:按钮样式内联,不依赖外部 .btn-*,便于在任何视图复用。 -->
|
||
<Transition name="confirm">
|
||
<div v-if="visible" class="confirm-mask" @click.self="$emit('result', false)">
|
||
<div class="confirm-box">
|
||
<div class="confirm-msg">{{ msg }}</div>
|
||
<div class="confirm-actions">
|
||
<button class="confirm-btn confirm-btn--cancel" @click="$emit('result', false)">{{ $t('common.cancel') }}</button>
|
||
<button class="confirm-btn confirm-btn--danger" @click="$emit('result', true)">{{ dangerLabel || $t('common.confirm') }}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onMounted, onBeforeUnmount, watch } from 'vue'
|
||
|
||
const props = withDefaults(defineProps<{
|
||
visible: boolean
|
||
msg: string
|
||
/** 危险按钮文案;不传则回退到 common.confirm(语义中性,删除等场景应显式传 common.delete) */
|
||
dangerLabel?: string
|
||
}>(), {
|
||
dangerLabel: '',
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
result: [ok: boolean]
|
||
}>()
|
||
|
||
// 键盘交互:可见时监听全局 keydown(Esc=取消,Enter=确认),不可见时移除监听。
|
||
// 与全屏对话框的 mask 点击同语义,补齐无鼠标可达性。
|
||
function onKey(e: KeyboardEvent) {
|
||
if (!props.visible) return
|
||
if (e.key === 'Escape') {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
emit('result', false)
|
||
} else if (e.key === 'Enter') {
|
||
// Enter 确认:危险操作(删除)仍走 confirm 分支,父级二次语义由 dangerLabel 区分;
|
||
// 避免在 input/textarea 内回车误触发(SVG/按钮 mask 场景无输入控件,守卫保留)。
|
||
const target = e.target as HTMLElement
|
||
// LW-1(安全):焦点在按钮上按 Enter 直接 return —— 否则「焦点在取消按钮按 Enter」
|
||
// 会先触发 cancel 按钮 click(emit false)再被此处 emit true 覆盖,点取消=执行删除。
|
||
if (target && target.tagName === 'BUTTON') return
|
||
if (target && (target.tagName === 'TEXTAREA' || (target.tagName === 'INPUT' && (target as HTMLInputElement).type !== 'submit' && (target as HTMLInputElement).type !== 'button'))) {
|
||
return
|
||
}
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
emit('result', true)
|
||
}
|
||
}
|
||
|
||
onMounted(() => document.addEventListener('keydown', onKey))
|
||
onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
|
||
// visible 切换不改监听器(props 读取实时值),watch 占位避免未来静态分析告警
|
||
watch(() => props.visible, () => {})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.confirm-mask {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
.confirm-box {
|
||
background: var(--df-bg-card, #fff);
|
||
border: 0.5px solid var(--df-border, #ddd);
|
||
border-radius: var(--df-radius-lg, 8px);
|
||
padding: 20px 24px;
|
||
min-width: 280px;
|
||
max-width: 360px;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
}
|
||
.confirm-msg {
|
||
font-size: 13px;
|
||
color: var(--df-text, #333);
|
||
line-height: 1.5;
|
||
margin-bottom: 16px;
|
||
}
|
||
.confirm-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
}
|
||
.confirm-btn {
|
||
padding: 5px 14px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
font-family: var(--df-font-sans);
|
||
border-radius: var(--df-radius-sm, 4px);
|
||
border: 0.5px solid var(--df-border, #ddd);
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
}
|
||
.confirm-btn--cancel {
|
||
background: transparent;
|
||
color: var(--df-text-dim, #888);
|
||
}
|
||
.confirm-btn--cancel:hover {
|
||
background: var(--df-bg-card-hover, #f0f0f0);
|
||
color: var(--df-text, #333);
|
||
}
|
||
.confirm-btn--danger {
|
||
background: var(--df-danger, #e5484d);
|
||
color: #fff;
|
||
border-color: var(--df-danger, #e5484d);
|
||
}
|
||
.confirm-btn--danger:hover {
|
||
filter: brightness(1.1);
|
||
}
|
||
.confirm-enter-active,
|
||
.confirm-leave-active {
|
||
transition: opacity 0.15s;
|
||
}
|
||
.confirm-enter-from,
|
||
.confirm-leave-to {
|
||
opacity: 0;
|
||
}
|
||
</style>
|