119 lines
4.0 KiB
Vue
119 lines
4.0 KiB
Vue
<script setup lang="ts">
|
||
import { nextTick, onUnmounted, ref, watch } from 'vue'
|
||
import Icon from './Icon.vue'
|
||
|
||
const props = defineProps<{
|
||
open: boolean
|
||
title: string
|
||
initial: { system: string; account: string; password: string; remark: string }
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'cancel'): void
|
||
(e: 'save', payload: { system: string; account: string; password: string; remark: string }): void
|
||
}>()
|
||
|
||
const fSystem = ref('')
|
||
const fAccount = ref('')
|
||
const fPassword = ref('')
|
||
const fRemark = ref('')
|
||
const fError = ref('')
|
||
const systemInput = 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) {
|
||
fSystem.value = props.initial.system
|
||
fAccount.value = props.initial.account
|
||
fPassword.value = props.initial.password
|
||
fRemark.value = props.initial.remark
|
||
fError.value = ''
|
||
window.addEventListener('keydown', onKey)
|
||
await nextTick()
|
||
systemInput.value?.focus()
|
||
} else {
|
||
window.removeEventListener('keydown', onKey)
|
||
}
|
||
})
|
||
onUnmounted(() => window.removeEventListener('keydown', onKey))
|
||
|
||
// 生成 15 位随机密码:大小写字母与数字为主体,末尾追加一个符号
|
||
function genPassword() {
|
||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'
|
||
const symbols = '!@#$%^&*'
|
||
const arr = crypto.getRandomValues(new Uint8Array(14))
|
||
let s = ''
|
||
for (let i = 0; i < arr.length; i++) s += chars[arr[i] % chars.length]
|
||
s += symbols[crypto.getRandomValues(new Uint8Array(1))[0] % symbols.length]
|
||
fPassword.value = s
|
||
fError.value = ''
|
||
}
|
||
|
||
function save() {
|
||
if (!fSystem.value.trim() || !fAccount.value.trim() || !fPassword.value) {
|
||
fError.value = '系统名称、账号、密码为必填项'
|
||
return
|
||
}
|
||
emit('save', {
|
||
system: fSystem.value.trim(),
|
||
account: fAccount.value.trim(),
|
||
password: fPassword.value,
|
||
remark: fRemark.value.trim()
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<teleport to="body">
|
||
<transition name="modal">
|
||
<div v-if="open" class="mask" @click.self="emit('cancel')">
|
||
<div class="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="form-grid">
|
||
<div class="field span2">
|
||
<label>系统名称(必填)</label>
|
||
<input ref="systemInput" class="input" v-model="fSystem" placeholder="如:企业邮箱 / OA / VPN" />
|
||
</div>
|
||
<div class="field span2">
|
||
<label>账号(必填)</label>
|
||
<input class="input" v-model="fAccount" placeholder="用户名、工号或邮箱" />
|
||
</div>
|
||
<div class="field span2">
|
||
<label>密码(必填)</label>
|
||
<div class="pwd-row">
|
||
<input class="input" v-model="fPassword" placeholder="输入或点击生成" />
|
||
<button class="btn btn-sm" @click="genPassword">生成</button>
|
||
</div>
|
||
</div>
|
||
<div class="field span2">
|
||
<label>备注</label>
|
||
<textarea class="textarea" v-model="fRemark" placeholder="用途、到期时间、负责人等(选填)"></textarea>
|
||
</div>
|
||
</div>
|
||
<div v-if="fError" class="form-error">{{ fError }}</div>
|
||
</div>
|
||
<div class="dialog-btns">
|
||
<button class="btn btn-sm" @click="emit('cancel')">取消</button>
|
||
<button class="btn btn-primary btn-sm" @click="save">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
</teleport>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.dialog { width: 480px; }
|
||
.pwd-row { display: flex; gap: 8px; }
|
||
.pwd-row .input { flex: 1; }
|
||
</style>
|