all tsx used, no vue SFC

This commit is contained in:
Sense T 2024-04-12 15:16:52 +08:00
parent 731504ae82
commit 01765c4e7f
15 changed files with 865 additions and 739 deletions

32
web/src/App.tsx Normal file
View File

@ -0,0 +1,32 @@
import {
NNotificationProvider,
NConfigProvider,
NGlobalStyle,
useOsTheme,
darkTheme,
lightTheme,
} from "naive-ui";
import { zhCN, dateZhCN, enUS, dateEnUS } from 'naive-ui'
import { RouterView } from "vue-router";
const osThemeRef = useOsTheme()
const theme = osThemeRef.value === 'dark' ? darkTheme : lightTheme
const locale = navigator.language === "zh-CN" ? zhCN : enUS
const dateLocale = navigator.language === "zh-CN" ? dateZhCN : dateEnUS
function App() {
document.title = 'reCoreD-UI'
return (
<NConfigProvider theme={theme} locale={locale} date-locale={dateLocale}>
<NGlobalStyle />
<NNotificationProvider max={3}>
<RouterView />
</NNotificationProvider>
</NConfigProvider>
)
}
App.displayName = 'App'
export default App

View File

@ -1,32 +0,0 @@
<script setup lang="ts">
import {
NNotificationProvider,
NConfigProvider,
NGlobalStyle,
useOsTheme,
darkTheme,
lightTheme,
type GlobalTheme
} from "naive-ui";
import { zhCN, dateZhCN, enUS, dateEnUS, type NLocale, type NDateLocale } from 'naive-ui'
import { RouterView } from "vue-router";
import { onMounted } from "vue";
const osThemeRef = useOsTheme()
const theme = osThemeRef.value === 'dark' ? darkTheme : lightTheme
const locale = navigator.language === "zh-CN" ? zhCN : enUS
const dateLocale = navigator.language === "zh-CN" ? dateZhCN : dateEnUS
onMounted(() => {
document.title = 'reCoreD-UI'
})
</script>
<template>
<NConfigProvider :theme="theme" :locale="locale" :date-locale="dateLocale">
<NGlobalStyle />
<NNotificationProvider :max="3">
<RouterView />
</NNotificationProvider>
</NConfigProvider>
</template>

View File

@ -76,4 +76,15 @@ function DomainOps({ domain }: Props, { emit }: SetupContext<Events>) {
) )
} }
DomainOps.props = {
domain: {
required: true
}
}
DomainOps.emits = {
removeDomain: (d:Domain) => d,
editDomain: (d:Domain) => d
} as Events
export default DomainOps export default DomainOps

View File

@ -0,0 +1,459 @@
import {
NModal,
NCard,
NForm,
NFormItem,
NFlex,
NButton,
NInput,
NInputNumber,
NInputGroup,
NSelect,
NIcon,
type FormRules,
type SelectOption,
createDiscreteApi,
type FormItemRule,
} from 'naive-ui'
import { getErrorInfo } from '@/apis/api';
import {
useRecordStore,
RecordTypes,
type Record,
type ARecord,
type AAAARecord,
type CAARecord,
type CNAMERecord,
type NSRecord,
type SRVRecord,
type TXTRecord,
type MXRecord,
type RecordT,
} from '@/stores/records';
import { Check, Times } from '@vicons/fa';
import { ref, type SetupContext } from 'vue';
import i18n from '@/locale/i18n';
const { t } = i18n.global
const enum validationFlags {
name = 1,
content = name << 1
}
type Props = {
record: Record
domain: string
show: boolean
}
type Events = {
reloadRecords(): void
'update:show': (v: boolean) => void
}
const invalidData = ref(validationFlags.content)
const loading = ref(false)
const recordStore = useRecordStore()
const { notification } = createDiscreteApi(['notification'])
const recordTypeOptions = Object.entries(RecordTypes).filter(
e => e[1] !== RecordTypes.RecordTypeSOA
).map(e => {
return {
label: e[1],
value: e[1]
} as SelectOption
})
function validateName(_rule: FormItemRule, value: string): boolean | Error {
invalidData.value |= validationFlags.name
if (!value || value === '') {
invalidData.value &= ~validationFlags.name
return new Error(t('common.mandatory'))
}
if (value.includes(' ')) {
invalidData.value &= ~validationFlags.name
return new Error(t('records.errors.hasSpace'))
}
if (value.startsWith('.') || value.endsWith('.')) {
invalidData.value &= ~validationFlags.name
return new Error(t('records.errors.badName.dotAndMinus'))
}
if (value.startsWith('-') || value.endsWith('-')) {
invalidData.value &= ~validationFlags.name
return new Error(t('records.errors.badName.dotAndMinus'))
}
if (value.includes('..')) {
invalidData.value &= ~validationFlags.name
return new Error(t('records.errors.badName.doubleDots'))
}
if (value.split('.').filter(e => e.length > 63).length > 0) {
invalidData.value &= ~validationFlags.name
return new Error(t('records.errors.badName.longerThan63'))
}
return true
}
function validateTXTRecord(record: Record) {
return () => {
invalidData.value |= validationFlags.content
if (record.record_type !== RecordTypes.RecordTypeTXT) return true
const r = (record.content as TXTRecord)
if (!r || !r.text || r.text === '') {
invalidData.value &= ~validationFlags.content
return new Error(t('common.mandatory'))
}
return true
}
}
function validateHostRecord(record: Record) {
return () => {
invalidData.value |= validationFlags.content
if ([RecordTypes.RecordTypeCNAME, RecordTypes.RecordTypeNS].indexOf(record.record_type) === -1) return true
const r = (record.content as CNAMERecord | NSRecord)
if (!r || !r.host || r.host === '') {
invalidData.value &= ~validationFlags.content
return new Error(t('common.mandatory'))
}
if (r.host.includes(' ')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.hasSpace'))
}
if (!r.host.endsWith('.')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.endWithDot'))
}
return true
}
}
function validateIPRecord(record: Record) {
return () => {
invalidData.value |= validationFlags.content
if ([RecordTypes.RecordTypeA, RecordTypes.RecordTypeAAAA].indexOf(record.record_type) === -1) return true
const r = (record.content as AAAARecord | ARecord)
if (!r || !r.ip || r.ip === '') {
invalidData.value &= ~validationFlags.content
return new Error(t('common.mandatory'))
}
switch (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
}
}
function validateMXRecord(record: Record) {
return () => {
invalidData.value |= validationFlags.content
if (record.record_type !== RecordTypes.RecordTypeMX) return true
const r = (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.includes(' ')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.hasSpace'))
}
if (!r.host.endsWith('.')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.endWithDot'))
}
return true
}
}
function validateSRVRecord(record: Record) {
return () => {
invalidData.value |= validationFlags.content
if (record.record_type !== RecordTypes.RecordTypeSRV) return true
const r = (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.includes(' ')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.hasSpace'))
}
if (!r.target.endsWith('.')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.endWithDot'))
}
return true
}
}
function validateCAARecord(record: Record) {
return () => {
invalidData.value |= validationFlags.content
if (record.record_type !== RecordTypes.RecordTypeCAA) return true
const r = (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'))
}
if (r.tag.includes(' ')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.hasSpace'))
}
return true
}
}
function buildRules(record: Record): FormRules {
return {
name: {
trigger: 'blur',
validator: validateName
},
txt: {
trigger: 'blur',
validator: validateTXTRecord(record)
},
host: {
trigger: 'blur',
validator: validateHostRecord(record)
},
ip: {
trigger: 'blur',
validator: validateIPRecord(record)
},
mx: {
trigger: 'blur',
validator: validateMXRecord(record)
},
srv: {
trigger: 'blur',
validator: validateSRVRecord(record)
},
caa: {
trigger: 'blur',
validator: validateCAARecord(record)
}
}
}
async function confirm({ record, domain }: Props) {
loading.value = true;
try {
if (!record.id || record.id < 1) {
await recordStore.addRecord(domain, record)
} else {
await recordStore.updateRecord(domain, record)
}
} catch (e) {
const msg = getErrorInfo(e)
notification.error(msg)
console.error(e)
}
loading.value = false;
}
function modalHeader({ record }: Props) {
return (
<>
{(!record || !record.id || record.id < 1) ? <span>{t('common.new')}</span> : <span> t('common.edit')</span>}
<span>{t('records._')}</span>
</>
)
}
function modalActions({ record, domain }: Props, { emit }: SetupContext<Events>) {
return (
<NFlex justify='end'>
<NButton size='small' onClick={() => emit('update:show', false)}>
{{
icon: () => <NIcon component={Times} />,
default: () => t('common.cancel')
}}
</NButton>
<NButton size='small' type='primary' loading={loading.value} attrType='submit'
disabled={invalidData.value !== (validationFlags.content | validationFlags.name)}
onClick={() => confirm({ record, domain, show: false }).then(() => { emit('reloadRecords'); emit('update:show', false) })}>
{{
icon: () => <NIcon component={Check} />,
default: () => t('common.confirm')
}}
</NButton>
</NFlex>
)
}
function modalBody({ record }: Props) {
const rules = buildRules(record)
return (
<>
<NForm model={record} rules={rules} inline>
<NFormItem label={t('records.recordType')}>
<NSelect value={record.record_type}
onUpdate:value={(v) => { record.record_type = v; record.content = {} as RecordT }}
options={recordTypeOptions} style={{ width: '8vw' }} />
</NFormItem>
<NFormItem label={t('records.name')} path='name'>
<NInput value={record.name} onUpdate:value={v => record.name = v} />
</NFormItem>
<NFormItem label='TTL' path='ttl'>
<NInputNumber value={record.ttl} onUpdate:value={v => v ? record.ttl = v : null} showButton={false} >
{{
suffix: () => t('common.unitForSecond')
}}
</NInputNumber>
</NFormItem>
</NForm>
<NForm model={record} rules={rules}>
<modalBodyContent type={record.record_type} record={record} />
</NForm>
</>
)
}
const IPRecordE = ({ record }: Props) => (
<NFormItem label={t('records.content')} path='ip'>
<NInput value={(record.content as ARecord | AAAARecord).ip} onUpdate:value={v => (record.content as ARecord | AAAARecord).ip = v} placeholder='IP' />
</NFormItem>
)
const HostRecordE = ({ record }: Props) => (
<NFormItem label={t('records.content')} path='host'>
<NInput value={(record.content as CNAMERecord | NSRecord).host} onUpdate:value={v => (record.content as CNAMERecord | NSRecord).host = v} placeholder={t('records.form.host')} />
</NFormItem>
)
const TXTRecordE = ({ record }: Props) => (
<NFormItem label={t('records.content')} path='txt'>
<NInput value={(record.content as TXTRecord).text} onUpdateValue={v => (record.content as TXTRecord).text = v} placeholder={t('records.form.text')} />
</NFormItem>
)
const MXRecordE = ({ record }: Props) => (
<NFormItem label={t('records.content')} path='mx'>
<NInputGroup>
<NInput placeholder={t('records.form.host')}
value={(record.content as MXRecord).host}
onUpdate:value={v => (record.content as MXRecord).host = v}
style={{ width: '75%' }} />
<NInputNumber placeholder={t('records.form.preference')}
value={(record.content as MXRecord).preference}
onUpdate:value={v => v ? (record.content as MXRecord).preference = v : null}
style={{ width: '25%' }} show-button={false} />
</NInputGroup>
</NFormItem>
)
const CAARecordE = ({ record }: Props) => (
<NFormItem label={t('records.content')} path='caa'>
<NInputGroup>
<NInputNumber placeholder={t('records.form.flag')}
value={(record.content as CAARecord).flag} style={{ width: '20%' }}
onUpdate:value={v => v ? (record.content as CAARecord).flag = v : null}
show-button={false} />
<NInput placeholder={t('records.form.tag')}
value={(record.content as CAARecord).tag}
onUpdate:value={v => v ? (record.content as CAARecord).tag = v : null}
style={{ width: '40%' }} />
<NInput placeholder={t('records.form.value')}
value={(record.content as CAARecord).value}
onUpdate:value={v => v ? (record.content as CAARecord).value = v : null}
style={{ width: '40%' }} />
</NInputGroup>
</NFormItem>
)
const SRVRecordE = ({ record }: Props) => (
<NFormItem label={t('records.content')} path='srv'>
<NInputGroup>
<NInputNumber placeholder={t('records.form.priority')}
value={(record.content as SRVRecord).priority} style={{ width: '15%' }}
onUpdateValue={v => v ? (record.content as SRVRecord).priority = v : null}
show-button={false} />
<NInputNumber placeholder={t('records.form.weight')}
value={(record.content as SRVRecord).weight} style={{ width: '15%' }}
onUpdate:value={v => v ? (record.content as SRVRecord).weight = v : null}
show-button={false} />
<NInputNumber placeholder={t('records.form.port')}
value={(record.content as SRVRecord).port} style={{ width: '15%' }} min={0} max={65535}
onUpdate:value={v => v ? (record.content as SRVRecord).port = v : null}
show-button={false} />
<NInput placeholder={t('records.form.target')}
value={(record.content as SRVRecord).target} style={{ width: '55%' }}
onUpdate:value={v => (record.content as SRVRecord).target = v}
/>
</NInputGroup>
</NFormItem>
)
const modalBodyContent = ({ type, record }: { type: RecordTypes, record: Record }) => {
const e = {
'A': IPRecordE,
'AAAA': IPRecordE,
'CNAME': HostRecordE,
'NS': HostRecordE,
'TXT': TXTRecordE,
'MX': MXRecordE,
'SRV': SRVRecordE,
'CAA': CAARecordE,
'SOA': ({ }: Props) => <></>
}[type]
return <e record={record} />
}
function RecordEditModal(
{ domain, show, record }: Props,
{ emit }: SetupContext<Events>) {
return (
<NModal maskClosable={false} show={show}>
<NCard style={{ width: '640px' }} role='dialog'>
{{
header: () => <modalHeader record={record} />,
default: () => <modalBody record={record} />,
action: () => <modalActions record={record} domain={domain}
onUpdate:show={(v: boolean) => emit('update:show', v)}
onReloadRecords={() => emit('reloadRecords')} />
}}
</NCard>
</NModal>
)
}
export default RecordEditModal

View File

@ -1,380 +0,0 @@
<template>
<NModal :mask-closable="false" :show="show">
<NCard style="width: 640px" role="dialog" aria-modal="true">
<template #header>
<span v-if="!record || !record.id || record.id < 1">{{ t('common.new') }}</span><span v-else>{{
t('common.edit') }}</span><span>{{ t('records._') }}</span>
</template>
<NForm :model="record" inline :rules="rules">
<NFormItem :label="t('records.recordType')">
<NSelect v-model:value="record.record_type" :options="recordTypeOptions"
@update:value="clearRecordContent" style="width: 8vw;" />
</NFormItem>
<NFormItem :label="t('records.name')" path="name">
<NInput v-model:value="record.name" />
</NFormItem>
<NFormItem label="TTL" path="ttl">
<NInputNumber v-model:value="record.ttl" :show-button="false">
<template #suffix>
{{ t('common.unitForSecond') }}
</template>
</NInputNumber>
</NFormItem>
</NForm>
<NForm :model="record" inline :rules="rules">
<!-- A or AAAA -->
<NFormItem :label="t('records.content')"
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" />
</NFormItem>
<!-- CNAME or NS -->
<NFormItem :label="t('records.content')"
v-if="[RecordTypes.RecordTypeCNAME, RecordTypes.RecordTypeNS].indexOf(record.record_type) > -1"
path="host">
<NInput v-model:value="(record.content as CNAMERecord | NSRecord).host"
:placeholder="t('records.form.host')" />
</NFormItem>
<!-- 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')" />
</NFormItem>
<!-- MX -->
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeMX === record.record_type"
path="mx">
<NInputGroup>
<NInput :placeholder="t('records.form.host')" v-model:value="(record.content as MXRecord).host"
style="width: 75%;" />
<NInputNumber :placeholder="t('records.form.preference')"
v-model:value="(record.content as MXRecord).preference" style="width: 25%;"
:show-button="false" />
</NInputGroup>
</NFormItem>
<!-- SRV -->
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeSRV === record.record_type"
path="srv">
<NInputGroup>
<NInputNumber :placeholder="t('records.form.priority')"
v-model:value="(record.content as SRVRecord).priority" style="width: 15%;"
:show-button="false" />
<NInputNumber :placeholder="t('records.form.weight')"
v-model:value="(record.content as SRVRecord).weight" style="width: 15%;"
:show-button="false" />
<NInputNumber :placeholder="t('records.form.port')"
v-model:value="(record.content as SRVRecord).port" style="width: 15%;" :min="0" :max="65535"
:show-button="false" />
<NInput :placeholder="t('records.form.target')"
v-model:value="(record.content as SRVRecord).target" style="width: 55%;" />
</NInputGroup>
</NFormItem>
<!-- CAA -->
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeCAA === record.record_type"
path="caa">
<NInputGroup>
<NInputNumber :placeholder="t('records.form.flag')"
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"
style="width: 40%;" />
<NInput :placeholder="t('records.form.value')"
v-model:value="(record.content as CAARecord).value" style="width: 40%;" />
</NInputGroup>
</NFormItem>
</NForm>
<template #action>
<NFlex justify="end">
<NButton size="small" @click="show = false">
<template #icon>
<NIcon>
<Times />
</NIcon>
</template>
{{ t('common.cancel') }}
</NButton>
<NButton size="small" type="primary" :loading="loading" :disabled="invalidData !== (validationFlags.content | validationFlags.name)" @click="confirm" attr-type="submit">
<template #icon>
<NIcon>
<Check />
</NIcon>
</template>
{{ t('common.confirm') }}
</NButton>
</NFlex>
</template>
</NCard>
</NModal>
</template>
<script setup lang="ts">
import {
NModal,
NCard,
NForm,
NFormItem,
NFlex,
NButton,
NInput,
NInputNumber,
NInputGroup,
NSelect,
NIcon,
useNotification,
type FormRules,
type SelectOption,
type FormItemRule,
} from 'naive-ui'
import { getErrorInfo } from '@/apis/api';
import {
useRecordStore,
RecordTypes,
type Record,
type ARecord,
type AAAARecord,
type CAARecord,
type CNAMERecord,
type NSRecord,
type SRVRecord,
type TXTRecord,
type MXRecord,
type RecordT,
} from '@/stores/records';
import { Check, Times } from '@vicons/fa';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const enum validationFlags {
name = 1,
content = name << 1
}
const { t } = useI18n()
const props = defineProps<{
record: Record,
domain: string,
}>()
const emit = defineEmits(['reload-records'])
const invalidData = ref(validationFlags.content)
const show = defineModel<boolean>('show', { default: false })
const loading = ref(false)
const notification = useNotification()
const recordStore = useRecordStore()
const recordTypeOptions = Object.entries(RecordTypes).filter(
e => e[1] !== RecordTypes.RecordTypeSOA
).map(e => {
return {
label: e[1],
value: e[1]
} as SelectOption
})
const rules = {
name: {
trigger: 'blur',
validator() {
invalidData.value |= validationFlags.name
if (!props.record.name || props.record.name === '') {
invalidData.value &= ~validationFlags.name
return new Error(t('common.mandatory'))
}
if (props.record.name.includes(' ')) {
invalidData.value &= ~validationFlags.name
return new Error(t('records.errors.hasSpace'))
}
if (props.record.name.startsWith('.') || props.record.name.endsWith('.')) {
invalidData.value &= ~validationFlags.name
return new Error(t('records.errors.badName.dotAndMinus'))
}
if (props.record.name.startsWith('-') || props.record.name.endsWith('-')) {
invalidData.value &= ~validationFlags.name
return new Error(t('records.errors.badName.dotAndMinus'))
}
if (props.record.name.includes('..')) {
invalidData.value &= ~validationFlags.name
return new Error(t('records.errors.badName.doubleDots'))
}
if (props.record.name.split('.').filter(e => e.length > 63).length > 0) {
invalidData.value &= ~validationFlags.name
return new Error(t('records.errors.badName.longerThan63'))
}
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.includes(' ')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.hasSpace'))
}
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.includes(' ')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.hasSpace'))
}
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.includes(' ')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.hasSpace'))
}
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'))
}
if (r.tag.includes(' ')) {
invalidData.value &= ~validationFlags.content
return new Error(t('records.errors.hasSpace'))
}
return true
}
}
} as FormRules
function clearRecordContent() {
props.record.content = {} as RecordT
}
async function confirm() {
loading.value = true;
try {
if (!props.record.id || props.record.id < 1) {
await recordStore.addRecord(props.domain, props.record)
} else {
await recordStore.updateRecord(props.domain, props.record)
}
emit('reload-records')
show.value = false
} catch (e) {
const msg = getErrorInfo(e)
notification.error(msg)
console.error(e)
}
loading.value = false;
}
</script>

