Browse Source

Merge remote-tracking branch 'origin/main'

main
cs 3 months ago
parent
commit
b2e0a8c14a
  1. 71
      package.json
  2. 14
      src/App.css
  3. 24
      src/components/action/Action.tsx
  4. 100
      src/components/r-form/index.tsx
  5. 26
      src/components/r-form/utils/index.tsx
  6. 89
      src/i18n.ts
  7. 6
      src/locales/lang/zh-CN.ts
  8. 179
      src/pages/login/index.tsx
  9. 60
      src/pages/r-form/index.tsx
  10. 648
      src/pages/websites/cert/apply.tsx
  11. 102
      src/service/websites.ts
  12. 325
      src/store/websites/cert.ts
  13. 3
      src/types/r-form/model.d.ts
  14. 2
      src/utils/index.ts
  15. 1
      vite.config.ts

71
package.json

@ -12,58 +12,59 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/cssinjs": "^1.21.1", "@ant-design/cssinjs": "^1.21.1",
"@ant-design/icons": "^5.3.6",
"@ant-design/pro-components": "^2.7.0",
"@ant-design/icons": "^5.4.0",
"@ant-design/pro-components": "^2.7.15",
"@ant-design/pro-layout": "^7.19.12", "@ant-design/pro-layout": "^7.19.12",
"@ant-design/pro-provider": "^2.14.9", "@ant-design/pro-provider": "^2.14.9",
"@formily/antd-v5": "^1.2.0",
"@formily/core": "^2.3.1",
"@formily/react": "^2.3.1",
"@formily/antd-v5": "^1.2.3",
"@formily/core": "^2.3.2",
"@formily/react": "^2.3.2",
"@icon-park/react": "^1.4.2", "@icon-park/react": "^1.4.2",
"@melloware/react-logviewer": "^5.2.0", "@melloware/react-logviewer": "^5.2.0",
"@tanstack/query-core": "^5.52.0",
"@tanstack/react-query": "^5.52.1",
"@tanstack/react-router": "^1.50.0",
"antd": "^5.16.1",
"@tanstack/query-core": "^5.54.1",
"@tanstack/react-query": "^5.54.1",
"@tanstack/react-router": "^1.53.1",
"antd": "^5.20.5",
"antd-style": "^3.6.2", "antd-style": "^3.6.2",
"axios": "^1.6.8",
"bunshi": "^2.1.4",
"dayjs": "^1.11.10",
"axios": "^1.7.7",
"bunshi": "^2.1.5",
"date-fns": "^3.6.0",
"dayjs": "^1.11.13",
"fast-copy": "^3.0.2", "fast-copy": "^3.0.2",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"i18next": "^23.11.2",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^7.2.1", "i18next-browser-languagedetector": "^7.2.1",
"jotai": "^2.8.0",
"jotai": "^2.9.3",
"jotai-devtools": "^0.9.1", "jotai-devtools": "^0.9.1",
"jotai-scope": "^0.5.1",
"jotai-tanstack-query": "^0.8.5",
"jotai-scope": "^0.5.2",
"jotai-tanstack-query": "^0.8.7",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"re-resizable": "^6.9.11",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.0",
"re-resizable": "^6.9.17",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^14.1.3",
"react-if": "^4.1.5", "react-if": "^4.1.5",
"react-layout-kit": "^1.9.0", "react-layout-kit": "^1.9.0",
"react-rnd": "^10.4.2-test2",
"react-use": "^17.5.0",
"throttle-debounce": "^5.0.0",
"react-rnd": "^10.4.12",
"react-use": "^17.5.1",
"throttle-debounce": "^5.0.2",
"use-merge-value": "^1.2.0", "use-merge-value": "^1.2.0",
"wonka": "^6.3.4" "wonka": "^6.3.4"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/router-devtools": "^1.26.20",
"@tanstack/router-vite-plugin": "^1.26.16",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react": "^4.2.1",
"@tanstack/router-devtools": "^1.53.1",
"@tanstack/router-vite-plugin": "^1.54.0",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.11",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"vite-plugin-mock": "^3.0.1"
"typescript": "^5.5.4",
"vite": "^5.4.3",
"vite-plugin-mock": "^3.0.2"
} }
} }

14
src/App.css

