dark
7 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
-
59src/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 { createCURD } from '@/service/base.ts' |
||||
|
import { ISSL } from '@/types/website/ssl' |
||||
|
import { IAcmeAccount } from '@/types/website/acme' |
||||
|
|
||||
const websitesServ = { |
const websitesServ = { |
||||
ssl: { |
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 { |
export interface ICA { |
||||
id: number; |
id: number; |
||||
createdAt: Date | null; |
|
||||
|
createdAt: string | null; |
||||
createdBy: number; |
createdBy: number; |
||||
updatedAt: Date | null; |
|
||||
|
updatedAt: string | null; |
||||
updatedBy: number; |
updatedBy: number; |
||||
csr: string; |
csr: string; |
||||
name: string; |
name: string; |
@ -1,8 +1,8 @@ |
|||||
export interface IDnsAccount { |
export interface IDnsAccount { |
||||
id: number; |
id: number; |
||||
createdAt: Date | null; |
|
||||
|
createdAt: string | null; |
||||
createdBy: number; |
createdBy: number; |
||||
updatedAt: Date | null; |
|
||||
|
updatedAt: string | null; |
||||
updatedBy: number; |
updatedBy: number; |
||||
name: string; |
name: string; |
||||
type: string; |
type: string; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue