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 { 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<IListPageLayoutProps> = (props) => { |
|||
const ListPageLayout: React.FC<IListPageLayoutProps> = ({ children, ...props }) => { |
|||
const { styles } = useStyle({ className: 'two-col' }) |
|||
|
|||
|
|||
return ( |
|||
<>{props.children} |
|||
<Outlet/> |
|||
<> |
|||
<PageContainer |
|||
breadcrumbRender={false} title={false} className={styles.container} |
|||
{...props} |
|||
> |
|||
{children} |
|||
</PageContainer> |
|||
</> |
|||
) |
|||
} |
|||
|
|||
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