Browse Source

完善SSL模块的页面状态

main
李金 7 months ago
parent
commit
8f1a866e10
  1. 76
      src/components/status/Status.tsx
  2. 325
      src/pages/websites/ssl/acme/AcmeList.tsx
  3. 447
      src/pages/websites/ssl/ca/CAList.tsx
  4. 551
      src/pages/websites/ssl/dns/DNSList.tsx
  5. 796
      src/pages/websites/ssl/index.tsx
  6. 418
      src/routes.tsx
  7. 82
      src/store/websites/acme.ts

76
src/components/status/Status.tsx

@ -3,57 +3,57 @@ import { useTranslation } from '@/i18n.ts'
import { SyncOutlined } from '@ant-design/icons'
export interface StatusProps extends TagProps {
status: string
status: string
}
const getColor = (status: string) => {
if (status.includes('error') || status.includes('err')) {
return 'danger'
}
switch (status) {
case 'running':
return 'success'
case 'stopped':
return 'danger'
case 'unhealthy':
case 'paused':
case 'exited':
case 'dead':
case 'removing':
return 'warning'
default:
return 'primary'
}
if (status.includes('error') || status.includes('err')) {
return 'danger'
}
switch (status) {
case 'running':
return 'success'
case 'stopped':
return 'danger'
case 'unhealthy':
case 'paused':
case 'exited':
case 'dead':
case 'removing':
return 'warning'
default:
return 'default'
}
}
const loadingStatus = [
'installing',
'building',
'restarting',
'upgrading',
'rebuilding',
'recreating',
'creating',
'starting',
'removing',
'applying',
'installing',
'building',
'restarting',
'upgrading',
'rebuilding',
'recreating',
'creating',
'starting',
'removing',
'applying',
]
const loadingIcon = (status: string): boolean => {
return loadingStatus.indexOf(status) > -1
return loadingStatus.indexOf(status) > -1
}
export const Status = ({ status = 'running', ...props }: StatusProps) => {
const { t } = useTranslation()
const icon = loadingIcon(status) ? <SyncOutlined spin/> : null
return (
<>
<Tag {...props}
color={getColor(status)}
icon={icon}>{t(`status.${status}`, status)}</Tag>
</>
)
const { t } = useTranslation()
const icon = loadingIcon(status) ? <SyncOutlined spin/> : null
return (
<>
<Tag {...props}
color={getColor(status)}
icon={icon}>{t(`status.${status}`, status)}</Tag>
</>
)
}
export default Status

325
src/pages/websites/ssl/acme/AcmeList.tsx

@ -1,171 +1,184 @@
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { useTranslation } from '@/i18n.ts'
import { AcmeAccountTypes, acmeListAtom, acmePageAtom, AcmeType, saveOrUpdateAcmeAtom } from '@/store/websites/acme.ts'
import {
AcmeAccountTypes,
acmeListAtom,
acmePageAtom,
AcmeType,
deleteAcmeAtom,
saveOrUpdateAcmeAtom
} from '@/store/websites/acme.ts'
import { useAtom, useAtomValue } from 'jotai'
import { Alert, Button, Form, Popconfirm } from 'antd'
import { KeyTypeEnum, KeyTypes } from '@/store/websites/ssl.ts'
import { deleteDNSAtom } from '@/store/websites/dns.ts'
import { WebSite } from '@/types'
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 { mutate: deleteDNS, isPending: isDeleting } = useAtomValue(deleteDNSAtom)
const [ open, setOpen ] = useState(false)
const { t } = useTranslation()
const [ form ] = Form.useForm()
const [ page, setPage ] = useAtom(acmePageAtom)
const { data, isLoading, isFetching, refetch } = useAtomValue(acmeListAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateAcmeAtom)
const { mutate: deleteAcme, isPending: isDeleting } = useAtomValue(deleteAcmeAtom)
const [ open, setOpen ] = useState(false)
const columns = useMemo<ProColumns<WebSite.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: AcmeAccountTypes
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
}
},
{
title: t('website.ssl.acme.columns.keyType', '密钥算法'),
dataIndex: 'keyType',
valueType: 'select',
fieldProps: {
options: KeyTypes
},
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 [
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteDNS(record.id)
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
]
}
}
]
}, [])
const columns = useMemo<ProColumns<WebSite.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: AcmeAccountTypes
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
}
},
{
title: t('website.ssl.acme.columns.keyType', '密钥算法'),
dataIndex: 'key_type',
valueType: 'select',
fieldProps: {
options: KeyTypes
},
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: t('website.ssl.acme.columns.option', '操作'),
valueType: 'option',
fixed: 'right',
render: (_, record) => {
return [
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteAcme(record.id)
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
]
}
}
]
}, [])
return (
<>
<Alert message={t('website.ssl.acme.tip', 'Acme账户用于申请免费证书')}/>
<ProTable<WebSite.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,
}
})
},
useEffect(() => {
if (isSuccess) {
setOpen(false)
}
}, [ isSuccess ])
}}
/>
<BetaSchemaForm<WebSite.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[]}/>
</>
)
return (
<>
<Alert message={t('website.ssl.acme.tip', 'Acme账户用于申请免费证书')}/>
<ProTable<WebSite.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 || isFetching}
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<WebSite.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)
}}
columns={columns as ProFormColumnsType[]}/>
</>
)
}
export default AcmeList

