dark
6 months ago
18 changed files with 435 additions and 84 deletions
-
38src/components/drawer-picker/DrawerPicker.tsx
-
1src/components/drawer-picker/index.ts
-
15src/components/drawer-picker/style.ts
-
11src/pages/system/roles/index.tsx
-
4src/pages/system/users/index.tsx
-
174src/pages/websites/ssl/components/AcmeList.tsx
-
67src/pages/websites/ssl/index.tsx
-
3src/request.ts
-
7src/service/websites.ts
-
49src/store/websites/acme.ts
-
16src/store/websites/ssl.ts
-
14src/types/website/acme.d.ts
-
4src/types/website/ca.d.ts
-
4src/types/website/dns.d.ts
-
4src/types/website/ssl.d.ts
@ -0,0 +1,38 @@ |
|||
import { Button, Drawer, DrawerProps } from 'antd' |
|||
import React, { useState } from 'react' |
|||
import { useStyle } from './style' |
|||
import { generateUUID } from '@/utils/uuid.ts' |
|||
|
|||
export interface DrawerPickerProps extends DrawerProps { |
|||
target?: React.ReactNode |
|||
children?: React.ReactNode |
|||
key?: string |
|||
} |
|||
|
|||
const DrawerPicker = ({ children, target, ...props }: DrawerPickerProps) => { |
|||
|
|||
const { styles } = useStyle() |
|||
|
|||
const [ open, setOpen ] = useState(false) |
|||
|
|||
const getTarget = () => { |
|||
const def = <Button>{props.title ?? 'Target'}</Button> |
|||
return <span className={styles.target}>{target ?? def}</span> |
|||
} |
|||
|
|||
return ( |
|||
<div className={styles.container} key={props.key ?? generateUUID()}> |
|||
<span className={styles.target} onClick={() => { |
|||
setOpen(true) |
|||
}}> |
|||
{getTarget()} |
|||
</span> |
|||
<Drawer {...props} |
|||
open={open} |
|||
onClose={() => setOpen(false)} |
|||
>{children}</Drawer> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default DrawerPicker |
@ -0,0 +1 @@ |
|||
export * from './DrawerPicker.tsx' |
@ -0,0 +1,15 @@ |
|||
import { createStyles } from '@/theme' |
|||
|
|||
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { |
|||
const prefix = `${prefixCls}-${token?.proPrefix}-drawer-picker-page` |
|||
|
|||
const container = css`
|
|||
`
|
|||
|
|||
const target = css`` |
|||
|
|||
return { |
|||
container: cx(prefix, props?.className, container), |
|||
target, |
|||
} |
|||
}) |
@ -0,0 +1,174 @@ |
|||
import { useMemo, useState } from 'react' |
|||
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components' |
|||
import { IAcmeAccount } from '@/types/website/acme' |
|||
import { useTranslation } from '@/i18n.ts' |
|||
import { acmeListAtom, acmePageAtom, AcmeType, saveOrUpdateAcmeAtom } from '@/store/websites/acme.ts' |
|||
import { useAtom, useAtomValue } from 'jotai' |
|||
import { Alert, Button, Form } from 'antd' |
|||
import { KeyTypeEnum } from '@/store/websites/ssl.ts' |
|||
|
|||
const AcmeList = () => { |
|||
|
|||
const { t } = useTranslation() |
|||
const [ form ] = Form.useForm() |
|||
const [ page, setPage ] = useAtom(acmePageAtom) |
|||
const { data, isLoading, refetch } = useAtomValue(acmeListAtom) |
|||
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateAcmeAtom) |
|||
const [ open, setOpen ] = useState(false) |
|||
|
|||
const columns = useMemo<ProColumns<IAcmeAccount>[]>(() => { |
|||
return [ |
|||
{ |
|||
title: 'ID', |
|||
dataIndex: 'id', |
|||
hideInTable: true, |
|||
formItemProps: { |
|||
hidden: true, |
|||
} |
|||
}, |
|||
{ |
|||
title: t('website.ssl.acme.columns.email', '邮箱'), |
|||
dataIndex: 'email', |
|||
valueType: 'text', |
|||
formItemProps: { |
|||
rules: [ |
|||
{ required: true, message: t('message.required', '请输入') } |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
title: t('website.ssl.acme.columns.type', '帐号类型'), |
|||
dataIndex: 'type', |
|||
valueType: 'select', |
|||
fieldProps: { |
|||
options: [ |
|||
{ label: 'Let\'s Encrypt', value: AcmeType.LetsEncrypt }, |
|||
{ label: 'ZeroSSl', value: AcmeType.ZeroSSl }, |
|||
{ label: 'Buypass', value: AcmeType.Buypass }, |
|||
{ label: 'Google Cloud', value: AcmeType.Google }, |
|||
] |
|||
}, |
|||
formItemProps: { |
|||
rules: [ |
|||
{ required: true, message: t('message.required', '请选择') } |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
title: t('website.ssl.acme.columns.keyType', '密钥算法'), |
|||
dataIndex: 'keyType', |
|||
valueType: 'select', |
|||
initialValue: KeyTypeEnum.EC256, |
|||
fieldProps: { |
|||
options: [ |
|||
{ label: t('website.ssl.keyTypeEnum.EC256', 'EC 256'), value: KeyTypeEnum.EC256 }, |
|||
{ label: t('website.ssl.keyTypeEnum.EC384', 'EC 384'), value: KeyTypeEnum.EC384 }, |
|||
{ label: t('website.ssl.keyTypeEnum.RSA2048', 'RSA 2048'), value: KeyTypeEnum.RSA2048 }, |
|||
{ label: t('website.ssl.keyTypeEnum.RSA3072', 'RSA 3072'), value: KeyTypeEnum.RSA3072 }, |
|||
{ label: t('website.ssl.keyTypeEnum.RSA4096', 'RSA 4096'), value: KeyTypeEnum.RSA4096 }, |
|||
] |
|||
}, |
|||
formItemProps: { |
|||
rules: [ |
|||
{ required: true, message: t('message.required', '请选择') } |
|||
] |
|||
}, |
|||
}, |
|||
{ |
|||
title: t('website.ssl.acme.columns.url', 'URL'), |
|||
dataIndex: 'url', |
|||
valueType: 'text', |
|||
ellipsis: true, // 文本溢出省略
|
|||
hideInForm: true, |
|||
}, { |
|||
title: '操作', |
|||
valueType: 'option', |
|||
render: (_, record) => { |
|||
return [ |
|||
<a key="edit" onClick={() => { |
|||
}}>{t('actions.edit', '编辑')}</a>, |
|||
<a key="delete" onClick={() => { |
|||
}}>{t('actions.delete', '删除')}</a>, |
|||
] |
|||
} |
|||
} |
|||
] |
|||
}, []) |
|||
|
|||
return ( |
|||
<> |
|||
<Alert message={t('website.ssl.acme.tip', 'Acme账户用于申请免费证书')}/> |
|||
<ProTable<IAcmeAccount> |
|||
cardProps={{ |
|||
bodyStyle: { |
|||
padding: 0, |
|||
} |
|||
}} |
|||
rowKey="id" |
|||
headerTitle={ |
|||
<Button |
|||
onClick={() => { |
|||
form.setFieldsValue({ |
|||
id: 0, |
|||
type: AcmeType.LetsEncrypt, |
|||
keyType: KeyTypeEnum.EC256, |
|||
}) |
|||
setOpen(true) |
|||
}} |
|||
type={'primary'}>{t('website.ssl.acme.add', '添加Acme帐户')}</Button> |
|||
} |
|||
loading={isLoading} |
|||
dataSource={data?.rows ?? []} |
|||
columns={columns} |
|||
search={false} |
|||
options={{ |
|||
reload: () => { |
|||
refetch() |
|||
}, |
|||
}} |
|||
pagination={{ |
|||
total: data?.total, |
|||
pageSize: page.pageSize, |
|||
current: page.page, |
|||
onChange: (current, pageSize) => { |
|||
setPage(prev => { |
|||
return { |
|||
...prev, |
|||
page: current, |
|||
pageSize: pageSize, |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
}} |
|||
/> |
|||
<BetaSchemaForm<IAcmeAccount> |
|||
shouldUpdate={false} |
|||
width={600} |
|||
form={form} |
|||
layout={'horizontal'} |
|||
scrollToFirstError={true} |
|||
title={t(`website.ssl.acme.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '证书编辑' : '证书添加')} |
|||
// colProps={{ span: 24 }}
|
|||
labelCol={{ span: 6 }} |
|||
wrapperCol={{ span: 14 }} |
|||
layoutType={'ModalForm'} |
|||
open={open} |
|||
modalProps={{ |
|||
maskClosable: false, |
|||
}} |
|||
onOpenChange={(open) => { |
|||
setOpen(open) |
|||
}} |
|||
loading={isSubmitting} |
|||
onFinish={async (values) => { |
|||
// console.log('values', values)
|
|||
saveOrUpdate(values) |
|||
return isSuccess |
|||
}} |
|||
columns={columns as ProFormColumnsType[]}/> |
|||
</> |
|||
) |
|||
} |
|||
|
|||
export default AcmeList |
@ -1,8 +1,13 @@ |
|||
import { createCURD } from '@/service/base.ts' |
|||
import { ISSL } from '@/types/website/ssl' |
|||
import { IAcmeAccount } from '@/types/website/acme' |
|||
|
|||
const websitesServ = { |
|||
ssl: { |
|||
...createCURD<any, ISsl>('/website/ssl') |
|||
...createCURD<any, ISSL>('/website/ssl') |
|||
}, |
|||
acme:{ |
|||
...createCURD<any, IAcmeAccount>('/website/acme') |
|||
} |
|||
} |
|||
|
@ -0,0 +1,49 @@ |
|||
import { atomWithMutation, atomWithQuery } from 'jotai-tanstack-query' |
|||
import { IAcmeAccount } from '@/types/website/acme' |
|||
import websitesServ from '@/service/websites.ts' |
|||
import { message } from 'antd' |
|||
import { t } from 'i18next' |
|||
import { IPage } from '@/global' |
|||
import { atom } from 'jotai' |
|||
|
|||
export enum AcmeType { |
|||
LetsEncrypt = 'letsencrypt', |
|||
//zerossl
|
|||
ZeroSSl = 'zerossl', |
|||
//buypass
|
|||
Buypass = 'buypass', |
|||
//google
|
|||
Google = 'google', |
|||
} |
|||
|
|||
export const acmePageAtom = atom<IPage>({ |
|||
page: 1, pageSize: 10, |
|||
}) |
|||
|
|||
//list
|
|||
export const acmeListAtom = atomWithQuery(get => ({ |
|||
queryKey: [ 'acmeList', get(acmePageAtom) ], |
|||
queryFn: async ({ queryKey: [ , page ] }) => { |
|||
return await websitesServ.acme.list(page) |
|||
}, |
|||
select: (data) => { |
|||
return data.data |
|||
} |
|||
})) |
|||
|
|||
//saveOrUpdate
|
|||
export const saveOrUpdateAcmeAtom = atomWithMutation<any, IAcmeAccount>(get => ({ |
|||
mutationKey: [ 'saveOrUpdateAcme' ], |
|||
mutationFn: async (data: IAcmeAccount) => { |
|||
if (data.id > 0) { |
|||
return await websitesServ.acme.update(data) |
|||
} |
|||
return await websitesServ.acme.add(data) |
|||
}, |
|||
onSuccess: (res) => { |
|||
const isAdd = !!res.data?.id |
|||
message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) |
|||
get(acmeListAtom).refetch() |
|||
return res |
|||
} |
|||
})) |
@ -0,0 +1,14 @@ |
|||
export interface IAcmeAccount { |
|||
id: number; |
|||
createdAt?: string; |
|||
createdBy: number; |
|||
updatedAt?: string; |
|||
updatedBy: number; |
|||
email: string; |
|||
url: string; |
|||
privateKey: string; |
|||
type: string; |
|||
eabKid: string; |
|||
eabHmacKey: string; |
|||
keyType: string; |
|||
} |
@ -1,8 +1,8 @@ |
|||
export interface ICA { |
|||
id: number; |
|||
createdAt: Date | null; |
|||
createdAt: string | null; |
|||
createdBy: number; |
|||
updatedAt: Date | null; |
|||
updatedAt: string | null; |
|||
updatedBy: number; |
|||
csr: string; |
|||
name: string; |
@ -1,8 +1,8 @@ |
|||
export interface IDnsAccount { |
|||
id: number; |
|||
createdAt: Date | null; |
|||
createdAt: string | null; |
|||
createdBy: number; |
|||
updatedAt: Date | null; |
|||
updatedAt: string | null; |
|||
updatedBy: number; |
|||
name: string; |
|||
type: string; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue