diff --git a/src/App.css b/src/App.css index 4414b48..275197a 100644 --- a/src/App.css +++ b/src/App.css @@ -29,4 +29,30 @@ color: #1c7ed6; } -} \ No newline at end of file +} + +/*灰色*/ +.color-gray{ + color: #999; +} +.color-65{ + color:rgba(0, 0, 0, 0.65); +} +.color-333{ + color: #333; +} +.text-bold{ + font-weight: bold; +} + +/* */ +/* .ant-table-thead > tr > th{ */ +/* background-color: #fcfcfc; */ +/* } */ + +/* */ +/* .ant-table-tbody > tr > td{ */ +/* background-color: #fff; */ +/* } */ + +/* */ diff --git a/src/layout/ListPageLayout.tsx b/src/layout/ListPageLayout.tsx index aa36c84..d404f2e 100644 --- a/src/layout/ListPageLayout.tsx +++ b/src/layout/ListPageLayout.tsx @@ -16,7 +16,11 @@ interface IListPageLayoutProps extends PageContainerProps { const ListPageLayout: React.FC = ( { - className, children, authHeight = true, ...props + className, + children, + authHeight = true, + title, + ...props }) => { const navigate = useNavigate() @@ -50,7 +54,7 @@ const ListPageLayout: React.FC = ( }) }}> } - {currentMenu?.title} + {title || currentMenu?.title} } className={cx(styles.container, styles.pageCard, styles.layoutTable, className)} diff --git a/src/locales/lang/en-US.ts b/src/locales/lang/en-US.ts index 1356777..de8dacf 100644 --- a/src/locales/lang/en-US.ts +++ b/src/locales/lang/en-US.ts @@ -1,6 +1,8 @@ import antdEN from 'antd/locale/en_US' import menus from './pages/system/menus/en-US' import roles from './pages/system/roles/en-US' +import record from './pages/websites/record/en-US.ts' +import websites from '@/locales/lang/pages/websites/en-US.ts' export default { ...antdEN, @@ -40,6 +42,12 @@ export default { home: { welcome: 'Welcome to' }, + websites: { + common: { + ...websites, + }, + record, + }, system: { menus, roles, diff --git a/src/locales/lang/pages/websites/en-US.ts b/src/locales/lang/pages/websites/en-US.ts new file mode 100644 index 0000000..6ddf170 --- /dev/null +++ b/src/locales/lang/pages/websites/en-US.ts @@ -0,0 +1,22 @@ +export default { + status: { + /** + * StatusSuccess StatusEnum = "success" // 成功 + * StatusFail StatusEnum = "fail" // 失败 + * StatusEnable StatusEnum = "enable" // 启用 + * StatusPending StatusEnum = "pending" // 等待 + * StatusDelete StatusEnum = "delete" // 删除 + * StatusDisable StatusEnum = "disable" // 禁用 + * StatusSyncing StatusEnum = "syncing" // 同步 + */ + success: 'Success', + fail: 'Fail', + enable: 'Enable', + pending: 'Pending', + delete: 'Delete', + disable: 'Disable', + syncing: 'Syncing', + + }, + +} \ No newline at end of file diff --git a/src/locales/lang/pages/websites/record/en-US.ts b/src/locales/lang/pages/websites/record/en-US.ts new file mode 100644 index 0000000..f5bc428 --- /dev/null +++ b/src/locales/lang/pages/websites/record/en-US.ts @@ -0,0 +1,20 @@ +export default { + + explain: { + 'PTR': 'Points an IP address to a domain name.', + 'forward_url': 'Redirects a domain name to another address but hides the destination address.', + 'AAAA': 'Points a domain name to an IPv6 address.', + 'TXT': 'Serves as an SPF record to protect against spam and can be up to 512 characters in length.', + 'SRV': 'Specifies the servers that host specific services.', + 'MX': 'Points a domain name to an email server address.', + 'NS': 'Delegates a subdomain name to third-party DNS servers.', + 'A': 'Points a domain name to an IPv4 address.', + 'CAA': 'Specifies a CA that is authorized to issue certificates for a domain name.', + 'redirect_url': 'Redirects a domain name to another address.', + 'CNAME': 'Points a domain name to another domain name.', + }, + + "forward_url": "Implicit URL Forwarding", + "redirect_url": "Explicit URL Forwarding", + +} \ No newline at end of file diff --git a/src/locales/lang/pages/websites/record/zh-CN.ts b/src/locales/lang/pages/websites/record/zh-CN.ts new file mode 100644 index 0000000..fdf7061 --- /dev/null +++ b/src/locales/lang/pages/websites/record/zh-CN.ts @@ -0,0 +1,26 @@ +export default { + + explain: { + 'AAAA': '将域名指向一个IPV6地址', + 'TXT': '文本长度限制512,通常做SPF记录(反垃圾邮件)', + 'SRV': '记录提供特定的服务的服务器', + 'MX': '将域名指向邮件服务器地址', + 'NS': '将子域名指定其他DNS服务器解析', + 'A': '将域名指向一个IPV4地址', + 'CAA': 'CA证书颁发机构授权校验', + 'REDIRECT_URL': '将域名重定向到另外一个地址', + 'CNAME': '将域名指向另外一个域名', + 'PTR': '将IP地址指向一个域名', + 'FORWARD_URL': '与显性URL类似,但是会隐藏真实目标地址', + }, + help:{ + "A": "请填写 IPv4 地址,通常为服务器IP地址", + "CNAME": '请填写 CNAME 指向的域名,如{{domain}}', + "AAAA": "请填写 IPv6 地址", + "NS": "详细说明", + "MX": '请填写邮箱厂商提供的MX记录值', + }, + "forward_url": "隐性URL", + "redirect_url": "显性URL", + +} \ No newline at end of file diff --git a/src/locales/lang/pages/websites/zh-CN.ts b/src/locales/lang/pages/websites/zh-CN.ts index 509a7af..3b5c58f 100644 --- a/src/locales/lang/pages/websites/zh-CN.ts +++ b/src/locales/lang/pages/websites/zh-CN.ts @@ -17,5 +17,6 @@ export default { disable: '禁用', syncing: '同步', - } + }, + } \ No newline at end of file diff --git a/src/locales/lang/zh-CN.ts b/src/locales/lang/zh-CN.ts index 043ec95..4d0a87c 100644 --- a/src/locales/lang/zh-CN.ts +++ b/src/locales/lang/zh-CN.ts @@ -8,6 +8,7 @@ import videoCloud from './pages/cms/videoCloud/zh-CN.ts' import videoMagnet from './pages/cms/videoMagnet/zh-CN.ts' import list from './pages/videos/list/zh-CN.ts' import websites from './pages/websites/zh-CN.ts' +import record from './pages/websites/record/zh-CN.ts' export default { ...antdZh, @@ -47,10 +48,11 @@ export default { home: { welcome: '欢迎使用' }, - websites:{ + websites: { common: { ...websites, }, + record, }, system: { menus, @@ -59,7 +61,7 @@ export default { cms: { collect, video, videoCloud, videoMagnet, }, - videos:{ + videos: { list, }, actions: { diff --git a/src/pages/websites/account/index.tsx b/src/pages/websites/account/index.tsx index c015a97..64466eb 100644 --- a/src/pages/websites/account/index.tsx +++ b/src/pages/websites/account/index.tsx @@ -1,6 +1,6 @@ import { useTranslation } from '../../../i18n.ts' import { Button, Form, Space, Tooltip, Badge, Divider } from 'antd' -import { useAtom, useAtomValue } from 'jotai' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { deleteWebsiteDnsAccountAtom, saveOrUpdateWebsiteDnsAccountAtom, websiteDnsAccountAtom, websiteDnsAccountsAtom, websiteDnsAccountSearchAtom, @@ -15,7 +15,7 @@ import { import ListPageLayout from '@/layout/ListPageLayout.tsx' import { useStyle } from './style.ts' import { FilterOutlined } from '@ant-design/icons' -import { getValueCount } from '@/utils' +import { getValueCount, unSetColumnRules } from '@/utils' import { Table as ProTable } from '@/components/table' import { DNSTypeEnum, DNSTypes, syncDNSAtom } from '@/store/websites/dns.ts' import { WebSite } from '@/types' @@ -162,7 +162,7 @@ const WebsiteDnsAccount = () => { const [ filterForm ] = Form.useForm() const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateWebsiteDnsAccountAtom) const [ search, setSearch ] = useAtom(websiteDnsAccountSearchAtom) - const [ setWebsiteDnsAccount ] = useAtom(websiteDnsAccountAtom) + const setWebsiteDnsAccount = useSetAtom(websiteDnsAccountAtom) const { data, isFetching, isLoading, refetch } = useAtomValue(websiteDnsAccountsAtom) const { mutate: deleteWebsiteDnsAccount, isPending: isDeleting } = useAtomValue(deleteWebsiteDnsAccountAtom) const { mutate: asyncDNS, isPending: isAsyncing } = useAtomValue(syncDNSAtom) @@ -283,12 +283,12 @@ const WebsiteDnsAccount = () => { ] } ] as ProColumns[] - }, [ isAsyncing, isDeleting, form, asyncDNS, deleteWebsiteDnsAccount]) + }, [ isAsyncing, isDeleting, form, asyncDNS, deleteWebsiteDnsAccount ]) useEffect(() => { setSearchKey(search?.title) filterForm.setFieldsValue(search) - }, [ search]) + }, [ search ]) useEffect(() => { if (isSuccess) { @@ -353,7 +353,7 @@ const WebsiteDnsAccount = () => { // 'ant-table-row-selected': currentWebsiteDnsAccount?.id === record.id }), onClick: () => { - setWebsiteDnsAccount(record) + setWebsiteDnsAccount(record as any) } } }} @@ -473,7 +473,7 @@ const WebsiteDnsAccount = () => { setSearch(values) }} - columns={columns.filter(item => !item.hideInSearch) as ProFormColumnsType[]}/> + columns={unSetColumnRules(columns.filter(item => !item.hideInSearch) as ProFormColumnsType[])}/> ) diff --git a/src/pages/websites/dns/DNSList.tsx b/src/pages/websites/dns/DNSList.tsx index 3830888..d1449a4 100644 --- a/src/pages/websites/dns/DNSList.tsx +++ b/src/pages/websites/dns/DNSList.tsx @@ -13,6 +13,7 @@ import { saveOrUpdateDNSAtom, syncDNSAtom } from '@/store/websites/dns.ts' import { WebSite } from '@/types' +import { unSetColumnRules } from '@/utils' const getKeyColumn = (type: string, t) => { const columns: ProColumns[] = [] @@ -309,7 +310,7 @@ const DNSList = () => { // console.log('values', values) saveOrUpdate(values) }} - columns={columns as ProFormColumnsType[]}/> + columns={unSetColumnRules(columns as ProFormColumnsType[])}/> ) } diff --git a/src/pages/websites/domain/index.tsx b/src/pages/websites/domain/index.tsx index be7e10a..ee429eb 100644 --- a/src/pages/websites/domain/index.tsx +++ b/src/pages/websites/domain/index.tsx @@ -15,7 +15,7 @@ import { import ListPageLayout from '@/layout/ListPageLayout.tsx' import { useStyle } from './style' import { FilterOutlined } from '@ant-design/icons' -import { getValueCount } from '@/utils' +import { getValueCount, unSetColumnRules } from '@/utils' import { Table as ProTable } from '@/components/table' import { Link } from '@tanstack/react-router' import Popconfirm from '@/components/popconfirm' @@ -78,7 +78,7 @@ const WebsiteDomain = () => { }} style={{ paddingBlockStart: 0 }} type={'EditTwo'} size={14}/> return - {record.name} + {record.name} {edit} } @@ -347,7 +347,7 @@ const WebsiteDomain = () => { setSearch(values) }} - columns={columns.filter(item => !item.hideInSearch) as ProFormColumnsType[]}/> + columns={unSetColumnRules(columns.filter(item => !item.hideInSearch) as ProFormColumnsType[])}/> { dialog } diff --git a/src/pages/websites/record/index.tsx b/src/pages/websites/record/index.tsx index c7b44e9..6138e13 100644 --- a/src/pages/websites/record/index.tsx +++ b/src/pages/websites/record/index.tsx @@ -1,9 +1,14 @@ import { useTranslation } from '@/i18n.ts' -import { Button, Form, Divider, Space, Tooltip, Badge } from 'antd' -import { useAtom, useAtomValue } from 'jotai' +import { Button, Form, Divider, Space, Tooltip, Badge, Input, Popover } from 'antd' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { deleteWebsiteDnsRecordsAtom, - saveOrUpdateWebsiteDnsRecordsAtom, websiteDnsRecordsAtom, websiteDnsRecordssAtom, websiteDnsRecordsSearchAtom, + explainTypes, + saveOrUpdateWebsiteDnsRecordsAtom, ttlOptions, + websiteDnsDomainIdAtom, + websiteDnsRecordsAtom, + websiteDnsRecordssAtom, + websiteDnsRecordsSearchAtom, } from '@/store/websites/record' import { useEffect, useMemo, useState } from 'react' import Action from '@/components/action/Action.tsx' @@ -14,19 +19,24 @@ import { } from '@ant-design/pro-components' import ListPageLayout from '@/layout/ListPageLayout.tsx' import { useStyle } from './style' -import { FilterOutlined } from '@ant-design/icons' -import { getValueCount } from '@/utils' +import { FilterOutlined, QuestionCircleOutlined } from '@ant-design/icons' +import { getValueCount, unSetColumnRules } from '@/utils' import { Table as ProTable } from '@/components/table' import Popconfirm from '@/components/popconfirm' +import { createFileRoute } from '@tanstack/react-router' +import { websiteDomainsAtom } from '@/store/websites/domain.ts' +import Switch from '@/components/switch' -const i18nPrefix = 'websiteDnsRecordss.list' +const i18nPrefix = 'websites.record' const WebsiteDnsRecords = () => { - + const { id } = Route.useSearch() const { styles, cx } = useStyle() const { t } = useTranslation() const [ form ] = Form.useForm() const [ filterForm ] = Form.useForm() + const setDomainId = useSetAtom(websiteDnsDomainIdAtom) + const { data: domainList } = useAtomValue(websiteDomainsAtom) const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateWebsiteDnsRecordsAtom) const [ search, setSearch ] = useAtom(websiteDnsRecordsSearchAtom) const [ currentWebsiteDnsRecords, setWebsiteDnsRecords ] = useAtom(websiteDnsRecordsAtom) @@ -37,7 +47,16 @@ const WebsiteDnsRecords = () => { const [ openFilter, setFilterOpen ] = useState(false) const [ searchKey, setSearchKey ] = useState(search?.title) + const currentDomain = domainList?.rows?.find?.(item => item.id === id) + + useEffect(() => { + if (id) { + setDomainId(id) + } + }, [ id ]) + const columns = useMemo(() => { + return [ { title: 'ID', @@ -49,63 +68,142 @@ const WebsiteDnsRecords = () => { { title: t(`${i18nPrefix}.columns.record_id`, 'record_id'), dataIndex: 'record_id', + hideInForm: true, + hideInSearch: true, + hideInTable: true, + hideInSetting: true, }, { title: t(`${i18nPrefix}.columns.domain_id`, 'domain_id'), dataIndex: 'domain_id', + hideInForm: true, + hideInSearch: true, + hideInTable: true, + hideInSetting: true, }, - { - title: t(`${i18nPrefix}.columns.name`, 'name'), - dataIndex: 'name', + title: t(`${i18nPrefix}.columns.type`, '记录类型'), + tooltip: '指解析记录的用途,例如:网站、邮箱', + dataIndex: 'type', + valueType: 'select', + width: 100, + fieldProps: { + style: { + width: '100%' + }, + options: explainTypes.map(item => { + return { + value: item.value, + label: <>{item.label} - {t(`websites.record.explain.${item.value}`)} + } + }), + }, + render(_dom, record) { + return record.type + } }, - { - title: t(`${i18nPrefix}.columns.content`, 'content'), - dataIndex: 'content', - }, + title: t(`${i18nPrefix}.columns.name`, '主机记录'), + dataIndex: 'name', + tooltip: '指域名前缀,例如:www', + formItemProps: { + rules: [ + { required: true, } + ] + }, + renderFormItem: (_schema, config) => { + return
+ + .{currentDomain?.name} + +主机记录就是域名前缀,常见用法有:
+www:解析后的域名为www.{currentDomain?.name}。
+@:直接解析主域名 {currentDomain?.name}。
+*:泛解析,匹配其他所有域名 *.{currentDomain?.name}。
+mail:将域名解析为mail.{currentDomain?.name},通常用于解析邮箱服务器。
+二级域名:如:abc.{currentDomain?.name},填写abc。
+手机网站:如:m.{currentDomain?.name},填写m。
+显性URL:不支持泛解析(泛解析:将所有子域名解析到同一地址) + }> + +
+ + }/> +
+ } + }, { - title: t(`${i18nPrefix}.columns.poxy`, 'poxy'), - dataIndex: 'poxy', + title: t(`${i18nPrefix}.columns.content`, '记录值'), + tooltip: '一般是填写服务器的IP地址', + dataIndex: 'content', + dependencies: [ 'type' ], + renderFormItem: (_schema, config, _form1) => { + const type = _form1.getFieldValue('type') + const help = t(`${i18nPrefix}.help.${type}`, '', { domain: currentDomain?.name }) + return + + + } }, - { - title: t(`${i18nPrefix}.columns.ttl`, 'ttl'), + title: t(`${i18nPrefix}.columns.ttl`, 'TTL'), + tooltip: '指解析结果在Local DNS中的缓存时间', dataIndex: 'ttl', + valueType: 'select', + fieldProps: { + options: ttlOptions + } }, - { - title: t(`${i18nPrefix}.columns.type`, 'type'), - dataIndex: 'type', + title: t(`${i18nPrefix}.columns.poxy`, 'Proxy'), + dataIndex: 'poxy', + valueType: 'switch', + colProps: { + span: 8 + }, + render(_dom, record){ + return + } }, - { - title: t(`${i18nPrefix}.columns.status`, 'status'), + title: t(`${i18nPrefix}.columns.status`, '状态'), dataIndex: 'status', + tooltip: '解析记录在云解析DNS中的启用情况', + valueType: 'switch', + colProps: { + span: 8 + }, + render(_dom, record){ + return + } }, { - title: t(`${i18nPrefix}.columns.tag`, 'tag'), + title: t(`${i18nPrefix}.columns.tag`, '标签'), dataIndex: 'tag', }, { - title: t(`${i18nPrefix}.columns.remark`, 'remark'), + title: t(`${i18nPrefix}.columns.remark`, '备注'), dataIndex: 'remark', }, { - title: t(`${i18nPrefix}.columns.created`, 'created'), + title: t(`${i18nPrefix}.columns.created`, '创建时间'), dataIndex: 'created', + hideInSearch: true, + hideInForm: true, }, - - { - title: t(`${i18nPrefix}.columns.modified`, 'modified'), - dataIndex: 'modified', - }, - { title: t(`${i18nPrefix}.columns.option`, '操作'), key: 'option', @@ -131,7 +229,7 @@ const WebsiteDnsRecords = () => { ] } ] as ProColumns[] - }, [ isDeleting, currentWebsiteDnsRecords, search ]) + }, [ isDeleting, currentWebsiteDnsRecords, search, currentDomain ]) useEffect(() => { @@ -147,21 +245,22 @@ const WebsiteDnsRecords = () => { }, [ isSuccess ]) return ( - + - - + + + } toolbar={{ search: { @@ -313,9 +412,25 @@ const WebsiteDnsRecords = () => { }) setSearch(values) }} - columns={columns.filter(item => !item.hideInSearch) as ProFormColumnsType[]}/> + columns={unSetColumnRules(columns.filter(item => !item.hideInSearch) as ProFormColumnsType[])}/> ) } +type RecordSearch = { + id: number +} + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore fix path +export const Route = createFileRoute('/websites/record')({ + validateSearch: (search: Record): RecordSearch => { + // validate and parse the search params into a typed state + // console.log(search.id) + return { + id: (search.id ?? 0) + } as RecordSearch + }, +}) + export default WebsiteDnsRecords \ No newline at end of file diff --git a/src/routes.tsx b/src/routes.tsx index 50d7955..0d46e76 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -247,7 +247,15 @@ const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => { return NotFound } - return module() + return module().then((d: any) => { + // console.log(d) + if (d.Route) { + d.Route.update({ + path: menu.path, + }) + } + return d + }) }), notFoundComponent: NotFound, }) diff --git a/src/store/websites/record.ts b/src/store/websites/record.ts index 488fbb5..0fafb50 100644 --- a/src/store/websites/record.ts +++ b/src/store/websites/record.ts @@ -6,34 +6,103 @@ import { t } from 'i18next' import websitesServ from '@/service/websites' import { IWebsiteDnsRecords } from '@/types/website/record' +const i18nPrifx = 'websites.record' + type SearchParams = IPage & { key?: string - [key: string]: any } +export const explainTypes = [ + { + label: 'A', + value: 'A' + }, { + label: 'CNAME', + value: 'CNAME' + }, { + label: 'AAAA', + value: 'AAAA' + }, { + label: 'NS', + value: 'NS' + }, { + label: 'MX', + value: 'MX' + }, { + label: 'SRV', + value: 'SRV' + }, { + label: 'TXT', + value: 'TXT' + }, { + label: 'CAA', + value: 'CAA' + }, { + label: 'PTR', + value: 'PTR' + }, { + label: t('websites.record.redirect_url'), + value: 'REDIRECT_URL' + }, { + label: t('websites.record.forward_url'), + value: 'FORWARD_URL' + } ] + +export const ttlOptions = [ + { + label: t(`${i18nPrifx}.ttl.1`, '自动'), + value: 1 + }, + { + label: t(`${i18nPrifx}.ttl.10`, '10分钟'), + value: 10 + }, + { + label: t(`${i18nPrifx}.ttl.30`, '30分钟'), + value: 30 + }, + { + label: t(`${i18nPrifx}.ttl.60`, '1小时'), + value: 60 + }, + { + label: t(`${i18nPrifx}.ttl.60`, '12小时'), + value: 60 * 12 + }, + { + label: t(`${i18nPrifx}.ttl.60`, '1天'), + value: 60 * 24 + }, +] + export const websiteDnsRecordsIdAtom = atom(0) export const websiteDnsRecordsIdsAtom = atom([]) -export const websiteDnsRecordsAtom = atom(undefined as unknown as IWebsiteDnsRecords ) +export const websiteDnsRecordsAtom = atom(undefined as unknown as IWebsiteDnsRecords) export const websiteDnsRecordsSearchAtom = atom({ - key: '', + // key: '', pageSize: 10, page: 1, } as SearchParams) -export const websiteDnsRecordsPageAtom = atom({ - pageSize: 10, - page: 1, -}) +export const websiteDnsDomainIdAtom = atom(0) export const websiteDnsRecordssAtom = atomWithQuery((get) => { return { - queryKey: [ 'websiteDnsRecordss', get(websiteDnsRecordsSearchAtom) ], - queryFn: async ({ queryKey: [ , params ] }) => { - return await websitesServ.record.list(params as SearchParams) + queryKey: [ 'websiteDnsRecordss', get(websiteDnsRecordsSearchAtom), get(websiteDnsDomainIdAtom) ], + queryFn: async ({ queryKey: [ , params, id ] }) => { + if (!id) { + return { + data: { + rows: [], + total: 0 + } + } + } + return await websitesServ.record.list({ ...(params as any), domain_id: id }) }, select: res => { const data = res.data diff --git a/src/utils/index.ts b/src/utils/index.ts index 586df62..dba14ca 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -172,4 +172,14 @@ export const getValueCount = (obj: any, filterObj: any = {}) => { } } return count +} + +export const unSetColumnRules = (columns: any[]) => { + + return columns.map(col => { + if (col.formItemProps?.rules?.length) { + col.formItemProps.rules = [] + } + return col + }) } \ No newline at end of file