447
src/pages/websites/ssl/ca/CAList.tsx

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { useTranslation } from '@/i18n.ts'
import { deleteCaAtom } from '@/store/websites/ca.ts'
@ -13,233 +13,238 @@ import SelfSign from './SelfSign.tsx'
const CAList = () => {
const { t } = useTranslation()
const [ form ] = Form.useForm()
const [ page, setPage ] = useAtom(caPageAtom)
const { data, isLoading, refetch } = useAtomValue(caListAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateCaAtom)
const { mutate: deleteCA, isPending: isDeleting } = useAtomValue(deleteCaAtom)
const [ open, setOpen ] = useState(false)
const updateUI = useSetAtom(detailAtom)
const selfSignUI = useSetAtom(selfSignAtom)
const columns = useMemo<ProColumns<WebSite.ICA>[]>(() => {
return [
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
formItemProps: {
hidden: true,
}
},
{
title: t('website.ssl.ca.columns.name', '名称'),
dataIndex: 'name',
valueType: 'text',
formItemProps: {
label: t('website.ssl.ca.form.name', '机构名称'),
rules: [
{ required: true, message: t('message.required', '请输入') }
]
}
},
{
title: t('website.ssl.ca.form.common_name', '证书主体名称(CN)'),
dataIndex: 'common_name',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请输入') }
]
}
},
const { t } = useTranslation()
const [ form ] = Form.useForm()
const [ page, setPage ] = useAtom(caPageAtom)
const { data, isLoading, isFetching, refetch } = useAtomValue(caListAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateCaAtom)
const { mutate: deleteCA, isPending: isDeleting } = useAtomValue(deleteCaAtom)
const [ open, setOpen ] = useState(false)
const updateUI = useSetAtom(detailAtom)
const selfSignUI = useSetAtom(selfSignAtom)
const columns = useMemo<ProColumns<WebSite.ICA>[]>(() => {
return [
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
formItemProps: {
hidden: true,
}
},
{
title: t('website.ssl.ca.columns.name', '名称'),
dataIndex: 'name',
valueType: 'text',
formItemProps: {
label: t('website.ssl.ca.form.name', '机构名称'),
rules: [
{ required: true, message: t('message.required', '请输入') }
]
}
},
{
title: t('website.ssl.ca.form.common_name', '证书主体名称(CN)'),
dataIndex: 'common_name',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请输入') }
]
}
},
{
title: t('website.ssl.ca.form.organization', '公司/组织'),
dataIndex: 'organization',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请输入') }
]
}
},
{
title: t('website.ssl.ca.form.organization', '公司/组织'),
dataIndex: 'organization',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请输入') }
]
}
},
{
title: t('website.ssl.ca.form.organization_uint', '部门'),
dataIndex: 'organization_uint',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {}
},
{
title: t('website.ssl.ca.form.country', '国家代号'),
dataIndex: 'country',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请输入') }
]
}
},
{
title: t('website.ssl.ca.form.province', '省份'),
dataIndex: 'province',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {}
},
{
title: t('website.ssl.ca.form.city', '城市'),
dataIndex: 'city',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {}
},
{
title: t('website.ssl.ca.form.organization_uint', '部门'),
dataIndex: 'organization_uint',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {}
},
{
title: t('website.ssl.ca.form.country', '国家代号'),
dataIndex: 'country',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请输入') }
]
{
title: t('website.ssl.ca.columns.keyType', '密钥算法'),
dataIndex: 'key_type',
valueType: 'select',
fieldProps: {
options: KeyTypes
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
},
},
{
title: t('website.ssl.ca.columns.createAt', '时间'),
dataIndex: 'created_at',
valueType: 'dateTime',
hideInForm: true,
},
{
title: '操作',
valueType: 'option',
render: (_, record) => {
return [
<a key="view" onClick={() => {
selfSignUI({
open: true,
record: {
id: record.id,
key_type: KeyTypeEnum.EC256,
unit: 'year',
auto_renew: true,
time: 10,
}
},
{
title: t('website.ssl.ca.form.province', '省份'),
dataIndex: 'province',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {}
},
{
title: t('website.ssl.ca.form.city', '城市'),
dataIndex: 'city',
hideInTable: true,
hideInSetting: true,
valueType: 'text',
formItemProps: {}
},
})
}}>{t('website.actions.selfSign', '签发证书')}</a>,
<a key="edit" onClick={() => {
updateUI({
open: true, record,
})
}}>{t('website.actions.detail', '详情')}</a>,
{
title: t('website.ssl.ca.columns.keyType', '密钥算法'),
dataIndex: 'key_type',
valueType: 'select',
fieldProps: {
options: KeyTypes
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
},
},
{
title: t('website.ssl.ca.columns.createAt', '时间'),
dataIndex: 'created_at',
valueType: 'dateTime',
hideInForm: true,
},
{
title: '操作',
valueType: 'option',
render: (_, record) => {
return [
<a key="view" onClick={() => {
selfSignUI({
open: true,
record: {
id: record.id,
key_type: KeyTypeEnum.EC256,
unit: 'year',
auto_renew: true,
time: 10,
}
})
}}>{t('website.actions.selfSign', '签发证书')}</a>,
<a key="edit" onClick={() => {
updateUI({
open: true, record,
})
}}>{t('website.actions.detail', '详情')}</a>,
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteCA(record.id)
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
]
}
}
]
}, [])
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteCA(record.id)
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
]
}
}
]
}, [])
useEffect(() => {
if (isSuccess) {
setOpen(false)
}
}, [ isSuccess ])
return (
<>
<ProTable<WebSite.ICA>
cardProps={{
bodyStyle: {
padding: 0,
}
}}
rowKey="id"
headerTitle={
<Button
onClick={() => {
form.setFieldsValue({
id: 0,
country: 'CN',
keyType: KeyTypeEnum.EC256,
})
setOpen(true)
}}
type={'primary'}>{t('website.ssl.ca.add', '创建机构')}</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,
}
})
},
return (
<>
<ProTable<WebSite.ICA>
cardProps={{
bodyStyle: {
padding: 0,
}
}}
rowKey="id"
headerTitle={
<Button
onClick={() => {
form.setFieldsValue({
id: 0,
country: 'CN',
keyType: KeyTypeEnum.EC256,
})
setOpen(true)
}}
type={'primary'}>{t('website.ssl.ca.add', '创建机构')}</Button>
}
loading={isLoading || isFetching}
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<WebSite.ICA>
shouldUpdate={false}
width={600}
form={form}
layout={'horizontal'}
scrollToFirstError={true}
title={t(`website.ssl.ca.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[]}/>
<Detail/>
<SelfSign/>
</>
)
}}
/>
<BetaSchemaForm<WebSite.ICA>
shouldUpdate={false}
width={600}
form={form}
layout={'horizontal'}
scrollToFirstError={true}
title={t(`website.ssl.ca.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)
}}
columns={columns as ProFormColumnsType[]}/>
<Detail/>
<SelfSign/>
</>
)
}
export default CAList

551
src/pages/websites/ssl/dns/DNSList.tsx

