record data validate done

This commit is contained in:
Sense T 2024-04-10 16:53:03 +08:00
parent 5e2ae637a0
commit 9cc2696bbe
6 changed files with 224 additions and 27 deletions

View File

@ -25,7 +25,7 @@
{{ t('common.cancel') }} {{ t('common.cancel') }}
</NButton> </NButton>
<NButton size="small" type="error" :disabled="domain_name !== domain?.domain_name" <NButton size="small" type="error" :disabled="domain_name !== domain?.domain_name" attr-type="submit"
:loading="loading" @click="confirm"> :loading="loading" @click="confirm">
<template #icon> <template #icon>
<NIcon> <NIcon>

View File

@ -8,7 +8,8 @@
<NForm :model="record" inline :rules="rules"> <NForm :model="record" inline :rules="rules">
<NFormItem :label="t('records.recordType')"> <NFormItem :label="t('records.recordType')">
<NSelect v-model:value="record.record_type" :options="recordTypeOptions" @update:value="clearRecordContent"/> <NSelect v-model:value="record.record_type" :options="recordTypeOptions"
@update:value="clearRecordContent" style="width: 8vw;" />
</NFormItem> </NFormItem>
<NFormItem :label="t('records.name')" path="name"> <NFormItem :label="t('records.name')" path="name">
<NInput v-model:value="record.name" /> <NInput v-model:value="record.name" />
@ -21,44 +22,65 @@
</NInputNumber> </NInputNumber>
</NFormItem> </NFormItem>
</NForm> </NForm>
<NForm :model="record" inline> <NForm :model="record" inline :rules="rules">
<!-- A or AAAA -->
<NFormItem :label="t('records.content')" <NFormItem :label="t('records.content')"
v-if="[RecordTypes.RecordTypeA, RecordTypes.RecordTypeAAAA].indexOf(record.record_type) > -1"> v-if="[RecordTypes.RecordTypeA, RecordTypes.RecordTypeAAAA].indexOf(record.record_type) > -1"
path="ip">
<NInput v-model:value="(record.content as ARecord | AAAARecord).ip" placeholder="IP" /> <NInput v-model:value="(record.content as ARecord | AAAARecord).ip" placeholder="IP" />
</NFormItem> </NFormItem>
<!-- CNAME or NS -->
<NFormItem :label="t('records.content')" <NFormItem :label="t('records.content')"
v-if="[RecordTypes.RecordTypeCNAME, RecordTypes.RecordTypeNS].indexOf(record.record_type) > -1"> v-if="[RecordTypes.RecordTypeCNAME, RecordTypes.RecordTypeNS].indexOf(record.record_type) > -1"
path="host">
<NInput v-model:value="(record.content as CNAMERecord | NSRecord).host" <NInput v-model:value="(record.content as CNAMERecord | NSRecord).host"
:placeholder="t('records.form.host')" /> :placeholder="t('records.form.host')" />
</NFormItem> </NFormItem>
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeTXT === record.record_type">
<!-- TXT -->
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeTXT === record.record_type"
path="txt">
<NInput v-model:value="(record.content as TXTRecord).text" :placeholder="t('records.form.text')" /> <NInput v-model:value="(record.content as TXTRecord).text" :placeholder="t('records.form.text')" />
</NFormItem> </NFormItem>
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeMX === record.record_type">
<!-- MX -->
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeMX === record.record_type"
path="mx">
<NInputGroup> <NInputGroup>
<NInput :placeholder="t('records.form.host')" v-model:value="(record.content as MXRecord).host" <NInput :placeholder="t('records.form.host')" v-model:value="(record.content as MXRecord).host"
style="width: 75%;" /> style="width: 75%;" />
<NInputNumber :placeholder="t('records.form.preference')" <NInputNumber :placeholder="t('records.form.preference')"
v-model:value="(record.content as MXRecord).preference" style="width: 25%;" /> v-model:value="(record.content as MXRecord).preference" style="width: 25%;"
:show-button="false" />
</NInputGroup> </NInputGroup>
</NFormItem> </NFormItem>
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeSRV === record.record_type">
<!-- SRV -->
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeSRV === record.record_type"
path="srv">
<NInputGroup> <NInputGroup>
<NInputNumber :placeholder="t('records.form.priority')" <NInputNumber :placeholder="t('records.form.priority')"
v-model:value="(record.content as SRVRecord).priority" style="width: 25%;" /> v-model:value="(record.content as SRVRecord).priority" style="width: 15%;"
:show-button="false" />
<NInputNumber :placeholder="t('records.form.weight')" <NInputNumber :placeholder="t('records.form.weight')"
v-model:value="(record.content as SRVRecord).weight" style="width: 25%;" /> v-model:value="(record.content as SRVRecord).weight" style="width: 15%;"
:show-button="false" />
<NInputNumber :placeholder="t('records.form.port')" <NInputNumber :placeholder="t('records.form.port')"
v-model:value="(record.content as SRVRecord).port" style="width: 25%;" :min="0" v-model:value="(record.content as SRVRecord).port" style="width: 15%;" :min="0" :max="65535"
:max="65535" /> :show-button="false" />
<NInput :placeholder="t('records.form.target')" <NInput :placeholder="t('records.form.target')"
v-model:value="(record.content as SRVRecord).target" style="width: 25%;" /> v-model:value="(record.content as SRVRecord).target" style="width: 55%;" />
</NInputGroup> </NInputGroup>
</NFormItem> </NFormItem>
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeCAA === record.record_type">
<!-- CAA -->
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeCAA === record.record_type"
path="caa">
<NInputGroup> <NInputGroup>
<NInputNumber :placeholder="t('records.form.flag')" <NInputNumber :placeholder="t('records.form.flag')"
v-model:value="(record.content as CAARecord).flag" style="width: 20%;" /> v-model:value="(record.content as CAARecord).flag" style="width: 20%;"
:show-button="false" />
<NInput :placeholder="t('records.form.tag')" v-model:value="(record.content as CAARecord).tag" <NInput :placeholder="t('records.form.tag')" v-model:value="(record.content as CAARecord).tag"
style="width: 40%;" /> style="width: 40%;" />
<NInput :placeholder="t('records.form.value')" <NInput :placeholder="t('records.form.value')"
@ -78,7 +100,7 @@
{{ t('common.cancel') }} {{ t('common.cancel') }}
</NButton> </NButton>
<NButton size="small" type="primary" :loading="loading" @click="confirm" attr-type="submit"> <NButton size="small" type="primary" :loading="loading" :disabled="invalidData !== (validationFlags.content | validationFlags.name)" @click="confirm" attr-type="submit">
<template #icon> <template #icon>
<NIcon> <NIcon>
<Check /> <Check />
@ -104,9 +126,11 @@ import {
NInputNumber, NInputNumber,
NInputGroup, NInputGroup,
NSelect, NSelect,
NIcon,
useNotification, useNotification,
type FormRules, type FormRules,
type SelectOption type SelectOption,
type FormItemRule,
} from 'naive-ui' } from 'naive-ui'
import { getErrorInfo } from '@/apis/api'; import { getErrorInfo } from '@/apis/api';
import { import {
@ -126,12 +150,20 @@ import {
import { Check, Times } from '@vicons/fa'; import { Check, Times } from '@vicons/fa';
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const enum validationFlags {
name = 1,
content = name << 1
}
const { t } = useI18n() const { t } = useI18n()
const props = defineProps<{ const props = defineProps<{
record: Record, record: Record,
domain: string, domain: string,
}>() }>()
const emit = defineEmits(['reload-records'])
const invalidData = ref(validationFlags.content)
const show = defineModel<boolean>('show', { default: false }) const show = defineModel<boolean>('show', { default: false })
const loading = ref(false) const loading = ref(false)
const notification = useNotification() const notification = useNotification()
@ -146,9 +178,138 @@ const recordTypeOptions = Object.entries(RecordTypes).filter(
}) })
const rules = { const rules = {
name: { name: {
required: true,
trigger: 'blur', trigger: 'blur',
message: t('common.mandatory') validator() {
invalidData.value |= validationFlags.name
if (!props.record.name || props.record.name === '') {
invalidData.value &= ~validationFlags.name
return new Error(t('common.mandatory'))
}
return true
}
},
txt : {
trigger: 'blur' ,
validator() {
invalidData.value |= validationFlags.content
if (props.record.record_type !== RecordTypes.RecordTypeTXT) return true
const r = (props.record.content as TXTRecord)
if (!r || !r.text || r.text === '') {
invalidData.value &= ~validationFlags.content
return new Error(t('common.mandatory'))
}
return true
}
},
host: {
trigger: 'blur',
validator() {
invalidData.value |= validationFlags.content
if ([RecordTypes.RecordTypeCNAME, RecordTypes.RecordTypeNS].indexOf(props.record.record_type) === -1) return true
const r = (props.record.content as CNAMERecord | NSRecord)
if (!r || !r.host || r.host === '') {
invalidData.value &= ~validationFlags.content
return new Error(t('common.mandatory'))
}
if (!r.host.endsWith('.')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.endWithDot'))
}
return true
}
},
ip: {
trigger: 'blur',
validator() {
invalidData.value |= validationFlags.content
if ([RecordTypes.RecordTypeA, RecordTypes.RecordTypeAAAA].indexOf(props.record.record_type) === -1) return true
const r = (props.record.content as AAAARecord | ARecord)
if (!r || !r.ip || r.ip === '') {
invalidData.value &= ~validationFlags.content
return new Error(t('common.mandatory'))
}
switch (props.record.record_type) {
case RecordTypes.RecordTypeA:
if (!/^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/.test(r.ip)) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.badIPv4'))
}
break
case RecordTypes.RecordTypeAAAA:
if (!/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(r.ip)) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.badIPv6'))
}
break
}
return true
}
},
mx: {
trigger: 'blur',
validator() {
invalidData.value |= validationFlags.content
if (props.record.record_type !== RecordTypes.RecordTypeMX) return true
const r = (props.record.content as MXRecord)
if (!r || !r.host || !r.preference || r.host === '') {
invalidData.value &= ~validationFlags.content
return new Error(t('common.mandatory'))
}
if (!r.host.endsWith('.')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.endWithDot'))
}
return true
}
},
srv: {
trigger: 'blur',
validator() {
invalidData.value |= validationFlags.content
if (props.record.record_type !== RecordTypes.RecordTypeSRV) return true
const r = (props.record.content as SRVRecord)
if (!r || !r.port || !r.priority || !r.weight || !r.target || r.target === '') {
invalidData.value &= ~validationFlags.content
return new Error(t('common.mandatory'))
}
if (!r.target.endsWith('.')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.endWithDot'))
}
return true
}
},
caa: {
trigger: 'blur',
validator() {
invalidData.value |= validationFlags.content
if (props.record.record_type !== RecordTypes.RecordTypeCAA) return true
const r = (props.record.content as CAARecord)
if (!r || !r.flag || !r.tag || r.tag === '' || !r.value || r.value === '') {
invalidData.value &= ~validationFlags.content
return new Error(t('common.mandatory'))
}
return true
}
} }
} as FormRules } as FormRules
@ -164,6 +325,7 @@ async function confirm() {
} else { } else {
await recordStore.updateRecord(props.domain, props.record) await recordStore.updateRecord(props.domain, props.record)
} }
emit('reload-records')
show.value = false show.value = false
} catch (e) { } catch (e) {
const msg = getErrorInfo(e) const msg = getErrorInfo(e)

View File

@ -3,7 +3,7 @@
<NButtonGroup> <NButtonGroup>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton size="tiny"> <NButton size="tiny" @click="edit">
<template #icon> <template #icon>
<NIcon :component="EditRegular" /> <NIcon :component="EditRegular" />
</template> </template>
@ -39,9 +39,13 @@ const props = defineProps<{
domain: string domain: string
}>(); }>();
const emit = defineEmits(['record-delete']) const emit = defineEmits(['record-delete', 'edit-record'])
function confirm() { function confirm() {
emit('record-delete', props.domain, props.record) emit('record-delete', props.domain, props.record)
} }
function edit() {
emit('edit-record', props.domain, props.record)
}
</script> </script>