@ -7,12 +7,10 @@
display: inherit; display: inherit;
line-height: 28px; line-height: 28px;
} }
} }
.top-breadcrumb { .top-breadcrumb {
.item { .item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -28,7 +26,6 @@
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: #1c7ed6; color: #1c7ed6;
} }
} }
@ -50,16 +47,19 @@
color: green; color: green;
} }
.color-green1{
color: rgb(18 185 128 / 1 );
.color-green1 {
color: rgb(18 185 128 / 1);
}
.color-blue {
color: #1c7ed6;
} }
.color-red { .color-red {
color: #F56C6C;
color: #f56c6c;
} }
.color-yellow { .color-yellow {
color: rgb(250 145 0)
color: rgb(250 145 0);
} }
.text-bold { .text-bold {

24
src/components/action/Action.tsx

@ -2,23 +2,27 @@ import { Button, ButtonProps } from 'antd'
import { useStyle } from './style' import { useStyle } from './style'
export interface ActionProps extends ButtonProps { export interface ActionProps extends ButtonProps {
as?: string
as?: string
} }
const Action = ({ title, as, children, ...props }: ActionProps) => { const Action = ({ title, as, children, ...props }: ActionProps) => {
const { styles } = useStyle()
const { styles } = useStyle()
const isLink = as === 'a' || props.type === 'link'
//fixme 如果外部同时设置 as={'a'} disabled={true} ,这里a标签会置灰,但是仍可点击,为什么不直接用Button?
const Comp = isLink ? 'a' : Button
return (
<span className={styles.container}>
const isLink = as === 'a' || props.type === 'link'
const Comp = isLink ? 'a' : Button
return (
<span className={styles.container}>
<Comp {...props} <Comp {...props}
type={isLink ? 'link' : props.type}
className={as === 'a' ? styles.actionA : ''}>{title ?? children}</Comp>
onClick={(e) => {
if (props.onClick && !props.disabled) {
props.onClick(e)
}
}}
type={isLink ? 'link' : props.type}
className={as === 'a' ? styles.actionA : ''}>{title ?? children}</Comp>
</span> </span>
)
)
} }
export default Action export default Action

100
src/components/r-form/index.tsx

@ -13,15 +13,32 @@ import { BetaSchemaForm, ProColumns, ProFormColumnsType } from '@ant-design/pro-
import { useApiContext } from '@/context.ts' import { useApiContext } from '@/context.ts'
import { useDeepCompareEffect } from 'react-use' import { useDeepCompareEffect } from 'react-use'
import { RFormTypes } from '@/types/r-form/model' import { RFormTypes } from '@/types/r-form/model'
import { ProCoreActionType } from '@ant-design/pro-utils/es/typing'
import { getI18nTitle } from '@/i18n.ts'
export interface RFormProps { export interface RFormProps {
title?: ReactNode title?: ReactNode
namespace?: string namespace?: string
columns?: ProColumns[] //重写columns columns?: ProColumns[] //重写columns
actions?: ReactNode[] | JSX.Element[] //左上角的操作按钮
toolbar?: ReactNode //工具栏
renderActions?: (addAction: ReactNode) => ReactNode //渲染操作按钮
resolveColumns?: (columns: ProColumns[]) => ProColumns[] //处理columns
renderColumnOptions?: (record: any, defaultOptions: ReactNode[], index: number, action: ProCoreActionType | undefined) => ReactNode //渲染列的操作
} }
const RForm = ({ namespace, columns: propColumns = [], title }: RFormProps) => {
const RForm = (
{
namespace,
actions = [],
toolbar,
resolveColumns,
renderActions,
renderColumnOptions,
columns: propColumns = [], title
}: RFormProps) => {
const { styles, cx } = useStyle() const { styles, cx } = useStyle()
const apiCtx = useApiContext() const apiCtx = useApiContext()
@ -50,7 +67,33 @@ const RForm = ({ namespace, columns: propColumns = [], title }: RFormProps) => {
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
const res = transformAntdTableProColumns(curdModal?.columns || [], propColumns)
let res = transformAntdTableProColumns(curdModal?.columns || [], propColumns, curdModal?.config?.i18n)
if (resolveColumns) {
res = resolveColumns(res)
}
const options = (record: any) => {
return [
<Action key="edit"
as={'a'}
onClick={() => {
form.setFieldsValue(record)
setOpen(true)
}}>{'编辑'}</Action>,
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteModel([ record.id ])
}}
title={'确定要删除吗?'}>
<a key="del">
</a>
</Popconfirm>
]
}
const _columns = [ { const _columns = [ {
title: 'ID', title: 'ID',
dataIndex: 'id', dataIndex: 'id',
@ -59,33 +102,20 @@ const RForm = ({ namespace, columns: propColumns = [], title }: RFormProps) => {
formItemProps: { hidden: true } formItemProps: { hidden: true }
} ].concat(res as any).concat([ } ].concat(res as any).concat([
{ {
title: '操作',
title: getI18nTitle(curdModal?.config?.i18n, { dataIndex: 'option', title: '操作' },),
dataIndex: 'option', dataIndex: 'option',
valueType: 'option', valueType: 'option',
fixed: 'right', fixed: 'right',
render: (_, record) => [
<Action key="edit"
as={'a'}
onClick={() => {
form.setFieldsValue(record)
setOpen(true)
}}>{'编辑'}</Action>,
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteModel([ record.id ])
}}
title={'确定要删除吗?'}>
<a key="del">
</a>
</Popconfirm>
]
render: (_, record, index, action) => {
if (renderColumnOptions) {
return renderColumnOptions(record, options(record), index, action)
}
return options(record)
}
} as any } as any
]) ])
setColumns(_columns) setColumns(_columns)
}, [ curdModal?.columns, propColumns, deleteModel, form, isDeleting, setOpen, ])
}, [ curdModal?.columns, curdModal?.config?.i18n, propColumns, renderColumnOptions, resolveColumns, deleteModel, form, isDeleting, setOpen, ])
useEffect(() => { useEffect(() => {
if (apiCtx.isApi && apiCtx.api) { if (apiCtx.isApi && apiCtx.api) {
@ -141,9 +171,18 @@ const RForm = ({ namespace, columns: propColumns = [], title }: RFormProps) => {
}) })
setOpen(true) setOpen(true)
}} }}
type={'primary'}>{'添加'}</Button>
type={'primary'}>{getI18nTitle('actions.add','添加')}</Button>
</> </>
const _renderActions = () => {
if (renderActions) {
return renderActions(tableTitle)
}
return <Space>
{[ tableTitle, ...actions ]}
</Space>
}
return ( return (
<> <>
<ListPageLayout <ListPageLayout
@ -153,7 +192,7 @@ const RForm = ({ namespace, columns: propColumns = [], title }: RFormProps) => {
<ProTable <ProTable
{...curdModal?.table} {...curdModal?.table}
rowKey="id" rowKey="id"
headerTitle={tableTitle}
headerTitle={_renderActions()}
toolbar={{ toolbar={{
/*search: { /*search: {
loading: isFetching && !!search?.key, loading: isFetching && !!search?.key,
@ -171,7 +210,7 @@ const RForm = ({ namespace, columns: propColumns = [], title }: RFormProps) => {
placeholder: '输入关键字搜索', placeholder: '输入关键字搜索',
},*/ },*/
actions: [ actions: [
<Tooltip key={'filter'} title={'高级查询'}>
<Tooltip key={'filter'} title={getI18nTitle('actions.advanceSearch','高级查询')}>
<Badge count={getValueCount(search)}> <Badge count={getValueCount(search)}>
<Button <Button
onClick={() => { onClick={() => {
@ -239,7 +278,7 @@ const RForm = ({ namespace, columns: propColumns = [], title }: RFormProps) => {
form={form} form={form}
layout={'vertical'} layout={'vertical'}
scrollToFirstError={true} scrollToFirstError={true}
title={model?.id !== 0 ? '编辑' : '添加'}
title={model?.id !== 0 ? getI18nTitle('actions.edit','编辑') : getI18nTitle('actions.add','添加')}
{...formProps as any} {...formProps as any}
open={open} open={open}
onOpenChange={(open) => { onOpenChange={(open) => {
@ -247,12 +286,13 @@ const RForm = ({ namespace, columns: propColumns = [], title }: RFormProps) => {
}} }}
loading={isSubmitting} loading={isSubmitting}
onFinish={async (values) => { onFinish={async (values) => {
console.log(values)
saveOrUpdate(values as any) saveOrUpdate(values as any)
}} }}
columns={columns as ProFormColumnsType[]}/> columns={columns as ProFormColumnsType[]}/>
<BetaSchemaForm <BetaSchemaForm
{...curdModal?.form} {...curdModal?.form}
title={'高级查询'}
title={getI18nTitle('actions.advanceSearch','高级查询')}
grid={true} grid={true}
shouldUpdate={false} shouldUpdate={false}
width={500} width={500}
@ -274,8 +314,8 @@ const RForm = ({ namespace, columns: propColumns = [], title }: RFormProps) => {
}} }}
submitter={{ submitter={{
searchConfig: { searchConfig: {
resetText: '清空',
submitText: '查询',
resetText: getI18nTitle('actions.clear', '清空'),
submitText: getI18nTitle('actions.search', '查询'),
}, },
onReset: () => { onReset: () => {
filterForm.resetFields() filterForm.resetFields()

26
src/components/r-form/utils/index.tsx

@ -6,6 +6,7 @@ import request from '@/request'
import { convertToBool } from '@/utils' import { convertToBool } from '@/utils'
import { has, get } from 'lodash' import { has, get } from 'lodash'
import { mapTree } from '@/utils/tree.ts' import { mapTree } from '@/utils/tree.ts'
import { getI18nTitle } from '@/i18n.ts'
const getValueType = (column: ProColumns) => { const getValueType = (column: ProColumns) => {
@ -40,7 +41,7 @@ const getComponent = (column: ProColumns) => {
} }
export const transformAntdTableProColumns = (columns: ProColumns[], overwriteColumns?: ProColumns[]) => {
export const transformAntdTableProColumns = (columns: ProColumns[], overwriteColumns?: ProColumns[], i18n?: string) => {
const overwriteKeys = [] as string[] const overwriteKeys = [] as string[]
@ -56,27 +57,40 @@ export const transformAntdTableProColumns = (columns: ProColumns[], overwriteCol
return { return {
...item, ...item,
title: getI18nTitle(i18n!, item),
request: item.request ? async (params) => { request: item.request ? async (params) => {
const { url: _url, method, params: p, fieldNames, resultPath } = item.request as unknown as RFormTypes.IRequest
const {
transform = true,
url: _url,
method,
params: p,
fieldNames,
resultPath
} = item.request as unknown as RFormTypes.IRequest
const { value, label, disabled: disabledKey, children = 'children' } = fieldNames || {} const { value, label, disabled: disabledKey, children = 'children' } = fieldNames || {}
const url = (_url.startsWith('/') || _url.startsWith('http')) ? _url : `/${_url}` const url = (_url.startsWith('/') || _url.startsWith('http')) ? _url : `/${_url}`
return request[method?.toLowerCase() || 'get'](url, { return request[method?.toLowerCase() || 'get'](url, {
...params, ...params,
...p, ...p,
}).then(res => { }).then(res => {
try { try {
const data = resultPath && has(res.data, resultPath) ? get(res.data, resultPath) : res.data const data = resultPath && has(res.data, resultPath) ? get(res.data, resultPath) : res.data
if (!transform) {
return data
}
return mapTree(data || [], (i: any) => { return mapTree(data || [], (i: any) => {
const disabled = disabledKey && has(i, disabledKey) ? get(i, disabledKey) : ('status' in i ? !convertToBool(i.status) : false) const disabled = disabledKey && has(i, disabledKey) ? get(i, disabledKey) : ('status' in i ? !convertToBool(i.status) : false)
const title = i.i18n ? getI18nTitle(i.i18n, i.label || i[label || 'name']) : i.label || i[label || 'name']
return { return {
title: i.label || i[label || 'name'],
label: i.label || i[label || 'name'],
value: i.value || i[value || 'id'],
title,
label: title,
value: i.value ?? i[value || 'id'],
disabled, disabled,
data: i data: i
} }
}, { children }) }, { children })
} catch (e) { } catch (e) {

89
src/i18n.ts

@ -3,49 +3,72 @@ import i18n, { InitOptions, t } from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector' import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next, useTranslation, } from 'react-i18next' import { initReactI18next, useTranslation, } from 'react-i18next'
import { zh, en } from './locales' import { zh, en } from './locales'
import { ProColumns } from '@ant-design/pro-components'
const detectionOptions = { const detectionOptions = {
// 探测器的选项
order: [ 'querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag' ],
lookupQuerystring: 'lng',
lookupCookie: 'i18next',
lookupLocalStorage: 'i18nextLng',
caches: [ 'localStorage', 'cookie' ],
excludeCacheFor: [ 'cimode' ], // 语言探测模式中排除缓存的语言
// 探测器的选项
order: [ 'querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag' ],
lookupQuerystring: 'lng',
lookupCookie: 'i18next',
lookupLocalStorage: 'i18nextLng',
caches: [ 'localStorage', 'cookie' ],
excludeCacheFor: [ 'cimode' ], // 语言探测模式中排除缓存的语言
} }
export const initI18n = (options?: InitOptions) => { export const initI18n = (options?: InitOptions) => {
i18n.on('initialized', () => {
const currentLanguage = i18n.language
changeLanguage(currentLanguage)
})
return i18n
.use(initReactI18next)
.use(LanguageDetector)
.init({
resources: {
en: {
translation: en.default,
},
zh: {
translation: zh.default,
},
},
fallbackLng: 'zh',
debug: false,
detection: detectionOptions,
interpolation: {
escapeValue: false,
},
...options,
})
i18n.on('initialized', () => {
const currentLanguage = i18n.language
changeLanguage(currentLanguage)
})
return i18n
.use(initReactI18next)
.use(LanguageDetector)
.init({
resources: {
en: {
translation: en.default,
},
zh: {
translation: zh.default,
},
},
fallbackLng: 'zh',
debug: false,
detection: detectionOptions,
interpolation: {
escapeValue: false,
},
...options,
})
}
export function getI18nTitle(key: string, defTitle: string): string;
export function getI18nTitle(key: string, column: ProColumns): string;
export function getI18nTitle(key: string, option: any): string {
if (option.dataIndex) {
let k = `${key}.columns.${option.dataIndex}`
if (option.i18n) {
k = option.i18n
}
//如果没有key也没有option.i18n, 说明没有i18n前缀, 直接返回
if (!option.i18n && !key) {
return option.title || option.label
}
return t(k, (option.title || option.label) as string)
}
if (!key) {
return option as unknown as string
}
return t(`${key}`, option as unknown as string)
} }
export { export {
useTranslation, t
useTranslation, t
} }
export default i18n export default i18n

6
src/locales/lang/zh-CN.ts

@ -77,6 +77,12 @@ export default {
close: '关闭', close: '关闭',
copy: '复制', copy: '复制',
clickCopy: '点击复制', clickCopy: '点击复制',
resetPass: '重置密码',
save: '保存',
submit: '提交',
refresh: '刷新',
search: '查询',
advanceSearch: '高级查询',
}, },
message: { message: {
infoTitle: '提示', infoTitle: '提示',

179
src/pages/login/index.tsx

@ -1,97 +1,102 @@
import SelectLang from '@/components/select-lang'
import { createFileRoute } from '@tanstack/react-router'
import { Button, Form, Input, Space } from 'antd'
import { useAtom, useAtomValue } from 'jotai'
import { useTranslation } from '@/i18n.ts'
import { loginAtom, loginFormAtom } from '@/store/system/user.ts'
import { memo, useLayoutEffect } from 'react'
import { useStyles } from './style.ts'
import SelectLang from "@/components/select-lang";
import { createFileRoute } from "@tanstack/react-router";
import { Button, Form, Input, Radio, Space } from "antd";
import { useAtom, useAtomValue } from "jotai";
import { useTranslation } from "@/i18n.ts";
import { loginAtom, loginFormAtom } from "@/store/system/user.ts";
import React, {memo, useLayoutEffect, useState} from "react";
import { useStyles } from "./style.ts";
const Login = memo(() => { const Login = memo(() => {
const { styles } = useStyles();
const { t } = useTranslation();
const [values, setValues] = useAtom(loginFormAtom);
const { isPending, mutate } = useAtomValue(loginAtom);
const [form] = Form.useForm();
const { styles } = useStyles()
const { t } = useTranslation()
const [ values, setValues ] = useAtom(loginFormAtom)
const { isPending, mutate } = useAtomValue(loginAtom)
const [ form ] = Form.useForm()
const handleSubmit = () => {
form.validateFields().then(() => {
mutate(values);
});
};
const handleSubmit = () => {
form.validateFields().then(() => {
mutate(values)
})
}
const [loginMod, setLoginMod] = useState([
{ value: "single", info: "帐号密码登录" },
{ value: "multiple-email", info: "邮箱登录" },
{ value: "multiple-plane", info: "飞机登录" },
]);
const [selectedMode, setSelectedMode] = useState(loginMod[0].value);
useLayoutEffect(() => {
const handleModeChange = (e: any) => {
setSelectedMode(e.target.value);
};
document.body.className = 'login'
return () => {
document.body.className = document.body.className.replace('login', '')
}
useLayoutEffect(() => {
document.body.className = "login";
return () => {
document.body.className = document.body.className.replace("login", "");
};
}, []);
}, [])
return (
<div className={styles.container}>
<div className={styles.language}>
<SelectLang />
</div>
<div className={styles.loginBlock}>
<div className={styles.innerBlock}>
<Radio.Group style={{ marginBottom: 8 }} value={selectedMode} onChange={handleModeChange}>
{loginMod.map((mod) => (
<Radio.Button key={mod.value} value={mod.value}>
{mod.info}
</Radio.Button>
))}
</Radio.Group>
<Form
form={form}
disabled={isPending}
initialValues={values}
onValuesChange={(_, allValues) => {
setValues(allValues);
}}
size="large"
>
<Form.Item name={"username"} rules={[{ required: true, message: t("login.usernameMsg") }]}>
<Input maxLength={20} placeholder={t("login.username")} />
</Form.Item>
<Form.Item name={"password"} rules={[{ required: true, message: t("login.passwordMsg") }]}>
<Input.Password placeholder={t("login.password")} />
</Form.Item>
<Form.Item noStyle>
<Space direction="horizontal">
<Form.Item name={"code"} rules={[{ required: true, message: t("login.codeMsg") }]}>
<Input placeholder={t("login.code")} />
{/*<img src="https://img.alicdn.com/tfs/TB1KtN6mKH2gK0jSZJnXXaT1FXa-1014-200.png" alt="验证码" />*/}
</Form.Item>
</Space>
</Form.Item>
<Form.Item style={{ marginBottom: 10 }}>
<Button
htmlType={"submit"}
type="primary"
onClick={handleSubmit}
className={"submitBtn"}
loading={isPending}
disabled={isPending}
>
{t("login.submit")}
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
);
});
return (
<div className={styles.container}>
<div className={styles.language}>
<SelectLang/>
</div>
<div className={styles.loginBlock}>
<div className={styles.innerBlock}>
export const Route = createFileRoute("/login")({
component: Login,
});
<div className={styles.desc}>
<span className={styles.active}>
{t('login.title')}
</span>
</div>
<Form form={form}
disabled={isPending}
initialValues={values}
onValuesChange={(_, allValues) => {
setValues(allValues)
}}
size="large">
<Form.Item name={'username'}
rules={[ { required: true, message: t('login.usernameMsg') } ]}>
<Input maxLength={20} placeholder={t('login.username')}/>
</Form.Item>
<Form.Item name={'password'}
rules={[ { required: true, message: t('login.passwordMsg') } ]}>
<Input.Password placeholder={t('login.password')}/>
</Form.Item>
<Form.Item noStyle>
<Space direction="horizontal">
<Form.Item name={'code'}
rules={[ { required: true, message: t('login.codeMsg') } ]}>
<Input placeholder={t('login.code')}/>
{/*<img src="https://img.alicdn.com/tfs/TB1KtN6mKH2gK0jSZJnXXaT1FXa-1014-200.png" alt="验证码" />*/}
</Form.Item>
</Space>
</Form.Item>
<Form.Item style={{ marginBottom: 10 }}>
<Button
htmlType={'submit'}
type="primary"
onClick={handleSubmit}
className={'submitBtn'}
loading={isPending}
disabled={isPending}
>
{t('login.submit')}
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
)
})
export const Route = createFileRoute('/login')({
component: Login
})
export default Login
export default Login;

60
src/pages/r-form/index.tsx

@ -1,10 +1,68 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import RForm from '@/components/r-form' import RForm from '@/components/r-form'
import { Button, Input, message, Modal } from 'antd'
import { ReactNode, useState } from 'react'
import { get, hasIn } from 'lodash'
import { ProCoreActionType } from '@ant-design/pro-utils/es/typing'
const RFormRender = () => { const RFormRender = () => {
const [ json, setJson ] = useState('')
const [ open, setOpen ] = useState(false)
const [ columns, setColumns ] = useState()
const actions = [
<Button key={'json'} onClick={() => {
setOpen(true)
}}>JSON</Button>
]
const renderOptions = (record: any, defaultOptions: ReactNode[], index: number, action: ProCoreActionType | undefined) => {
return [
...defaultOptions,
<Button key={'ext'} onClick={() => {
console.log('扩展按钮', record)
}}></Button>
]
}
return <> return <>
<RForm/>
<RForm actions={actions}
renderColumnOptions={renderOptions}
resolveColumns={(cols) => columns ?? cols}/>
<Modal
title={'导入JSON'}
open={open}
afterOpenChange={setOpen}
onCancel={() => setOpen(false)}
onOk={() => {
try {
const data = JSON.parse(json)
//如果是Array, 检查元素是否为ProColumns
if (Array.isArray(data)) {
if (data.length > 0 && hasIn(data[0], 'dataIndex')) {
setColumns(data as any)
setOpen(false)
return
}
}
if (hasIn(data, 'data.page.columns')) {
setColumns(get(data, 'data.page.columns'))
setOpen(false)
return
}
message.error('JSON格式错误, 请确保是ProColumns或者包含data.page.columns的对象')
} catch (e) {
message.error('JSON格式错误')
}
}}
>
<Input.TextArea rows={10} value={json} onChange={(e) => {
setJson(e.target.value)
}}/>
<span style={{ color: '#aeaeae', lineHeight: 2.5 }}>Antd.Table.Columns数组或者xxx/ui/curd返回的结构</span>
</Modal>
</> </>
} }

648
src/pages/websites/cert/apply.tsx

@ -2,122 +2,228 @@ import { t } from "@/i18n.ts";
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { import {
algorithmTypes, algorithmTypes,
dnsConfigAtom,
dnsVerifyAtom,
dnsVerifyOKAtom,
saveOrUpdateCertAtom,
certAddCnameAtom,
checkDomainAtom,
applyTxtCertificateAtom,
Req_ApplyTxtCertificate,
getCertConfigAtom,
} from "@/store/websites/cert.ts"; } from "@/store/websites/cert.ts";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Button, Flex, Form, Input, Progress, Select, Space, Table, Tooltip } from "antd";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Button, Flex, Form, Input, message, Radio, Select, Space, Steps, Table, Tooltip } from "antd";
import google from "@/pages/websites/cert/assets/google.png"; import google from "@/pages/websites/cert/assets/google.png";
import zerossl from "@/pages/websites/cert/assets/zerossl.png"; import zerossl from "@/pages/websites/cert/assets/zerossl.png";
import lets_encrypt from "@/pages/websites/cert/assets/lets_encrypt.png"; import lets_encrypt from "@/pages/websites/cert/assets/lets_encrypt.png";
import { useStyle } from "./style"; import { useStyle } from "./style";
import ListPageLayout from "@/layout/ListPageLayout.tsx"; import ListPageLayout from "@/layout/ListPageLayout.tsx";
import { ColumnsType } from "antd/es/table"; import { ColumnsType } from "antd/es/table";
import { atomWithStorage } from "jotai/utils";
import { atomWithStorage, RESET } from "jotai/utils";
import Copy from "@/components/copy"; import Copy from "@/components/copy";
import { InfoCircleOutlined, LoadingOutlined } from "@ant-design/icons";
import {
ClusterOutlined,
CopyOutlined,
InfoCircleOutlined,
LoadingOutlined,
LoginOutlined,
TagOutlined,
} from "@ant-design/icons";
import { useQueryClient } from "@tanstack/react-query";
const i18nPrefix = "cert.apply"; const i18nPrefix = "cert.apply";
const BrandSelect = (props: any) => {
const { styles, cx } = useStyle();
const [value, setValue] = useState(() => props.value);
const DomainsInput = (props: { domains; setDomains; currentDomainMod; setCurrentDomainMod; currentStep }) => {
useEffect(() => { useEffect(() => {
setValue(props.value);
}, [props.value]);
if (props.currentDomainMod === "single" && props.domains !== "") {
props.setDomains(props.domains.replace(/[\r\n]/g, ""));
}
}, [props.currentDomainMod]);
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Radio.Group
style={{ marginBottom: 8 }}
value={props.currentDomainMod}
onChange={(e) => props.setCurrentDomainMod(e.target.value)}
>
<Radio.Button value="single" disabled={props.currentStep !== 0}>
</Radio.Button>
<Radio.Button value="multiple" disabled={props.currentStep !== 0}>
</Radio.Button>
</Radio.Group>
{props.currentDomainMod === "single" && (
<Input
disabled={props.currentStep !== 0}
placeholder="请输入域名,支持泛解析域名。如果为多个域名注册到一个证书,域名之间用<逗号(英文输入)>分割。如:a.com,*.b.com"
onChange={(e) => {
props.setDomains(e.target.value);
}}
value={props.domains} // 设置输入框的值
/>
)}
{props.currentDomainMod === "multiple" && (
<Input.TextArea
disabled={props.currentStep !== 0}
rows={6}
placeholder={`多个域名多个证书申请,用回车分隔。以下示例为6个域名申请3个证书,每行对应一个证书,支持泛解析域名;如:
*.a.baidu.com, *.baidu.com
hello.alibaba.com,b.com,*.c.com
sss.ddd.com`}
onChange={(e) => {
props.setDomains(e.target.value);
}}
value={props.domains} // 设置输入框的值
/>
)}
</Space>
);
};
const BrandSelect = (props: { acme_type; setAcme_type }) => {
const { styles, cx } = useStyle();
const onChange = useCallback((val: string) => {
props.onChange?.(val);
}, []);
const onChange = (val: string) => {
props.setAcme_type(val);
};
return ( return (
<>
<Space className={styles.bandSelect}>
<Flex
vertical={true}
onClick={() => onChange("Google")}
className={cx("band-normal", {
"band-active": value === "Google",
})}
>
<img src={google} style={{ height: "2rem" }} />
<span>Google Trust Services</span>
</Flex>
<Flex
vertical={true}
onClick={() => onChange("ZeroSSL")}
className={cx("band-normal", {
"band-active": value === "ZeroSSL",
})}
>
<img src={zerossl} style={{ height: "2rem" }} />
<span>ZeroSSL</span>
</Flex>
<Flex
vertical={true}
onClick={() => onChange("Let's Encrypt")}
className={cx("band-normal", {
"band-active": value === "Let's Encrypt",
})}
>
<img src={lets_encrypt} style={{ height: "2rem" }} />
<span>Let's Encrypt</span>
</Flex>
</Space>
</>
<>
<Space className={styles.bandSelect}>
<Flex
vertical={true}
onClick={() => onChange("Google")}
className={cx("band-normal", {
"band-active": props.acme_type === "Google",
})}
>
<img src={google} style={{ height: "2rem" }} />
<span>Google Trust Services</span>
</Flex>
<Flex
vertical={true}
onClick={() => onChange("ZeroSSL")}
className={cx("band-normal", {
"band-active": props.acme_type === "ZeroSSL",
})}
>
<img src={zerossl} style={{ height: "2rem" }} />
<span>ZeroSSL</span>
</Flex>
<Flex
vertical={true}
onClick={() => onChange("Let's Encrypt")}
className={cx("band-normal", {
"band-active": props.acme_type === "Let's Encrypt",
})}
>
<img src={lets_encrypt} style={{ height: "2rem" }} />
<span>Let's Encrypt</span>
</Flex>
</Space>
</>
); );
}; };
const StatusTable = (props: { value: string }) => {
const [data, setData] = useState<any>(null); // 临时状态来存储模拟数据
const { isFetching } = useAtomValue(useMemo(() => dnsConfigAtom(props.value), [props.value]));
const {
data: dnsVerifyStatus,
isFetching: isVerifyFetching,
refetch,
} = useAtomValue(useMemo(() => dnsVerifyAtom(props.value, isFetching), [props.value, isFetching]));
const StatusTable = (props: { value: string; setCurrentStep; setDns_list }) => {
// const [data, setData] = useState<any>(null); // 临时状态来存储模拟数据
const [forceUpdate, setForceUpdate] = useState(0);
const { data, isFetching: isVerifyFetching } = useAtomValue(
useMemo(() => checkDomainAtom(props.value, forceUpdate === 0), [props.value, forceUpdate]),
);
const refetch = () => {
setForceUpdate((prev) => prev + 1);
};
const setDnsVerifyOK = useSetAtom(dnsVerifyOKAtom);
const { mutate: addCnameFun, isPending: addCnamePending, isSuccess } = useAtomValue(certAddCnameAtom);
const handleAddCname = async (info: any) => {
addCnameFun(info);
};
// const {
// data: dnsVerifyStatus,
// isFetching: isVerifyFetching,
// refetch,
// } = useAtomValue(useMemo(() => dnsVerifyAtom(props.value, isFetching), [props.value, isFetching]));
//
// const setDnsVerifyOK = useSetAtom(dnsVerifyOKAtom);
const timerRef = useRef<number>(); const timerRef = useRef<number>();
const handleCopy = (str: string) => {
navigator.clipboard
.writeText(str)
.then(() => {
message.info("复制成功!");
})
.catch((err) => {
message.info(err);
});
};
const columns = useMemo<ColumnsType>(() => { const columns = useMemo<ColumnsType>(() => {
return [ return [
{ {
title: ( title: (
<>
{t(`${i18nPrefix}.status.columns.status`, "状态")}
<Tooltip title={t(`${i18nPrefix}.status.columns.statusTip`, "正确配置DNS解析后,域名验证会自动通过")}>
<InfoCircleOutlined style={{ paddingInlineStart: 5 }} />
</Tooltip>{" "}
</>
<>
{t(`${i18nPrefix}.status.columns.status`, "状态")}
<Tooltip title={t(`${i18nPrefix}.status.columns.statusTip`, "正确配置DNS解析后,域名验证会自动通过")}>
<InfoCircleOutlined style={{ paddingInlineStart: 5 }} />
</Tooltip>{" "}
</>
), ),
dataIndex: "status", dataIndex: "status",
width: 100,
width: 150,
render: (_, record) => { render: (_, record) => {
if (isFetching) {
return <span>{t(`${i18nPrefix}.actions.dnsVerifyStatus.0`, "等待")}</span>;
}
if (isVerifyFetching) {
//0,等待 1,域名OK,2,域名分析错误,3:检测中 4:检测成功,匹配失败 5:检测失败,9:检测成功
return (
<span>
<LoadingOutlined style={{ paddingInlineEnd: 5 }} />
{t(`${i18nPrefix}.actions.dnsVerifyStatus.3`, "检测中")}
</span>
);
}
const dns = record.dns_name; const dns = record.dns_name;
const info = (dnsVerifyStatus as any)?.find((item) => item.dns_name === dns) as any;
const info = (data as any)?.dns_list?.find((item) => item.dns_name === dns) as any;
if (info) { if (info) {
return <span>{t(`${i18nPrefix}.actions.dnsVerifyStatus.${info.status}`, `${info?.status_txt}`)}</span>;
if (info.is_self === true) {
return (
// <Button
// size="small"
// type="text"
// className={"color-blue"}
// // icon={<ClusterOutlined />}
// style={{ width: "100%", textAlign: "left" }}
// // onClick={() => handleCopy(text)}
// >
// 自动注册
// </Button>
<span
style={{
width: "100%",
textAlign: "right",
color: "#1c7ed6",
cursor: "pointer",
}}
onClick={() => {
handleAddCname(info);
}}
>
<TagOutlined style={{ paddingInlineEnd: 5 }} />
</span>
);
} else {
if (isVerifyFetching) {
//0,等待 1,域名OK,2,域名分析错误,3:检测中 4:检测成功,匹配失败 5:检测失败,9:检测成功
return (
<span style={{ width: "100%", textAlign: "right" }}>
<LoadingOutlined style={{ paddingInlineEnd: 5 }} />
{t(`${i18nPrefix}.actions.dnsVerifyStatus.3`, "检测中")}
</span>
);
}
//0,等待 1,域名OK,2,域名分析错误,3:检测中 4:检测成功,匹配失败 5:检测失败,9:检测成功
return (
<span style={{ width: "100%", textAlign: "left" }}>
{t(`${i18nPrefix}.actions.dnsVerifyStatus.${info.status}`, `${info?.status_txt}`)}
</span>
);
}
} }
return <span>{t(`${i18nPrefix}.actions.dnsVerifyStatus.0`, "等待")}</span>;
return (
<span style={{ width: "100%", textAlign: "left" }}>
{t(`${i18nPrefix}.actions.dnsVerifyStatus.0`, "等待")}
</span>
);
}, },
}, },
{ {
@ -127,9 +233,9 @@ const StatusTable = (props: { value: string }) => {
width: 150, width: 150,
render(text) { render(text) {
if (text) { if (text) {
return <span className={"color-green1"}>{text}</span>;
return <span className={"color-yellow"}>{text}</span>;
} }
return <span className={"color-yellow"}></span>;
return <span className={"color-red"}></span>;
}, },
}, },
{ {
@ -137,6 +243,20 @@ const StatusTable = (props: { value: string }) => {
title: t(`${i18nPrefix}.status.columns.domain`, "域名"), title: t(`${i18nPrefix}.status.columns.domain`, "域名"),
dataIndex: "dns_name", dataIndex: "dns_name",
width: 200, width: 200,
render(text) {
return (
<Button
size="small"
type="text"
className={"color-green1"}
icon={<CopyOutlined />}
iconPosition={"end"}
onClick={() => handleCopy(text)}
>
{text}
</Button>
);
},
}, },
{ {
//主机记录 //主机记录
@ -162,39 +282,29 @@ const StatusTable = (props: { value: string }) => {
dataIndex: "record_value", dataIndex: "record_value",
width: 200, width: 200,
render: (text) => { render: (text) => {
return <Copy {...{ text: text, tooltips: t(`actions.clickCopy`) }} />;
return (
<Button
size="small"
type="text"
className={"color-green1"}
icon={<CopyOutlined />}
iconPosition={"end"}
onClick={() => handleCopy(text)}
>
{text}
</Button>
);
}, },
}, },
] as ColumnsType; ] as ColumnsType;
}, [isFetching, isVerifyFetching, dnsVerifyStatus]);
}, [isVerifyFetching, data]);
useEffect(() => { useEffect(() => {
// 模拟数据 // 模拟数据
const mockData = {
dns_list: [
{
status: 1,
name_servers: "Example NS",
dns_name: "example.com",
host: "example",
type: "A",
record_value: "192.168.1.1"
},
{
status: 3,
name_servers: "Another NS",
dns_name: "another.com",
host: "another",
type: "CNAME",
record_value: "cname.another.com"
}
]
};
setData(mockData);
if ((dnsVerifyStatus as any)?.every((item) => item.status === 9)) {
setDnsVerifyOK(true);
if ((data as any)?.check_ok === true) {
props.setCurrentStep(2);
props.setDns_list((data as any).dns_list);
return; return;
} }
@ -203,8 +313,12 @@ const StatusTable = (props: { value: string }) => {
return; return;
} }
// dnsVerifyStatus 如果所有status 为 9 则说明域名验证通过 // dnsVerifyStatus 如果所有status 为 9 则说明域名验证通过
if ((dnsVerifyStatus as any)?.every((item) => item.status === 9)) {
setDnsVerifyOK(true);
// if ((data as any)?.dns_list?.every((item) => item.status === 9)) {
// props.setCurrentStep(2);
// window.clearInterval(timerRef.current);
if ((data as any)?.check_ok === true) {
props.setCurrentStep(2);
props.setDns_list((data as any).dns_list);
window.clearInterval(timerRef.current); window.clearInterval(timerRef.current);
} else { } else {
refetch(); refetch();
@ -214,144 +328,204 @@ const StatusTable = (props: { value: string }) => {
return () => { return () => {
window.clearInterval(timerRef.current); window.clearInterval(timerRef.current);
}; };
}, [dnsVerifyStatus, isVerifyFetching]);
}, [data, isVerifyFetching]);
return ( return (
<>
<div style={{ paddingBlock: 5, color: "#5a5a5a" }}>
<Flex vertical={false} align={"center"}>
<div>DNS解析记录</div>
<Button type="link"></Button>
</Flex>
<div> 1. </div>
<div> 2. 1~2</div>
</div>
<Table
columns={columns}
dataSource={(data as any)?.dns_list}
loading={isFetching}
size={"small"}
pagination={false}
bordered={true}
/>
</>
<>
<div style={{ paddingBlock: 5, color: "#5a5a5a" }}>
<Flex vertical={false} align={"center"}>
<div>DNS解析记录</div>
<Button type="link"></Button>
</Flex>
<div> 1. </div>
<div> 2. 1~2</div>
</div>
<Table
columns={columns}
dataSource={(data as any)?.dns_list}
loading={isVerifyFetching}
size={"small"}
pagination={false}
bordered={true}
/>
</>
); );
}; };
const domainsAtom = atomWithStorage<string>("domains", "");
const domainsAtom = atomWithStorage<string>("cert_domains", "");
// 打印初始值
console.log("初始文本值:", localStorage.getItem("cert_domains") || "");
const Apply = () => { const Apply = () => {
const { styles } = useStyle(); const { styles } = useStyle();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { mutate: saveOrUpdate, isPending: isSubmitting } = useAtomValue(saveOrUpdateCertAtom);
const {
mutate: applyTxtCertificateFun,
isPending: applyTxtCertificatePending,
isSuccess: applyTxtCertificateIsSuccess,
} = useAtomValue(applyTxtCertificateAtom);
const [domains, setDomains] = useAtom(domainsAtom); const [domains, setDomains] = useAtom(domainsAtom);
const dnsVerifyOK = useAtomValue(dnsVerifyOKAtom);
const [currentStep, setCurrentStep] = useState(0);
const [currentDomainMod, setCurrentDomainMod] = useState<"single" | "multiple">("single");
const [dns_list, setDns_list] = useState<ICertificate[]>([]);
const [acme_type, setAcme_type] = useState<string>("Google");
const [key_rsa, setKey_rsa] = useState<string>("");
const { data: configData } = useAtomValue(useMemo(() => getCertConfigAtom(), []));
const handleAlgorithmChange = (value: string) => {
setKey_rsa(value);
};
const applyTxtCertificateClick = () => {
const data: Req_ApplyTxtCertificate = {
is_sync: true,
acme_type: acme_type,
key_rsa: key_rsa,
dns_list: dns_list,
remark: form.getFieldValue("remark"),
};
applyTxtCertificateFun(data);
};
useEffect(() => { useEffect(() => {
if (domains) {
form.setFieldsValue({
domains,
});
}
}, [domains]);
form.setFieldsValue({ domains: domains });
form.setFieldsValue({ brand: acme_type });
form.setFieldsValue({ algorithm: key_rsa });
}, [domains, acme_type, key_rsa, form]);
return ( return (
<ListPageLayout
childrenClassName={styles.applyContent}
className={styles.applyContainer}
title={t(`${i18nPrefix}.apply.title`, "证书申请")}
>
<Form
form={form}
{...{
labelCol: { span: 3 },
wrapperCol: { span: 16 },
}}
onValuesChange={(values) => {
// console.log('onValuesChange', values)
if (values.domains) {
// setDomains(values.domains)
}
}}
onFinish={async (values) => {
if (dnsVerifyOK) {
saveOrUpdate(values);
}
}}
<ListPageLayout
childrenClassName={styles.applyContent}
className={styles.applyContainer}
title={t(`${i18nPrefix}.apply.title`, "证书申请")}
>
<div style={{ padding: "50px 50px 0 50px" }}>
<Steps
size="small"
current={currentStep}
items={[
{
title: "填写域名",
},
{
title: "等待检测",
},
{
title: "提交申请",
},
]}
/>
</div>
<Form
form={form}
{...{
labelCol: { span: 3 },
wrapperCol: { span: 16 },
}}
onValuesChange={(values) => {
// console.log('onValuesChange', values)
if (values.domains) {
// setDomains(values.domains)
}
}}
onFinish={async (values) => {
// if (dnsVerifyOK) {
// saveOrUpdate(values);
// }
}}
>
{/* <Form.Item*/}
{/* name={"domains"}*/}
{/* label={t(`${i18nPrefix}.columns.quota`, "证书额度")}*/}
{/* >*/}
{/* <Flex vertical gap="small" style={{ width: 300 }}>*/}
{/*<span>*/}
{/* <span style={{ color: "#00762B" }}>{t(`${i18nPrefix}.apply.remaining`, "剩余5张")}</span>*/}
{/* <span style={{ color: "#000000" }}>/ {t(`${i18nPrefix}.apply.total`, "共5张")}</span>*/}
{/*</span>*/}
{/* <Progress percent={30} strokeColor="#00762B" />*/}
{/* </Flex>*/}
{/* </Form.Item>*/}
<Form.Item
name={"domains"}
label={t(`${i18nPrefix}.columns.domains`, "域名")}
rules={[{ required: true, message: t(`${i18nPrefix}.columns.domains.required`, "请输入域名") }]}
>
<DomainsInput
domains={domains}
setDomains={setDomains}
currentDomainMod={currentDomainMod}
setCurrentDomainMod={setCurrentDomainMod}
currentStep={currentStep}
/>
</Form.Item>
{currentStep !== 0 && (
<>
<Form.Item
label={t(`${i18nPrefix}.columns.type`, "域名验证")}
rules={[{ required: true, message: t(`${i18nPrefix}.columns.type`, "域名验证没有通过") }]}
> >
{/* <Form.Item*/}
{/* name={"domains"}*/}
{/* label={t(`${i18nPrefix}.columns.quota`, "证书额度")}*/}
{/* >*/}
{/* <Flex vertical gap="small" style={{ width: 300 }}>*/}
{/*<span>*/}
{/* <span style={{ color: "#00762B" }}>{t(`${i18nPrefix}.apply.remaining`, "剩余5张")}</span>*/}
{/* <span style={{ color: "#000000" }}>/ {t(`${i18nPrefix}.apply.total`, "共5张")}</span>*/}
{/*</span>*/}
{/* <Progress percent={30} strokeColor="#00762B" />*/}
{/* </Flex>*/}
{/* </Form.Item>*/}
<Form.Item
name={"domains"}
label={t(`${i18nPrefix}.columns.domains`, "域名")}
rules={[{ required: true, message: t(`${i18nPrefix}.columns.domains.required`, "请输入域名") }]}
>
<Input.TextArea
rows={5}
placeholder={`请输入域名,每行一个,支持泛解析域名;如:
*.google.com
*.a.baidu.com
hello.alibaba.com`}
onBlur={(e) => {
setDomains(e.target.value);
}}
/>
</Form.Item>
<Form.Item
label={t(`${i18nPrefix}.columns.type`, "域名验证")}
rules={[{ required: true, message: t(`${i18nPrefix}.columns.type`, "域名验证没有通过") }]}
>
<StatusTable value={domains} />
</Form.Item>
<Form.Item
name={"brand"}
label={t(`${i18nPrefix}.columns.brand`, "证书品牌")}
rules={[
{
required: true,
message: t(`${i18nPrefix}.columns.brand.required`, "请选择证书品牌"),
},
]}
>
<BrandSelect />
</Form.Item>
<Form.Item
name={"algorithm"}
label={t(`${i18nPrefix}.columns.algorithm`, "加密方式")}
rules={[
{
required: true,
message: t(`${i18nPrefix}.columns.algorithm.required`, "请选择加密方式"),
},
]}
<StatusTable value={domains} setCurrentStep={setCurrentStep} setDns_list={setDns_list} />
</Form.Item>
<Form.Item
name={"brand"}
label={t(`${i18nPrefix}.columns.brand`, "证书品牌")}
rules={[
{
required: true,
message: t(`${i18nPrefix}.columns.brand.required`, "请选择证书品牌"),
},
]}
>
<BrandSelect acme_type={acme_type} setAcme_type={setAcme_type} />
</Form.Item>
<Form.Item
name={"algorithm"}
label={t(`${i18nPrefix}.columns.algorithm`, "加密方式")}
rules={[
{
required: true,
message: t(`${i18nPrefix}.columns.algorithm.required`, "请选择加密方式"),
},
]}
>
<Select style={{ width: 120 }} options={(configData as any)?.key_rsa} onChange={handleAlgorithmChange} />
</Form.Item>
<Form.Item name={"remark"} label={t(`${i18nPrefix}.columns.remark`, "备注 ")}>
<Input style={{ width: 400 }} />
</Form.Item>
</>
)}
<Form.Item label={" "} colon={false}>
<Space>
{currentStep !== 0 && (
<Button type="primary" onClick={() => setCurrentStep(0)} htmlType={"submit"}>
{t(`${i18nPrefix}.apply.prev`, "上一步")}
</Button>
)}
{currentStep === 0 && (
<Button type="primary" onClick={() => setCurrentStep(currentStep + 1)}>
{t(`${i18nPrefix}.apply.next`, "下一步")}
</Button>
)}
{currentStep !== 0 && (
<Button
type="primary"
htmlType={"submit"}
disabled={currentStep === 1}
onClick={applyTxtCertificateClick}
> >
<Select style={{ width: 120 }} options={algorithmTypes} />
</Form.Item>
<Form.Item name={"remark"} label={t(`${i18nPrefix}.columns.remark`, "备注 ")}>
<Input style={{ width: 400 }} />
</Form.Item>
<Form.Item label={" "} colon={false}>
<Space>
<Button disabled={!dnsVerifyOK || isSubmitting} htmlType={"submit"}>
{t(`${i18nPrefix}.apply.prev`, "上一步")}
</Button>
<Button type={"primary"} disabled={!dnsVerifyOK || isSubmitting} htmlType={"submit"}>
{t(`${i18nPrefix}.apply.submit`, "提交申请")}
</Button>
</Space>
</Form.Item>
</Form>
</ListPageLayout>
{t(`${i18nPrefix}.apply.submit`, "提交申请")}
</Button>
)}
</Space>
</Form.Item>
</Form>
</ListPageLayout>
); );
}; };

102
src/service/websites.ts

@ -1,84 +1,100 @@
import { createCURD } from '@/service/base.ts'
import { WebSite } from '@/types'
import request from '@/request.ts'
import { IWebsiteDomain, INameServer } from '@/types/website/domain'
import { IWebsiteDnsRecords } from '@/types/website/record'
import { IWebsiteDnsAccount } from '@/types/website/dns_account'
import { createCURD } from "@/service/base.ts";
import { WebSite } from "@/types";
import request from "@/request.ts";
import { IWebsiteDomain, INameServer } from "@/types/website/domain";
import { IWebsiteDnsRecords } from "@/types/website/record";
import { IWebsiteDnsAccount } from "@/types/website/dns_account";
const websitesServ = { const websitesServ = {
cert: { cert: {
...createCURD<any, ICertificate>('/website/cert'),
//dns_config
dnsConfig: async (params: any) => {
return request.post<any, any>('/website/cert/dns_config', params)
...createCURD<any, ICertificate>("/website/cert"),
// 发起域名检测
checkDomain: async (params: any) => {
return request.post<any, any>("/cert/apply/dns_config", params);
}, },
//dns_verify
dnsVerify: async (params: any) => {
return request.post<any, any>('/website/cert/dns_verify', params)
getCertConfig: async () => {
return request.get<any, any>("/cert/apply/acme/key");
}, },
//cert-apply
certApply: async (params: any) => {
return request.post<any, any>('/website/cert/dns_verify', params)
// 申请list
getCertList: async (arams: any) => {
return request.post<any, any>("/cert/apply/list");
},
// 证书续签
renewCertificate: async (params: any) => {
return request.post<any, any>("/website/cert/renew_certificate", params);
},
// 添加记录
addCnameCertificate: async (params: any) => {
return request.post<any, any>("/cert/apply/add/cname", params);
},
// 下载证书
downloadCertificate: async (params: any) => {
return request.post<any, any>("/website/cert/download_certificate", params);
},
// 获取证书申请日志
getCertificateLogs: async (params: any) => {
return request.get<any, any>("/website/cert/get_certificate_logs", { params });
},
applyTxtCertificate: async (params: any) => {
return request.post<any, any>("/cert/apply/resolve", params);
}, },
}, },
ssl: { ssl: {
...createCURD<any, WebSite.ISSL>('/website/ssl'),
...createCURD<any, WebSite.ISSL>("/website/ssl"),
upload: async (params: WebSite.SSLUploadDto) => { upload: async (params: WebSite.SSLUploadDto) => {
return request.post<any, WebSite.SSLUploadDto>('/website/ssl/upload', params)
return request.post<any, WebSite.SSLUploadDto>("/website/ssl/upload", params);
}, },
download: async (params: any) => { download: async (params: any) => {
return request.download('/website/ssl/download', params)
return request.download("/website/ssl/download", params);
}, },
}, },
acme: { acme: {
...createCURD<any, WebSite.IAcmeAccount>('/website/acme')
...createCURD<any, WebSite.IAcmeAccount>("/website/acme"),
}, },
dns: { dns: {
...createCURD<any, WebSite.IDnsAccount>('/cert/dns_account'),
...createCURD<any, WebSite.IDnsAccount>("/cert/dns_account"),
sync: async (id: any) => { sync: async (id: any) => {
return request.post<any, WebSite.IDnsAccount>('/cert/dns_account/sync', { id: id })
}
return request.post<any, WebSite.IDnsAccount>("/cert/dns_account/sync", { id: id });
},
}, },
ca: { ca: {
...createCURD<any, WebSite.ICA>('/website/ca'),
...createCURD<any, WebSite.ICA>("/website/ca"),
obtainSsl: async (params: WebSite.ISSLObtainByCA) => { obtainSsl: async (params: WebSite.ISSLObtainByCA) => {
return request.post<any, WebSite.ISSLObtainByCA>('/website/ca/obtain_ssl', params)
return request.post<any, WebSite.ISSLObtainByCA>("/website/ca/obtain_ssl", params);
}, },
}, },
domain: { domain: {
...createCURD<any, IWebsiteDomain>('/cert/domain'),
...createCURD<any, IWebsiteDomain>("/cert/domain"),
//remark //remark
remark: async (params: { id: string, remark: string }) => {
return request.post<any, any>('/cert/domain/remark', params)
remark: async (params: { id: string; remark: string }) => {
return request.post<any, any>("/cert/domain/remark", params);
}, },
//tag //tag
tag: async (params: { id: string, tags: string }) => {
return request.post<any, any>('/cert/domain/tag', params)
tag: async (params: { id: string; tags: string }) => {
return request.post<any, any>("/cert/domain/tag", params);
}, },
//binding //binding
binding: async (params: { id: string, user_id: string }) => {
return request.post<any, any>('/cert/domain/binding', params)
binding: async (params: { id: string; user_id: string }) => {
return request.post<any, any>("/cert/domain/binding", params);
}, },
//group //group
group: async (params: { id: string[], group_id: string }) => {
return request.post<any, any>('/cert/domain/group', params)
group: async (params: { id: string[]; group_id: string }) => {
return request.post<any, any>("/cert/domain/group", params);
}, },
describeDomainNS: async (params: { id: number }) => { describeDomainNS: async (params: { id: number }) => {
return request.post<INameServer, any>('/cert/domain/describe_domain_ns', params)
return request.post<INameServer, any>("/cert/domain/describe_domain_ns", params);
}, },
}, },
record: { record: {
...createCURD<any, IWebsiteDnsRecords>('/cert/dns_records'),
...createCURD<any, IWebsiteDnsRecords>("/cert/dns_records"),
// //
}, },
dnsAccount: { dnsAccount: {
...createCURD<any, IWebsiteDnsAccount>('/cert/dns_account'),
...createCURD<any, IWebsiteDnsAccount>("/cert/dns_account"),
sync: async (params: IWebsiteDnsAccount) => { sync: async (params: IWebsiteDnsAccount) => {
return request.post<any, IWebsiteDnsAccount>('/cert/dns_account/sync', params)
}
return request.post<any, IWebsiteDnsAccount>("/cert/dns_account/sync", params);
},
}, },
}
};
export default websitesServ
export default websitesServ;

325
src/store/websites/cert.ts

@ -1,185 +1,248 @@
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
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";
export type Req_SearchParams = IPage & {
name?: string;
};
export interface Req_AddCname {
is_sync: boolean;
dns_list: ICertificate[];
} }
export interface Req_ApplyTxtCertificate {
is_sync: boolean;
acme_type: string;
key_rsa: string;
dns_list: ICertificate[];
remark: string;
}
//=========================证书列表
export interface Req_CertList {
order: string;
prop: string;
page: number;
pageSize: number;
}
export interface Resp_CertList {
page: number;
pageSize: number;
total: number;
rows: any;
}
//=========================证书列表
export const bandTypes = [ export const bandTypes = [
{ label: 'Google', value: 'Google' },
{ label: 'ZeroSSL', value: 'ZeroSSL' },
{ label: 'Let\'s Encrypt', value: 'Let\'s Encrypt' },
]
{
label: "LetsEncrypt",
value: "LetsEncrypt",
},
{
label: "ZeroSsl",
value: "ZeroSsl",
},
{
label: "Google",
value: "Google",
},
];
export const algorithmTypes = [ export const algorithmTypes = [
{ label: 'RSA', value: 'RSA' },
{ label: 'ECC', value: 'ECC' },
]
{ label: "RSA", value: "RSA" },
{ label: "ECC", value: "ECC" },
];
export const StatusText = { export const StatusText = {
1: [ '已签发', 'green' ],
2: [ '申请中', 'default' ],
3: [ '申请失败', 'red' ]
}
1: ["已签发", "green"],
2: ["申请中", "default"],
3: ["申请失败", "red"],
};
export const certIdAtom = atom(0)
export const certIdAtom = atom(0);
export const certIdsAtom = atom<number[]>([])
export const certIdsAtom = atom<number[]>([]);
export const certAtom = atom<ICertificate>(undefined as unknown as ICertificate)
export const certAtom = atom<ICertificate>(undefined as unknown as ICertificate);
export const certSearchAtom = atom<SearchParams>({
export const certSearchAtom = atom<Req_SearchParams>({
// key: '', // key: '',
pageSize: 10, pageSize: 10,
page: 1, page: 1,
} as SearchParams)
} as Req_SearchParams);
export const certPageAtom = atom<IPage>({ export const certPageAtom = atom<IPage>({
pageSize: 10, pageSize: 10,
page: 1, page: 1,
})
//certApple
export const certAppleCertAtom = atomWithMutation<IApiResult, ICertificate>((get) => {
});
//=================================================================================================================================================kelis
export const getCertConfigAtom = () =>
atomWithQuery<IApiResult, any>(() => {
return {
queryKey: ["getCertConfig"],
queryFn: async () => {
return await websitesServ.cert.getCertConfig();
},
select: (res) => {
return res.data;
},
};
});
export const checkDomainAtom = (domains: string, isClear: boolean) =>
atomWithQuery<IApiResult, any>(() => {
return {
enabled: domains.length > 0 && domains.includes("."),
queryKey: ["checkDomain", domains],
queryFn: async ({ queryKey: [, domains] }) => {
if ((domains as string).length === 0) {
return Promise.reject({
data: [],
});
}
return await websitesServ.cert.checkDomain({
dns_full_list: domains,
parse: true,
is_clear: isClear,
});
},
select: (res) => {
return res.data;
},
};
});
export const certListAtom = (params: Req_CertList) =>
atomWithQuery<IApiResult, any>(() => {
return {
queryKey: ["certList", params],
queryFn: async ({ queryKey: [, params] }) => {
return await websitesServ.cert.getCertList(params);
},
select: (res) => {
return res.data;
},
};
});
export const certAddCnameAtom = atomWithMutation<IApiResult, ICertificate>(() => {
return { return {
mutationKey: [ 'appleCert' ],
mutationKey: ["certAddCname"],
mutationFn: async (data) => { mutationFn: async (data) => {
//data.status = data.status ? '1' : '0'
return await websitesServ.cert.certApply(data)
const dData: Req_AddCname = {
is_sync: true,
dns_list: [data],
};
return await websitesServ.cert.addCnameCertificate(dData);
}, },
onSuccess: (res) => { 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
}
}
})
const status = res.data?.item[0]?.status || 0;
const status_txt = res.data?.item[0]?.status_txt;
if (status || status === 5) {
message.error(status_txt);
} else {
message.success(status_txt);
}
return res;
},
};
});
export const applyTxtCertificateAtom = atomWithMutation<IApiResult, Req_ApplyTxtCertificate>(() => {
return {
mutationKey: ["applyTxtCertificate"],
mutationFn: async (data: Req_ApplyTxtCertificate) => {
return await websitesServ.cert.applyTxtCertificate(data);
},
onSuccess: (res) => {
return res;
},
};
});
//==================================================================================================================================================kelis
// //certApple
// export const certAppleCertAtom = atomWithMutation<IApiResult, ICertificate>((get) => {
// return {
// mutationKey: ["appleCert"],
// mutationFn: async (data) => {
// //data.status = data.status ? '1' : '0'
// return await websitesServ.cert.certApply(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 certsAtom = atomWithQuery((get) => { export const certsAtom = atomWithQuery((get) => {
return { return {
queryKey: [ 'certs', get(certSearchAtom) ],
queryFn: async ({ queryKey: [ , params ] }) => {
return await websitesServ.cert.list(params as SearchParams)
queryKey: ["certs", get(certSearchAtom)],
queryFn: async ({ queryKey: [, params] }) => {
return await websitesServ.cert.list(params as Req_SearchParams);
}, },
select: res => {
const data = res.data
data.rows = data.rows?.map(row => {
select: (res) => {
const data = res.data;
data.rows = data.rows?.map((row) => {
return { return {
...row, ...row,
//status: convertToBool(row.status) //status: convertToBool(row.status)
}
})
return data
}
}
})
};
});
return data;
},
};
});
//saveOrUpdateAtom //saveOrUpdateAtom
export const saveOrUpdateCertAtom = atomWithMutation<IApiResult, ICertificate>((get) => { export const saveOrUpdateCertAtom = atomWithMutation<IApiResult, ICertificate>((get) => {
return { return {
mutationKey: [ 'updateCert' ],
mutationKey: ["updateCert"],
mutationFn: async (data) => { mutationFn: async (data) => {
//data.status = data.status ? '1' : '0' //data.status = data.status ? '1' : '0'
if (data.id) { if (data.id) {
return await websitesServ.cert.update(data)
return await websitesServ.cert.update(data);
} }
return await websitesServ.cert.add(data)
return await websitesServ.cert.add(data);
}, },
onSuccess: (res) => { onSuccess: (res) => {
const isAdd = !!res.data?.id
message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功'))
const isAdd = !!res.data?.id;
message.success(t(isAdd ? "message.saveSuccess" : "message.editSuccess", "保存成功"));
//更新列表 //更新列表
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore fix // @ts-ignore fix
get(queryClientAtom).invalidateQueries({ queryKey: [ 'certs', get(certSearchAtom) ] })
get(queryClientAtom).invalidateQueries({ queryKey: ["certs", get(certSearchAtom)] });
return res
}
}
})
return res;
},
};
});
export const deleteCertAtom = atomWithMutation((get) => { export const deleteCertAtom = atomWithMutation((get) => {
return { return {
mutationKey: [ 'deleteCert' ],
mutationKey: ["deleteCert"],
mutationFn: async (ids: number[]) => { mutationFn: async (ids: number[]) => {
return await websitesServ.cert.batchDelete(ids ?? get(certIdsAtom))
return await websitesServ.cert.batchDelete(ids ?? get(certIdsAtom));
}, },
onSuccess: (res) => { onSuccess: (res) => {
message.success('message.deleteSuccess')
message.success("message.deleteSuccess");
//更新列表 //更新列表
get(queryClientAtom).invalidateQueries({ queryKey: [ 'certs', get(certSearchAtom) ] })
return res
}
}
})
//dnsConfig
export const dnsConfigAtom = (domains: string) => atomWithQuery<IApiResult, any>(() => {
return {
enabled: domains.length > 0 && domains.includes('.'),
queryKey: [ 'dnsConfig', domains ],
queryFn: async ({ queryKey: [ , domains ] }) => {
if ((domains as string).length === 0) {
return Promise.reject({
data: []
})
}
return await websitesServ.cert.dnsConfig({
dns_full_list: domains,
parse: (domains as string)?.includes('*')
})
},
select: res => {
return res.data
}
}
})
export const dnsVerifyOKAtom = atom<boolean>(false)
//query dnsVerify
export const dnsVerifyAtom = (domains: string, block: boolean) => atomWithQuery<IApiResult, any>(() => {
return {
enabled: !block && domains.length > 0 && domains.includes('.'),
queryKey: [ 'dnsVerify', domains ],
queryFn: async ({ queryKey: [ , domains ] }) => {
if ((domains as string).length === 0) {
return Promise.reject({
data: []
})
}
return await websitesServ.cert.dnsVerify({
dns_list: domains,
})
get(queryClientAtom).invalidateQueries({ queryKey: ["certs", get(certSearchAtom)] });
return res;
}, },
select: res => {
return res.data?.dns_list
}
}
})
};
});

3
src/types/r-form/model.d.ts

@ -27,6 +27,9 @@ export namespace RFormTypes {
params?: any; params?: any;
resultPath?: string; resultPath?: string;
//是否需要转换成下拉框的数据或者树形数据格式, 默认为true
transform: boolean;
fieldNames?: { fieldNames?: {
label: string; label: string;
value: string; value: string;

2
src/utils/index.ts

@ -216,3 +216,5 @@ export const genProTableColumnWidthProps = (width: string | number) => {
} }
} }

1
vite.config.ts

@ -13,6 +13,7 @@ const proxyMap = {
'/api/v1/movie': 'http://47.113.117.106:10000', '/api/v1/movie': 'http://47.113.117.106:10000',
//'/api/v1/certold': 'http://192.168.31.41:8000', //'/api/v1/certold': 'http://192.168.31.41:8000',
'/api/v1/cert': 'http://127.0.0.1:8000', '/api/v1/cert': 'http://127.0.0.1:8000',
//'/api/v1/cert': 'http://192.168.31.41:8000',
} as Record<any, string> } as Record<any, string>
const proxyConfig = Object.keys(proxyMap).reduce((acc, key) => { const proxyConfig = Object.keys(proxyMap).reduce((acc, key) => {

Loading…
Cancel
Save