@ -1,296 +1,301 @@
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { useTranslation } from '@/i18n.ts'
import { useAtom, useAtomValue } from 'jotai'
import { Button, Form, Popconfirm } from 'antd'
import {
deleteDNSAtom,
dnsListAtom,
dnsPageAtom,
DNSTypeEnum,
DNSTypes,
saveOrUpdateDNSAtom
deleteDNSAtom,
dnsListAtom,
dnsPageAtom,
DNSTypeEnum,
DNSTypes,
saveOrUpdateDNSAtom
} from '@/store/websites/dns.ts'
import { WebSite } from '@/types'
const getKeyColumn = (type: string, t) => {
const columns: ProColumns<IDnsAccount>[] = []
switch (type) {
case DNSTypeEnum.AliYun: {
columns.push(...[
{
title: t('website.ssl.dns.columns.accessKey', 'Access Key'),
dataIndex: 'accessKey',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{
title: t('website.ssl.dns.columns.secretKey', 'Secret Key'),
dataIndex: 'secretKey',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
])
}
break
case DNSTypeEnum.TencentCloud: {
columns.push(...[
{
title: t('website.ssl.dns.columns.secretID', 'Secret ID'),
dataIndex: 'secretID',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
}, {
title: t('website.ssl.dns.columns.secretKey', 'Secret Key'),
dataIndex: 'secretKey',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
])
break
}
case DNSTypeEnum.DnsPod: {
columns.push(...[
{
title: t('website.ssl.dns.columns.apiId', 'ID'),
dataIndex: 'apiId',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
}, {
title: t('website.ssl.dns.columns.token', 'Token'),
dataIndex: 'token',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
])
break
}
case DNSTypeEnum.CloudFlare: {
columns.push(...[
{
title: t('website.ssl.dns.columns.email', 'Email'),
dataIndex: 'email',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
}, {
title: t('website.ssl.dns.columns.apiKey', 'API ToKen'),
dataIndex: 'apiKey',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
]
)
break
}
case DNSTypeEnum.Godaddy:
case DNSTypeEnum.NameCheap:
case DNSTypeEnum.NameSilo:
columns.push({
title: t('website.ssl.dns.columns.apiKey', 'API Key'),
dataIndex: 'apiKey',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
)
if (type === DNSTypeEnum.NameCheap) {
columns.push({
title: t('website.ssl.dns.columns.apiUser', 'API User'),
dataIndex: 'apiUser',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
})
} else if (type === DNSTypeEnum.Godaddy) {
columns.push({
title: t('website.ssl.dns.columns.apiSecret', 'API Secret'),
dataIndex: 'apiSecret',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
})
const columns: ProColumns<WebSite.IDnsAccount>[] = []
switch (type) {
case DNSTypeEnum.AliYun: {
columns.push(...[
{
title: t('website.ssl.dns.columns.accessKey', 'Access Key'),
dataIndex: 'accessKey',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{
title: t('website.ssl.dns.columns.secretKey', 'Secret Key'),
dataIndex: 'secretKey',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
])
}
break
case DNSTypeEnum.TencentCloud: {
columns.push(...[
{
title: t('website.ssl.dns.columns.secretID', 'Secret ID'),
dataIndex: 'secretID',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
}, {
title: t('website.ssl.dns.columns.secretKey', 'Secret Key'),
dataIndex: 'secretKey',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
])
break
}
case DNSTypeEnum.DnsPod: {
columns.push(...[
{
title: t('website.ssl.dns.columns.apiId', 'ID'),
dataIndex: 'apiId',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
}, {
title: t('website.ssl.dns.columns.token', 'Token'),
dataIndex: 'token',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
])
break
}
case DNSTypeEnum.CloudFlare: {
columns.push(...[
{
title: t('website.ssl.dns.columns.email', 'Email'),
dataIndex: 'email',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
}, {
title: t('website.ssl.dns.columns.apiKey', 'API ToKen'),
dataIndex: 'apiKey',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
break
case DNSTypeEnum.NameCom: {
columns.push(
{
title: t('website.ssl.dns.columns.apiUser', 'UserName'),
dataIndex: 'apiUser',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{
title: t('website.ssl.dns.columns.token', 'Token'),
dataIndex: 'token',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
}
)
break
},
]
)
break
}
case DNSTypeEnum.Godaddy:
case DNSTypeEnum.NameCheap:
case DNSTypeEnum.NameSilo:
columns.push({
title: t('website.ssl.dns.columns.apiKey', 'API Key'),
dataIndex: 'apiKey',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
)
if (type === DNSTypeEnum.NameCheap) {
columns.push({
title: t('website.ssl.dns.columns.apiUser', 'API User'),
dataIndex: 'apiUser',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
})
} else if (type === DNSTypeEnum.Godaddy) {
columns.push({
title: t('website.ssl.dns.columns.apiSecret', 'API Secret'),
dataIndex: 'apiSecret',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
})
}
break
case DNSTypeEnum.NameCom: {
columns.push(
{
title: t('website.ssl.dns.columns.apiUser', 'UserName'),
dataIndex: 'apiUser',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{
title: t('website.ssl.dns.columns.token', 'Token'),
dataIndex: 'token',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
}
default:
break
)
break
}
return columns
default:
break
}
return columns
}
const DNSList = () => {
const { t } = useTranslation()
const [ form ] = Form.useForm()
const [ page, setPage ] = useAtom(dnsPageAtom)
const { data, isLoading, refetch } = useAtomValue(dnsListAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateDNSAtom)
const { mutate: deleteDNS, isPending: isDeleting } = useAtomValue(deleteDNSAtom)
const [ open, setOpen ] = useState(false)
const { t } = useTranslation()
const [ form ] = Form.useForm()
const [ page, setPage ] = useAtom(dnsPageAtom)
const { data, isLoading, isFetching, refetch } = useAtomValue(dnsListAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateDNSAtom)
const { mutate: deleteDNS, isPending: isDeleting } = useAtomValue(deleteDNSAtom)
const [ open, setOpen ] = useState(false)
const columns = useMemo<ProColumns<WebSite.IDnsAccount>[]>(() => {
return [
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
formItemProps: {
hidden: true,
}
},
{
title: t('website.ssl.dns.columns.name', '名称'),
dataIndex: 'name',
valueType: 'text',
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请输入') }
]
}
},
const columns = useMemo<ProColumns<WebSite.IDnsAccount>[]>(() => {
return [
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
formItemProps: {
hidden: true,
}
},
{
title: t('website.ssl.dns.columns.name', '名称'),
dataIndex: 'name',
valueType: 'text',
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请输入') }
]
}
},
{
title: t('website.ssl.dns.columns.type', '类型'),
dataIndex: 'type',
valueType: 'select',
fieldProps: {
options: DNSTypes
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
},
},
{
name: [ 'type' ],
valueType: 'dependency',
hideInSetting: true,
columns: ({ type }) => {
return getKeyColumn(type, t)
}
},
{
title: '操作',
valueType: 'option',
render: (_, record) => {
return [
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteDNS(record.id)
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
]
}
}
]
}, [])
{
title: t('website.ssl.dns.columns.type', '类型'),
dataIndex: 'type',
valueType: 'select',
fieldProps: {
options: DNSTypes
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
},
},
{
name: [ 'type' ],
valueType: 'dependency',
hideInSetting: true,
columns: ({ type }) => {
return getKeyColumn(type, t)
}
},
{
title: '操作',
valueType: 'option',
render: (_, record) => {
return [
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteDNS(record.id)
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
]
}
}
]
}, [])
return (
<>
<ProTable<WebSite.IDnsAccount>
cardProps={{
bodyStyle: {
padding: 0,
}
}}
rowKey="id"
headerTitle={
<Button
onClick={() => {
form.setFieldsValue({
id: 0,
type: DNSTypeEnum.DnsPod,
})
setOpen(true)
}}
type={'primary'}>{t('website.ssl.dns.add', '添加DNS帐户')}</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,
}
})
},
useEffect(() => {
if (isSuccess) {
setOpen(false)
}
}, [ isSuccess ])
return (
<>
<ProTable<WebSite.IDnsAccount>
cardProps={{
bodyStyle: {
padding: 0,
}
}}
rowKey="id"
headerTitle={
<Button
onClick={() => {
form.setFieldsValue({
id: 0,
type: DNSTypeEnum.DnsPod,
})
setOpen(true)
}}
type={'primary'}>{t('website.ssl.dns.add', '添加DNS帐户')}</Button>
}
loading={isLoading || isFetching}
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<WebSite.IDnsAccount>
shouldUpdate={false}
width={600}
form={form}
layout={'horizontal'}
scrollToFirstError={true}
title={t(`website.ssl.dns.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? 'DNS帐号编辑' : 'DNS帐号添加')}
// 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[]}/>
</>
)
}}
/>
<BetaSchemaForm<WebSite.IDnsAccount>
shouldUpdate={false}
width={600}
form={form}
layout={'horizontal'}
scrollToFirstError={true}
title={t(`website.ssl.dns.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? 'DNS帐号编辑' : 'DNS帐号添加')}
// 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)
}}
columns={columns as ProFormColumnsType[]}/>
</>
)
}
export default DNSList

796
src/pages/websites/ssl/index.tsx

@ -1,17 +1,17 @@
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import {
deleteSslAtom, getProvider,
KeyTypeEnum,
KeyTypes,
ProviderTypeEnum,
saveOrUpdateSslAtom,
sslListAtom,
sslPageAtom,
sslSearchAtom, uploadSslAtom
deleteSslAtom, getProvider,
KeyTypeEnum,
KeyTypes,
ProviderTypeEnum,
saveOrUpdateSslAtom,
sslListAtom,
sslPageAtom,
sslSearchAtom, uploadSslAtom
} from '@/store/websites/ssl.ts'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { memo, useMemo, useRef, useState } from 'react'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from '@/i18n.ts'
import { Button, Form, Popconfirm, Space } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
@ -35,403 +35,409 @@ import Download from '@/components/download/Download.tsx'
const SSL = () => {
const { t } = useTranslation()
const [ form ] = Form.useForm()
const uploadFormRef = useRef<FormInstance>()
const [ page, setPage ] = useAtom(sslPageAtom)
const [ search, setSearch ] = useAtom(sslSearchAtom)
const { data: acmeData, isLoading: acmeLoading } = useAtomValue(acmeListAtom)
const { data: dnsData, isLoading: dnsLoading } = useAtomValue(dnsListAtom)
const { data, isLoading, isFetching, refetch } = useAtomValue(sslListAtom)
const { mutate: saveOrUpdate, isSuccess, isPending: isSubmitting } = useAtomValue(saveOrUpdateSslAtom)
const { mutate: deleteSSL, isPending: isDeleting } = useAtomValue(deleteSslAtom)
const { mutate: uploadSSL, isSuccess: isUploadSuccess, isPending: isUploading } = useAtomValue(uploadSslAtom)
const updateDetail = useSetAtom(detailAtom)
const uploadDrawerRef = useRef<DrawerPickerRef>()
const [ open, setOpen ] = useState(false)
const { t } = useTranslation()
const [ form ] = Form.useForm()
const uploadFormRef = useRef<FormInstance>()
const [ page, setPage ] = useAtom(sslPageAtom)
const [ search, setSearch ] = useAtom(sslSearchAtom)
const { data: acmeData, isLoading: acmeLoading } = useAtomValue(acmeListAtom)
const { data: dnsData, isLoading: dnsLoading } = useAtomValue(dnsListAtom)
const { data, isLoading, isFetching, refetch } = useAtomValue(sslListAtom)
const { mutate: saveOrUpdate, isSuccess, isPending: isSubmitting } = useAtomValue(saveOrUpdateSslAtom)
const { mutate: deleteSSL, isPending: isDeleting } = useAtomValue(deleteSslAtom)
const { mutate: uploadSSL, isSuccess: isUploadSuccess, isPending: isUploading } = useAtomValue(uploadSslAtom)
const updateDetail = useSetAtom(detailAtom)
const uploadDrawerRef = useRef<DrawerPickerRef>()
const [ open, setOpen ] = useState(false)
const columns = useMemo<ProColumns<WebSite.ISSL>[]>(() => {
return [
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
hideInSearch: false,
formItemProps: {
hidden: true,
}
},
{
title: t('website.ssl.columns.primaryDomain', '域名'),
dataIndex: 'primary_domain',
formItemProps: {
label: t('website.ssl.form.primaryDomain', '主域名'),
rules: [ { required: true, message: t('message.required', '主域名') } ]
}
},
{
title: t('website.ssl.columns.otherDomains', '其它域名'),
dataIndex: 'domains',
},
{
title: t('website.ssl.columns.acmeAccountId', 'Acme帐号'),
dataIndex: 'acme_account_id',
valueType: 'select',
fieldProps: {
loading: acmeLoading,
options: acmeData?.rows?.map(item => ({
label: `${item.email} [${getAcmeAccountTypeName(item.type as AcmeType)}]`,
value: item.id
}))
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
}
},
{
title: t('website.ssl.columns.status', '状态'),
dataIndex: 'status',
render: (_, record) => {
return <Status status={record.status}/>
},
hideInForm: true,
},
{
title: t('website.ssl.columns.keyType', '密钥算法'),
dataIndex: 'key_type',
hideInTable: true,
valueType: 'select',
fieldProps: {
options: KeyTypes
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
},
},
{
title: t('website.ssl.columns.provider', '申请方式'),
dataIndex: 'provider',
valueType: 'radio',
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' ],
renderText: (text) => {
return getProvider(text)
},
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',
hideInSetting: true,
hideInTable: true,
columns: ({ provider }) => {
if (provider === ProviderTypeEnum.DnsAccount) {
return [ {
title: t('website.ssl.columns.dnsAccountId', 'DNS帐号'),
dataIndex: 'dns_account_id',
valueType: 'select',
formItemProps: {
rules: [ { required: true, message: t('message.required', '请输入DNS帐号') } ]
},
fieldProps: {
loading: dnsLoading,
options: dnsData?.rows.map(item => ({
label: `${item.name} [${getDNSTypeName(item.type)}]`,
value: item.id
}))
},
} ]
}
return []
const columns = useMemo<ProColumns<WebSite.ISSL>[]>(() => {
return [
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
hideInSearch: false,
formItemProps: {
hidden: true,
}
},
{
title: t('website.ssl.columns.primaryDomain', '域名'),
dataIndex: 'primary_domain',
formItemProps: {
label: t('website.ssl.form.primaryDomain', '主域名'),
rules: [ { required: true, message: t('message.required', '主域名') } ]
}
},
{
title: t('website.ssl.columns.otherDomains', '其它域名'),
dataIndex: 'domains',
},
{
title: t('website.ssl.columns.acmeAccountId', 'Acme帐号'),
dataIndex: 'acme_account_id',
valueType: 'select',
fieldProps: {
loading: acmeLoading,
options: acmeData?.rows?.map(item => ({
label: `${item.email} [${getAcmeAccountTypeName(item.type as AcmeType)}]`,
value: item.id
}))
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
}
},
{
title: t('website.ssl.columns.status', '状态'),
dataIndex: 'status',
render: (_, record) => {
return <Status status={record.status}/>
},
hideInForm: true,
},
{
title: t('website.ssl.columns.keyType', '密钥算法'),
dataIndex: 'key_type',
hideInTable: true,
valueType: 'select',
fieldProps: {
options: KeyTypes
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
},
},
{
title: t('website.ssl.columns.provider', '申请方式'),
dataIndex: 'provider',
valueType: 'radio',
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' ],
renderText: (text) => {
return getProvider(text)
},
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',
hideInSetting: true,
hideInTable: true,
columns: ({ provider }) => {
if (provider === ProviderTypeEnum.DnsAccount) {
return [ {
title: t('website.ssl.columns.dnsAccountId', 'DNS帐号'),
dataIndex: 'dns_account_id',
valueType: 'select',
formItemProps: {
rules: [ { required: true, message: t('message.required', '请输入DNS帐号') } ]
},
fieldProps: {
loading: dnsLoading,
options: dnsData?.rows?.map(item => ({
label: `${item.name} [${getDNSTypeName(item.type)}]`,
value: item.id
}))
},
} ]
}
return []
}
},
{
title: t('website.ssl.columns.autoRenew', '自动续签'),
dataIndex: 'auto_renew',
valueType: 'switch',
render: (_, record) => {
return <Switch value={record.auto_renew} size={'small'}/>
}
},
{
title: t('website.ssl.columns.pushDir', '推送证书到本地目录'),
dataIndex: 'push_dir',
valueType: 'switch',
hideInTable: true,
hideInSearch: true,
}
},
{
title: t('website.ssl.columns.autoRenew', '自动续签'),
dataIndex: 'auto_renew',
valueType: 'switch',
render: (_, record) => {
return <Switch value={record.auto_renew} size={'small'}/>
}
},
{
title: t('website.ssl.columns.pushDir', '推送证书到本地目录'),
dataIndex: 'push_dir',
valueType: 'switch',
hideInTable: true,
hideInSearch: true,
},
{
name: [ 'push_dir' ],
valueType: 'dependency',
hideInSetting: true,
hideInTable: true,
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 []
},
{
name: [ 'push_dir' ],
valueType: 'dependency',
hideInSetting: true,
hideInTable: true,
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.expire_date', '过期时间'),
dataIndex: 'expire_date',
valueType: 'dateTime',
hideInForm: true,
},
{
title: t('website.ssl.columns.option', '操作'), valueType: 'option',
key: 'option',
fixed: 'right',
width: 300,
render: (_, record) => [
}
},
{
title: t('website.ssl.columns.description', '备注'),
dataIndex: 'description',
},
{
title: t('website.ssl.columns.expire_date', '过期时间'),
dataIndex: 'expire_date',
valueType: 'dateTime',
hideInForm: true,
},
{
title: t('website.ssl.columns.option', '操作'), valueType: 'option',
key: 'option',
fixed: 'right',
width: 300,
render: (_, record) => [
<Action key="detail"
as={'a'}
disabled={record.status === 'init' || record.status === 'error'}
onClick={() => {
updateDetail({
open: true,
record
})
}}
>
{t('actions.detail', '详情')}
</Action>,
<If condition={() => record.status !== 'manual'}>
<Then>
<Action key="apply"
as={'a'}
disabled={record.status === 'applying' || record.status === 'manual'}
<Action key="detail"
as={'a'}
disabled={record.status === 'init' || record.status === 'error'}
onClick={() => {
updateDetail({
open: true,
record
})
}}
>
{t('actions.detail', '详情')}
</Action>,
<If condition={() => record.status !== 'manual'}>
<Then>
<Action key="apply"
as={'a'}
disabled={record.status === 'applying' || record.status === 'manual'}
onClick={() => {
onClick={() => {
}}
>
{t('actions.apply', '申请')}
</Action>
</Then>
<Else>
<Action key="update"
as={'a'}
onClick={() => {
}}
>
{t('actions.apply', '申请')}
</Action>
</Then>
<Else>
<Action key="update"
as={'a'}
onClick={() => {
}}
>
{t('actions.update', '更新')}
</Action>
</Else>
}}
>
{t('actions.update', '更新')}
</Action>
</Else>
</If>,
<Download key="download"
server={async () => {
}}
>
<a>{t('actions.download', '下载')}</a>
</Download>,
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteSSL(record.id)
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
,
],
},
]
}, [])
</If>,
<Download key="download"
server={async () => {
}}
>
<a>{t('actions.download', '下载')}</a>
</Download>,
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteSSL(record.id)
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
,
],
},
]
}, [ acmeData, dnsData ])
return (
<ListPageLayout>
<ProTable<WebSite.ISSL>
headerTitle={t('website.ssl.title', '证书列表')}
search={false}
loading={isLoading || isFetching}
rowKey={'id'}
dataSource={data?.rows ?? []}
columns={columns}
columnsState={{
defaultValue: {
option: { fixed: 'right', disable: true },
},
}}
options={{
reload: () => {
refetch()
},
}}
toolbar={{
search: {
loading: isFetching && !!search.key,
onSearch: (value: string) => {
setSearch({ key: value })
},
placeholder: t('website.ssl.search.placeholder', '输入域名')
},
actions: [
<DrawerPicker
maskClosable={false}
title={t('website.ssl.ca.title', '证书颁发机构')}
width={1000}
target={<Button type={'primary'} ghost={true}>
{t('website.ssl.actions.selfSigned', '自签证书')}
</Button>}
>
<CAList/>
</DrawerPicker>,
<DrawerPicker
maskClosable={false}
title={t('website.ssl.acme.title', 'Acme帐户')}
width={1000}
target={<Button type={'primary'} ghost={true}>
{t('website.ssl.actions.acme', 'Acme帐户')}
</Button>}
>
<AcmeList/>
</DrawerPicker>,
<DrawerPicker
maskClosable={false}
title={t('website.ssl.dns.title', 'DNS帐户')}
width={1000}
target={<Button type={'primary'} ghost={true}>
{t('website.ssl.actions.dns', 'DNS帐户')}
</Button>}>
<DNSList/>
</DrawerPicker>,
<Button type={'primary'} onClick={() => {
uploadDrawerRef.current?.open()
}}>
{t('website.ssl.actions.upload', '上传证书')}
</Button>,
<Button
key="button"
icon={<PlusOutlined/>}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
keyType: KeyTypeEnum.EC256,
})
setOpen(true)
}}
type="primary"
>
{t('actions.sslApply', '申请证书')}
</Button>,
]
}}
pagination={{
pageSize: page?.pageSize ?? 10,
total: data?.total ?? 0,
current: page?.page ?? 1,
onChange: (page, pageSize) => {
setPage(prev => ({
...prev,
page,
pageSize,
}))
},
}}
>
useEffect(() => {
if (isSuccess) {
setOpen(false)
}
}, [ isSuccess ])
</ProTable>
<BetaSchemaForm<WebSite.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 isSuccess
}}
columns={columns as ProFormColumnsType[]}/>
<DrawerPicker
id={'upload-drawer-picker'}
ref={uploadDrawerRef}
maskClosable={false}
title={t('website.ssl.upload.title', '上传证书')}
width={800}
footer={[
<Space wrap={false} style={{ display: 'flex', justifyContent: 'end' }}>
<Button onClick={() => uploadDrawerRef.current?.close()}>{t('actions.cancel')}</Button>
<Button onClick={() => {
uploadFormRef.current?.validateFields?.().then(values => {
uploadSSL(values)
if (isUploadSuccess) {
uploadDrawerRef.current?.close()
}
})
}}
loading={isUploading}
type={'primary'}>{t('actions.ok')}</Button>
</Space>
]}
target={false}
>
<Upload formRef={uploadFormRef}/>
</DrawerPicker>
<SSLDetail/>
</ListPageLayout>
)
return (
<ListPageLayout>
<ProTable<WebSite.ISSL>
headerTitle={t('website.ssl.title', '证书列表')}
search={false}
loading={isLoading || isFetching}
rowKey={'id'}
dataSource={data?.rows ?? []}
columns={columns}
columnsState={{
defaultValue: {
option: { fixed: 'right', disable: true },
},
}}
options={{
reload: () => {
refetch()
},
}}
toolbar={{
search: {
loading: isFetching && !!search.key,
onSearch: (value: string) => {
setSearch({ key: value })
},
placeholder: t('website.ssl.search.placeholder', '输入域名')
},
actions: [
<DrawerPicker
maskClosable={false}
title={t('website.ssl.ca.title', '证书颁发机构')}
width={1000}
target={<Button type={'primary'} ghost={true}>
{t('website.ssl.actions.selfSigned', '自签证书')}
</Button>}
>
<CAList/>
</DrawerPicker>,
<DrawerPicker
maskClosable={false}
title={t('website.ssl.acme.title', 'Acme帐户')}
width={1000}
target={<Button type={'primary'} ghost={true}>
{t('website.ssl.actions.acme', 'Acme帐户')}
</Button>}
>
<AcmeList/>
</DrawerPicker>,
<DrawerPicker
maskClosable={false}
title={t('website.ssl.dns.title', 'DNS帐户')}
width={1000}
target={<Button type={'primary'} ghost={true}>
{t('website.ssl.actions.dns', 'DNS帐户')}
</Button>}>
<DNSList/>
</DrawerPicker>,
<Button type={'primary'} onClick={() => {
uploadDrawerRef.current?.open()
}}>
{t('website.ssl.actions.upload', '上传证书')}
</Button>,
<Button
key="button"
icon={<PlusOutlined/>}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
keyType: KeyTypeEnum.EC256,
})
setOpen(true)
}}
type="primary"
>
{t('actions.sslApply', '申请证书')}
</Button>,
]
}}
pagination={{
pageSize: page?.pageSize ?? 10,
total: data?.total ?? 0,
current: page?.page ?? 1,
onChange: (page, pageSize) => {
setPage(prev => ({
...prev,
page,
pageSize,
}))
},
}}
>
</ProTable>
<BetaSchemaForm<WebSite.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)
}}
columns={columns as ProFormColumnsType[]}/>
<DrawerPicker
id={'upload-drawer-picker'}
ref={uploadDrawerRef}
maskClosable={false}
title={t('website.ssl.upload.title', '上传证书')}
width={800}
footer={[
<Space wrap={false} style={{ display: 'flex', justifyContent: 'end' }}>
<Button onClick={() => uploadDrawerRef.current?.close()}>{t('actions.cancel')}</Button>
<Button onClick={() => {
uploadFormRef.current?.validateFields?.().then(values => {
uploadSSL(values)
if (isUploadSuccess) {
uploadDrawerRef.current?.close()
}
})
}}
loading={isUploading}
type={'primary'}>{t('actions.ok')}</Button>
</Space>
]}
target={false}
>
<Upload formRef={uploadFormRef}/>
</DrawerPicker>
<SSLDetail/>
</ListPageLayout>
)
}
export default memo(SSL)

