90 lines
2.8 KiB
Vue
90 lines
2.8 KiB
Vue
<script setup lang="ts">
|
|
import { nextTick, onUnmounted, ref, watch } from 'vue'
|
|
import Icon from './Icon.vue'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
open: boolean
|
|
title: string
|
|
label?: string
|
|
placeholder?: string
|
|
initial?: string
|
|
confirmText?: string
|
|
validate?: (v: string) => string | null
|
|
}>(), { label: '', placeholder: '', initial: '', confirmText: '确定' })
|
|
|
|
const emit = defineEmits<{ (e: 'cancel'): void; (e: 'confirm', value: string): void }>()
|
|
|
|
const value = ref('')
|
|
const err = ref('')
|
|
const input = ref<HTMLInputElement | null>(null)
|
|
|
|
function onKey(e: KeyboardEvent) {
|
|
if (!props.open) return
|
|
if (e.key === 'Escape') emit('cancel')
|
|
}
|
|
watch(() => props.open, async v => {
|
|
if (v) {
|
|
value.value = props.initial
|
|
err.value = ''
|
|
window.addEventListener('keydown', onKey)
|
|
await nextTick()
|
|
input.value?.focus()
|
|
input.value?.select()
|
|
} else {
|
|
window.removeEventListener('keydown', onKey)
|
|
}
|
|
})
|
|
onUnmounted(() => window.removeEventListener('keydown', onKey))
|
|
|
|
function confirm() {
|
|
const v = value.value.trim()
|
|
if (!v) { err.value = '请输入内容'; return }
|
|
const e = props.validate ? props.validate(v) : null
|
|
if (e) { err.value = e; return }
|
|
emit('confirm', v)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<teleport to="body">
|
|
<transition name="modal">
|
|
<div v-if="open" class="mask" @click.self="emit('cancel')">
|
|
<div class="dialog input-dialog" role="dialog" :aria-label="title">
|
|
<div class="dialog-head">
|
|
<span class="dialog-title">{{ title }}</span>
|
|
<button class="icon-btn" title="关闭" @click="emit('cancel')">
|
|
<Icon name="close" :size="16" />
|
|
</button>
|
|
</div>
|
|
<div class="dialog-body">
|
|
<div class="field">
|
|
<label v-if="label">{{ label }}</label>
|
|
<input
|
|
ref="input" class="input" :class="{ invalid: err }"
|
|
v-model="value" :placeholder="placeholder" maxlength="20"
|
|
@keydown.enter="confirm"
|
|
/>
|
|
<div v-if="err" class="form-error">{{ err }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="dialog-btns">
|
|
<button class="btn btn-sm" @click="emit('cancel')">取消</button>
|
|
<button class="btn btn-primary btn-sm" @click="confirm">{{ confirmText }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.input-dialog { width: 400px; }
|
|
.field label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
|
|
.modal-enter-active, .modal-leave-active { transition: opacity 0.15s ease; }
|
|
.modal-enter-from, .modal-leave-to { opacity: 0; }
|
|
@media (max-width: 720px) {
|
|
.input-dialog { width: calc(100vw - 16px) !important; max-width: calc(100vw - 16px) !important; }
|
|
.dialog-btns .btn { flex: 1; height: 40px; }
|
|
}
|
|
</style>
|