View File

@ -3,7 +3,7 @@ import './assets/main.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import App from './App.vue' import App from './App'
import router from './router' import router from './router'
import i18n from './locale/i18n' import i18n from './locale/i18n'

View File

@ -1,23 +1,27 @@
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: [ routes: [
{ {
path: '/', path: '/',
name: 'home', redirect: '/domains'
component: HomeView
}, },
{ {
path: '/domains', path: '/domains',
name: 'domains', name: 'domains',
component: () => import('../views/DomainsView.vue') meta: {
type: 'domains'
},
component: () => import('@/views/DomainsView')
}, },
{ {
path: '/records/:domain', path: '/records/:domain',
name: 'records', name: 'records',
component: () => import('../views/RecordsView.vue'), meta: {
type: 'records'
},
component: () => import('@/views/RecordsView'),
props: true props: true
} }
] ]

View File

@ -0,0 +1,3 @@
.n-card {
width: 32vw;
}

View File

@ -0,0 +1,87 @@
import './DomainsView.css'
import { NSpin, NFlex, NCard, NButton, NIcon, NModalProvider, createDiscreteApi } from 'naive-ui'
import { PlusSquare } from "@vicons/fa"
import { type Domain, useDomainStore } from '@/stores/domains'
import { getErrorInfo } from '@/apis/api'
import DomainInfo from '@/components/domains/DomainInfo'
import DomainOps from '@/components/domains/DomainOps'
import DomainRemoveModal from '@/components/domains/DomainRemoveModal'
import DomainEditModal from '@/components/domains/DomainEditModal'
import { ref } from 'vue'
const domainStore = useDomainStore()
const { notification } = createDiscreteApi(['notification'])
const loading = ref(true);
const removeModalShow = ref(false);
const editModalShow = ref(false);
const operationDomain = ref({} as Domain)
function showRemoveModal(domain: Domain) {
operationDomain.value = domain
removeModalShow.value = true
}
function showEditModal(domain: Domain) {
operationDomain.value = domain
editModalShow.value = true
}
function addDomain() {
const domain = {
refresh_interval: 86400,
retry_interval: 7200,
expiry_period: 3600000,
negative_ttl: 86400,
serial_number: 1,
} as Domain
showEditModal(domain)
}
function DomainsView() {
try {
domainStore.loadDomains()
loading.value = false
} catch (e) {
const msg = getErrorInfo(e)
notification.error(msg)
console.error(e)
}
return (
<>
{
loading.value ? <NSpin size="large" /> :
<NModalProvider>
<NFlex vertical>
{
domainStore.domains.map((domain: Domain) => (
<NCard title={domain.domain_name} key={domain.id} size='large' hoverable>
{{
default: () => <DomainInfo domain={domain} />,
action: () => <DomainOps domain={domain} onRemoveDomain={showRemoveModal} onEditDomain={showEditModal} />
}}
</NCard>
))
}
<NCard hoverable>
<NButton block quaternary size="large" onClick={addDomain}>
{{
icon: () => <NIcon component={PlusSquare} depth={5} />
}}
</NButton>
</NCard>
</NFlex>
<DomainRemoveModal show={removeModalShow.value} domain={operationDomain.value} onUpdate:show={(v: boolean) => removeModalShow.value = v} />
<DomainEditModal show={editModalShow.value} domain={operationDomain.value} onUpdate:show={(v: boolean) => editModalShow.value = v} />
</NModalProvider>
}
</>
)
}
DomainsView.displayName = 'DomainsView'
export default DomainsView

