Browse Source

添加证书页面

main
dark 7 months ago
parent
commit
e2bd578dce
  1. 24
      src/layout/ListPageLayout.tsx
  2. 242
      src/pages/websites/ssl/index.tsx
  3. 13
      src/pages/websites/ssl/style.ts
  4. 4
      src/routes.tsx
  5. 9
      src/service/websites.ts
  6. 0
      src/store/websites/ca.ts
  7. 10
      src/store/websites/dns.ts
  8. 64
      src/store/websites/ssl.ts
  9. 11
      src/types/ca.ts
  10. 10
      src/types/dns.ts
  11. 39
      src/types/ssl.d.ts

24
src/layout/ListPageLayout.tsx

@ -1,21 +1,25 @@
import React from 'react' 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 children: React.ReactNode
} }
const ListPageLayout: React.FC<IListPageLayoutProps> = (props) => {
const ListPageLayout: React.FC<IListPageLayoutProps> = ({ children, ...props }) => {
const { styles } = useStyle({ className: 'two-col' })
return ( return (
<>{props.children}
<Outlet/>
<>
<PageContainer
breadcrumbRender={false} title={false} className={styles.container}
{...props}
>
{children}
</PageContainer>
</> </>
) )
} }
export default ListPageLayout export default ListPageLayout
export const GenRoute = (id: string) => createLazyRoute(id)({
component: ListPageLayout,
})

242
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<ProColumns<ISSL>[]>(() => {
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<br/><span style="color:red;">HTTP 模式无法申请泛域名证书</span>', { v: val }),
}
return {
label: t('website.ssl.form.provider', '验证方式'),
help: <span dangerouslySetInnerHTML={{__html: help[val]}} />,
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) => [
<a key="editable"
onClick={() => {
}}
>
{t('actions.edit', '编辑')}
</a>,
<Popconfirm
key={'del_confirm'}
// disabled={isPending}
onConfirm={() => {
// deleteUser([ record.id ])
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
,
],
},
]
}, [])
return (
<ListPageLayout>
<ProTable<ISSL>
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: [
<Button
key="button"
icon={<PlusOutlined/>}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
})
setOpen(true)
}}
type="primary"
>
{t('actions.add', '添加')}
</Button>,
]
}}
pagination={{
pageSize: page?.pageSize ?? 10,
total: data?.total ?? 0,
current: page?.page ?? 1,
onChange: (page, pageSize) => {
setPage(prev => ({
...prev,
page,
pageSize,
}))
},
}}
>
</ProTable>
<BetaSchemaForm<ISSL>
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[]}/>
</ListPageLayout>
)
}
export default memo(SSL)

13
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),
}
})

4
src/routes.tsx

@ -21,7 +21,7 @@ import {
redirect, redirect,
RouterProvider, RouterProvider,
} from '@tanstack/react-router' } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { memo, useEffect, useRef } from 'react' import { memo, useEffect, useRef } from 'react'
import RootLayout from './layout/RootLayout' import RootLayout from './layout/RootLayout'
import { IRootContext, MenuItem } from './global' import { IRootContext, MenuItem } from './global'
@ -51,7 +51,7 @@ const rootRoute = createRootRouteWithContext<IRootContext>()({
<FetchLoading/> <FetchLoading/>
<Outlet/> <Outlet/>
<DevTools/> <DevTools/>
<TanStackRouterDevtools position={'bottom-right'}/>
{/*<TanStackRouterDevtools position={'bottom-right'}/>*/}
</> </>
), ),
beforeLoad: ({ location }) => { beforeLoad: ({ location }) => {

9
src/service/websites.ts

@ -0,0 +1,9 @@
import { createCURD } from '@/service/base.ts'
const websitesServ = {
ssl: {
...createCURD<any, ISsl>('/website/ssl')
}
}
export default websitesServ

0
src/store/websites/ca.ts

10
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;
}

64
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<IPage>({
page: 1,
pageSize: 20,
})
export const sslSearchAtom = atom<SSLSearchParam>({})
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<IApiResult, ISSL>(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<IApiResult, number>(get => ({
mutationKey: [ 'sslDelete' ],
mutationFn: async (id) => {
return await websitesServ.ssl.delete(id)
},
onSuccess: () => {
message.success(t('message.deleteSuccess', '删除成功'))
get(sslListAtom).refetch()
}
}))

11
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;
}

10
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;
}

39
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
}
Loading…
Cancel
Save