Squashed commit of the following:
commit 1e92328a0fc570fe9419ad5dbaaef77f7dc9ad2e Author: Sense T <me@sense-t.eu.org> Date: Mon Apr 15 21:52:44 2024 +0800 yes, react it! commit 09fffff6139b4cecb81cb1444139f225e95e8917 Author: Sense T <me@sense-t.eu.org> Date: Mon Apr 15 17:33:26 2024 +0800 actions to be done commit 1611b0b338cfd965d15f43fb10308bc56015895f Author: Sense T <me@sense-t.eu.org> Date: Mon Apr 15 15:22:08 2024 +0800 modal needed. commit 88453e7382618fb6774ff1cc4c0f7045d4dfcf46 Author: Sense T <me@sense-t.eu.org> Date: Mon Apr 15 10:52:13 2024 +0800 Domain View done commit 8cedca27c79ca2ba69c8777dfcb6019799875e31 Author: Sense T <me@sense-t.eu.org> Date: Sun Apr 14 21:24:14 2024 +0800 domain delete modal done commit 60cd00c0cad0774bae5b57bcfc4723a29d28d221 Author: Sense T <me@sense-t.eu.org> Date: Sun Apr 14 07:55:11 2024 +0800 1 commit 285853e988db6e6a6371135869da0129fd73afd7 Author: Sense T <me@sense-t.eu.org> Date: Sat Apr 13 17:29:43 2024 +0800 eslint commit 8f0ffbf744fd85a612daacd7bd6cbc45d58907d3 Author: Sense T <me@sense-t.eu.org> Date: Sat Apr 13 17:20:50 2024 +0800 f commit 9762b632225f185d83388e58d93ed49f62fe6b3f Author: Sense T <me@sense-t.eu.org> Date: Sat Apr 13 17:08:37 2024 +0800 views, components to be done commit 321e5255f2b1e705844179dd910d5f5a1ae58298 Author: Sense T <me@sense-t.eu.org> Date: Sat Apr 13 14:29:04 2024 +0800 prepare for react
This commit is contained in:
parent
3305d8d618
commit
b583720223
12
web/.eslintrc
Normal file
12
web/.eslintrc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
]
|
||||||
|
}
|
@ -1,33 +1,30 @@
|
|||||||
# reCoreD-UI
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 in Vite.
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
## Recommended IDE Setup
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
## Type Support for `.vue` Imports in TS
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
## Customize configuration
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
```js
|
||||||
|
export default {
|
||||||
## Project Setup
|
// other rules...
|
||||||
|
parserOptions: {
|
||||||
```sh
|
ecmaVersion: 'latest',
|
||||||
npm install
|
sourceType: 'module',
|
||||||
|
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||||
|
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||||
```sh
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type-Check, Compile and Minify for Production
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite App</title>
|
<title>Vite + React + TS</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
4264
web/package-lock.json
generated
4264
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,34 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "recored-ui",
|
"name": "web",
|
||||||
"version": "0.0.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=dev vite",
|
"dev": "vite",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"build-only": "vite build",
|
"preview": "vite preview"
|
||||||
"type-check": "vue-tsc --build --force"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.3.6",
|
||||||
|
"antd": "^5.16.1",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"pinia": "^2.1.7",
|
"i18next": "^23.11.1",
|
||||||
"vue": "^3.4.21",
|
"i18next-browser-languagedetector": "^7.2.1",
|
||||||
"vue-i18n": "^9.11.0",
|
"react": "^18.2.0",
|
||||||
"vue-router": "^4.3.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"react-i18next": "^14.1.0",
|
||||||
|
"react-redux": "^9.1.0",
|
||||||
|
"react-router-dom": "^6.22.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node20": "^20.1.2",
|
"@types/react": "^18.2.66",
|
||||||
"@types/node": "^20.11.28",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@vicons/fa": "^0.12.0",
|
"@typescript-eslint/eslint-plugin": "^7.6.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@typescript-eslint/parser": "^7.6.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"eslint": "^8.57.0",
|
||||||
"naive-ui": "^2.38.1",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"npm-run-all2": "^6.1.2",
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
"typescript": "~5.4.0",
|
"typescript": "^5.4.5",
|
||||||
"vfonts": "^0.0.3",
|
"vite": "^5.2.0"
|
||||||
"vite": "^5.1.6",
|
|
||||||
"vue-tsc": "^2.0.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
42
web/src/App.css
Normal file
42
web/src/App.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
@ -1,32 +1,34 @@
|
|||||||
import {
|
import { RouterProvider } from 'react-router-dom'
|
||||||
NNotificationProvider,
|
import router from './router'
|
||||||
NConfigProvider,
|
import { App, ConfigProvider, Spin, theme } from 'antd'
|
||||||
NGlobalStyle,
|
import zhCN from 'antd/locale/zh_CN'
|
||||||
useOsTheme,
|
import enUS from 'antd/locale/en_US'
|
||||||
darkTheme,
|
|
||||||
lightTheme,
|
|
||||||
} from "naive-ui";
|
|
||||||
|
|
||||||
import { zhCN, dateZhCN, enUS, dateEnUS } from 'naive-ui'
|
import './App.css'
|
||||||
import { RouterView } from "vue-router";
|
import isBrowserDarkTheme from './isBrowserDarkTheme'
|
||||||
|
import i18n from './locale'
|
||||||
|
|
||||||
const osThemeRef = useOsTheme()
|
function detectLanguage() {
|
||||||
const theme = osThemeRef.value === 'dark' ? darkTheme : lightTheme
|
switch (i18n.language) {
|
||||||
const locale = navigator.language === "zh-CN" ? zhCN : enUS
|
case 'zh-CN':
|
||||||
const dateLocale = navigator.language === "zh-CN" ? dateZhCN : dateEnUS
|
case 'zh':
|
||||||
|
return zhCN
|
||||||
|
default:
|
||||||
|
return enUS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function ReactApp() {
|
||||||
document.title = 'reCoreD-UI'
|
document.title = 'reCoreD-UI'
|
||||||
|
const themeUsed = isBrowserDarkTheme() ? theme.darkAlgorithm : theme.defaultAlgorithm
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NConfigProvider theme={theme} locale={locale} date-locale={dateLocale}>
|
<ConfigProvider theme={{ algorithm: themeUsed }} locale={detectLanguage()}>
|
||||||
<NGlobalStyle />
|
<App>
|
||||||
<NNotificationProvider max={3}>
|
<RouterProvider router={router} fallbackElement={<Spin size='large' />} />
|
||||||
<RouterView />
|
</App>
|
||||||
</NNotificationProvider>
|
</ConfigProvider>
|
||||||
</NConfigProvider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
App.displayName = 'App'
|
export default ReactApp
|
||||||
|
|
||||||
export default App
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig } from "axios";
|
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from "axios"
|
||||||
import { type Record } from '@/stores/records';
|
import { type Record } from '../stores/records'
|
||||||
import { type Domain } from "@/stores/domains";
|
import { type Domain } from "../stores/domains"
|
||||||
|
import i18n from "../locale"
|
||||||
import i18n from "@/locale/i18n";
|
|
||||||
|
|
||||||
type Result<T> = {
|
type Result<T> = {
|
||||||
success: boolean
|
success: boolean
|
||||||
@ -10,45 +9,52 @@ type Result<T> = {
|
|||||||
data: T
|
data: T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const t = i18n.t
|
||||||
// 5 second.
|
// 5 second.
|
||||||
const notificationDuration = 5000
|
const notificationDuration = 5000
|
||||||
const messages = new Map<number, {
|
const messages = new Map<number, {
|
||||||
title: string, content: string, duration: number
|
message: string, description: string, duration: number
|
||||||
}>(
|
}>(
|
||||||
[
|
[
|
||||||
[400, {
|
[400, {
|
||||||
title: i18n.global.t("api.error400.title"),
|
message: t("api.error400.title"),
|
||||||
content: i18n.global.t("api.error400.content"),
|
description: t("api.error400.content"),
|
||||||
duration: notificationDuration
|
duration: notificationDuration
|
||||||
}],
|
}],
|
||||||
[401, {
|
[401, {
|
||||||
title: i18n.global.t("api.error401.title"),
|
message: t("api.error401.title"),
|
||||||
content: i18n.global.t("api.error401.content"),
|
description: t("api.error401.content"),
|
||||||
duration: notificationDuration
|
duration: notificationDuration
|
||||||
}],
|
}],
|
||||||
[403, {
|
[403, {
|
||||||
title: i18n.global.t("api.error403.title"),
|
message: t("api.error403.title"),
|
||||||
content: i18n.global.t("api.error403.content"),
|
description: t("api.error403.content"),
|
||||||
duration: notificationDuration
|
duration: notificationDuration
|
||||||
}],
|
}],
|
||||||
[404, {
|
[404, {
|
||||||
title: i18n.global.t("api.error404.title"),
|
message: t("api.error404.title"),
|
||||||
content: i18n.global.t("api.error404.content"),
|
description: t("api.error404.content"),
|
||||||
duration: notificationDuration
|
duration: notificationDuration
|
||||||
}],
|
}],
|
||||||
[500, {
|
[500, {
|
||||||
title: i18n.global.t("api.error500.title"),
|
message: t("api.error500.title"),
|
||||||
content: i18n.global.t("api.error500.content"),
|
description: t("api.error500.content"),
|
||||||
duration: notificationDuration
|
duration: notificationDuration
|
||||||
}]
|
}]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
export function getErrorInfo(err: any) {
|
export interface ResponseError {
|
||||||
|
response: {
|
||||||
|
status: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getErrorInfo(err: ResponseError) {
|
||||||
const msg = messages.get(err.response.status)
|
const msg = messages.get(err.response.status)
|
||||||
return msg ? msg : {
|
return msg ? msg : {
|
||||||
title: i18n.global.t("api.errorUnknown.title"),
|
message: t("api.errorUnknown.title"),
|
||||||
content: i18n.global.t("api.errorUnknown.content"),
|
description: t("api.errorUnknown.content"),
|
||||||
duration: notificationDuration
|
duration: notificationDuration
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,86 +0,0 @@
|
|||||||
/* color palette from <https://github.com/vuejs/theme> */
|
|
||||||
:root {
|
|
||||||
--vt-c-white: #ffffff;
|
|
||||||
--vt-c-white-soft: #f8f8f8;
|
|
||||||
--vt-c-white-mute: #f2f2f2;
|
|
||||||
|
|
||||||
--vt-c-black: #181818;
|
|
||||||
--vt-c-black-soft: #222222;
|
|
||||||
--vt-c-black-mute: #282828;
|
|
||||||
|
|
||||||
--vt-c-indigo: #2c3e50;
|
|
||||||
|
|
||||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
|
||||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
|
||||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
|
||||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
|
||||||
|
|
||||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
|
||||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
|
||||||
--vt-c-text-dark-1: var(--vt-c-white);
|
|
||||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* semantic color variables for this project */
|
|
||||||
:root {
|
|
||||||
--color-background: var(--vt-c-white);
|
|
||||||
--color-background-soft: var(--vt-c-white-soft);
|
|
||||||
--color-background-mute: var(--vt-c-white-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-light-2);
|
|
||||||
--color-border-hover: var(--vt-c-divider-light-1);
|
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-light-1);
|
|
||||||
--color-text: var(--vt-c-text-light-1);
|
|
||||||
|
|
||||||
--section-gap: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--color-background: var(--vt-c-black);
|
|
||||||
--color-background-soft: var(--vt-c-black-soft);
|
|
||||||
--color-background-mute: var(--vt-c-black-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-dark-2);
|
|
||||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-dark-1);
|
|
||||||
--color-text: var(--vt-c-text-dark-2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
color: var(--color-text);
|
|
||||||
background: var(--color-background);
|
|
||||||
transition:
|
|
||||||
color 0.5s,
|
|
||||||
background-color 0.5s;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-family:
|
|
||||||
Inter,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
'Segoe UI',
|
|
||||||
Roboto,
|
|
||||||
Oxygen,
|
|
||||||
Ubuntu,
|
|
||||||
Cantarell,
|
|
||||||
'Fira Sans',
|
|
||||||
'Droid Sans',
|
|
||||||
'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
@import './base.css';
|
|
||||||
|
|
||||||
#app {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
font-weight: normal;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
.green {
|
|
||||||
text-decoration: none;
|
|
||||||
color: hsla(160, 100%, 37%, 1);
|
|
||||||
transition: 0.4s;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
a:hover {
|
|
||||||
background-color: hsla(160, 100%, 37%, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
display: flex;
|
|
||||||
padding: 2rem 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
32
web/src/components/domains/DomainCard.tsx
Normal file
32
web/src/components/domains/DomainCard.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Card, Tooltip } from "antd"
|
||||||
|
import { Domain } from "../../stores/domains"
|
||||||
|
import { BookOutlined, DeleteOutlined, EditOutlined } from "@ant-design/icons"
|
||||||
|
import DomainInfo from "./DomainInfo"
|
||||||
|
import i18n from '../../locale'
|
||||||
|
const { t } = i18n
|
||||||
|
|
||||||
|
type DomainCardProps = {
|
||||||
|
domain: Domain
|
||||||
|
onRecordClick(): void
|
||||||
|
onEditClick(): void
|
||||||
|
onDeleteClick(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DomainCard({ domain, onRecordClick, onEditClick, onDeleteClick }: DomainCardProps) {
|
||||||
|
return (
|
||||||
|
<Card className='domain-info' title={domain.domain_name} actions={[
|
||||||
|
<Tooltip title={t('domains.dnsRecord')}>
|
||||||
|
<BookOutlined key='records' className='icon' onClick={onRecordClick} />
|
||||||
|
</Tooltip>,
|
||||||
|
<Tooltip title={t('common.edit')} >
|
||||||
|
<EditOutlined key='edit' className='icon' onClick={onEditClick} />
|
||||||
|
</Tooltip>,
|
||||||
|
<Tooltip title={t('common.delete')}>
|
||||||
|
<DeleteOutlined key='delete' className='icon' onClick={onDeleteClick} />
|
||||||
|
</Tooltip>
|
||||||
|
]} key={domain.id}>
|
||||||
|
<DomainInfo domain={domain} />
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
59
web/src/components/domains/DomainDeleteModal.tsx
Normal file
59
web/src/components/domains/DomainDeleteModal.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { App, Input, Modal } from "antd"
|
||||||
|
import { Domain } from "../../stores/domains"
|
||||||
|
import { useState } from "react"
|
||||||
|
import i18n from '../../locale'
|
||||||
|
import { CloseOutlined, DeleteOutlined } from "@ant-design/icons"
|
||||||
|
import { ResponseError, getErrorInfo } from "../../api"
|
||||||
|
const { t } = i18n
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
domain: Domain
|
||||||
|
removeDomain(domain: Domain): Promise<void>
|
||||||
|
onOk(): void
|
||||||
|
onCancel(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DomainDeleteModal({ open, domain, removeDomain, onOk, onCancel }: Props) {
|
||||||
|
const [input, setInput] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const { notification } = App.useApp()
|
||||||
|
|
||||||
|
function confirm() {
|
||||||
|
setLoading(true)
|
||||||
|
removeDomain(domain).then(onOk).finally(() => setLoading(false)).catch(e => {
|
||||||
|
const msg = getErrorInfo(e as ResponseError)
|
||||||
|
notification.error(msg)
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal onOk={confirm} onCancel={onCancel}
|
||||||
|
title={`${t('domains.delete')} - ${domain.domain_name}`}
|
||||||
|
confirmLoading={loading}
|
||||||
|
okButtonProps={{
|
||||||
|
disabled: input !== domain.domain_name,
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
danger: true
|
||||||
|
}}
|
||||||
|
cancelButtonProps={{
|
||||||
|
icon: <CloseOutlined />
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
closeIcon={false}
|
||||||
|
maskClosable={false}
|
||||||
|
|
||||||
|
centered
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<p>{t('common.deleteConfirm')}</p>
|
||||||
|
<p>{t('domains.deleteHint')}</p>
|
||||||
|
<p>{t('domains.confirm1')} <b id="boldit">{domain.domain_name}</b> {t('domains.confirm2')}</p>
|
||||||
|
<p />
|
||||||
|
<p>
|
||||||
|
<Input placeholder={domain.domain_name} onChange={e => setInput(e.target.value)} />
|
||||||
|
</p>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
@ -1,220 +1,131 @@
|
|||||||
import {
|
import { App, Form, FormInstance, Input, InputNumber, Modal, Space } from "antd"
|
||||||
NModal,
|
import { Domain } from "../../stores/domains"
|
||||||
NCard,
|
import i18n from '../../locale'
|
||||||
NForm,
|
import { useEffect, useState } from "react"
|
||||||
NFormItem,
|
import { CheckOutlined, CloseOutlined } from "@ant-design/icons"
|
||||||
NFlex,
|
import { ResponseError, getErrorInfo } from "../../api"
|
||||||
NIcon,
|
const { t } = i18n
|
||||||
NButton,
|
|
||||||
NInput,
|
|
||||||
NInputNumber,
|
|
||||||
type FormRules,
|
|
||||||
type FormItemRule,
|
|
||||||
createDiscreteApi
|
|
||||||
} from 'naive-ui'
|
|
||||||
import { getErrorInfo } from '@/apis/api';
|
|
||||||
import { useDomainStore, type Domain } from '@/stores/domains';
|
|
||||||
import { Check, Times } from '@vicons/fa';
|
|
||||||
import i18n from '@/locale/i18n'
|
|
||||||
import { ref, type SetupContext } from 'vue';
|
|
||||||
|
|
||||||
const { t } = i18n.global
|
|
||||||
const domainStore = useDomainStore()
|
|
||||||
const { notification } = createDiscreteApi(['notification'])
|
|
||||||
const enum validFlags {
|
|
||||||
domainNameValid = 1,
|
|
||||||
mainNsValid = domainNameValid << 1,
|
|
||||||
adminEmailValid = mainNsValid << 1
|
|
||||||
}
|
|
||||||
const allFlags = validFlags.adminEmailValid | validFlags.mainNsValid | validFlags.domainNameValid
|
|
||||||
const rules = {
|
|
||||||
domain_name: [{
|
|
||||||
required: true,
|
|
||||||
trigger: 'blur',
|
|
||||||
validator: (_rule: FormItemRule, value: string) => {
|
|
||||||
return validate(
|
|
||||||
value,
|
|
||||||
/^([\w-]+\.)+[\w-]+$/,
|
|
||||||
'domains.errors.domainName',
|
|
||||||
validFlags.domainNameValid
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
main_dns: [{
|
|
||||||
required: true,
|
|
||||||
trigger: 'blur',
|
|
||||||
validator: (_rule: FormItemRule, value: string) => {
|
|
||||||
return validate(
|
|
||||||
value,
|
|
||||||
/^([\w-]+\.)+[\w-]+$/,
|
|
||||||
'domains.errors.domainName',
|
|
||||||
validFlags.mainNsValid,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
admin_email: [{
|
|
||||||
required: true,
|
|
||||||
trigger: 'blur',
|
|
||||||
validator: (_rule: FormItemRule, value: string) => {
|
|
||||||
return validate(
|
|
||||||
value,
|
|
||||||
/^[\w-.]+@([\w-]+\.)+[\w-]+$/,
|
|
||||||
'domains.errors.mail',
|
|
||||||
validFlags.adminEmailValid
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
refresh_interval: [{
|
|
||||||
required: true,
|
|
||||||
trigger: 'blur',
|
|
||||||
type: 'number',
|
|
||||||
}],
|
|
||||||
retry_interval: [{
|
|
||||||
required: true,
|
|
||||||
trigger: 'blur',
|
|
||||||
type: 'number',
|
|
||||||
}],
|
|
||||||
expiry_period: [{
|
|
||||||
required: true,
|
|
||||||
trigger: 'blur',
|
|
||||||
type: 'number',
|
|
||||||
}],
|
|
||||||
negative_ttl: [{
|
|
||||||
required: true,
|
|
||||||
trigger: 'blur',
|
|
||||||
type: 'number'
|
|
||||||
}]
|
|
||||||
} as FormRules
|
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
open: boolean
|
||||||
domain: Domain
|
domain: Domain
|
||||||
show: boolean
|
editDomain(domain: Domain): Promise<void>
|
||||||
'onUpdate:show': (v: boolean) => void
|
createDomain(domain: Domain): Promise<void>
|
||||||
|
onCancel(): void
|
||||||
|
onOk(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
type Events = {
|
export default function DomainEditModal({ open, domain, editDomain, createDomain, onCancel, onOk }: Props) {
|
||||||
'update:show': (v: boolean) => void
|
const [loading, setLoading] = useState(false)
|
||||||
'update:value': (v: string | number | null) => void
|
const [form] = Form.useForm<Domain>()
|
||||||
}
|
const { notification } = App.useApp()
|
||||||
|
|
||||||
const loading = ref(false)
|
useEffect(() => {
|
||||||
const invalidData = ref(0)
|
form.setFieldsValue(domain)
|
||||||
|
}, [open])
|
||||||
|
|
||||||
function validate(value: string, reg: RegExp, msg: string, flag: validFlags): Promise<void> {
|
async function confirm() {
|
||||||
return new Promise<void>((resolve, reject) => {
|
const commitFunction = (!domain.id || domain.id < 1) ? createDomain : editDomain
|
||||||
if (!value) {
|
setLoading(true)
|
||||||
invalidData.value &= ~flag
|
|
||||||
reject(Error(t('common.mandatory')))
|
|
||||||
} else if (!reg.test(value)) {
|
|
||||||
invalidData.value &= ~flag
|
|
||||||
reject(Error(t(msg)))
|
|
||||||
} else {
|
|
||||||
invalidData.value |= flag
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirm(domain: Domain) {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
try {
|
||||||
if (!domain.id || domain.id < 1) {
|
domain = await form.validateFields()
|
||||||
await domainStore.addDomain(domain)
|
await commitFunction(domain)
|
||||||
} else {
|
onOk()
|
||||||
await domainStore.updateDomain(domain)
|
} catch (error) {
|
||||||
}
|
const msg = getErrorInfo(error as ResponseError)
|
||||||
} catch (e) {
|
|
||||||
const msg = getErrorInfo(e)
|
|
||||||
notification.error(msg)
|
notification.error(msg)
|
||||||
console.error(e)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function easyInput(domain_name: string, domain: Domain) {
|
function easyInput(form: FormInstance<Domain>, domain_name: string) {
|
||||||
domain.admin_email = `admin@${domain_name}`
|
form.setFieldValue('admin_email', `admin@${domain_name}`)
|
||||||
domain.main_dns = `ns1.${domain_name}`
|
form.setFieldValue('main_dns', `ns1.${domain_name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function modalHeader({ domain }: Props) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Modal
|
||||||
{(!domain || !domain.id || domain.id < 1) ? <span>{t('common.new')}</span> : <span>{t('common.edit')}</span>}
|
onCancel={onCancel} onOk={confirm}
|
||||||
<span>{t('domains._')}</span>
|
title={
|
||||||
</>
|
<span>
|
||||||
)
|
{
|
||||||
|
(!domain || !domain.id || domain.id < 1) ? t('common.new') : t('common.edit')
|
||||||
}
|
}
|
||||||
|
{
|
||||||
function modalInputNumbers({ value, label, path }: { value: number, label: string, path: string }, { emit }: SetupContext<Events>) {
|
t('domains._')
|
||||||
return (
|
}
|
||||||
<NFormItem label={t(label)} path={path}>
|
</span>
|
||||||
<NInputNumber value={value} onUpdate:value={v => emit('update:value', v)} showButton={false}>
|
}
|
||||||
{{
|
confirmLoading={loading}
|
||||||
suffix: () => t('common.unitForSecond')
|
cancelButtonProps={{
|
||||||
|
icon: <CloseOutlined />,
|
||||||
}}
|
}}
|
||||||
</NInputNumber>
|
okButtonProps={{
|
||||||
</NFormItem>
|
icon: <CheckOutlined />,
|
||||||
)
|
htmlType: 'submit'
|
||||||
}
|
|
||||||
|
|
||||||
function modalBody({ domain }: Props) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<NForm model={domain} rules={rules}>
|
|
||||||
<NFormItem label={t('domains._')} path='domain_name'>
|
|
||||||
<NInput placeholder='example.com' value={domain.domain_name} onUpdate:value={v => domain.domain_name = v} onInput={v => easyInput(v, domain)} />
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem label={t('domains.form.mainDNS')} path='main_dns'>
|
|
||||||
<NInput placeholder="ns1.example.com" value={domain.main_dns} onUpdate:value={v => domain.main_dns = v} />
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem label={t('domains.form.adminMail')} path='admin_email'>
|
|
||||||
<NInput placeholder="admin@example.com" value={domain.admin_email} onUpdate:value={v => domain.admin_email = v} inputProps={{ type: 'email' }} />
|
|
||||||
</NFormItem>
|
|
||||||
</NForm>
|
|
||||||
<NForm model={domain} rules={rules} inline>
|
|
||||||
<modalInputNumbers value={domain.refresh_interval} onUpdate:value={(v: number) => domain.refresh_interval = v} path='refresh_interval' label='records.refresh' />
|
|
||||||
<modalInputNumbers value={domain.retry_interval} onUpdate:value={(v: number) => domain.retry_interval = v} path='retry_interval' label='records.retry' />
|
|
||||||
<modalInputNumbers value={domain.expiry_period} onUpdate:value={(v: number) => domain.expiry_period = v} path='expiry_period' label='records.expire' />
|
|
||||||
<modalInputNumbers value={domain.negative_ttl} onUpdate:value={(v: number) => domain.negative_ttl = v} path='negative_ttl' label='records.ttl' />
|
|
||||||
</NForm>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function modalActions({ domain }: Props, { emit }: SetupContext<Events>) {
|
|
||||||
return (
|
|
||||||
<NFlex justify='end'>
|
|
||||||
<NButton size='small' onClick={() => emit("update:show", false)} >
|
|
||||||
{{
|
|
||||||
default: () => t('common.cancel'),
|
|
||||||
icon: () => <NIcon><Times /></NIcon>
|
|
||||||
}}
|
}}
|
||||||
</NButton>
|
|
||||||
|
|
||||||
<NButton size='small' type='primary' disabled={invalidData.value !== allFlags} loading={loading.value} onClick={() => confirm(domain).then(() => emit('update:show', false))} attrType='submit'>
|
open={open}
|
||||||
{{
|
closeIcon={false}
|
||||||
default: () => t('common.confirm'),
|
maskClosable={false}
|
||||||
icon: () => <NIcon><Check /></NIcon>
|
|
||||||
}}
|
centered
|
||||||
</NButton>
|
destroyOnClose
|
||||||
</NFlex>
|
forceRender
|
||||||
|
>
|
||||||
|
<Form<Domain> name="domain" form={form}
|
||||||
|
scrollToFirstError
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<Form.Item<Domain>
|
||||||
|
label={t('domains._')}
|
||||||
|
name='domain_name'
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: t('common.mandatory') },
|
||||||
|
{ pattern: /^([\w-]+\.)+[\w-]+$/, message: t('domains.errors.domainName') },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder='example.com' onChange={v => easyInput(form, v.target.value)} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<Domain>
|
||||||
|
label={t('domains.form.mainDNS')}
|
||||||
|
name='main_dns'
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: t('common.mandatory') },
|
||||||
|
{ pattern: /^([\w-]+\.)+[\w-]+$/, message: t('domains.errors.domainName') },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="ns1.example.com" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<Domain>
|
||||||
|
label={t('domains.form.adminMail')}
|
||||||
|
name='admin_email'
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: t('common.mandatory') },
|
||||||
|
{ pattern: /^[\w-.]+@([\w-]+\.)+[\w-]+$/, message: t('domains.errors.mail') }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="admin@example.com" />
|
||||||
|
</Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Form.Item<Domain> name='refresh_interval' label={t('records.refresh')}>
|
||||||
|
<InputNumber controls={false} addonAfter={t('common.unitForSecond')} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<Domain> name='retry_interval' label={t('records.retry')}>
|
||||||
|
<InputNumber controls={false} addonAfter={t('common.unitForSecond')} />
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Form.Item<Domain> name='expiry_period' label={t('records.expire')}>
|
||||||
|
<InputNumber controls={false} addonAfter={t('common.unitForSecond')} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<Domain> name='negative_ttl' label={t('records.ttl')}>
|
||||||
|
<InputNumber controls={false} addonAfter={t('common.unitForSecond')} />
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DomainEditModal({ domain, show, }: Props, { emit }: SetupContext<Events>) {
|
|
||||||
return (
|
|
||||||
<NModal maskClosable={false} show={show}>
|
|
||||||
<NCard style={{ width: '640px' }} role='dialog'>
|
|
||||||
{{
|
|
||||||
headler: () => <modalHeader domain={domain} />,
|
|
||||||
default: () => <modalBody domain={domain} />,
|
|
||||||
action: () => <modalActions domain={domain} onUpdate:show={(v: boolean) => { emit("update:show", v) }} />
|
|
||||||
}}
|
|
||||||
</NCard>
|
|
||||||
</NModal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DomainEditModal
|
|
@ -1,11 +1,7 @@
|
|||||||
div {
|
.icon-info {
|
||||||
display: block;
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span.info {
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
|
||||||
transform: translateY(2px);
|
|
||||||
}
|
|
@ -1,27 +1,22 @@
|
|||||||
|
import { GlobalOutlined, MailOutlined } from "@ant-design/icons"
|
||||||
|
import { Domain } from "../../stores/domains"
|
||||||
import './DomainInfo.css'
|
import './DomainInfo.css'
|
||||||
|
|
||||||
import { type Domain } from "../../stores/domains";
|
|
||||||
import { NIcon } from "naive-ui";
|
|
||||||
import { AddressCard, Server } from "@vicons/fa";
|
|
||||||
import { defineComponent, defineProps } from "vue";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
domain: Domain
|
domain: Domain
|
||||||
}
|
}
|
||||||
|
|
||||||
function DomainInfo({domain}: Props) {
|
export default function DomainInfo({ domain }: Props) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<p>
|
<p>
|
||||||
<NIcon class="icon" component={AddressCard} />
|
<MailOutlined className="icon-info" />
|
||||||
<span> {domain.admin_email}</span>
|
<span className="info">{domain.admin_email}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<NIcon class="icon" component={Server} />
|
<GlobalOutlined className="icon-info" />
|
||||||
<span> {domain.main_dns}</span>
|
<span className="info">{domain.domain_name}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DomainInfo
|
|
@ -1,92 +0,0 @@
|
|||||||
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';
|
|
||||||
import i18n from '@/locale/i18n'
|
|
||||||
import type { SetupContext } from 'vue';
|
|
||||||
const { t } = i18n.global
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
domain: Domain
|
|
||||||
onRemoveDomain: (d: Domain) => void
|
|
||||||
onEditDomain: (d: Domain) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type Events = {
|
|
||||||
removeDomain(domain: Domain): void
|
|
||||||
editDomain(domain: Domain): void
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadRecord({ domain }: Props) {
|
|
||||||
return (
|
|
||||||
<NTooltip trigger="hover">
|
|
||||||
{{
|
|
||||||
trigger: () =>
|
|
||||||
<NButton size="tiny" type="primary" onClick={() => { router.push(`/records/${domain.domain_name}`) }}>
|
|
||||||
{{ icon: () => <NIcon component={Book} /> }}
|
|
||||||
</NButton>,
|
|
||||||
default: () => t('domains.dnsRecord')
|
|
||||||
}}
|
|
||||||
</NTooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function editDomain({ domain }: Props, { emit }: SetupContext<Events>) {
|
|
||||||
return (
|
|
||||||
<NTooltip trigger="hover">
|
|
||||||
{{
|
|
||||||
default: () => t('common.edit'),
|
|
||||||
trigger: () =>
|
|
||||||
<NButton size="tiny" onClick={() => emit("editDomain", domain)}>
|
|
||||||
{{
|
|
||||||
icon: () =>
|
|
||||||
<NIcon component={EditRegular} />
|
|
||||||
}}
|
|
||||||
</NButton>
|
|
||||||
}}
|
|
||||||
|
|
||||||
</NTooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteDomain({ domain }: Props, { emit }: SetupContext<Events>) {
|
|
||||||
return (
|
|
||||||
<NTooltip trigger="hover">
|
|
||||||
{{
|
|
||||||
default: () => t('common.delete'),
|
|
||||||
trigger: () =>
|
|
||||||
<NButton type="error" size="tiny" onClick={() => emit("removeDomain", domain)}>
|
|
||||||
{{
|
|
||||||
icon: () =>
|
|
||||||
<NIcon component={TrashAlt} />
|
|
||||||
}}
|
|
||||||
</NButton>
|
|
||||||
}}
|
|
||||||
</NTooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DomainOps({ domain }: Props, { emit }: SetupContext<Events>) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<NFlex justify='end'>
|
|
||||||
<loadRecord domain={domain} />
|
|
||||||
<editDomain domain={domain} onEditDomain={(d: Domain) => emit("editDomain", d)} />
|
|
||||||
<deleteDomain domain={domain} onRemoveDomain={(d: Domain) => emit("removeDomain", d)} />
|
|
||||||
</NFlex>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DomainOps.props = {
|
|
||||||
domain: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DomainOps.emits = {
|
|
||||||
removeDomain: (d: Domain) => d,
|
|
||||||
editDomain: (d: Domain) => d
|
|
||||||
} as Events
|
|
||||||
|
|
||||||
export default DomainOps
|
|
@ -1,7 +0,0 @@
|
|||||||
.icon-down {
|
|
||||||
transform: translateY(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
b#boldit {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
import './DomainRemoveModal.css'
|
|
||||||
|
|
||||||
import { useDomainStore, type Domain } from '@/stores/domains';
|
|
||||||
import { NModal, NCard, NFlex, NButton, NIcon, NInput, createDiscreteApi } from 'naive-ui'
|
|
||||||
import { Times, TrashAlt, QuestionCircle } from '@vicons/fa';
|
|
||||||
import { getErrorInfo } from '@/apis/api';
|
|
||||||
import i18n from '@/locale/i18n';
|
|
||||||
import { ref, type EmitsOptions, type ObjectEmitsOptions, type SetupContext } from 'vue';
|
|
||||||
|
|
||||||
const t = i18n.global.t
|
|
||||||
const domainStore = useDomainStore()
|
|
||||||
const { notification } = createDiscreteApi(['notification'])
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
domain: Domain
|
|
||||||
show: boolean
|
|
||||||
'onUpdate:show': (value: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type Events = {
|
|
||||||
'update:show': (value: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const domain_name = ref('')
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
async function confirm(domain: Domain) {
|
|
||||||
domain_name.value = ''
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (domain)
|
|
||||||
await domainStore.removeDomain(domain)
|
|
||||||
} catch (e) {
|
|
||||||
const msg = getErrorInfo(e)
|
|
||||||
notification.error(msg)
|
|
||||||
console.error(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function modalBody({ domain }: Props) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p>{t('common.deleteConfirm')}</p>
|
|
||||||
<p>{t('domains.deleteHint')}</p>
|
|
||||||
<p>{t('domains.confirm1')} <b id="boldit">{domain.domain_name}</b> {t('domains.confirm2')}</p>
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
<NInput onUpdate:value={(v) => domain_name.value = v} placeholder={domain.domain_name} />
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function modalActions({ domain }: Props, { emit }: SetupContext<Events>) {
|
|
||||||
return <>
|
|
||||||
<NFlex justify='end'>
|
|
||||||
<NButton size='small' onClick={() => { emit('update:show', false) }}>
|
|
||||||
{{
|
|
||||||
icon: () => <NIcon><Times /></NIcon>,
|
|
||||||
default: () => t('common.cancel')
|
|
||||||
}}
|
|
||||||
</NButton>
|
|
||||||
|
|
||||||
<NButton size='small' type='error' disabled={domain_name.value !== domain.domain_name} attrType='submit' loading={loading.value} onClick={() => confirm(domain).then(() => emit('update:show', false))}>
|
|
||||||
{{
|
|
||||||
icon: () => <NIcon><TrashAlt /></NIcon>,
|
|
||||||
default: () => t('common.confirm')
|
|
||||||
}}
|
|
||||||
</NButton>
|
|
||||||
</NFlex>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function DomainRemoveModal({ domain, show }: Props, { emit }: SetupContext<Events>) {
|
|
||||||
return (
|
|
||||||
<NModal maskClosable={false} show={show}>
|
|
||||||
<NCard role='dialog' style={{ width: '600px' }}>
|
|
||||||
{{
|
|
||||||
header: () => <><NIcon class="icon-down" color='red' />{t('domains.delete')} - {domain.domain_name}</>,
|
|
||||||
default: () => <modalBody domain={domain} />,
|
|
||||||
action: () => <modalActions domain={domain} onUpdate:show={(v: boolean) => emit('update:show', v)} />
|
|
||||||
}}
|
|
||||||
</NCard>
|
|
||||||
</NModal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DomainRemoveModal
|
|
@ -1,461 +1,259 @@
|
|||||||
import {
|
import { App, Form, Input, InputNumber, Modal, Select } from 'antd'
|
||||||
NModal,
|
import i18n from '../../locale'
|
||||||
NCard,
|
import { AAAARecord, ARecord, CAARecord, CNAMERecord, MXRecord, NSRecord, Record, RecordTypes, SRVRecord, TXTRecord } from '../../stores/records'
|
||||||
NForm,
|
import { useEffect, useState } from 'react'
|
||||||
NFormItem,
|
import { CheckOutlined, CloseOutlined } from '@ant-design/icons'
|
||||||
NFlex,
|
import { ResponseError, getErrorInfo } from '../../api'
|
||||||
NButton,
|
import { FormInstance } from 'antd/lib/form/Form'
|
||||||
NInput,
|
const { t } = i18n
|
||||||
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 = {
|
type Props = {
|
||||||
|
open: boolean
|
||||||
record: Record
|
record: Record
|
||||||
domain: string
|
//domain: string
|
||||||
show: boolean
|
onCancel(): void
|
||||||
'onReloadRecords': () => void
|
onOk(): void
|
||||||
'onUpdate:show': (v: boolean) => void
|
|
||||||
|
editRecord(record: Record): Promise<void>
|
||||||
|
createRecord(record: Record): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Events = {
|
const recordTypeOptions = Object.entries(RecordTypes).filter(e => e[1] !== RecordTypes.RecordTypeSOA).map(e => {
|
||||||
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 {
|
return {
|
||||||
label: e[1],
|
value: e[1],
|
||||||
value: e[1]
|
label: e[1]
|
||||||
} as SelectOption
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function validateName(_rule: FormItemRule, value: string): boolean | Error {
|
export default function RecordEditModal({ open, record, onOk, onCancel, editRecord, createRecord }: Props) {
|
||||||
invalidData.value |= validationFlags.name
|
const [loading, setLoading] = useState(false)
|
||||||
if (!value || value === '') {
|
const [form] = Form.useForm<Record>()
|
||||||
invalidData.value &= ~validationFlags.name
|
const { notification } = App.useApp()
|
||||||
return new Error(t('common.mandatory'))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.includes(' ')) {
|
useEffect(() => { form.setFieldsValue(record) }, [open])
|
||||||
invalidData.value &= ~validationFlags.name
|
|
||||||
return new Error(t('records.errors.hasSpace'))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.startsWith('.') || value.endsWith('.')) {
|
async function confirm() {
|
||||||
invalidData.value &= ~validationFlags.name
|
const commitFunction = (!record.id || record.id < 1) ? createRecord : editRecord
|
||||||
return new Error(t('records.errors.badName.dotAndMinus'))
|
setLoading(true)
|
||||||
}
|
|
||||||
|
|
||||||
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: Record, domain: string) {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
try {
|
||||||
if (!record.id || record.id < 1) {
|
record = await form.validateFields()
|
||||||
await recordStore.addRecord(domain, record)
|
await commitFunction(record)
|
||||||
} else {
|
onOk()
|
||||||
await recordStore.updateRecord(domain, record)
|
} catch (error) {
|
||||||
}
|
const msg = getErrorInfo(error as ResponseError)
|
||||||
} catch (e) {
|
|
||||||
const msg = getErrorInfo(e)
|
|
||||||
notification.error(msg)
|
notification.error(msg)
|
||||||
console.error(e)
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function modalHeader({ record }: Props) {
|
const controls = new Map<RecordTypes, (f: FormInstance<Record>) => JSX.Element>([
|
||||||
return (
|
[
|
||||||
|
RecordTypes.RecordTypeA, (
|
||||||
|
({ getFieldValue }) =>
|
||||||
|
<Form.Item<Record<ARecord>> label='IP' name={['content', 'ip']} required rules={[{
|
||||||
|
validator() {
|
||||||
|
|
||||||
|
const result = ARecord.validate(getFieldValue('content') as ARecord)
|
||||||
|
return (result === true) ? Promise.resolve() : Promise.reject(result)
|
||||||
|
}
|
||||||
|
}]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
RecordTypes.RecordTypeAAAA, (
|
||||||
|
({ getFieldValue }) =>
|
||||||
|
<Form.Item<Record<AAAARecord>> label='IP' name={['content', 'ip']} required rules={[{
|
||||||
|
validator() {
|
||||||
|
const result = AAAARecord.validate(getFieldValue('content') as AAAARecord)
|
||||||
|
return (result === true) ? Promise.resolve() : Promise.reject(result)
|
||||||
|
}
|
||||||
|
}]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
RecordTypes.RecordTypeCNAME, (
|
||||||
|
({ getFieldValue }) =>
|
||||||
|
<Form.Item<Record<CNAMERecord>> label={t('records.form.host')} required name={['content', 'host']}
|
||||||
|
rules={[{
|
||||||
|
validator() {
|
||||||
|
const result = CNAMERecord.validate(getFieldValue('content') as CNAMERecord)
|
||||||
|
return (result === true) ? Promise.resolve() : Promise.reject(result)
|
||||||
|
}
|
||||||
|
}]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
RecordTypes.RecordTypeNS, (
|
||||||
|
({ getFieldValue }) =>
|
||||||
|
<Form.Item<Record<NSRecord>> label={t('records.form.host')} name={['content', 'host']} required
|
||||||
|
rules={[{
|
||||||
|
validator() {
|
||||||
|
const result = NSRecord.validate(getFieldValue('content') as NSRecord)
|
||||||
|
return (result === true) ? Promise.resolve() : Promise.reject(result)
|
||||||
|
}
|
||||||
|
}]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
RecordTypes.RecordTypeTXT, (
|
||||||
|
({ getFieldValue }) =>
|
||||||
|
<Form.Item<Record<TXTRecord>> label={t('records.form.text')} name={['content', 'text']} required
|
||||||
|
rules={[{
|
||||||
|
validator() {
|
||||||
|
const result = TXTRecord.validate(getFieldValue('content') as TXTRecord)
|
||||||
|
return (result === true) ? Promise.resolve() : Promise.reject(result)
|
||||||
|
}
|
||||||
|
}]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
RecordTypes.RecordTypeMX, (
|
||||||
|
({ getFieldValue }) =>
|
||||||
<>
|
<>
|
||||||
{(!record || !record.id || record.id < 1) ? <span>{t('common.new')}</span> : <span> {t('common.edit')}</span>}
|
<Form.Item<Record<MXRecord>> label={t('records.form.host')} name={['content', 'host']} required
|
||||||
<span>{t('records._')}</span>
|
rules={[{
|
||||||
|
validator() {
|
||||||
|
const result = MXRecord.validate(getFieldValue('content') as MXRecord)
|
||||||
|
return (result === true) ? Promise.resolve() : Promise.reject(result)
|
||||||
|
}
|
||||||
|
}]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<Record<MXRecord>> label={t('records.form.preference')} name={['content', 'preference']} required>
|
||||||
|
<InputNumber controls={false} />
|
||||||
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
],
|
||||||
|
[
|
||||||
function modalActions({ record, domain }: Props, { emit }: SetupContext<Events>) {
|
RecordTypes.RecordTypeCAA, (
|
||||||
return (
|
({ getFieldValue }) =>
|
||||||
<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).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>
|
<Form.Item<Record<CAARecord>> label={t('records.form.flag')} name={['content', 'flag']} required>
|
||||||
<NFormItem label={t('records.recordType')}>
|
<InputNumber controls={false} />
|
||||||
<NSelect value={record.record_type}
|
</Form.Item>
|
||||||
onUpdate:value={(v) => { record.record_type = v; record.content = {} as RecordT }}
|
<Form.Item<Record<CAARecord>> label={t('records.form.tag')} name={['content', 'tag']} required rules={[{
|
||||||
options={recordTypeOptions} style={{ width: '8vw' }} />
|
validator() {
|
||||||
</NFormItem>
|
const result = CAARecord.validate(getFieldValue('content') as CAARecord)
|
||||||
<NFormItem label={t('records.name')} path='name'>
|
return (result === true) ? Promise.resolve() : Promise.reject(result)
|
||||||
<NInput value={record.name} onUpdate:value={v => record.name = v} />
|
}
|
||||||
</NFormItem>
|
}]}>
|
||||||
<NFormItem label='TTL' path='ttl'>
|
<Input />
|
||||||
<NInputNumber value={record.ttl} onUpdate:value={v => v ? record.ttl = v : null} showButton={false} >
|
</Form.Item>
|
||||||
{{
|
<Form.Item<Record<CAARecord>> label={t('records.form.value')} name={['content', 'value']} required rules={[
|
||||||
suffix: () => t('common.unitForSecond')
|
{ required: true, message: t('common.mandatory') }
|
||||||
}}
|
]} >
|
||||||
</NInputNumber>
|
<Input />
|
||||||
</NFormItem>
|
</Form.Item>
|
||||||
</NForm>
|
|
||||||
<NForm model={record} rules={rules}>
|
|
||||||
<modalBodyContent type={record.record_type} record={record} />
|
|
||||||
</NForm>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
RecordTypes.RecordTypeSRV, (
|
||||||
|
({ getFieldValue }) =>
|
||||||
|
<>
|
||||||
|
<Form.Item<Record<SRVRecord>> label={t('records.form.priority')} name={['content', 'priority']} required>
|
||||||
|
<InputNumber controls={false} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<Record<SRVRecord>> label={t('records.form.weight')} name={['content', 'weight']} required>
|
||||||
|
<InputNumber controls={false} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<Record<SRVRecord>> label={t('records.form.port')} name={['content', 'port']} required>
|
||||||
|
<InputNumber controls={false} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<Record<SRVRecord>> label={t('records.form.target')} name={['content', 'target']} required rules={[{
|
||||||
|
validator() {
|
||||||
|
const result = SRVRecord.validate(getFieldValue('content') as SRVRecord)
|
||||||
|
return (result === true) ? Promise.resolve() : Promise.reject(result)
|
||||||
}
|
}
|
||||||
|
}]}>
|
||||||
const IPRecordE = ({ record }: Props) => (
|
<Input />
|
||||||
<NFormItem label={t('records.content')} path='ip'>
|
</Form.Item>
|
||||||
<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 (
|
return (
|
||||||
<NModal maskClosable={false} show={show}>
|
<Modal
|
||||||
<NCard style={{ width: '640px' }} role='dialog'>
|
onCancel={onCancel} onOk={confirm}
|
||||||
{{
|
title={
|
||||||
header: () => <modalHeader record={record} />,
|
<span>
|
||||||
default: () => <modalBody record={record} />,
|
{
|
||||||
action: () => <modalActions record={record} domain={domain}
|
(!record || !record.id || record.id < 1) ? t('common.new') : t('common.edit')
|
||||||
onUpdate:show={(v: boolean) => emit('update:show', v)}
|
}
|
||||||
onReloadRecords={() => emit('reloadRecords')} />
|
{
|
||||||
|
t('records._')
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
confirmLoading={loading}
|
||||||
|
cancelButtonProps={{
|
||||||
|
icon: <CloseOutlined />,
|
||||||
}}
|
}}
|
||||||
</NCard>
|
okButtonProps={{
|
||||||
</NModal>
|
icon: <CheckOutlined />,
|
||||||
|
htmlType: 'submit'
|
||||||
|
}}
|
||||||
|
|
||||||
|
open={open}
|
||||||
|
closeIcon={false}
|
||||||
|
maskClosable={false}
|
||||||
|
|
||||||
|
centered
|
||||||
|
destroyOnClose
|
||||||
|
forceRender
|
||||||
|
>
|
||||||
|
<Form<Record> name='record' form={form}
|
||||||
|
scrollToFirstError
|
||||||
|
autoComplete='off'
|
||||||
|
labelCol={{ span: 4 }}
|
||||||
|
wrapperCol={{ span: 20 }}
|
||||||
|
validateTrigger='onBlur'
|
||||||
|
>
|
||||||
|
<Form.Item<Record> label={t('records.recordType')} required name='record_type'>
|
||||||
|
<Select allowClear={false} options={recordTypeOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<Record> label={t('records.name')} required name='name' rules={[
|
||||||
|
{
|
||||||
|
validator(_rule, value) {
|
||||||
|
const result = Record.validateName(value)
|
||||||
|
return (result === true) ? Promise.resolve() : Promise.reject(result)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<Record> label='TTL' required name='ttl'>
|
||||||
|
<InputNumber controls={false} addonAfter={t('common.unitForSecond')} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<Record> noStyle shouldUpdate={(p, c) => p.record_type !== c.record_type}>
|
||||||
|
{
|
||||||
|
({ getFieldValue }: FormInstance<Record>) => {
|
||||||
|
const e = controls.get(getFieldValue('record_type'))
|
||||||
|
if (!e) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
return e({ getFieldValue } as FormInstance<Record>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RecordEditModal
|
|
@ -1,49 +1,26 @@
|
|||||||
import { NButton, NButtonGroup, NTooltip, NIcon, NPopconfirm, NFlex } from 'naive-ui'
|
import { Button, Flex, Popconfirm, Tooltip } from "antd"
|
||||||
import { TrashAlt, EditRegular } from '@vicons/fa'
|
import { DeleteOutlined, EditFilled } from "@ant-design/icons"
|
||||||
import type { Record } from '@/stores/records'
|
import i18n from '../../locale'
|
||||||
import i18n from '@/locale/i18n'
|
|
||||||
import type { SetupContext } from 'vue'
|
const { t } = i18n
|
||||||
const { t } = i18n.global
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
record: Record
|
onEdit(): void
|
||||||
domain: string
|
onDelete(): void
|
||||||
onRecordDelete: (domain: string, record: Record) => void
|
|
||||||
onEditRecord: (domain: string, record: Record) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Events = {
|
export default function RecordOps({ onEdit, onDelete }: Props) {
|
||||||
recordDelete(domain: string, record: Record): void
|
|
||||||
editRecord(domain: string, record: Record): void
|
|
||||||
}
|
|
||||||
|
|
||||||
function RecordOps({ record, domain }: Props, { emit }: SetupContext<Events>) {
|
|
||||||
return (
|
return (
|
||||||
<NFlex justify='end'>
|
<Flex justify="end" gap='small'>
|
||||||
<NButtonGroup>
|
<Tooltip title={t("common.edit")}>
|
||||||
<NTooltip>
|
<Button icon={<EditFilled />} size="small" onClick={onEdit}/>
|
||||||
{{
|
</Tooltip>
|
||||||
trigger: () => <NButton size='tiny' onClick={() => emit('editRecord', domain, record)}>
|
|
||||||
{{
|
<Popconfirm onConfirm={onDelete} title={t("common.deleteConfirm")}>
|
||||||
icon: () => <NIcon component={EditRegular} />
|
<Tooltip title={t("common.delete")}>
|
||||||
}}
|
<Button danger type="primary" icon={<DeleteOutlined />} size="small" />
|
||||||
</NButton>,
|
</Tooltip>
|
||||||
default: () => t("common.edit")
|
</Popconfirm>
|
||||||
}}
|
</Flex>
|
||||||
</NTooltip>
|
|
||||||
<NPopconfirm onPositiveClick={() => emit('recordDelete', domain, record)}>
|
|
||||||
{{
|
|
||||||
trigger: () => <NButton type='error' size='tiny'>
|
|
||||||
{{
|
|
||||||
icon: () => <NIcon component={TrashAlt} />
|
|
||||||
}}
|
|
||||||
</NButton>,
|
|
||||||
default: () => t("common.deleteConfirm")
|
|
||||||
}}
|
|
||||||
</NPopconfirm>
|
|
||||||
</NButtonGroup>
|
|
||||||
</NFlex>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RecordOps
|
|
78
web/src/index.css
Normal file
78
web/src/index.css
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
--records-layout-header-bgcolor: #eee;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
.records-layout-header{
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--records-layout-header-bgcolor: #333;
|
||||||
|
}
|
||||||
|
}
|
18
web/src/isBrowserDarkTheme.ts
Normal file
18
web/src/isBrowserDarkTheme.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const isBrowserDarkTheme = () => {
|
||||||
|
const [isDarkTheme, setIsDarkTheme] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const handleChange = (e: { matches: boolean | ((prevState: boolean) => boolean); }) => setIsDarkTheme(e.matches);
|
||||||
|
mediaQueryList.addEventListener('change', handleChange);
|
||||||
|
setIsDarkTheme(mediaQueryList.matches);
|
||||||
|
|
||||||
|
return () => mediaQueryList.removeEventListener('change', handleChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isDarkTheme
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isBrowserDarkTheme;
|
@ -86,6 +86,7 @@ export default {
|
|||||||
hasSpace: 'shoule have no space',
|
hasSpace: 'shoule have no space',
|
||||||
badIPv4: 'invalid IPv4 address',
|
badIPv4: 'invalid IPv4 address',
|
||||||
badIPv6: 'invalid IPv6 address',
|
badIPv6: 'invalid IPv6 address',
|
||||||
|
badEmail: 'no @ for this email address',
|
||||||
badName: {
|
badName: {
|
||||||
dotAndMinus: 'should not start or end with "." "-"',
|
dotAndMinus: 'should not start or end with "." "-"',
|
||||||
doubleDots: 'should have no contianus "."',
|
doubleDots: 'should have no contianus "."',
|
@ -1,23 +0,0 @@
|
|||||||
import { createI18n } from "vue-i18n";
|
|
||||||
import zhCN from "./zh-CN";
|
|
||||||
import enUS from "./en-US";
|
|
||||||
|
|
||||||
export default createI18n({
|
|
||||||
locale: navigator.language,
|
|
||||||
legacy: false,
|
|
||||||
messages: {
|
|
||||||
zh: {
|
|
||||||
...zhCN
|
|
||||||
},
|
|
||||||
'zh-CN': {
|
|
||||||
...zhCN
|
|
||||||
},
|
|
||||||
|
|
||||||
en: {
|
|
||||||
...enUS
|
|
||||||
},
|
|
||||||
'en-US': {
|
|
||||||
...enUS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
24
web/src/locale/index.ts
Normal file
24
web/src/locale/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import i18n from 'i18next'
|
||||||
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||||
|
import zh from './zh'
|
||||||
|
import en from './en'
|
||||||
|
|
||||||
|
i18n.use(LanguageDetector).use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
debug: import.meta.env.DEV,
|
||||||
|
fallbackLng: 'zh',
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
en: {
|
||||||
|
translation: en
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
translation: zh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
@ -86,6 +86,7 @@ export default {
|
|||||||
hasSpace: '不能有空格',
|
hasSpace: '不能有空格',
|
||||||
badIPv4: '不是有效的 IPv4 地址',
|
badIPv4: '不是有效的 IPv4 地址',
|
||||||
badIPv6: '不是有效的 IPv6 地址',
|
badIPv6: '不是有效的 IPv6 地址',
|
||||||
|
badEmail: '这里的邮箱不能有 @ 符号',
|
||||||
badName: {
|
badName: {
|
||||||
dotAndMinus: '资源记录不能以 "."、"-" 开头或结尾',
|
dotAndMinus: '资源记录不能以 "."、"-" 开头或结尾',
|
||||||
doubleDots: '资源记录不能有连续的 "."',
|
doubleDots: '资源记录不能有连续的 "."',
|
@ -1,16 +0,0 @@
|
|||||||
import './assets/main.css'
|
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
|
||||||
import { createPinia } from 'pinia'
|
|
||||||
|
|
||||||
import App from './App'
|
|
||||||
import router from './router'
|
|
||||||
import i18n from './locale/i18n'
|
|
||||||
|
|
||||||
const app = createApp(App)
|
|
||||||
|
|
||||||
app.use(createPinia())
|
|
||||||
app.use(router)
|
|
||||||
app.use(i18n)
|
|
||||||
|
|
||||||
app.mount('#app')
|
|
10
web/src/main.tsx
Normal file
10
web/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.tsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
@ -1,30 +0,0 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHashHistory(),
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
redirect: '/domains'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/domains',
|
|
||||||
name: 'domains',
|
|
||||||
meta: {
|
|
||||||
type: 'domains'
|
|
||||||
},
|
|
||||||
component: () => import('@/views/DomainsView')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/records/:domain',
|
|
||||||
name: 'records',
|
|
||||||
meta: {
|
|
||||||
type: 'records'
|
|
||||||
},
|
|
||||||
component: () => import('@/views/RecordsView'),
|
|
||||||
props: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
28
web/src/router/index.tsx
Normal file
28
web/src/router/index.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { createHashRouter, redirect } from 'react-router-dom'
|
||||||
|
import { Suspense, lazy } from 'react'
|
||||||
|
import { Spin } from 'antd'
|
||||||
|
|
||||||
|
const DomainsView = lazy(() => import('../views/DomainsView'))
|
||||||
|
const RecordsView = lazy(() => import('../views/RecordsView'))
|
||||||
|
|
||||||
|
const router = createHashRouter([
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
loader: async () => {
|
||||||
|
return redirect('/domains')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/domains',
|
||||||
|
id: 'domains',
|
||||||
|
element: <Suspense fallback={<Spin size='large' />}><DomainsView /></Suspense>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/records/:domain',
|
||||||
|
id: 'records',
|
||||||
|
loader: args => args.params,
|
||||||
|
element: <Suspense fallback={<Spin size='large' />}><RecordsView /></Suspense>
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
export default router
|
@ -1,17 +1,16 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { useState } from 'react'
|
||||||
import { ref, computed } from 'vue'
|
import api from '../api'
|
||||||
import api from '@/apis/api'
|
|
||||||
|
|
||||||
export type Domain = {
|
export class Domain {
|
||||||
id: number;
|
id?: number
|
||||||
domain_name: string;
|
domain_name?: string
|
||||||
main_dns: string;
|
main_dns?: string
|
||||||
admin_email: string;
|
admin_email?: string
|
||||||
serial_number: number;
|
serial_number?: number
|
||||||
refresh_interval: number;
|
refresh_interval?: number
|
||||||
retry_interval: number;
|
retry_interval?: number
|
||||||
expiry_period: number;
|
expiry_period?: number
|
||||||
negative_ttl: number;
|
negative_ttl?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const domainDevData: Domain[] = [
|
const domainDevData: Domain[] = [
|
||||||
@ -39,43 +38,37 @@ const domainDevData: Domain[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const useDomainStore = defineStore('domains', () => {
|
export const useDomainStore = () => {
|
||||||
const domains = ref<Domain[]>([])
|
const [domains, setDomains] = useState<Domain[]>([])
|
||||||
const domainsGetter = computed(() => domains.value)
|
|
||||||
|
|
||||||
async function loadDomains() {
|
async function loadDomains() {
|
||||||
// TODO: load from api
|
setDomains(import.meta.env.DEV ? domainDevData : (await api.get<Domain[]>('/domains')).data.data)
|
||||||
domains.value = import.meta.env.DEV ?
|
|
||||||
domainDevData :
|
|
||||||
(await api.get<Domain[]>('/domains')).data.data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addDomain(domain: Domain) {
|
async function addDomain(domain: Domain) {
|
||||||
// TODO: load from api
|
|
||||||
if (!import.meta.env.DEV) {
|
if (!import.meta.env.DEV) {
|
||||||
domain = (await api.post("/domains", domain)).data.data
|
domain = (await api.post("/domains", domain)).data.data
|
||||||
|
} else if (!domain.id) {
|
||||||
|
domain.id = Math.floor(1000 + Math.random() * 9000)
|
||||||
}
|
}
|
||||||
|
|
||||||
domains.value.push(domain)
|
setDomains(domains.concat(domain))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDomain(domain: Domain) {
|
async function updateDomain(domain: Domain) {
|
||||||
// TODO: load from api
|
|
||||||
if (!import.meta.env.DEV) {
|
if (!import.meta.env.DEV) {
|
||||||
await api.put("/domains", domain)
|
await api.put("/domains", domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
domains.value = domains.value.map(e => (e.id === domain.id || e.domain_name === domain.domain_name) ? domain : e)
|
setDomains(domains.map((e: Domain) => (e.id === domain.id || e.domain_name === domain.domain_name) ? domain : e))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeDomain(domain: Domain) {
|
async function removeDomain(domain: Domain) {
|
||||||
// TODO: load from api
|
|
||||||
if (!import.meta.env.DEV) {
|
if (!import.meta.env.DEV) {
|
||||||
await api.delete(`/domains/${domain.id}`)
|
await api.delete(`/domains/${domain.id}`)
|
||||||
}
|
}
|
||||||
|
setDomains(domains.filter(e => e.id !== domain.id))
|
||||||
domains.value = domains.value.filter(e => e.id !== domain.id)
|
|
||||||
}
|
}
|
||||||
return { domains, domainsGetter, loadDomains, addDomain, updateDomain, removeDomain }
|
|
||||||
})
|
|
||||||
|
|
||||||
|
return { domains, loadDomains, addDomain, updateDomain, removeDomain }
|
||||||
|
}
|
@ -1,52 +1,147 @@
|
|||||||
import api from '@/apis/api';
|
import { useState } from 'react'
|
||||||
import { defineStore } from 'pinia'
|
import i18n from '../locale'
|
||||||
import { ref, computed } from 'vue'
|
import api from '../api';
|
||||||
|
const { t } = i18n
|
||||||
|
export class ARecord {
|
||||||
|
ip?: string
|
||||||
|
|
||||||
export type ARecord = {
|
static validate(v: ARecord): true | Error {
|
||||||
ip: string;
|
if (!v.ip || v.ip === '') return new Error(t('common.mandatory'))
|
||||||
|
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(v.ip)) return new Error(t('records.errors.badIPv4'))
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AAAARecord = {
|
toString(): string | undefined {
|
||||||
ip: string;
|
return this.ip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TXTRecord = {
|
export class AAAARecord {
|
||||||
text: string;
|
ip?: string
|
||||||
|
|
||||||
|
static validate(v: AAAARecord): true | Error {
|
||||||
|
if (!v.ip || v.ip === '') return new Error(t('common.mandatory'))
|
||||||
|
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(v.ip)) return new Error(t('records.errors.badIPv4'))
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CNAMERecord = {
|
toString(): string | undefined {
|
||||||
host: string;
|
return this.ip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NSRecord = {
|
export class TXTRecord {
|
||||||
host: string;
|
text?: string
|
||||||
|
|
||||||
|
static validate(v: TXTRecord): true | Error {
|
||||||
|
if (!v.text || v.text === '') return new Error(t('common.mandatory'))
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MXRecord = {
|
toString(): string | undefined {
|
||||||
host: string;
|
return this.text
|
||||||
preference: number;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SRVRecord = {
|
export class CNAMERecord {
|
||||||
priority: number;
|
host?: string
|
||||||
weight: number;
|
|
||||||
port: number;
|
static validate(v: CNAMERecord): true | Error {
|
||||||
target: string;
|
if (!v.host || v.host === '') return new Error(t('common.mandatory'))
|
||||||
|
if (v.host.includes(' ')) return new Error(t('records.errors.hasSpace'))
|
||||||
|
if (!v.host.endsWith('.')) return new Error(t('records.errors.endWithDot'))
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SOARecord = {
|
toString(): string | undefined {
|
||||||
ns: string;
|
return this.host
|
||||||
MBox: string;
|
}
|
||||||
refresh: number;
|
|
||||||
retry: number;
|
|
||||||
expire: number;
|
|
||||||
minttl: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CAARecord = {
|
export class NSRecord {
|
||||||
flag: number;
|
host?: string
|
||||||
tag: string;
|
|
||||||
value: string;
|
static validate(v: NSRecord): true | Error {
|
||||||
|
if (!v.host || v.host === '') return new Error(t('common.mandatory'))
|
||||||
|
if (v.host.includes(' ')) return new Error(t('records.errors.hasSpace'))
|
||||||
|
if (!v.host.endsWith('.')) return new Error(t('records.errors.endWithDot'))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string | undefined {
|
||||||
|
return this.host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MXRecord {
|
||||||
|
host?: string
|
||||||
|
preference?: number
|
||||||
|
|
||||||
|
static validate(v: MXRecord): true | Error {
|
||||||
|
if (!v.host || v.host === '' || !v.preference) return new Error(t('common.mandatory'))
|
||||||
|
if (v.host.includes(' ')) return new Error(t('records.errors.hasSpace'))
|
||||||
|
if (!v.host.endsWith('.')) return new Error(t('records.errors.endWithDot'))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string | undefined {
|
||||||
|
return Object.entries(this).map(i => i[1]).join(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SRVRecord {
|
||||||
|
priority?: number;
|
||||||
|
weight?: number;
|
||||||
|
port?: number;
|
||||||
|
target?: string;
|
||||||
|
|
||||||
|
static validate(v: SRVRecord): true | Error {
|
||||||
|
if (!v.port || !v.priority || !v.weight || !v.target || v.target === '') return new Error(t('common.mandatory'))
|
||||||
|
if (v.target?.includes(' ')) return new Error(t('records.errors.hasSpace'))
|
||||||
|
if (!v.target?.endsWith('.')) return new Error(t('records.errors.endWithDot'))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string | undefined {
|
||||||
|
return Object.entries(this).map(i => i[1]).join(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SOARecord {
|
||||||
|
ns?: string;
|
||||||
|
MBox?: string;
|
||||||
|
refresh?: number;
|
||||||
|
retry?: number;
|
||||||
|
expire?: number;
|
||||||
|
minttl?: number;
|
||||||
|
|
||||||
|
static validate(v: SOARecord): true | Error {
|
||||||
|
if (!v.refresh || !v.retry || !v.expire! || !v.minttl || !v.MBox || v.MBox === '' || !v.ns || v.ns === '') return new Error(t('common.mandatory'))
|
||||||
|
if (v.ns?.includes(' ') || v.MBox?.includes(' ')) return new Error(t('records.errors.hasSpace'))
|
||||||
|
if (!v.ns?.endsWith('.') || !v.MBox?.endsWith('.')) return new Error(t('records.errors.endWithDot'))
|
||||||
|
if (v.MBox?.includes('@')) return new Error(t('records.errors.badEmail'))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string | undefined {
|
||||||
|
return Object.entries(this).map(i => i[1]).join(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CAARecord {
|
||||||
|
flag?: number
|
||||||
|
tag?: string
|
||||||
|
value?: string
|
||||||
|
|
||||||
|
static validate(v: CAARecord): true | Error {
|
||||||
|
if (!v.flag || !v.tag || v.tag === '' || !v.value || v.value === '') return new Error(t('common.mandatory'))
|
||||||
|
if (v.tag?.includes(' ')) return new Error(t('records.errors.hasSpace'))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string | undefined {
|
||||||
|
return Object.entries(this).map(i => i[1]).join(" ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RecordTypes {
|
export enum RecordTypes {
|
||||||
@ -63,18 +158,42 @@ export enum RecordTypes {
|
|||||||
|
|
||||||
export type RecordT = ARecord | AAAARecord | TXTRecord | CNAMERecord | NSRecord | MXRecord | SRVRecord | SOARecord | CAARecord
|
export type RecordT = ARecord | AAAARecord | TXTRecord | CNAMERecord | NSRecord | MXRecord | SRVRecord | SOARecord | CAARecord
|
||||||
|
|
||||||
export type Record = {
|
export class Record<T = RecordT> {
|
||||||
id: number;
|
id?: number
|
||||||
zone: string;
|
zone?: string
|
||||||
name: string;
|
name?: string
|
||||||
ttl: number;
|
ttl?: number
|
||||||
content: RecordT;
|
content?: T
|
||||||
record_type: RecordTypes;
|
record_type?: RecordTypes
|
||||||
|
|
||||||
|
validate(): true | Error {
|
||||||
|
const zone = Record.validateZone(this.zone!)
|
||||||
|
if (zone !== true) return zone
|
||||||
|
const name = Record.validateName(this.name!)
|
||||||
|
if (name !== true) return name
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static validateZone(zone: string): true | Error {
|
||||||
|
if (zone === '') return new Error(t('common.mandatory'))
|
||||||
|
if (zone.includes(' ')) new Error(t('records.errors.hasSpace'))
|
||||||
|
if (zone.endsWith('.')) return new Error(t('records.errors.endWithDot'))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static validateName(name: string): true | Error {
|
||||||
|
if (name === '') return new Error(t('common.mandatory'))
|
||||||
|
if (name.includes(' ')) return new Error(t('records.errors.hasSpace'))
|
||||||
|
if (name.startsWith('.') || name.endsWith('.')) return new Error(t('records.errors.badName.dotAndMinus'))
|
||||||
|
if (name.startsWith('-') || name.endsWith('.')) return new Error(t('records.errors.badName.dotAndMinus'))
|
||||||
|
if (name.includes('..')) new Error(t('records.errors.badName.doubleDots'))
|
||||||
|
if (name.split('.').filter(e => e.length > 63).length > 0) return new Error(t('records.errors.badName.longerThan63'))
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordDevData = new Map<string, Record[]>([
|
const recordDevData = new Map<string, Record[]>([
|
||||||
[
|
['example.com', [
|
||||||
'example.com', [
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
zone: "example.com",
|
zone: "example.com",
|
||||||
@ -130,10 +249,8 @@ const recordDevData = new Map<string, Record[]>([
|
|||||||
host: "www.example.com."
|
host: "www.example.com."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
] as Record[]],
|
||||||
],
|
['example.org', [
|
||||||
[
|
|
||||||
'example.org', [
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
zone: "example.org",
|
zone: "example.org",
|
||||||
@ -189,20 +306,17 @@ const recordDevData = new Map<string, Record[]>([
|
|||||||
host: "www.example.org."
|
host: "www.example.org."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
] as Record[]]
|
||||||
]
|
|
||||||
])
|
])
|
||||||
|
|
||||||
|
export const useRecordStore = () => {
|
||||||
export const useRecordStore = defineStore('records', () => {
|
const [records, setRecords] = useState<Record[]>([])
|
||||||
const records = ref<Record[] | undefined>([])
|
|
||||||
const recordsGetter = computed(() => records.value)
|
|
||||||
|
|
||||||
async function loadRecords(domain: string) {
|
async function loadRecords(domain: string) {
|
||||||
// TODO: load from api
|
// TODO: load from api
|
||||||
records.value = import.meta.env.DEV ?
|
setRecords(import.meta.env.DEV ?
|
||||||
recordDevData.get(domain) :
|
recordDevData.get(domain)! :
|
||||||
(await api.get<Record[]>(`/records/${domain}`)).data.data
|
(await api.get<Record[]>(`/records/${domain}`)).data.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addRecord(domain: string, record: Record) {
|
async function addRecord(domain: string, record: Record) {
|
||||||
@ -211,7 +325,7 @@ export const useRecordStore = defineStore('records', () => {
|
|||||||
record = (await api.post(`/records/${domain}`, record)).data.data
|
record = (await api.post(`/records/${domain}`, record)).data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
records.value?.push(record)
|
setRecords(records.concat(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateRecord(domain: string, record: Record) {
|
async function updateRecord(domain: string, record: Record) {
|
||||||
@ -220,7 +334,7 @@ export const useRecordStore = defineStore('records', () => {
|
|||||||
await api.put(`/records/${domain}`, record)
|
await api.put(`/records/${domain}`, record)
|
||||||
}
|
}
|
||||||
|
|
||||||
records.value = records.value?.map(e => e.id === record.id ? record : e)
|
setRecords(records.map(e => e.id === record.id ? record : e))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeRecord(domain: string, record: Record) {
|
async function removeRecord(domain: string, record: Record) {
|
||||||
@ -229,8 +343,8 @@ export const useRecordStore = defineStore('records', () => {
|
|||||||
await api.delete(`/records/${domain}/${record.id}`)
|
await api.delete(`/records/${domain}/${record.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
records.value = records.value?.filter(e => e.id !== record.id)
|
setRecords(records.filter(e => e.id !== record.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
return { records, recordsGetter, loadRecords, addRecord, updateRecord, removeRecord }
|
return { records, loadRecords, addRecord, updateRecord, removeRecord }
|
||||||
})
|
}
|
@ -1,3 +1,4 @@
|
|||||||
.n-card {
|
.domain-info {
|
||||||
width: 32vw;
|
width: 32vw;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
@ -1,87 +1,102 @@
|
|||||||
import './DomainsView.css'
|
import './DomainsView.css'
|
||||||
|
import { Domain, useDomainStore } from '../stores/domains'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { App, Button, Card, Space, Spin } from 'antd'
|
||||||
|
import { PlusOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
import { NSpin, NFlex, NCard, NButton, NIcon, NModalProvider, createDiscreteApi } from 'naive-ui'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { PlusSquare } from "@vicons/fa"
|
import DomainDeleteModal from '../components/domains/DomainDeleteModal'
|
||||||
import { type Domain, useDomainStore } from '@/stores/domains'
|
import DomainCard from '../components/domains/DomainCard'
|
||||||
import { getErrorInfo } from '@/apis/api'
|
import DomainEditModal from '../components/domains/DomainEditModal'
|
||||||
import DomainInfo from '@/components/domains/DomainInfo'
|
import { ResponseError, getErrorInfo } from '../api'
|
||||||
import DomainOps from '@/components/domains/DomainOps'
|
|
||||||
import DomainRemoveModal from '@/components/domains/DomainRemoveModal'
|
|
||||||
import DomainEditModal from '@/components/domains/DomainEditModal'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
|
const emptyDomain: Domain = { domain_name: '' }
|
||||||
|
|
||||||
|
export default function DomainsView() {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [deleteModalShow, setDeleteModalShow] = useState(false)
|
||||||
|
const [editModalShow, setEditModalShow] = useState(false)
|
||||||
|
const [currentDomain, setCurrentDomain] = useState(emptyDomain)
|
||||||
|
const { notification } = App.useApp()
|
||||||
const domainStore = useDomainStore()
|
const domainStore = useDomainStore()
|
||||||
const { notification } = createDiscreteApi(['notification'])
|
const go = useNavigate()
|
||||||
|
|
||||||
const loading = ref(true);
|
function openDeleteModal(domain: Domain) {
|
||||||
const removeModalShow = ref(false);
|
setCurrentDomain(domain)
|
||||||
const editModalShow = ref(false);
|
setDeleteModalShow(true)
|
||||||
const operationDomain = ref({} as Domain)
|
|
||||||
|
|
||||||
function showRemoveModal(domain: Domain) {
|
|
||||||
operationDomain.value = domain
|
|
||||||
removeModalShow.value = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showEditModal(domain: Domain) {
|
function closeDeleteModdal() {
|
||||||
operationDomain.value = domain
|
setDeleteModalShow(false)
|
||||||
editModalShow.value = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDomain() {
|
function openEditModal(domain: Domain) {
|
||||||
const domain = {
|
setCurrentDomain(domain)
|
||||||
|
setEditModalShow(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditModal() {
|
||||||
|
setEditModalShow(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function newDomain() {
|
||||||
|
openEditModal({
|
||||||
|
domain_name: '',
|
||||||
|
admin_email: '',
|
||||||
|
main_dns: '',
|
||||||
refresh_interval: 86400,
|
refresh_interval: 86400,
|
||||||
retry_interval: 7200,
|
retry_interval: 7200,
|
||||||
expiry_period: 3600000,
|
expiry_period: 3600000,
|
||||||
negative_ttl: 86400,
|
negative_ttl: 86400,
|
||||||
serial_number: 1,
|
serial_number: 1,
|
||||||
} as Domain
|
})
|
||||||
showEditModal(domain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DomainsView() {
|
// called once only.
|
||||||
try {
|
useEffect(() => {
|
||||||
domainStore.loadDomains()
|
domainStore.loadDomains().then(() => setLoading(false)).catch(e => {
|
||||||
loading.value = false
|
const msg = getErrorInfo(e as ResponseError)
|
||||||
} catch (e) {
|
|
||||||
const msg = getErrorInfo(e)
|
|
||||||
notification.error(msg)
|
notification.error(msg)
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
loading.value ? <NSpin size="large" /> :
|
loading ? <Spin size='large' /> :
|
||||||
<NModalProvider>
|
<>
|
||||||
<NFlex vertical>
|
<Space direction="vertical" >
|
||||||
{
|
{
|
||||||
domainStore.domains.map((domain: Domain) => (
|
domainStore.domains.map(e => (
|
||||||
<NCard title={domain.domain_name} key={domain.id} size='large' hoverable>
|
<DomainCard domain={e}
|
||||||
{{
|
onDeleteClick={() => openDeleteModal(e)}
|
||||||
default: () => <DomainInfo domain={domain} />,
|
onRecordClick={() => go(`/records/${e.domain_name}`)}
|
||||||
action: () => <DomainOps domain={domain} onRemoveDomain={showRemoveModal} onEditDomain={showEditModal} />
|
onEditClick={() => openEditModal(e)}
|
||||||
}}
|
key={e.id}
|
||||||
|
/>
|
||||||
</NCard>
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
<Card>
|
||||||
<NCard hoverable>
|
<Button icon={<PlusOutlined className='icon' />}
|
||||||
<NButton block quaternary size="large" onClick={addDomain}>
|
block type="text" onClick={newDomain} />
|
||||||
{{
|
</Card>
|
||||||
icon: () => <NIcon component={PlusSquare} depth={5} />
|
</Space>
|
||||||
}}
|
<DomainDeleteModal open={deleteModalShow}
|
||||||
</NButton>
|
onCancel={closeDeleteModdal}
|
||||||
</NCard>
|
onOk={closeDeleteModdal}
|
||||||
</NFlex>
|
domain={currentDomain}
|
||||||
<DomainRemoveModal show={removeModalShow.value} domain={operationDomain.value} onUpdate:show={(v: boolean) => removeModalShow.value = v} />
|
removeDomain={domainStore.removeDomain}
|
||||||
<DomainEditModal show={editModalShow.value} domain={operationDomain.value} onUpdate:show={(v: boolean) => editModalShow.value = v} />
|
/>
|
||||||
</NModalProvider>
|
<DomainEditModal open={editModalShow}
|
||||||
|
onCancel={closeEditModal}
|
||||||
|
onOk={closeEditModal}
|
||||||
|
domain={currentDomain}
|
||||||
|
editDomain={domainStore.updateDomain}
|
||||||
|
createDomain={domainStore.addDomain}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DomainsView.displayName = 'DomainsView'
|
|
||||||
|
|
||||||
export default DomainsView
|
|
@ -1,7 +1,38 @@
|
|||||||
div#records {
|
.records-layout {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
|
display: block;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
padding: 1.5rem;
|
height: 100vh;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
position:absolute ;
|
||||||
|
right: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-layout-header {
|
||||||
|
background-color: var(--records-layout-header-bgcolor);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-as-inline {
|
||||||
|
display:inline-block;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.title {
|
||||||
|
font-weight:normal;
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2.subtitle {
|
||||||
|
font-weight:normal;
|
||||||
|
font-size: 12pt;
|
||||||
}
|
}
|
@ -1,250 +1,119 @@
|
|||||||
|
import { LeftOutlined, PlusOutlined, SearchOutlined } from "@ant-design/icons"
|
||||||
|
import { App, Button, Flex, Input, Layout, Spin, Table, Typography, theme } from "antd"
|
||||||
|
import { Params, useLoaderData, useNavigate } from "react-router-dom"
|
||||||
import './RecordsView.css'
|
import './RecordsView.css'
|
||||||
|
import i18n from '../locale'
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { type Record, RecordT, useRecordStore, RecordTypes } from "../stores/records"
|
||||||
|
import { ResponseError, getErrorInfo } from "../api"
|
||||||
|
import RecordOps from "../components/records/RecordOps"
|
||||||
|
import RecordEditModal from "../components/records/RecordEditModal"
|
||||||
|
|
||||||
import {
|
const { t } = i18n
|
||||||
NSpin, NPageHeader,
|
const emptyRecord: Record<RecordT> = {} as Record
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export default function RecordsView() {
|
||||||
|
const { domain } = useLoaderData() as Params<string>
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [searchText, setSearchText] = useState<string>('')
|
||||||
|
const [editModalShow, setEditModalShow] = useState(false)
|
||||||
|
const [currentRecord, setCurrentRecord] = useState<Record>(emptyRecord)
|
||||||
|
const { notification } = App.useApp()
|
||||||
|
const {
|
||||||
|
token: { colorBgContainer, borderRadiusLG },
|
||||||
|
} = theme.useToken();
|
||||||
|
const go = useNavigate()
|
||||||
const recordStore = useRecordStore()
|
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) {
|
useEffect(() => {
|
||||||
try {
|
if (domain)
|
||||||
await recordStore.loadRecords(domain)
|
recordStore.loadRecords(domain).then(() => setLoading(false)).catch(e => {
|
||||||
reloadRecords()
|
const msg = getErrorInfo(e as ResponseError)
|
||||||
soa.value = recordStore.records?.find(e => e.record_type === RecordTypes.RecordTypeSOA)?.content as SOARecord
|
|
||||||
} catch (err) {
|
|
||||||
const msg = getErrorInfo(err)
|
|
||||||
notification.error(msg)
|
notification.error(msg)
|
||||||
console.error(err)
|
console.error(e)
|
||||||
} finally {
|
})
|
||||||
loading.value = false;
|
}, [domain])
|
||||||
}
|
|
||||||
|
function closeEditModal() {
|
||||||
|
setCurrentRecord(emptyRecord)
|
||||||
|
setEditModalShow(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
function openEditModal(record: Record) {
|
||||||
router.push('/domains')
|
setCurrentRecord(record)
|
||||||
|
setEditModalShow(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchRecord(value: string) {
|
function newRecord() {
|
||||||
if (value.length > 0) {
|
openEditModal({
|
||||||
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}.`,
|
zone: `${domain}.`,
|
||||||
ttl: 500,
|
name: '',
|
||||||
record_type: RecordTypes.RecordTypeA,
|
record_type: RecordTypes.RecordTypeA,
|
||||||
content: {
|
ttl: 600
|
||||||
ip: ''
|
|
||||||
} as ARecord
|
|
||||||
} as Record)
|
} 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} onRecordDelete={deleteRecord} onEditRecord={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 (
|
return (
|
||||||
<NGrid cols={4} >
|
<>
|
||||||
<statRefresh />
|
{
|
||||||
<statRetry />
|
loading ? <Spin size='large' /> :
|
||||||
<statExpire />
|
<>
|
||||||
<statTTL />
|
<Layout className="records-layout">
|
||||||
</NGrid>
|
<Layout.Header className="records-layout-header">
|
||||||
)
|
<Flex align='center' className="toolbar">
|
||||||
|
<Flex align="center" gap='small'>
|
||||||
|
<Button onClick={() => go('/domains')} type="text" icon={<LeftOutlined />} />
|
||||||
|
<Typography.Title level={1} className="display-as-inline title">{t('domains.dnsRecord')}</Typography.Title>
|
||||||
|
<Typography.Title level={2} type='secondary' className="display-as-inline subtitle">{domain}</Typography.Title>
|
||||||
|
</Flex>
|
||||||
|
<Flex align="center" gap='small' className="right">
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} onClick={newRecord}>
|
||||||
|
{t('common.add')}
|
||||||
|
</Button>
|
||||||
|
<Input prefix={<SearchOutlined />} placeholder={t('records.search')} onChange={v => setSearchText(v.target.value)} />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Layout.Header>
|
||||||
|
<Layout.Content style={{
|
||||||
|
margin: 24,
|
||||||
|
borderRadius: borderRadiusLG,
|
||||||
|
minHeight: 480,
|
||||||
|
background: colorBgContainer,
|
||||||
|
}}>
|
||||||
|
<Table<Record<RecordT>>
|
||||||
|
dataSource={recordStore.records
|
||||||
|
.filter(i => i.record_type !== RecordTypes.RecordTypeSOA)
|
||||||
|
.filter(i => i.name?.includes(searchText) || Object.entries(i.content as RecordT).map(i => i[1]).join(" ").includes(searchText))
|
||||||
}
|
}
|
||||||
|
pagination={{ defaultPageSize: 20 }}
|
||||||
function recordsViewBody({ domain }: Props) {
|
rowKey={e => `${e.id}`}
|
||||||
const columns = generateColumns(domain)
|
>
|
||||||
return (
|
<Table.Column<Record<RecordT>> title='#' render={(_v, _r, index) => index + 1} />
|
||||||
<NModalProvider>
|
<Table.Column<Record<RecordT>> title={t("records.name")} dataIndex='name' key='name' />
|
||||||
<NPageHeader title={t('domains.dnsRecord')} subtitle={domain} onBack={goBack}>
|
<Table.Column<Record<RecordT>> title={t('records.recordType')} dataIndex='record_type' key='record_type' />
|
||||||
{{
|
<Table.Column<Record<RecordT>> title={t('records.content')} render={(v) => Object.entries(v.content).map(i => i[1]).join(" ")} />
|
||||||
extra: () => (
|
<Table.Column<Record<RecordT>> title='TTL' key='ttl' dataIndex='ttl' />
|
||||||
<NFlex wrap={false} justify="end" inline>
|
<Table.Column<Record<RecordT>> key='op' render={(v: Record) =>
|
||||||
<NButton type="primary" onClick={() => newRecord(domain)}>
|
<RecordOps
|
||||||
{{
|
onDelete={() => {
|
||||||
icon: () => <NIcon component={PlusSquare} />,
|
if (domain)
|
||||||
default: () => t('common.add')
|
recordStore.removeRecord(domain, v).catch(e => {
|
||||||
}}
|
const msg = getErrorInfo(e as ResponseError)
|
||||||
</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)
|
notification.error(msg)
|
||||||
console.error(err)
|
console.error(e)
|
||||||
|
})
|
||||||
|
}} onEdit={() => openEditModal(v)}
|
||||||
|
/>
|
||||||
|
} />
|
||||||
|
</Table>
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout>
|
||||||
|
<RecordEditModal open={editModalShow} onCancel={closeEditModal}
|
||||||
|
onOk={closeEditModal} record={currentRecord}
|
||||||
|
editRecord={v => recordStore.updateRecord(domain!, v)}
|
||||||
|
createRecord={v => recordStore.addRecord(domain!, v)} />
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
return (
|
</>
|
||||||
<div id='records'>
|
|
||||||
<RecordEditModal show={editModalShow.value} domain={domain} record={editingRecord.value} onReloadRecords={reloadRecords} onUpdate:show={(v: boolean) => editModalShow.value = v} />
|
|
||||||
{
|
|
||||||
loading.value ? <NSpin size='large' /> : <recordsViewBody domain={domain} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
RecordsView.displayName = 'RecordsView'
|
|
||||||
export default RecordsView
|
|
0
web/env.d.ts → web/src/vite-env.d.ts
vendored
0
web/env.d.ts → web/src/vite-env.d.ts
vendored
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
|
||||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
|
||||||
"exclude": ["src/**/__tests__/*"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +1,25 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.node.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.app.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "preserve",
|
"target": "ES2020",
|
||||||
"jsxFactory": "h",
|
"useDefineForClassFields": true,
|
||||||
"jsxFragmentFactory": "Fragment",
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
}
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/node20/tsconfig.json",
|
|
||||||
"include": [
|
|
||||||
"vite.config.*",
|
|
||||||
"vitest.config.*",
|
|
||||||
"cypress.config.*",
|
|
||||||
"nightwatch.conf.*",
|
|
||||||
"playwright.config.*"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"noEmit": true,
|
"skipLibCheck": true,
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
||||||
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "bundler",
|
||||||
"types": ["node"]
|
"allowSyntheticDefaultImports": true,
|
||||||
}
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,7 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import react from '@vitejs/plugin-react-swc'
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [react()],
|
||||||
vue(),
|
|
||||||
vueJsx(),
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user