View File

@ -1,83 +0,0 @@
<script setup lang="ts">
import { NSpin, NFlex, NCard, NButton, NIcon, useNotification, NModalProvider } from 'naive-ui'
import { PlusSquare } from "@vicons/fa"
import { onMounted, ref } from 'vue'
import { type Domain, useDomainStore } from '@/stores/domains'
import { getErrorInfo } from '@/apis/api'
import DomainInfo from '@/components/domains/DomainInfo'
import DomainOps from '@/components/domains/DomainOps'
import DomainRemoveModal from '@/components/domains/DomainRemoveModal'
import DomainEditModal from '@/components/domains/DomainEditModal'
const domainStore = useDomainStore()
const notification = useNotification()
const loading = ref(true);
const removeModalShow = ref(false);
const editModalShow = ref(false);
const operationDomain = ref({} as Domain)
onMounted(() => {
try {
domainStore.loadDomains()
loading.value = false
} catch (error) {
const msg = getErrorInfo(error)
notification.error(msg)
console.error(error)
}
})
function showRemoveModal(domain: Domain) {
operationDomain.value = domain
removeModalShow.value = true
}
function showEditModal(domain: Domain) {
operationDomain.value = domain
editModalShow.value = true
}
function addDomain() {
const domain = {
refresh_interval: 86400,
retry_interval: 7200,
expiry_period: 3600000,
negative_ttl: 86400,
serial_number: 1,
} as Domain
showEditModal(domain)
}
</script>
<template>
<div>
<NSpin size="large" v-if="loading" />
<NModalProvider v-else>
<NFlex id="domains" vertical>
<NCard v-for="domain in domainStore.domains" :title="domain.domain_name" v-bind:key="domain.id"
size="large" hoverable>
<DomainInfo :domain="domain" />
<template #action>
<DomainOps :domain="domain" @remove-domain="showRemoveModal" @edit-domain="showEditModal" />
</template>
</NCard>
<NCard hoverable>
<NButton block quaternary size="large" @click="addDomain">
<template #icon>
<NIcon :component="PlusSquare" :depth="5" />
</template>
</NButton>
</NCard>
</NFlex>
<DomainRemoveModal v-model:show="removeModalShow" :domain="operationDomain" />
<DomainEditModal v-model:show="editModalShow" :domain="operationDomain" />
</NModalProvider>
</div>
</template>
<style scoped>
.n-card {
width: 32vw;
}
</style>