View File

@ -79,6 +79,12 @@ export default {
flag: 'Flag', flag: 'Flag',
tag: 'Tag', tag: 'Tag',
value: 'Value' value: 'Value'
},
errors: {
endWithDot: 'should end with a dot',
badIPv4: 'invalid IPv4 address',
badIPv6: 'invalid IPv6 address',
} }
} }
} }

View File

@ -79,6 +79,12 @@ export default {
flag: '标志', flag: '标志',
tag: '标签', tag: '标签',
value: '值' value: '值'
},
errors: {
endWithDot: '应当以 . 结尾',
badIPv4: '不是有效的 IPv4 地址',
badIPv6: '不是有效的 IPv6 地址',
} }
} }
} }

View File

@ -12,6 +12,7 @@ import { getErrorInfo } from '@/apis/api'
import { PlusSquare, RedoAlt, CheckCircle, Clock, Cogs, Search } from '@vicons/fa' import { PlusSquare, RedoAlt, CheckCircle, Clock, Cogs, Search } from '@vicons/fa'
import router from '@/router'; import router from '@/router';
import RecordOps from '@/components/records/RecordOps.vue' import RecordOps from '@/components/records/RecordOps.vue'
import RecordEditModal from '@/components/records/RecordEditModal.vue'
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n() const { t } = useI18n()
@ -49,7 +50,7 @@ const columns = [
{ {
key: '', key: '',
render(row: Record) { render(row: Record) {
return <RecordOps record={row} domain={props.domain} onRecord-delete={deleteRecord} /> return <RecordOps record={row} domain={props.domain} onRecord-delete={deleteRecord} onEdit-record={showEditing} />
} }
} }
] as DataTableColumns<Record> ] as DataTableColumns<Record>
@ -57,8 +58,10 @@ const columns = [
const recordStore = useRecordStore() const recordStore = useRecordStore()
const notification = useNotification() const notification = useNotification()
const records = ref<Record[] | undefined>([]); const records = ref<Record[] | undefined>([]as Record[]);
const soa = ref<SOARecord>({} as SOARecord) const soa = ref<SOARecord>({} as SOARecord)
const editModalShow = ref(false)
const editingRecord = ref<Record>({} as Record)
const loading = ref(true); const loading = ref(true);
onMounted(() => { onMounted(() => {
try { try {
@ -70,10 +73,13 @@ onMounted(() => {
} }
}) })
const reloadRecords = () => records.value = recordStore.records?.filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
async function refreshRecords() { async function refreshRecords() {
try { try {
await recordStore.loadRecords(props.domain) await recordStore.loadRecords(props.domain)
records.value = recordStore.records?.filter(e => e.record_type !== RecordTypes.RecordTypeSOA) reloadRecords()
soa.value = recordStore.records?.find(e => e.record_type === RecordTypes.RecordTypeSOA)?.content as SOARecord soa.value = recordStore.records?.find(e => e.record_type === RecordTypes.RecordTypeSOA)?.content as SOARecord
} catch (err) { } catch (err) {
const msg = getErrorInfo(err) const msg = getErrorInfo(err)
@ -102,24 +108,37 @@ function searchRecord(value: string) {
async function deleteRecord(domain: string, record: Record) { async function deleteRecord(domain: string, record: Record) {
try { try {
await recordStore.removeRecord(domain, record) await recordStore.removeRecord(domain, record)
records.value = recordStore.records?.filter(e => e.record_type !== RecordTypes.RecordTypeSOA) reloadRecords()
} catch (err) { } catch (err) {
const msg = getErrorInfo(err) const msg = getErrorInfo(err)
notification.error(msg) notification.error(msg)
console.error(err) console.error(err)
} }
} }
function showEditing(domain: string, record: Record) {
editModalShow.value = true
editingRecord.value = record
}
function newRecord() {
showEditing(props.domain, {
zone: `${props.domain}.`,
ttl: 500
} as Record)
}
</script> </script>
<template> <template>
<div id="records"> <div id="records">
<RecordEditModal v-model:show="editModalShow" :domain="domain" :record="editingRecord" @reload-records="reloadRecords"/>
<NSpin size="large" v-if="loading" /> <NSpin size="large" v-if="loading" />
<div v-else class="content"> <div v-else class="content">
<NModalProvider> <NModalProvider>
<NPageHeader :title="t('domains.dnsRecord')" :subtitle="domain" @back="goBack"> <NPageHeader :title="t('domains.dnsRecord')" :subtitle="domain" @back="goBack">
<template #extra> <template #extra>
<NFlex :wrap="false" justify="end" inline> <NFlex :wrap="false" justify="end" inline>
<NButton type="primary"> <NButton type="primary" @click="newRecord">
<template #icon> <template #icon>
<NIcon> <NIcon>
<PlusSquare /> <PlusSquare />