dark
7 months ago
11 changed files with 414 additions and 12 deletions
-
24src/layout/ListPageLayout.tsx
-
242src/pages/websites/ssl/index.tsx
-
13src/pages/websites/ssl/style.ts
-
4src/routes.tsx
-
9src/service/websites.ts
-
0src/store/websites/ca.ts
-
10src/store/websites/dns.ts
-
64src/store/websites/ssl.ts
-
11src/types/ca.ts
-
10src/types/dns.ts
-
39src/types/ssl.d.ts
@ -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, |
|
||||
}) |
|
@ -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) |
@ -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), |
||||
|
} |
||||
|
}) |
@ -0,0 +1,9 @@ |
|||||
|
import { createCURD } from '@/service/base.ts' |
||||
|
|
||||
|
const websitesServ = { |
||||
|
ssl: { |
||||
|
...createCURD<any, ISsl>('/website/ssl') |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default websitesServ |
@ -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; |
||||
|
} |
@ -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() |
||||
|
} |
||||
|
})) |
@ -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; |
||||
|
} |
@ -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; |
||||
|
} |
@ -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 |
||||
|
} |
||||
|
|
||||
|
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue