This commit is contained in:
Sense T 2024-04-08 09:37:32 +08:00
parent a67b2d7724
commit 8c0b79066f
14 changed files with 371 additions and 186 deletions

18
package-lock.json generated Normal file
View File

@ -0,0 +1,18 @@
{
"name": "recored-ui",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"@vicons/fa": "^0.12.0"
}
},
"node_modules/@vicons/fa": {
"version": "0.12.0",
"resolved": "https://registry.npmmirror.com/@vicons/fa/-/fa-0.12.0.tgz",
"integrity": "sha512-g2PIeJLsTHUjt6bK63LxqC0uYQB7iu+xViJOxvp1s8b9/akpXVPVWjDTTsP980/0KYyMMe4U7F/aUo7wY+MsXA==",
"dev": true
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"devDependencies": {
"@vicons/fa": "^0.12.0"
}
}

View File

@ -1,85 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router' import {
import HelloWorld from './components/HelloWorld.vue' NNotificationProvider,
NConfigProvider,
NGlobalStyle,
useOsTheme,
darkTheme,
lightTheme,
type GlobalTheme
} from "naive-ui";
import { RouterView } from "vue-router";
import { onMounted } from "vue";
const osThemeRef = useOsTheme()
const theme = defineModel<GlobalTheme>()
theme.value = osThemeRef.value === 'dark' ? darkTheme : lightTheme
onMounted(() => {
document.title = 'reCoreD-UI'
})
</script> </script>
<template> <template>
<header> <NConfigProvider :theme="theme">
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" /> <NGlobalStyle />
<NNotificationProvider :max="3">
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView /> <RouterView />
</NNotificationProvider>
</NConfigProvider>
</template> </template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style>

View File

@ -1,5 +1,4 @@
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig } from "axios"; import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig } from "axios";
import { useLoadingBar, useNotification } from 'naive-ui'
import { type Record } from '@/stores/records'; import { type Record } from '@/stores/records';
import { type Domain } from "@/stores/domains"; import { type Domain } from "@/stores/domains";
@ -9,84 +8,57 @@ type Result<T> = {
data: T data: T
} }
export class Request { // 5 second.
private instance: AxiosInstance; const notificationDuration = 5000
private baseConfig: AxiosRequestConfig = { baseURL: "api/v1" } const messages = new Map<number, {
private loadingBar = useLoadingBar() title: string, content: string, duration: number
private notification = useNotification()
private messages = new Map<number, {
title: string, content: string
}>( }>(
// TODO: i18n // TODO: i18n
[ [
[400, { [400, {
title: "请求错误 (400)", title: "请求错误 (400)",
content: "参数提交错误" content: "参数提交错误",
duration: notificationDuration
}], }],
[401, { [401, {
title: "未授权 (401)", title: "未授权 (401)",
content: "请刷新页面重新登录" content: "请刷新页面重新登录",
duration: notificationDuration
}], }],
[403, { [403, {
title: "拒绝访问 (403)", title: "拒绝访问 (403)",
content: "你没有权限!" content: "你没有权限!",
duration: notificationDuration
}], }],
[404, { [404, {
title: "查无此项 (404)", title: "查无此项 (404)",
content: "没有该项内容" content: "没有该项内容",
duration: notificationDuration
}], }],
[500, { [500, {
title: "服务器错误 (500)", title: "服务器错误 (500)",
content: "请检查系统日志" content: "请检查系统日志",
duration: notificationDuration
}] }]
] ]
) )
export function getErrorInfo(err: any) {
const msg = messages.get(err.response.status)
return msg? msg: {
title: "未知错误",
content: "请打开控制台了解详情",
duration: notificationDuration
}
}
export class Request {
private instance: AxiosInstance;
private baseConfig: AxiosRequestConfig = { baseURL: "api/v1" }
constructor(config: AxiosRequestConfig) { constructor(config: AxiosRequestConfig) {
this.instance = axios.create(Object.assign(this.baseConfig, config)) this.instance = axios.create(Object.assign(this.baseConfig, config))
this.setupInceptors()
}
private setupInceptors() {
this.setupRequestInterceptors()
this.setupResponseInterceptors()
}
private setupRequestInterceptors() {
const fulFilled = (res: InternalAxiosRequestConfig<any>) => {
this.loadingBar.start()
return res
}
const onError = (err: any) => {
this.loadingBar.error()
return Promise.reject(err)
}
this.instance.interceptors.request.use(fulFilled, onError)
}
private setupResponseInterceptors() {
const fulFilled = (res: AxiosResponse) => {
this.loadingBar.finish()
return res
}
const onError = (err: any) => {
this.loadingBar.error()
const msg = this.messages.get(err.response.status)
if (msg) {
this.notification.error(msg)
} else {
console.log(err.response)
this.notification.error({
title: "未知错误",
content: "请打开控制台了解详情"
})
}
return Promise.reject(err.response)
}
this.instance.interceptors.response.use(fulFilled, onError)
} }
public request(config: AxiosRequestConfig): Promise<AxiosResponse> { public request(config: AxiosRequestConfig): Promise<AxiosResponse> {

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

Before

Width:  |  Height:  |  Size: 276 B

View File

@ -28,8 +28,8 @@ a,
} }
#app { #app {
display: grid; display: flex;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr;
padding: 0 2rem; padding: 2rem 2rem;
} }
} }

