diff --git a/src/components/drawer-picker/DrawerPicker.tsx b/src/components/drawer-picker/DrawerPicker.tsx new file mode 100644 index 0000000..ef36f4a --- /dev/null +++ b/src/components/drawer-picker/DrawerPicker.tsx @@ -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 = + return {target ?? def} + } + + return ( +
+ { + setOpen(true) + }}> + {getTarget()} + + setOpen(false)} + >{children} +
+ ) +} + +export default DrawerPicker \ No newline at end of file diff --git a/src/components/drawer-picker/index.ts b/src/components/drawer-picker/index.ts new file mode 100644 index 0000000..e85d6cd --- /dev/null +++ b/src/components/drawer-picker/index.ts @@ -0,0 +1 @@ +export * from './DrawerPicker.tsx' \ No newline at end of file diff --git a/src/components/drawer-picker/style.ts b/src/components/drawer-picker/style.ts new file mode 100644 index 0000000..0a848c5 --- /dev/null +++ b/src/components/drawer-picker/style.ts @@ -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, + } +}) \ No newline at end of file diff --git a/src/pages/system/roles/index.tsx b/src/pages/system/roles/index.tsx index 11be0ee..5a4f44f 100644 --- a/src/pages/system/roles/index.tsx +++ b/src/pages/system/roles/index.tsx @@ -9,7 +9,7 @@ import { BetaSchemaForm, ProFormColumnsType, } from '@ant-design/pro-components' import { useStyle } from './style.ts' -import { memo, useEffect, useMemo, useRef, useState } from 'react' +import { memo, useMemo, useRef, useState } from 'react' import { useAtom, useAtomValue } from 'jotai' import { deleteRoleAtom, @@ -138,13 +138,6 @@ const Roles = memo(() => { ] as ProColumns[] }, []) - useEffect(() => { - if (isSuccess) { - setOpen(false) - } - }, [ isSuccess ]) - - return (
@@ -240,7 +233,7 @@ const Roles = memo(() => { onFinish={async (values) => { // console.log('values', values) mutate(values) - return true + return isSuccess }} columns={columns as ProFormColumnsType[]}/> diff --git a/src/pages/system/users/index.tsx b/src/pages/system/users/index.tsx index c55d0af..dd80e62 100644 --- a/src/pages/system/users/index.tsx +++ b/src/pages/system/users/index.tsx @@ -31,7 +31,7 @@ const Users = () => { const [ page, setPage ] = useAtom(userPageAtom) const [ search, setSearch ] = useAtom(userSearchAtom) const [ , setCurrent ] = useAtom(userSelectedAtom) - const { mutate: saveOrUpdate, isPending: isSubmitting } = useAtomValue(saveOrUpdateUserAtom) + const { mutate: saveOrUpdate, isSuccess, isPending: isSubmitting } = useAtomValue(saveOrUpdateUserAtom) const { data, isFetching, isLoading, refetch } = useAtomValue(userListAtom) const { mutate: deleteUser, isPending, } = useAtomValue(deleteUserAtom) const { mutate: resetPass, isPending: isResetting } = useAtomValue(resetPasswordAtom) @@ -236,7 +236,7 @@ const Users = () => { onFinish={async (values) => { // console.log('values', values) saveOrUpdate(values) - return true + return isSuccess }} columns={columns as ProFormColumnsType[]}/> diff --git a/src/pages/websites/ssl/components/AcmeList.tsx b/src/pages/websites/ssl/components/AcmeList.tsx new file mode 100644 index 0000000..9575329 --- /dev/null +++ b/src/pages/websites/ssl/components/AcmeList.tsx @@ -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[]>(() => { + 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 [ + { + }}>{t('actions.edit', '编辑')}, + { + }}>{t('actions.delete', '删除')}, + ] + } + } + ] + }, []) + + return ( + <> + + + cardProps={{ + bodyStyle: { + padding: 0, + } + }} + rowKey="id" + headerTitle={ + + } + 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, + } + }) + }, + + }} + /> + + 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 \ No newline at end of file diff --git a/src/pages/websites/ssl/index.tsx b/src/pages/websites/ssl/index.tsx index cf2a28c..538492e 100644 --- a/src/pages/websites/ssl/index.tsx +++ b/src/pages/websites/ssl/index.tsx @@ -1,12 +1,21 @@ import { useAtom, useAtomValue } from 'jotai' -import { ProviderTypeEnum, saveOrUpdateSslAtom, sslListAtom, sslPageAtom, sslSearchAtom } from '@/store/websites/ssl.ts' +import { + KeyTypeEnum, + 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' +import { ISSL } from '@/types/website/ssl' +import DrawerPicker from '@/components/drawer-picker/DrawerPicker.tsx' +import AcmeList from '@/pages/websites/ssl/components/AcmeList.tsx' const SSL = () => { @@ -16,7 +25,7 @@ const SSL = () => { 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 { mutate: saveOrUpdate, isSuccess, isPending: isSubmitting } = useAtomValue(saveOrUpdateSslAtom) const [ open, setOpen ] = useState(false) @@ -34,7 +43,7 @@ const SSL = () => { { title: t('website.ssl.columns.primaryDomain', '域名'), dataIndex: 'primaryDomain', - formItemProps:{ + formItemProps: { label: t('website.ssl.form.primaryDomain', '主域名'), rules: [ { required: true, message: t('message.required', '主域名') } ] } @@ -48,6 +57,27 @@ const SSL = () => { dataIndex: 'acmeAccountId', }, { + title: t('website.ssl.columns.keyType', '密钥算法'), + dataIndex: 'keyType', + hideInTable: true, + 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.columns.provider', '申请方式'), dataIndex: 'provider', valueType: 'radio', @@ -63,8 +93,8 @@ const SSL = () => { text: t('website.ssl.providerTypeEnum.Http', 'HTTP'), } }, - dependencies : [ 'provider' ], - formItemProps: (form, config)=> { + dependencies: [ 'provider' ], + formItemProps: (form, config) => { const val = form.getFieldValue(config.dataIndex) const help = { [ProviderTypeEnum.DnsAccount]: t('website.ssl.form.provider_{{v}}', '', { v: val }), @@ -73,7 +103,7 @@ const SSL = () => { } return { label: t('website.ssl.form.provider', '验证方式'), - help: , + help: , rules: [ { required: true, message: t('message.required', '请选择') } ] } }, @@ -86,6 +116,10 @@ const SSL = () => { return [ { title: t('website.ssl.columns.dnsAccountId', 'DNS帐号'), dataIndex: 'dnsAccountId', + formItemProps: { + rules: [ { required: true, message: t('message.required', '请输入DNS帐号') } ] + } + } ] } return [] @@ -179,6 +213,21 @@ const SSL = () => { placeholder: t('website.ssl.search.placeholder', '输入域名') }, actions: [ + + {t('website.ssl.actions.acme', 'Acme帐户')} + } + > + + , + + {t( 'website.ssl.actions.dns', 'DNS帐户')} + }> + + , , ] }} @@ -232,7 +281,7 @@ const SSL = () => { onFinish={async (values) => { // console.log('values', values) saveOrUpdate(values) - return true + return isSuccess }} columns={columns as ProFormColumnsType[]}/> diff --git a/src/request.ts b/src/request.ts index d8ab432..8df22eb 100644 --- a/src/request.ts +++ b/src/request.ts @@ -84,6 +84,7 @@ axiosInstance.interceptors.response.use( }, (error) => { // console.log('error', error) + message.destroy() const { response } = error if (response) { switch (response.status) { @@ -105,7 +106,7 @@ axiosInstance.interceptors.response.use( window.location.href = `/login?redirect=${encodeURIComponent(redirect)}` return default: - message.error(response.data.message) + message.error(response.data.message ?? response.data ?? error.message ?? '请求失败') return Promise.reject(response) } } diff --git a/src/service/websites.ts b/src/service/websites.ts index 0458552..2e7546a 100644 --- a/src/service/websites.ts +++ b/src/service/websites.ts @@ -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('/website/ssl') + ...createCURD('/website/ssl') + }, + acme:{ + ...createCURD('/website/acme') } } diff --git a/src/store/websites/acme.ts b/src/store/websites/acme.ts new file mode 100644 index 0000000..88e4bb5 --- /dev/null +++ b/src/store/websites/acme.ts @@ -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({ + 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(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 + } +})) diff --git a/src/store/websites/ssl.ts b/src/store/websites/ssl.ts index 6cc0770..e4c7804 100644 --- a/src/store/websites/ssl.ts +++ b/src/store/websites/ssl.ts @@ -4,7 +4,7 @@ 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' +import { ISSL, SSLSearchParam } from '@/types/website/ssl' export enum ProviderTypeEnum { DnsAccount = 'dnsAccount', @@ -12,6 +12,14 @@ export enum ProviderTypeEnum { Http = 'http' } +export enum KeyTypeEnum { + EC256 = 'P256', + EC384 = 'P384', + RSA2048 = '2048', + RSA3072 = '3072', + RSA4096 = '4096', +} + export const sslPageAtom = atom({ page: 1, pageSize: 20, @@ -42,10 +50,14 @@ export const saveOrUpdateSslAtom = atomWithMutation(get => ({ return await websitesServ.ssl.update(data) } }, + // onError: (error: any) => { + // const msg = error?.data?.message ||t('message.saveFailed', '保存失败') + // message.error(msg) + // return error + // }, onSuccess: (res) => { const isAdd = !!res.data?.id message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) - get(sslListAtom).refetch() return res }, diff --git a/src/types/website/acme.d.ts b/src/types/website/acme.d.ts new file mode 100644 index 0000000..e62f62a --- /dev/null +++ b/src/types/website/acme.d.ts @@ -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; +} \ No newline at end of file diff --git a/src/types/ca.ts b/src/types/website/ca.d.ts similarity index 73% rename from src/types/ca.ts rename to src/types/website/ca.d.ts index de2d5ed..182d70f 100644 --- a/src/types/ca.ts +++ b/src/types/website/ca.d.ts @@ -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; diff --git a/src/types/dns.ts b/src/types/website/dns.d.ts similarity index 71% rename from src/types/dns.ts rename to src/types/website/dns.d.ts index 36a8c9d..0b9cdbc 100644 --- a/src/types/dns.ts +++ b/src/types/website/dns.d.ts @@ -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; diff --git a/src/types/ssl.d.ts b/src/types/website/ssl.d.ts similarity index 91% rename from src/types/ssl.d.ts rename to src/types/website/ssl.d.ts index 17f674a..446fbe1 100644 --- a/src/types/ssl.d.ts +++ b/src/types/website/ssl.d.ts @@ -16,8 +16,8 @@ export interface ISSL { acmeAccountId: number; caId: number; autoRenew: boolean; - expireDate: Date | null; - startDate: Date | null; + expireDate: string | null; + startDate: string | null; status: string; message: string; keyType: string;