View File

@ -1,6 +0,0 @@
<script setup lang="ts">
import router from '@/router';
router.push("/domains")
</script>
<template></template>

View File

@ -0,0 +1,7 @@
div#records {
position: absolute;
top: 0;
left: 0;
width: 100vw;
padding: 1.5rem;
}

View File

@ -0,0 +1,250 @@
import './RecordsView.css'
import {
NSpin, NPageHeader,
NFlex, NButton, NIcon, NGrid, NGi,
NStatistic, NDataTable, NInput,
NModalProvider,
createDiscreteApi
} from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { useRecordStore, type Record, type SOARecord, RecordTypes, type ARecord } from '@/stores/records'
import { getErrorInfo } from '@/apis/api'
import { PlusSquare, RedoAlt, CheckCircle, Clock, Cogs, Search } from '@vicons/fa'
import router from '@/router';
import RecordOps from '@/components/records/RecordOps'
import RecordEditModal from '@/components/records/RecordEditModal'
import i18n from '@/locale/i18n'
import { ref } from 'vue'
const { t } = i18n.global
type Props = {
domain: string
}
const recordStore = useRecordStore()
const { notification } = createDiscreteApi(['notification'])
const editModalShow = ref(false)
const editingRecord = ref<Record>({} as Record)
const loading = ref(true);
const records = ref<Record[] | undefined>([] as Record[]);
const soa = ref<SOARecord>({} as SOARecord)
const reloadRecords = () => records.value = recordStore.records?.filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
async function refreshRecords(domain: string) {
try {
await recordStore.loadRecords(domain)
reloadRecords()
soa.value = recordStore.records?.find(e => e.record_type === RecordTypes.RecordTypeSOA)?.content as SOARecord
} catch (err) {
const msg = getErrorInfo(err)
notification.error(msg)
console.error(err)
} finally {
loading.value = false;
}
}
function goBack() {
router.push('/domains')
}
function searchRecord(value: string) {
if (value.length > 0) {
records.value = recordStore.records?.
filter(e => e.record_type !== RecordTypes.RecordTypeSOA).
filter(e => !!~e.name.indexOf(value))
} else {
records.value = recordStore.records?.
filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
}
}
async function deleteRecord(domain: string, record: Record) {
try {
await recordStore.removeRecord(domain, record)
reloadRecords()
} catch (err) {
const msg = getErrorInfo(err)
notification.error(msg)
console.error(err)
}
}
function showEditing(domain: string, record: Record) {
editModalShow.value = true
editingRecord.value = record
}
function newRecord(domain: string) {
showEditing(domain, {
zone: `${domain}.`,
ttl: 500,
record_type: RecordTypes.RecordTypeA,
content: {
ip: ''
} as ARecord
} as Record)
}
const generateColumns = (domain: string) => [
{
key: 'no',
title: '#',
render(_, index) {
return index + 1
}
},
{
key: 'name',
title: t("records.name"),
},
{
key: 'record_type',
title: t('records.recordType')
},
{
key: 'content',
title: t('records.content'),
render(row: Record) {
return Object.entries(row.content).map(i => i[1]).join(" ")
}
},
{
key: 'ttl',
title: 'TTL (s)'
},
{
key: '',
render(row: Record) {
return <RecordOps record={row} domain={domain} onRecord-delete={deleteRecord} onEdit-record={showEditing} />
}
}
] as DataTableColumns<Record>
const statRefresh = () => (
<NGi>
<NStatistic value={soa.value.refresh}>
{{
suffix: () => 's',
label: () => (
<>
<NIcon component={RedoAlt} style={{ transform: 'translateY(2px)' }} />
<span>{t('records.refresh')}</span>
</>
)
}}
</NStatistic>
</NGi>
)
const statRetry = () => (
<NGi>
<NStatistic value={soa.value.retry}>
{{
suffix: () => 's',
label: () => (
<>
<NIcon component={CheckCircle} style={{ transform: 'translateY(2px)' }} />
<span>{t('records.retry')}</span>
</>
)
}}
</NStatistic>
</NGi>
)
const statExpire = () => (
<NGi>
<NStatistic value={soa.value.expire}>
{{
suffix: () => 's',
label: () => (
<>
<NIcon component={Clock} style={{ transform: 'translateY(2px)' }} />
<span>{t('records.expire')}</span>
</>
)
}}
</NStatistic>
</NGi>
)
const statTTL = () => (
<NGi>
<NStatistic value={soa.value.minttl}>
{{
suffix: () => 's',
label: () => (
<>
<NIcon component={Cogs} style={{ transform: 'translateY(2px)' }} />
<span>{t('records.ttl')}</span>
</>
)
}}
</NStatistic>
</NGi>
)
function recordsViewBodyHeaderExtra() {
return (
<NGrid cols={4} >
<statRefresh />
<statRetry />
<statExpire />
<statTTL />
</NGrid>
)
}
function recordsViewBody({ domain }: Props) {
const columns = generateColumns(domain)
return (
<NModalProvider>
<NPageHeader title={t('domains.dnsRecord')} subtitle={domain} onBack={goBack}>
{{
extra: () => (
<NFlex wrap={false} justify="end" inline>
<NButton type="primary" onClick={() => newRecord(domain)}>
{{
icon: () => <NIcon component={PlusSquare} />,
default: () => t('common.add')
}}
</NButton>
<NInput placeholder={t('records.search')} onUpdate:value={searchRecord} clearable>
{{
prefix: () => <NIcon component={Search} />
}}
</NInput>
</NFlex>
),
default: () => <recordsViewBodyHeaderExtra />
}}
</NPageHeader>
<br />
<NDataTable data={records.value} columns={columns} pagination={{ pageSize: 20 }} />
</NModalProvider>
)
}
function RecordsView({ domain }: Props) {
try {
refreshRecords(domain)
} catch (err) {
const msg = getErrorInfo(err)
notification.error(msg)
console.error(err)
}
return (
<div id='records'>
<RecordEditModal show={editModalShow.value} domain={domain} record={editingRecord.value} onReloadRecords={reloadRecords} />
{
loading.value ? <NSpin size='large' /> : <recordsViewBody domain={domain} />
}
</div>
)
}
RecordsView.displayName = 'RecordsView'
export default RecordsView