View File

@ -0,0 +1,35 @@
<template>
<div>
<p>
<NIcon class="icon" :component="AddressCard" />
<span> {{ domain.admin_email }}</span>
</p>
<p>
<NIcon class="icon" :component="Server" />
<span> {{ domain.main_dns }}</span>
</p>
</div>
</template>
<script setup lang="ts">
import { type Domain } from "../../stores/domains";
import { NIcon } from "naive-ui";
import { AddressCard, Server } from "@vicons/fa";
defineProps<{
domain: Domain;
}>();
</script>
<style scoped>
div {
display: block;
}
span {
padding-left: 0.5em;
}
.icon {
transform: translateY(2px);
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<div>
<NFlex justify="end">
<NTooltip trigger="hover">
<template #trigger>
<NButton size="tiny" type="primary">
<template #icon>
<NIcon :component="Book" @click="loadRecord" />
</template>
</NButton>
</template>
查看/修改记录
</NTooltip>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="tiny">
<template #icon>
<NIcon :component="EditRegular" />
</template>
</NButton>
</template>
修改
</NTooltip>
<NTooltip trigger="hover">
<template #trigger>
<NButton type="error" size="tiny">
<template #icon>
<NIcon :component="TrashAlt" />
</template>
</NButton>
</template>
删除
</NTooltip>
</NFlex>
</div>
</template>
<script setup lang="ts">
import { NSpace, NButton, NIcon, NTooltip, NFlex } from 'naive-ui'
import { TrashAlt, EditRegular, Book } from '@vicons/fa'
import { type Domain } from "../../stores/domains"
import router from '@/router';
const props = defineProps<{
domain: Domain
}>()
function loadRecord() {
router.push(`/records/${props.domain.domain_name}`)
}
</script>

View File

@ -10,12 +10,15 @@ const router = createRouter({
component: HomeView component: HomeView
}, },
{ {
path: '/about', path: '/domains',
name: 'about', name: 'domains',
// route level code-splitting component: () => import('../views/DomainsView.vue')
// this generates a separate chunk (About.[hash].js) for this route },
// which is lazy-loaded when the route is visited. {
component: () => import('../views/AboutView.vue') path: '/records/:domain',
name: 'records',
component: () => import('../views/RecordsView.vue'),
props: true
} }
] ]
}) })

View File

