diff --git a/src/App.css b/src/App.css index cffcefd..06896a5 100644 --- a/src/App.css +++ b/src/App.css @@ -50,6 +50,10 @@ color: green; } +.color-red { + color: #F56C6C; +} + .color-yellow { color: rgb(250 145 0) } diff --git a/src/components/action/Action.tsx b/src/components/action/Action.tsx index e2e0ea9..c48319a 100644 --- a/src/components/action/Action.tsx +++ b/src/components/action/Action.tsx @@ -10,12 +10,12 @@ const Action = ({ title, as, children, ...props }: ActionProps) => { const { styles } = useStyle() const isLink = as === 'a' || props.type === 'link' - + const Comp = isLink ? 'a' : Button return ( - + className={as === 'a' ? styles.actionA : ''}>{title ?? children} ) } diff --git a/src/components/modal-pro/index.tsx b/src/components/modal-pro/index.tsx index b6af677..b7f990c 100644 --- a/src/components/modal-pro/index.tsx +++ b/src/components/modal-pro/index.tsx @@ -7,10 +7,11 @@ export interface AlterProps extends ModalFuncProps { onLoad?: () => void alterType: keyof HookAPI | 'dialog' children: JSX.Element | React.ReactNode + disabled?: boolean } -const ModalPro = ({ onLoad, alterType, children, ...props }: AlterProps) => { +const ModalPro = ({ onLoad, alterType, children, disabled, ...props }: AlterProps) => { const [ modal, modalHolder ] = Modal.useModal() const [ dialogRef, dialog, ] = useDialog(props) @@ -19,6 +20,8 @@ const ModalPro = ({ onLoad, alterType, children, ...props }: AlterProps) => { return ( <> { + if (disabled) return + if (onLoad) { onLoad() } diff --git a/src/components/table/style.ts b/src/components/table/style.ts index 4804c32..7ba8688 100644 --- a/src/components/table/style.ts +++ b/src/components/table/style.ts @@ -50,8 +50,7 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) const pagination = { - borderTop: '1px solid #ebeef5', - height: '51px', + margin: '0', alignContent: 'center', display: 'flex', @@ -82,6 +81,8 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) justify-content: space-between; align-items: center; overflow: hidden; + border-top: 1px solid #ebeef5; + height: 51px; .ant-pro-table-alert{ max-width: 50%; diff --git a/src/pages/websites/cert/assets/google.png b/src/pages/websites/cert/assets/google.png new file mode 100644 index 0000000..f553f3e Binary files /dev/null and b/src/pages/websites/cert/assets/google.png differ diff --git a/src/pages/websites/cert/assets/lets_encrypt.png b/src/pages/websites/cert/assets/lets_encrypt.png new file mode 100644 index 0000000..bad091d Binary files /dev/null and b/src/pages/websites/cert/assets/lets_encrypt.png differ diff --git a/src/pages/websites/cert/assets/zerossl.png b/src/pages/websites/cert/assets/zerossl.png new file mode 100644 index 0000000..0b54a4a Binary files /dev/null and b/src/pages/websites/cert/assets/zerossl.png differ diff --git a/src/pages/websites/cert/index.tsx b/src/pages/websites/cert/index.tsx new file mode 100644 index 0000000..e11c70a --- /dev/null +++ b/src/pages/websites/cert/index.tsx @@ -0,0 +1,394 @@ +import { useTranslation } from '@/i18n.ts' +import { Button, Form, Popconfirm, Divider, Space, Tooltip, Tag, Input, Progress, Flex } from 'antd' +import { useAtom, useAtomValue } from 'jotai' +import { + algorithmTypes, + bandTypes, + certAtom, certsAtom, + certSearchAtom, + deleteCertAtom, + saveOrUpdateCertAtom, StatusText, +} from '@/store/websites/cert' +import { useEffect, useMemo, useState } from 'react' +import Action from '@/components/action/Action.tsx' +import { + BetaSchemaForm, + ProColumns, + ProFormColumnsType, +} from '@ant-design/pro-components' +import ListPageLayout from '@/layout/ListPageLayout.tsx' +import { useStyle } from './style' +import { CheckCircleFilled, ExclamationCircleFilled, } from '@ant-design/icons' +import { Table as ProTable } from '@/components/table' +import google from './assets/google.png' +import lets_encrypt from './assets/lets_encrypt.png' +import zerossl from './assets/zerossl.png' +import ModalPro from '@/components/modal-pro' + +const i18nPrefix = 'cert.list' + +const Cert = () => { + + const { styles, cx } = useStyle() + const { t } = useTranslation() + const [ form ] = Form.useForm() + const [ filterForm ] = Form.useForm() + const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateCertAtom) + const [ search, setSearch ] = useAtom(certSearchAtom) + const [ currentCert, setCert ] = useAtom(certAtom) + const { data, isFetching, isLoading, refetch } = useAtomValue(certsAtom) + const { mutate: deleteCert, isPending: isDeleting } = useAtomValue(deleteCertAtom) + + const [ open, setOpen ] = useState(false) + const [ openFilter, setFilterOpen ] = useState(false) + const [ searchKey, setSearchKey ] = useState(search?.name) + + const columns = useMemo(() => { + return [ + { + title: 'ID', + dataIndex: 'id', + hideInTable: true, + hideInSearch: true, + formItemProps: { hidden: true } + }, + { + title: t(`${i18nPrefix}.columns.domains`, '域名'), + dataIndex: 'domains', + width: 350, + fieldProps: { + style: { width: '100%' }, + }, + formItemProps: { + rules: [ { required: true, message: t(`${i18nPrefix}.columns.domains.required`, '请输入域名') } ] + }, + renderFormItem: (_schema, config) => { + return + } + }, + { + title: t(`${i18nPrefix}.columns.type`, '域名验证'), + dataIndex: 'type', + hideInSearch: true, + align: 'center', + render: (_text, record) => { + if (record.type === 4) + return <> + return + } + }, + { + title: t(`${i18nPrefix}.columns.brand`, '证书品牌'), + dataIndex: 'brand', + valueType: 'select', + width: 100, + fieldProps: { + style: { width: '100%' }, + options: bandTypes + }, + render: (_text, record) => { + + if (record.brand === 'Google') + return + if (record.brand === 'ZeroSSL') + return + return + }, + formItemProps: { + rules: [ { required: true, message: t(`${i18nPrefix}.columns.brand.required`, '请选择证书品牌') } ] + } + }, + { + title: t(`${i18nPrefix}.columns.createTime`, '有效期(天)'), + dataIndex: 'createTime', + hideInSearch: true, + hideInForm: true, + width: 220, + render: (_text, record) => { + + const content = () => { + + if (record.lifeTime) + return { + return `${record.remainingTime}/${record.lifeTime}` + }}>{record.expire} + return ``}/> + } + return + +
生效时间:{record.notBefore}
+
失效时间:{record.notAfter}
+
创建时间:{record.createTime}
+ } + > + {content()} +
+
+ } + }, + { + title: t(`${i18nPrefix}.columns.algorithm`, '加密方式'), + dataIndex: 'algorithm', + valueType: 'select', + width: 100, + fieldProps: { + style: { width: '100%' }, + options: algorithmTypes, + }, + render: (text, record) => { + if (record.algorithm === 'ECC') + return {text} + return {text} + }, + formItemProps: { + rules: [ { required: true, message: t(`${i18nPrefix}.columns.algorithm.required`, '请选择加密方式') } ] + } + }, + + { + title: t(`${i18nPrefix}.columns.status`, '状态'), + dataIndex: 'status', + width: 100, + hideInSearch: true, + hideInForm: true, + render: (_text, record) => { + const [ text, color ] = StatusText[record.status] + return {text} + } + }, + { + title: t(`${i18nPrefix}.columns.remark`, '备注 '), + dataIndex: 'remark', + }, + { + title: t(`${i18nPrefix}.columns.option`, '操作'), + key: 'option', + width: 150, + valueType: 'option', + fixed: 'right', + render: (_, record) => [ + { + form.setFieldsValue(record) + setOpen(true) + }}>{t('actions.edit')}, + , + + {t(`actions.download`, '下载')} + , + , + { + deleteCert([ record.id ]) + }} + title={t('message.deleteConfirm')}> + + {t('actions.delete', '删除')} + + + ] + } + ] as ProColumns[] + }, [ isDeleting, currentCert, search ]) + + useEffect(() => { + + setSearchKey(search?.name) + filterForm.setFieldsValue(search) + + }, [ search ]) + + useEffect(() => { + if (isSuccess) { + setOpen(false) + } + }, [ isSuccess ]) + + return ( + + { + form.resetFields() + form.setFieldsValue({ + id: 0, + }) + setOpen(true) + }} + type={'primary'}>{t(`${i18nPrefix}.add`, '免费申请证明')} + + } + toolbar={{ + search: { + loading: isFetching && !!search?.name, + onSearch: (value: string) => { + setSearch(prev => ({ + ...prev, + name: value + })) + }, + allowClear: true, + onChange: (e) => { + setSearchKey(e.target?.value) + }, + value: searchKey, + placeholder: t(`${i18nPrefix}.placeholder`, '输入域名') + }, + actions: [] + }}/* + + + + + + ) + }, + + }} + onValuesChange={(values) => { + + }} + + onFinish={async (values) => { + //处理,变成数组 + Object.keys(values).forEach(key => { + if (typeof values[key] === 'string' && values[key].includes(',')) { + values[key] = values[key].split(',') + } + }) + + setSearch(values) + + }} + columns={columns.filter(item => !item.hideInSearch) as ProFormColumnsType[]}/> + + ) +} + +export default Cert \ No newline at end of file diff --git a/src/pages/websites/cert/style.ts b/src/pages/websites/cert/style.ts new file mode 100644 index 0000000..70e5394 --- /dev/null +++ b/src/pages/websites/cert/style.ts @@ -0,0 +1,26 @@ +import { createStyles } from '@/theme' + +export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { + const prefix = `${prefixCls}-${token?.proPrefix}-domainGroup-list-page` + + const container = css` + .ant-table-cell{ + .ant-tag{ + padding-inline: 3px; + margin-inline-end: 3px; + } + } + .ant-table-empty { + .ant-table-body{ + height: calc(100vh - 350px) + } + } + .ant-pro-table-highlight{ + + } + ` + + return { + container: cx(prefix, props?.className, container), + } +}) \ No newline at end of file diff --git a/src/service/websites.ts b/src/service/websites.ts index 0faa708..581139f 100644 --- a/src/service/websites.ts +++ b/src/service/websites.ts @@ -6,6 +6,66 @@ import { IWebsiteDnsRecords } from '@/types/website/record' import { IWebsiteDnsAccount } from '@/types/website/dns_account' const websitesServ = { + cert: { + ...createCURD('/website/cert'), + list: async (params?: any) => { + console.log(params) + return Promise.resolve({ + 'code': 0, + 'msg': 'success', + 'data': { + 'total': 3, + 'rows': [ + { + 'id': 10266, + 'brand': 'ZeroSSL', + 'domains': '*.aa.com,*.abc.com,aa.com,abc.com', + 'type': 4, + 'level': 1, + 'status': 3, + 'createTime': '2024-06-29 14:36:02', + 'remark': '', + 'algorithm': 'ECC', + 'fromType': 2 + }, + { + 'id': 10265, + 'brand': 'ZeroSSL', + 'domains': '*.aa.com,*.abc.com,aa.com,abc.com', + 'type': 4, + 'level': 1, + 'status': 3, + 'createTime': '2024-06-29 14:35:58', + 'remark': '', + 'algorithm': 'ECC', + 'fromType': 2 + }, + { + 'id': 10261, + 'brand': 'Google', + 'domains': '*.3456.world,3456.world', + 'type': 3, + 'level': 1, + 'status': 1, + 'createTime': '2024-06-29 14:17:46', + 'notAfter': '2024-09-27 13:18:31', + 'notBefore': '2024-06-29 13:18:32', + 'version': 3, + 'sigAlgName': 'SHA256withRSA', + 'remark': '3456.world', + 'name': '*.3456.world', + 'lifeTime': 90, + 'remainingTime': 82, + 'algorithm': 'RSA', + 'bit': 2048, + 'fromType': 2 + } + ] + }, + }) + // return request.post('/website/cert/list', params) + } + }, ssl: { ...createCURD('/website/ssl'), upload: async (params: WebSite.SSLUploadDto) => { @@ -33,22 +93,22 @@ const websitesServ = { domain: { ...createCURD('/website/domain'), //remark - remark: async (params: { id: string, remark: string }) => { + remark: async (params: { id: string, remark: string }) => { return request.post('/website/domain/remark', params) }, //tag - tag: async (params: { id: string, tags: string}) => { + tag: async (params: { id: string, tags: string }) => { return request.post('/website/domain/tag', params) }, //binding - binding: async (params: { id:string , user_id: string }) => { + binding: async (params: { id: string, user_id: string }) => { return request.post('/website/domain/binding', params) }, //group group: async (params: { id: string[], group_id: string }) => { return request.post('/website/domain/group', params) }, - describeDomainNS: async (params: { id: number }) => { + describeDomainNS: async (params: { id: number }) => { return request.post('/website/domain/describe_domain_ns', params) }, diff --git a/src/store/websites/cert.ts b/src/store/websites/cert.ts new file mode 100644 index 0000000..2eed857 --- /dev/null +++ b/src/store/websites/cert.ts @@ -0,0 +1,107 @@ +import { atom } from 'jotai' +import { IApiResult, IPage } from '@/global' +import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' +import { message } from 'antd' +import { t } from 'i18next' +import websitesServ from '@/service/websites.ts' + + +type SearchParams = IPage & { + name?: string +} + +export const bandTypes = [ + { label: 'Google', value: 'Google' }, + { label: 'ZeroSSL', value: 'ZeroSSL' }, + { label: 'Let\'s Encrypt', value: 'Let\'s Encrypt' }, +] + +export const algorithmTypes = [ + { label: 'RSA', value: 'RSA' }, + { label: 'ECC', value: 'ECC' }, +] + + +export const StatusText = { + 1: [ '已签发', 'green' ], + 2: [ '申请中', 'default' ], + 3: [ '申请失败', 'red' ] +} + + +export const certIdAtom = atom(0) + +export const certIdsAtom = atom([]) + +export const certAtom = atom(undefined as unknown as ICertificate) + +export const certSearchAtom = atom({ + // key: '', + pageSize: 10, + page: 1, +} as SearchParams) + +export const certPageAtom = atom({ + pageSize: 10, + page: 1, +}) + +export const certsAtom = atomWithQuery((get) => { + return { + queryKey: [ 'certs', get(certSearchAtom) ], + queryFn: async ({ queryKey: [ , params ] }) => { + return await websitesServ.cert.list(params as SearchParams) + }, + select: res => { + const data = res.data + data.rows = data.rows?.map(row => { + return { + ...row, + //status: convertToBool(row.status) + } + }) + return data + } + } +}) + +//saveOrUpdateAtom +export const saveOrUpdateCertAtom = atomWithMutation((get) => { + + return { + mutationKey: [ 'updateCert' ], + mutationFn: async (data) => { + //data.status = data.status ? '1' : '0' + if (data.id === 0) { + return await websitesServ.cert.add(data) + } + return await websitesServ.cert.update(data) + }, + onSuccess: (res) => { + const isAdd = !!res.data?.id + message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) + + //更新列表 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore fix + get(queryClientAtom).invalidateQueries({ queryKey: [ 'certs', get(certSearchAtom) ] }) + + return res + } + } +}) + +export const deleteCertAtom = atomWithMutation((get) => { + return { + mutationKey: [ 'deleteCert' ], + mutationFn: async (ids: number[]) => { + return await websitesServ.cert.batchDelete(ids ?? get(certIdsAtom)) + }, + onSuccess: (res) => { + message.success('message.deleteSuccess') + //更新列表 + get(queryClientAtom).invalidateQueries({ queryKey: [ 'certs', get(certSearchAtom) ] }) + return res + } + } +}) diff --git a/src/types/website/cert.d.ts b/src/types/website/cert.d.ts new file mode 100644 index 0000000..b2fbd43 --- /dev/null +++ b/src/types/website/cert.d.ts @@ -0,0 +1,20 @@ +interface ICertificate { + id: number; + brand: string; + domains: string; + type: number; + level: number; + status: number; + createTime: string; + notAfter: string; + notBefore: string; + version: number; + sigAlgName: string; + remark: string; + name: string; + lifeTime: number; + remainingTime: number; + algorithm: string; + bit: number; + fromType: number; +} \ No newline at end of file