From e2bd578dce9d92ee0424a4e63286370225409a16 Mon Sep 17 00:00:00 2001 From: dark Date: Fri, 3 May 2024 01:00:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AF=81=E4=B9=A6=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layout/ListPageLayout.tsx | 24 ++-- src/pages/websites/ssl/index.tsx | 242 +++++++++++++++++++++++++++++++++++++++ src/pages/websites/ssl/style.ts | 13 +++ src/routes.tsx | 4 +- src/service/websites.ts | 9 ++ src/store/websites/ca.ts | 0 src/store/websites/dns.ts | 10 ++ src/store/websites/ssl.ts | 64 +++++++++++ src/types/ca.ts | 11 ++ src/types/dns.ts | 10 ++ src/types/ssl.d.ts | 39 +++++++ 11 files changed, 414 insertions(+), 12 deletions(-) create mode 100644 src/pages/websites/ssl/index.tsx create mode 100644 src/pages/websites/ssl/style.ts create mode 100644 src/service/websites.ts create mode 100644 src/store/websites/ca.ts create mode 100644 src/store/websites/dns.ts create mode 100644 src/store/websites/ssl.ts create mode 100644 src/types/ca.ts create mode 100644 src/types/dns.ts create mode 100644 src/types/ssl.d.ts diff --git a/src/layout/ListPageLayout.tsx b/src/layout/ListPageLayout.tsx index b256269..291f839 100644 --- a/src/layout/ListPageLayout.tsx +++ b/src/layout/ListPageLayout.tsx @@ -1,21 +1,25 @@ import React from 'react' -import { createLazyRoute, Outlet } from '@tanstack/react-router' +import { useStyle } from '@/layout/style.ts' +import { PageContainer, PageContainerProps } from '@ant-design/pro-components' -interface IListPageLayoutProps { +interface IListPageLayoutProps extends PageContainerProps { children: React.ReactNode - } -const ListPageLayout: React.FC = (props) => { +const ListPageLayout: React.FC = ({ children, ...props }) => { + const { styles } = useStyle({ className: 'two-col' }) + + return ( - <>{props.children} - + <> + + {children} + ) } export default ListPageLayout - -export const GenRoute = (id: string) => createLazyRoute(id)({ - component: ListPageLayout, -}) diff --git a/src/pages/websites/ssl/index.tsx b/src/pages/websites/ssl/index.tsx new file mode 100644 index 0000000..cf2a28c --- /dev/null +++ b/src/pages/websites/ssl/index.tsx @@ -0,0 +1,242 @@ +import { useAtom, useAtomValue } from 'jotai' +import { ProviderTypeEnum, saveOrUpdateSslAtom, sslListAtom, sslPageAtom, sslSearchAtom } from '@/store/websites/ssl.ts' +import ListPageLayout from '@/layout/ListPageLayout.tsx' +import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components' +import { memo, useMemo, useState } from 'react' +import { useTranslation } from '@/i18n.ts' +import { Button, Form, Popconfirm } from 'antd' +import { PlusOutlined } from '@ant-design/icons' +import { ISSL } from '@/types/ssl' + + +const SSL = () => { + + const { t } = useTranslation() + const [ form ] = Form.useForm() + const [ page, setPage ] = useAtom(sslPageAtom) + const [ search, setSearch ] = useAtom(sslSearchAtom) + const { data, isLoading, isFetching, refetch } = useAtomValue(sslListAtom) + const { mutate: saveOrUpdate, isPending: isSubmitting } = useAtomValue(saveOrUpdateSslAtom) + + const [ open, setOpen ] = useState(false) + + const columns = useMemo[]>(() => { + return [ + { + title: 'ID', + dataIndex: 'id', + hideInTable: true, + hideInSearch: false, + formItemProps: { + hidden: true, + } + }, + { + title: t('website.ssl.columns.primaryDomain', '域名'), + dataIndex: 'primaryDomain', + formItemProps:{ + label: t('website.ssl.form.primaryDomain', '主域名'), + rules: [ { required: true, message: t('message.required', '主域名') } ] + } + }, + { + title: t('website.ssl.columns.otherDomains', '其它域名'), + dataIndex: 'otherDomains', + }, + { + title: t('website.ssl.columns.acmeAccountId', 'Acme帐号'), + dataIndex: 'acmeAccountId', + }, + { + title: t('website.ssl.columns.provider', '申请方式'), + dataIndex: 'provider', + valueType: 'radio', + initialValue: ProviderTypeEnum.DnsAccount, + valueEnum: { + [ProviderTypeEnum.DnsAccount]: { + text: t('website.ssl.providerTypeEnum.DnsAccount', 'DNS帐号'), + }, + [ProviderTypeEnum.DnsManual]: { + text: t('website.ssl.providerTypeEnum.DnsManual', '手动验证'), + }, + [ProviderTypeEnum.Http]: { + text: t('website.ssl.providerTypeEnum.Http', 'HTTP'), + } + }, + dependencies : [ 'provider' ], + formItemProps: (form, config)=> { + const val = form.getFieldValue(config.dataIndex) + const help = { + [ProviderTypeEnum.DnsAccount]: t('website.ssl.form.provider_{{v}}', '', { v: val }), + [ProviderTypeEnum.DnsManual]: t('website.ssl.form.provider_{{v}}', '手动解析模式需要在创建完之后点击申请按钮获取 DNS 解析值', { v: val }), + [ProviderTypeEnum.Http]: t('website.ssl.form.provider_{{v}}', 'HTTP 模式需要安装 OpenResty
HTTP 模式无法申请泛域名证书', { v: val }), + } + return { + label: t('website.ssl.form.provider', '验证方式'), + help: , + rules: [ { required: true, message: t('message.required', '请选择') } ] + } + }, + }, + { + name: [ 'provider' ], + valueType: 'dependency', + columns: ({ provider }) => { + if (provider === ProviderTypeEnum.DnsAccount) { + return [ { + title: t('website.ssl.columns.dnsAccountId', 'DNS帐号'), + dataIndex: 'dnsAccountId', + } ] + } + return [] + + } + }, + { + title: t('website.ssl.columns.autoRenew', '自动续签'), + dataIndex: 'autoRenew', + valueType: 'switch', + + }, + { + title: t('website.ssl.columns.pushDir', '推送证书到本地目录'), + dataIndex: 'pushDir', + valueType: 'switch', + hideInTable: true, + hideInSearch: true, + + }, + { + name: [ 'pushDir' ], + valueType: 'dependency', + columns: ({ pushDir }) => { + if (pushDir) { + return [ { + title: t('website.ssl.columns.dir', '目录'), + dataIndex: 'dir', + formItemProps: { + help: t('website.ssl.form.dir_help', '会在此目录下生成两个文件,证书文件:fullchain.pem 密钥文件:privkey.pem'), + rules: [ { required: true, message: t('message.required', '请输入目录') } ] + } + } ] + } + return [] + + } + }, + { + title: t('website.ssl.columns.description', '备注'), + dataIndex: 'description', + }, + { + title: t('website.ssl.columns.option', '操作'), valueType: 'option', + key: 'option', + render: (_, record) => [ + { + + }} + > + {t('actions.edit', '编辑')} + , + { + // deleteUser([ record.id ]) + }} + title={t('message.deleteConfirm')}> + + {t('actions.delete', '删除')} + + + , + ], + }, + ] + }, []) + + return ( + + + headerTitle={t('website.ssl.title', '证书列表')} + search={false} + loading={isLoading || isFetching} + rowKey={'id'} + dataSource={data?.rows ?? []} + columns={columns} + options={{ + reload: () => { + refetch() + }, + }} + toolbar={{ + search: { + loading: isFetching && !!search.key, + onSearch: (value: string) => { + setSearch({ key: value }) + }, + placeholder: t('website.ssl.search.placeholder', '输入域名') + }, + actions: [ + , + ] + }} + pagination={{ + pageSize: page?.pageSize ?? 10, + total: data?.total ?? 0, + current: page?.page ?? 1, + onChange: (page, pageSize) => { + setPage(prev => ({ + ...prev, + page, + pageSize, + })) + }, + }} + > + + + + shouldUpdate={false} + width={600} + form={form} + layout={'vertical'} + scrollToFirstError={true} + title={t(`website.ssl.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '证书编辑' : '证书添加')} + // colProps={{ span: 24 }} + labelCol={{ span: 12 }} + wrapperCol={{ span: 24 }} + layoutType={'DrawerForm'} + open={open} + drawerProps={{ + maskClosable: false, + }} + onOpenChange={(open) => { + setOpen(open) + }} + loading={isSubmitting} + onFinish={async (values) => { + // console.log('values', values) + saveOrUpdate(values) + return true + }} + columns={columns as ProFormColumnsType[]}/> + + ) +} + +export default memo(SSL) \ No newline at end of file diff --git a/src/pages/websites/ssl/style.ts b/src/pages/websites/ssl/style.ts new file mode 100644 index 0000000..b0c5061 --- /dev/null +++ b/src/pages/websites/ssl/style.ts @@ -0,0 +1,13 @@ +import { createStyles } from '@/theme' + +export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { + const prefix = `${prefixCls}-${token?.proPrefix}-ssl-page` + + const container = css` + + ` + + return { + container: cx(prefix, props?.className, container), + } +}) \ No newline at end of file diff --git a/src/routes.tsx b/src/routes.tsx index ff0735c..87c7f46 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -21,7 +21,7 @@ import { redirect, RouterProvider, } from '@tanstack/react-router' -import { TanStackRouterDevtools } from '@tanstack/router-devtools' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' import { memo, useEffect, useRef } from 'react' import RootLayout from './layout/RootLayout' import { IRootContext, MenuItem } from './global' @@ -51,7 +51,7 @@ const rootRoute = createRootRouteWithContext()({ - + {/**/} ), beforeLoad: ({ location }) => { diff --git a/src/service/websites.ts b/src/service/websites.ts new file mode 100644 index 0000000..0458552 --- /dev/null +++ b/src/service/websites.ts @@ -0,0 +1,9 @@ +import { createCURD } from '@/service/base.ts' + +const websitesServ = { + ssl: { + ...createCURD('/website/ssl') + } +} + +export default websitesServ \ No newline at end of file diff --git a/src/store/websites/ca.ts b/src/store/websites/ca.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/store/websites/dns.ts b/src/store/websites/dns.ts new file mode 100644 index 0000000..36a8c9d --- /dev/null +++ b/src/store/websites/dns.ts @@ -0,0 +1,10 @@ +export interface IDnsAccount { + id: number; + createdAt: Date | null; + createdBy: number; + updatedAt: Date | null; + updatedBy: number; + name: string; + type: string; + authorization: string; +} \ No newline at end of file diff --git a/src/store/websites/ssl.ts b/src/store/websites/ssl.ts new file mode 100644 index 0000000..6cc0770 --- /dev/null +++ b/src/store/websites/ssl.ts @@ -0,0 +1,64 @@ +import { atom } from 'jotai' +import { IApiResult, IPage } from '@/global' +import { atomWithMutation, atomWithQuery } from 'jotai-tanstack-query' +import websitesServ from '@/service/websites.ts' +import { message } from 'antd' +import { t } from 'i18next' +import { ISSL, SSLSearchParam } from '@/types/ssl' + +export enum ProviderTypeEnum { + DnsAccount = 'dnsAccount', + DnsManual = 'dnsManual', + Http = 'http' +} + +export const sslPageAtom = atom({ + page: 1, + pageSize: 20, +}) + +export const sslSearchAtom = atom({}) + +export const sslListAtom = atomWithQuery(get => ({ + queryKey: [ 'sslList', get(sslPageAtom), get(sslSearchAtom) ], + queryFn: async ({ queryKey: [ , page, search ] }) => { + return await websitesServ.ssl.list({ + ...page as any, + ...search as any, + }) + }, + select: (data) => data.data + +})) + +//saveOrUpdate +export const saveOrUpdateSslAtom = atomWithMutation(get => ({ + mutationKey: [ 'sslSaveOrUpdate' ], + mutationFn: async (data: ISSL) => { + const isAdd = data.id === 0 + if (isAdd) { + return await websitesServ.ssl.add(data) + } else { + return await websitesServ.ssl.update(data) + } + }, + onSuccess: (res) => { + const isAdd = !!res.data?.id + message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) + + get(sslListAtom).refetch() + return res + }, +})) + +//delete +export const deleteSslAtom = atomWithMutation(get => ({ + mutationKey: [ 'sslDelete' ], + mutationFn: async (id) => { + return await websitesServ.ssl.delete(id) + }, + onSuccess: () => { + message.success(t('message.deleteSuccess', '删除成功')) + get(sslListAtom).refetch() + } +})) \ No newline at end of file diff --git a/src/types/ca.ts b/src/types/ca.ts new file mode 100644 index 0000000..de2d5ed --- /dev/null +++ b/src/types/ca.ts @@ -0,0 +1,11 @@ +export interface ICA { + id: number; + createdAt: Date | null; + createdBy: number; + updatedAt: Date | null; + updatedBy: number; + csr: string; + name: string; + privateKey: string; + keyType: string; +} \ No newline at end of file diff --git a/src/types/dns.ts b/src/types/dns.ts new file mode 100644 index 0000000..36a8c9d --- /dev/null +++ b/src/types/dns.ts @@ -0,0 +1,10 @@ +export interface IDnsAccount { + id: number; + createdAt: Date | null; + createdBy: number; + updatedAt: Date | null; + updatedBy: number; + name: string; + type: string; + authorization: string; +} \ No newline at end of file diff --git a/src/types/ssl.d.ts b/src/types/ssl.d.ts new file mode 100644 index 0000000..17f674a --- /dev/null +++ b/src/types/ssl.d.ts @@ -0,0 +1,39 @@ +export interface ISSL { + id: number; + createdAt: Date | null; + createdBy: number; + updatedAt: Date | null; + updatedBy: number; + primaryDomain: string; + privateKey: string; + pem: string; + domains: string; + certUrl: string; + type: string; + provider: string; + organization: string; + dnsAccountId: number; + acmeAccountId: number; + caId: number; + autoRenew: boolean; + expireDate: Date | null; + startDate: Date | null; + status: string; + message: string; + keyType: string; + pushDir: boolean; + dir: string; + description: string; +} + + +export type ProviderType = 'dnsAccount' | 'dnsManual' | 'http' + + +export type SSLSearchParam = { + key?: string + order?: string + prop?: string +} + +