418
src/routes.tsx

@ -13,13 +13,13 @@ import { Route as LoginRouteImport } from '@/pages/login'
import { generateUUID } from '@/utils/uuid.ts'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import {
AnyRoute,
createRootRouteWithContext,
createRoute,
createRouter, lazyRouteComponent,
Outlet,
redirect,
RouterProvider,
AnyRoute,
createRootRouteWithContext,
createRoute,
createRouter, lazyRouteComponent,
Outlet,
redirect,
RouterProvider,
} from '@tanstack/react-router'
// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { memo, useEffect, useRef } from 'react'
@ -30,70 +30,70 @@ import { useAtomValue } from 'jotai'
import { userMenuDataAtom } from '@/store/system/user.ts'
const PageRootLayout = () => {
return <PageStoreProvider>
<RootLayout/>
</PageStoreProvider>
return <PageStoreProvider>
<RootLayout/>
</PageStoreProvider>
}
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
}
defaultOptions: {
queries: {
retry: false,
}
}
})
const rootRoute = createRootRouteWithContext<IRootContext>()({
component: () => (
<>
<FetchLoading/>
<Outlet/>
<DevTools/>
{/*<TanStackRouterDevtools position={'bottom-right'}/>*/}
</>
),
beforeLoad: ({ location }) => {
if (location.pathname === '/') {
return redirect({ to: '/dashboard' })
}
},
loader: () => {
component: () => (
<>
<FetchLoading/>
<Outlet/>
<DevTools/>
{/*<TanStackRouterDevtools position={'bottom-right'}/>*/}
</>
),
beforeLoad: ({ location }) => {
if (location.pathname === '/') {
return redirect({ to: '/dashboard' })
}
},
loader: () => {
},
notFoundComponent: NotFound,
pendingComponent: PageLoading,
errorComponent: ({ error }) => <ErrorPage error={error}/>,
},
notFoundComponent: NotFound,
pendingComponent: PageLoading,
errorComponent: ({ error }) => <ErrorPage error={error}/>,
})
const emptyRoute = createRoute({
getParentRoute: () => rootRoute,
id: '/_empty',
component: EmptyLayout,
getParentRoute: () => rootRoute,
id: '/_empty',
component: EmptyLayout,
})
const authRoute = AuthenticatedImport.update({
getParentRoute: () => rootRoute,
id: '/_authenticated',
getParentRoute: () => rootRoute,
id: '/_authenticated',
} as any)
const layoutNormalRoute = createRoute({
getParentRoute: () => rootRoute,
id: '/_normal_layout',
component: PageRootLayout,
getParentRoute: () => rootRoute,
id: '/_normal_layout',
component: PageRootLayout,
})
const layoutAuthRoute = createRoute({
getParentRoute: () => authRoute,
id: '/_auth_layout',
component: PageRootLayout,
getParentRoute: () => authRoute,
id: '/_auth_layout',
component: PageRootLayout,
})
const notAuthRoute = createRoute({
getParentRoute: () => layoutNormalRoute,
path: '/not-auth',
component: NotPermission
getParentRoute: () => layoutNormalRoute,
path: '/not-auth',
component: NotPermission
})
// const dashboardRoute = DashboardImport.update({
@ -102,8 +102,8 @@ const notAuthRoute = createRoute({
// } as any)
const loginRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => emptyRoute,
path: '/login',
getParentRoute: () => emptyRoute,
} as any)
//
@ -129,189 +129,191 @@ const loginRoute = LoginRouteImport.update({
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/_authenticated': {
preLoaderRoute: typeof AuthenticatedImport
parentRoute: typeof rootRoute
},
'/_normal_layout': {
preLoaderRoute: typeof layoutNormalRoute
parentRoute: typeof rootRoute
},
'/_layout': {
preLoaderRoute: typeof layoutAuthRoute
parentRoute: typeof rootRoute
},
// '/': {
// preLoaderRoute: typeof DashboardImport
// parentRoute: typeof layoutAuthRoute
// },
// '/dashboard': {
// preLoaderRoute: typeof DashboardImport
// parentRoute: typeof layoutAuthRoute
// },
'/login': {
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRoute
},
// '/system/menus': {
// preLoaderRoute: typeof menusRoute
// parentRoute: typeof layoutAuthRoute
// },
// '/system/departments': {
// preLoaderRoute: typeof departmentsRoute
// parentRoute: typeof layoutAuthRoute
// },
// '/system/users': {
// preLoaderRoute: typeof usersRoute
// parentRoute: typeof layoutAuthRoute
// },
// '/system/roles': {
// preLoaderRoute: typeof rolesRoute
// parentRoute: typeof layoutAuthRoute
// },
'/welcome': {
preLoaderRoute: typeof rootRoute
parentRoute: typeof layoutAuthRoute
},
}
interface FileRoutesByPath {
'/_authenticated': {
preLoaderRoute: typeof AuthenticatedImport
parentRoute: typeof rootRoute
},
'/_normal_layout': {
preLoaderRoute: typeof layoutNormalRoute
parentRoute: typeof rootRoute
},
'/_layout': {
preLoaderRoute: typeof layoutAuthRoute
parentRoute: typeof rootRoute
},
// '/': {
// preLoaderRoute: typeof DashboardImport
// parentRoute: typeof layoutAuthRoute
// },
// '/dashboard': {
// preLoaderRoute: typeof DashboardImport
// parentRoute: typeof layoutAuthRoute
// },
'/login': {
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRoute
},
// '/system/menus': {
// preLoaderRoute: typeof menusRoute
// parentRoute: typeof layoutAuthRoute
// },
// '/system/departments': {
// preLoaderRoute: typeof departmentsRoute
// parentRoute: typeof layoutAuthRoute
// },
// '/system/users': {
// preLoaderRoute: typeof usersRoute
// parentRoute: typeof layoutAuthRoute
// },
// '/system/roles': {
// preLoaderRoute: typeof rolesRoute
// parentRoute: typeof layoutAuthRoute
// },
'/welcome': {
preLoaderRoute: typeof rootRoute
parentRoute: typeof layoutAuthRoute
},
}
}
const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => {
// 递归生成路由,如果有routes则递归生成子路由
const generateRoutes = (menu: MenuItem, parentRoute: AnyRoute) => {
const path = menu.path?.replace(parentRoute.options?.path, '')
const isLayout = menu.children && menu.children.length > 0 && menu.type === 'menu'
if (isLayout && (!menu.path || !menu.component)) {
//没有component的layout,直接返回
return createRoute({
getParentRoute: () => layoutAuthRoute,
id: `/layout-no-path-${generateUUID()}`,
component: EmptyLayout,
})
}
// @ts-ignore 添加menu属性,方便后面获取
const options = {
getParentRoute: () => parentRoute,
menu,
} as any
if (isLayout) {
options.id = path ?? `/layout-${generateUUID()}`
} else {
if (!path) {
console.log(`${menu.name}没有设置视图`)
} else {
options.path = path
}
}
let component = menu.component
// menu.type
// 1,组件(页面),2,IFrame,3,外链接,4,按钮
if (menu.type === 'iframe') {
component = '@/components/Iframe'
}
//处理component路径
component = component.replace(/^\/pages/, '')
component = component.replace(/^\//, '')
// 递归生成路由,如果有routes则递归生成子路由
const generateRoutes = (menu: MenuItem, parentRoute: AnyRoute) => {
const path = menu.path?.replace(parentRoute.options?.path, '')
const isLayout = menu.children && menu.children.length > 0 && menu.type === 'menu'
if (isLayout && (!menu.path || !menu.component)) {
//没有component的layout,直接返回
return createRoute({
getParentRoute: () => layoutAuthRoute,
id: `/layout-no-path-${generateUUID()}`,
component: EmptyLayout,
})
}
return createRoute({
...options,
component: lazyRouteComponent(() => import(`./pages/${component}`)),
notFoundComponent: NotFound,
})
// @ts-ignore 添加menu属性,方便后面获取
const options = {
getParentRoute: () => parentRoute,
menu,
} as any
if (isLayout) {
options.id = path ?? `/layout-${generateUUID()}`
} else {
if (!path) {
console.log(`${menu.name}没有设置视图`)
} else {
options.path = path
}
}
// 对menuData递归生成路由,只处理type =1 的菜单
const did = (menus: MenuItem[], parentRoute: AnyRoute) => {
return menus.filter((item) => item.type === 'menu').map((item, index) => {
// 如果有children则递归生成子路由,同样只处理type =1 的菜单
const route = generateRoutes(item, parentRoute)
// console.log(route)
if (item.children && item.children.length > 0) {
const children = did(item.children, route)
if (children.length > 0) {
route.addChildren(children)
}
}
route.init({ originalIndex: index })
return route
})
let component = menu.component
// menu.type
// 1,组件(页面),2,IFrame,3,外链接,4,按钮
if (menu.type === 'iframe') {
component = '@/components/Iframe'
}
const routes = did(menuData, parentRoute)
//处理component路径
component = component.replace(/^\/pages/, '')
component = component.replace(/^\//, '')
return createRoute({
...options,
component: lazyRouteComponent(() => import(`./pages/${component}`)),
notFoundComponent: NotFound,
})
}
// 对menuData递归生成路由,只处理type =1 的菜单
const did = (menus: MenuItem[], parentRoute: AnyRoute) => {
return menus.filter((item) => item.type === 'menu').map((item, index) => {
// 如果有children则递归生成子路由,同样只处理type =1 的菜单
const route = generateRoutes(item, parentRoute)
// console.log(route)
if (item.children && item.children.length > 0) {
const children = did(item.children, route)
if (children.length > 0) {
route.addChildren(children)
}
}
route.init({ originalIndex: index })
return route
})
}
const routes = did(menuData, parentRoute)
parentRoute.addChildren(routes)
parentRoute.addChildren(routes)
}
const routeTree = rootRoute.addChildren(
[
//非Layout
loginRoute,
emptyRoute,
//不带权限Layout
layoutNormalRoute.addChildren([
notAuthRoute,
]),
//带权限Layout
// dashboardRoute,
authRoute.addChildren(
[
layoutAuthRoute
/*.addChildren(
[
menusRoute,
departmentsRoute,
usersRoute,
rolesRoute,
]
),*/
]),
]
[
//非Layout
loginRoute,
emptyRoute,
//不带权限Layout
layoutNormalRoute.addChildren([
notAuthRoute,
]),
//带权限Layout
// dashboardRoute,
authRoute.addChildren(
[
layoutAuthRoute
/*.addChildren(
[
menusRoute,
departmentsRoute,
usersRoute,
rolesRoute,
]
),*/
]),
]
)
export const RootProvider = memo((props: { context: Partial<IRootContext> }) => {
const { data: menuData, isLoading, refetch } = useAtomValue(userMenuDataAtom)
const { data: menuData, isLoading, refetch } = useAtomValue(userMenuDataAtom)
const isFetchRef = useRef(false)
const isFetchRef = useRef(false)
useEffect(() => {
useEffect(() => {
if (isFetchRef.current) {
return
}
isFetchRef.current = true
refetch()
if (isFetchRef.current) {
return
}
isFetchRef.current = true
refetch()
}, [])
}, [])
if (isLoading) {
return <PageLoading/>
}
if (isLoading) {
return <PageLoading/>
}
generateDynamicRoutes(menuData ?? [], layoutAuthRoute)
generateDynamicRoutes(menuData ?? [], layoutAuthRoute)
const router = createRouter({
routeTree,
context: { queryClient, menuData: [] },
defaultPreload: 'intent',
defaultPendingComponent: () => <Loading loading={true} delay={300}/>
})
const router = createRouter({
routeTree,
context: { queryClient, menuData: [] },
defaultPreload: 'intent',
defaultPendingComponent: () => <Loading loading={true} delay={300}/>
})
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} context={{ ...props.context, menuData, queryClient }}/>
</QueryClientProvider>
)
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} context={{ ...props.context, menuData, queryClient }}/>
</QueryClientProvider>
)
})
export default RootProvider

