Files
DevFlow/src/components/ConfirmDialog.vue
T
lxy 0168617503 优化: 溢出修复与视觉token收敛+三处小清理
- diff长行折行可读/任务描述与产出宽表格横滚/确认弹窗长路径折行
- 滚动条色抽token浅色主题可见/阶段徽章与优先级色对齐语义token
- git状态色与错误色替换散落硬编码
- alert残留换toast走i18n/分离窗口缺参数补错误态/dev路由生产守卫
2026-09-29 14:58:48 +08:00

130 lines
4.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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;
/* anywhere:长 Windows 路径等无空格长串可断行,不撑破 360px 弹窗 */
overflow-wrap: anywhere;
}
.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>