base UI
This commit is contained in:
parent
a67b2d7724
commit
8c0b79066f
18
package-lock.json
generated
Normal file
18
package-lock.json
generated
Normal 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
5
package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@vicons/fa": "^0.12.0"
|
||||
}
|
||||
}
|
103
web/src/App.vue
103
web/src/App.vue
@ -1,85 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
|
||||
|
||||
<div class="wrapper">
|
||||
<HelloWorld msg="You did it!" />
|
||||
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
<RouterLink to="/about">About</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<NConfigProvider :theme="theme">
|
||||
<NGlobalStyle />
|
||||
<NNotificationProvider :max="3">
|
||||
<RouterView />
|
||||
</NNotificationProvider>
|
||||
</NConfigProvider>
|
||||
</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>
|
||||
|
@ -1,5 +1,4 @@
|
||||
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 Domain } from "@/stores/domains";
|
||||
|
||||
@ -9,84 +8,57 @@ type Result<T> = {
|
||||
data: T
|
||||
}
|
||||
|
||||
export class Request {
|
||||
private instance: AxiosInstance;
|
||||
private baseConfig: AxiosRequestConfig = { baseURL: "api/v1" }
|
||||
private loadingBar = useLoadingBar()
|
||||
private notification = useNotification()
|
||||
private messages = new Map<number, {
|
||||
title: string, content: string
|
||||
// 5 second.
|
||||
const notificationDuration = 5000
|
||||
const messages = new Map<number, {
|
||||
title: string, content: string, duration: number
|
||||
}>(
|
||||
// TODO: i18n
|
||||
[
|
||||
[400, {
|
||||
title: "请求错误 (400)",
|
||||
content: "参数提交错误"
|
||||
content: "参数提交错误",
|
||||
duration: notificationDuration
|
||||
}],
|
||||
[401, {
|
||||
title: "未授权 (401)",
|
||||
content: "请刷新页面重新登录"
|
||||
content: "请刷新页面重新登录",
|
||||
duration: notificationDuration
|
||||
}],
|
||||
[403, {
|
||||
title: "拒绝访问 (403)",
|
||||
content: "你没有权限!"
|
||||
content: "你没有权限!",
|
||||
duration: notificationDuration
|
||||
}],
|
||||
[404, {
|
||||
title: "查无此项 (404)",
|
||||
content: "没有该项内容"
|
||||
content: "没有该项内容",
|
||||
duration: notificationDuration
|
||||
}],
|
||||
[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) {
|
||||
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> {
|
||||
|
@ -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 |
@ -28,8 +28,8 @@ a,
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
display: flex;
|
||||
grid-template-columns: 1fr;
|
||||
padding: 2rem 2rem;
|
||||
}
|
||||
}
|
||||
|
35
web/src/components/domains/DomainInfo.vue
Normal file
35
web/src/components/domains/DomainInfo.vue
Normal 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>
|
51
web/src/components/domains/DomainOps.vue
Normal file
51
web/src/components/domains/DomainOps.vue
Normal 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>
|
@ -10,12 +10,15 @@ const router = createRouter({
|
||||
component: HomeView
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// 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: '/domains',
|
||||
name: 'domains',
|
||||
component: () => import('../views/DomainsView.vue')
|
||||
},
|
||||
{
|
||||
path: '/records/:domain',
|
||||
name: 'records',
|
||||
component: () => import('../views/RecordsView.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -36,7 +36,7 @@ const domainDevData: Domain[] = [
|
||||
retry_interval: 7200,
|
||||
expiry_period: 3600000,
|
||||
negative_ttl: 86400
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
export const useDomainStore = defineStore('domains', () => {
|
||||
|
@ -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>
|
51
web/src/views/DomainsView.vue
Normal file
51
web/src/views/DomainsView.vue
Normal 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>
|
@ -1,9 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import TheWelcome from '../components/TheWelcome.vue'
|
||||
import router from '@/router';
|
||||
router.push("/domains")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<TheWelcome />
|
||||
</main>
|
||||
</template>
|
||||
|
126
web/src/views/RecordsView.vue
Normal file
126
web/src/views/RecordsView.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user