82
src/store/websites/acme.ts

@ -7,65 +7,65 @@ import { atom } from 'jotai'
import { WebSite } from '@/types'
export enum AcmeType {
LetsEncrypt = 'LetsEncrypt',
//zerossl
ZeroSSl = 'ZeroSSl',
//buypass
Buypass = 'Buypass',
//google
Google = 'Google',
LetsEncrypt = 'LetsEncrypt',
//zerossl
ZeroSSl = 'ZeroSsl',
//buypass
Buypass = 'Buypass',
//google
Google = 'Google',
}
export const AcmeAccountTypes = [
{ label: 'Let\'s Encrypt', value: AcmeType.LetsEncrypt },
{ label: 'ZeroSSL', value: AcmeType.ZeroSSl },
{ label: 'Buypass', value: AcmeType.Buypass },
{ label: 'Google Cloud', value: AcmeType.Google },
{ label: 'Let\'s Encrypt', value: AcmeType.LetsEncrypt },
{ label: 'ZeroSSL', value: AcmeType.ZeroSSl },
{ label: 'Buypass', value: AcmeType.Buypass },
{ label: 'Google Cloud', value: AcmeType.Google },
]
export const getAcmeAccountTypeName = (type: AcmeType) => {
return AcmeAccountTypes.find(item => item.value === type)?.label
return AcmeAccountTypes.find(item => item.value === type)?.label
}
export const acmePageAtom = atom<IPage>({
page: 1, pageSize: 10,
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
}
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, WebSite.IAcmeAccount>(get => ({
mutationKey: [ 'saveOrUpdateAcme' ],
mutationFn: async (data: WebSite.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
export const saveOrUpdateAcmeAtom = atomWithMutation<IApiResult, WebSite.IAcmeAccount>(get => ({
mutationKey: [ 'saveOrUpdateAcme' ],
mutationFn: async (data: WebSite.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
}
}))
export const deleteAcmeAtom = atomWithMutation<IApiResult, number>(get => ({
mutationKey: [ 'sslDelete' ],
mutationFn: async (id) => {
return await websitesServ.acme.delete(id)
},
onSuccess: () => {
message.success(t('message.deleteSuccess', '删除成功'))
get(acmeListAtom).refetch()
}
mutationKey: [ 'sslDelete' ],
mutationFn: async (id) => {
return await websitesServ.acme.delete(id)
},
onSuccess: () => {
message.success(t('message.deleteSuccess', '删除成功'))
get(acmeListAtom).refetch()
}
}))
Loading…
Cancel
Save