@ -36,7 +36,7 @@ const domainDevData: Domain[] = [
retry_interval: 7200, retry_interval: 7200,
expiry_period: 3600000, expiry_period: 3600000,
negative_ttl: 86400 negative_ttl: 86400
} },
] ]
export const useDomainStore = defineStore('domains', () => { export const useDomainStore = defineStore('domains', () => {

View File

@ -1,15 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { NSpin, NFlex, NCard, NButton, NIcon, useNotification } from 'naive-ui'
import { PlusSquare } from "@vicons/fa"
import { onMounted } from 'vue'
import { useDomainStore } from '@/stores/domains'
import { getErrorInfo } from '@/apis/api'
import DomainInfo from '@/components/domains/DomainInfo.vue'
import DomainOps from '@/components/domains/DomainOps.vue'
const loading = defineModel<boolean>({ default: true });
const domainStore = useDomainStore()
const notification = useNotification()
onMounted(() => {
try {
domainStore.loadDomains()
loading.value = false
} catch (error) {
const msg = getErrorInfo(error)
notification.error(msg)
}
})
</script>
<template>
<div>
<NSpin size="large" v-if="loading" />
<NFlex v-else 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" />
</template>
</NCard>
<NCard hoverable>
<NButton block quaternary size="large">
<template #icon>
<NIcon :component="PlusSquare" :depth="5" />
</template>
</NButton>
</NCard>
</NFlex>
</div>
</template>
<style scoped>
.n-card {
width: 32vw;
}
</style>

View File

@ -1,9 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue' import router from '@/router';
router.push("/domains")
</script> </script>
<template>
<main>
<TheWelcome />
</main>
</template>

View File

@ -0,0 +1,126 @@
<script setup lang="ts">
import { NSpin, NPageHeader, useNotification, NFlex, NButton, NIcon, NGrid, NGi, NStatistic, NDataTable, NInput } from 'naive-ui'
import { onMounted } 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';
const props = defineProps<{
domain: string
}>()
const loading = defineModel<boolean>('loading', { default: true });
const records = defineModel<Record[]>('records');
const search = defineModel<string>('search', { default: '' })
const soa = defineModel<SOARecord | undefined>('soa')
const recordStore = useRecordStore()
const notification = useNotification()
onMounted(() => {
try {
refreshRecords()
} catch (err) {
const msg = getErrorInfo(err)
notification.error(msg)
}
})
function refreshRecords() {
recordStore.loadRecords(props.domain)
records.value = recordStore.records
soa.value = records.value?.find(e => e.record_type === RecordTypes.RecordTypeSOA)?.content as SOARecord
loading.value = false;
}
function goBack() {
router.push('/domains')
}
</script>
<template>
<div id="records">
<NSpin size="large" v-if="loading" />
<NPageHeader v-else title="DNS 记录" :subtitle="domain" @back="goBack">
<template #extra>
<NFlex :wrap="false" justify="end" inline>
<NButton type="primary">
<template #icon>
<NIcon>
<PlusSquare />
</NIcon>
</template>
新增
</NButton>
<NInput v-model:value="search" placeholder="搜索...">
<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>
刷新时间
</template>
</NStatistic>
</NGi>
<NGi>
<NStatistic :value="soa?.retry">
<template #suffix>
s
</template>
<template #label>
<NIcon class="icon">
<CheckCircle />
</NIcon>
重试间隔
</template>
</NStatistic>
</NGi>
<NGi>
<NStatistic :value="soa?.expire">
<template #suffix>
s
</template>
<template #label>
<NIcon class="icon">
<Clock />
</NIcon>
超期时间
</template>
</NStatistic>
</NGi>
<NGi>
<NStatistic :value="soa?.minttl">
<template #suffix>
s
</template>
<template #label>
<NIcon class="icon">
<Cogs />
</NIcon>
缓存时间
</template>
</NStatistic>
</NGi>
</NGrid>
</NPageHeader>
<NDataTable :data="records">
</NDataTable>
</div>
</template>
<style scoped>
.icon {
transform: translateY(2px);
}
</style>