View File

@ -1,231 +0,0 @@
<script setup lang="tsx">
import {
NSpin, NPageHeader, useNotification,
NFlex, NButton, NIcon, NGrid, NGi,
NStatistic, NDataTable, NInput,
NModalProvider
} from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { onMounted, ref } from 'vue'
import { useRecordStore, type Record, type SOARecord, RecordTypes } from '@/stores/records'
import { getErrorInfo } from '@/apis/api'
import { PlusSquare, RedoAlt, CheckCircle, Clock, Cogs, Search } from '@vicons/fa'
import router from '@/router';
import RecordOps from '@/components/records/RecordOps'
import RecordEditModal from '@/components/records/RecordEditModal.vue'
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
const props = defineProps<{
domain: string
}>()
const columns = [
{
key: 'no',
title: '#',
render(_, index) {
return index + 1
}
},
{
key: 'name',
title: t("records.name"),
},
{
key: 'record_type',
title: t('records.recordType')
},
{
key: 'content',
title: t('records.content'),
render(row: Record) {
return Object.entries(row.content).map(i => i[1]).join(" ")
}
},
{
key: 'ttl',
title: 'TTL (s)'
},
{
key: '',
render(row: Record) {
return <RecordOps record={row} domain={props.domain} onRecord-delete={deleteRecord} onEdit-record={showEditing} />
}
}
] as DataTableColumns<Record>
const recordStore = useRecordStore()
const notification = useNotification()
const records = ref<Record[] | undefined>([] as Record[]);
const soa = ref<SOARecord>({} as SOARecord)
const editModalShow = ref(false)
const editingRecord = ref<Record>({} as Record)
const loading = ref(true);
onMounted(() => {
try {
refreshRecords()
} catch (err) {
const msg = getErrorInfo(err)
notification.error(msg)
console.error(err)
}
})
const reloadRecords = () => records.value = recordStore.records?.filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
async function refreshRecords() {
try {
await recordStore.loadRecords(props.domain)
reloadRecords()
soa.value = recordStore.records?.find(e => e.record_type === RecordTypes.RecordTypeSOA)?.content as SOARecord
} catch (err) {
const msg = getErrorInfo(err)
notification.error(msg)
console.error(err)
} finally {
loading.value = false;
}
}
function goBack() {
router.push('/domains')
}
function searchRecord(value: string) {
if (value.length > 0) {
records.value = recordStore.records?.
filter(e => e.record_type !== RecordTypes.RecordTypeSOA).
filter(e => !!~e.name.indexOf(value))
} else {
records.value = recordStore.records?.
filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
}
}
async function deleteRecord(domain: string, record: Record) {
try {
await recordStore.removeRecord(domain, record)
reloadRecords()
} catch (err) {
const msg = getErrorInfo(err)
notification.error(msg)
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>
<template>
<div id="records">
<RecordEditModal v-model:show="editModalShow" :domain="domain" :record="editingRecord"
@reload-records="reloadRecords" />
<NSpin size="large" v-if="loading" />
<div v-else class="content">
<NModalProvider>
<NPageHeader :title="t('domains.dnsRecord')" :subtitle="domain" @back="goBack">
<template #extra>
<NFlex :wrap="false" justify="end" inline>
<NButton type="primary" @click="newRecord">
<template #icon>
<NIcon>
<PlusSquare />
</NIcon>
</template>
{{ t('common.add') }}
</NButton>
<NInput :placeholder="t('records.search')" @update:value="searchRecord" clearable>
<template #prefix>
<NIcon :component="Search" />
</template>
</NInput>
</NFlex>
</template>
<NGrid :cols="4">
<NGi>
<NStatistic :value="soa?.refresh">
<template #suffix>
s
</template>
<template #label>
<NIcon class="icon">
<RedoAlt />
</NIcon>
{{ t('records.refresh') }}
</template>
</NStatistic>
</NGi>
<NGi>
<NStatistic :value="soa?.retry">
<template #suffix>
s
</template>
<template #label>
<NIcon class="icon">
<CheckCircle />
</NIcon>
{{ t('records.retry') }}
</template>
</NStatistic>
</NGi>
<NGi>
<NStatistic :value="soa?.expire">
<template #suffix>
s
</template>
<template #label>
<NIcon class="icon">
<Clock />
</NIcon>
{{ t('records.expire') }}
</template>
</NStatistic>
</NGi>
<NGi>
<NStatistic :value="soa?.minttl">
<template #suffix>
s
</template>
<template #label>
<NIcon class="icon">
<Cogs />
</NIcon>
{{ t('records.ttl') }}
</template>
</NStatistic>
</NGi>
</NGrid>
</NPageHeader>
<br />
<NDataTable :data="records" :columns="columns" :pagination="{ pageSize: 20 }" />
</NModalProvider>
</div>
</div>
</template>
<style scoped>
.icon {
transform: translateY(2px);
}
div#records {
position: absolute;
top: 0;
left: 0;
width: 100vw;
padding: 1.5rem;
}
</style>

View File

@ -7,5 +7,10 @@
{ {
"path": "./tsconfig.app.json" "path": "./tsconfig.app.json"
} }
] ],
"compilerOptions": {
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
}
} }