lk
2 months ago
1 changed files with 590 additions and 0 deletions
@ -0,0 +1,590 @@ |
|||
import React, { useEffect, useMemo, useState } from "react"; |
|||
import { |
|||
Table, |
|||
Input, |
|||
Button, |
|||
Tag, |
|||
Progress, |
|||
Space, |
|||
Drawer, |
|||
Typography, |
|||
Form, |
|||
DatePicker, |
|||
Tooltip, |
|||
Modal, |
|||
ProgressProps, |
|||
} from "antd"; |
|||
import { CheckCircleFilled, CheckCircleOutlined, CopyOutlined, SearchOutlined } from "@ant-design/icons"; |
|||
import { t } from "@/i18n.ts"; |
|||
import ListPageLayout from "@/layout/ListPageLayout.tsx"; |
|||
import TextArea from "antd/es/input/TextArea"; |
|||
import { useNavigate } from "@tanstack/react-router"; |
|||
import { |
|||
certListAtom, |
|||
checkDomainAtom, |
|||
deletesCertificateAtom, |
|||
downloadCertificateAtom, |
|||
editCertificateAtom, |
|||
getCertificateLogsAtom, |
|||
Req_CertList, |
|||
req_CertLogs, |
|||
Req_DeletesCert, |
|||
Req_DownloadCert, |
|||
Req_UpdateCert, |
|||
saveOrUpdateCertAtom, |
|||
} from "@/store/websites/cert.ts"; |
|||
import { useAtomValue } from "jotai/index"; |
|||
import { format } from "date-fns"; |
|||
import websitesServ from "@/service/websites.ts"; |
|||
import { useAtom } from "jotai"; |
|||
import { getToken } from "@/store/system.ts"; |
|||
|
|||
const { confirm } = Modal; |
|||
const { Text, Link } = Typography; |
|||
const i18nPrefix = "cert.management"; |
|||
|
|||
const CertApplyingDrawer = (props: { id: number; onCloseApplyingDrawer; applyingDrawerVisible }) => { |
|||
const [dData, setDdata] = useState<req_CertLogs>({ |
|||
log_id: props.id, |
|||
log_type: "cert_apply", |
|||
websocket: false, |
|||
log_pos: 0, |
|||
// 根据实际数据结构添加其他属性的初始值
|
|||
}); |
|||
const [showTextInfo, setShowTextInfo] = useState(""); |
|||
const [onChangeDT, setOnChangeDT] = useState(0); |
|||
const { data: certLogsData, isFetching: certLogsFetching } = useAtomValue( |
|||
useMemo(() => getCertificateLogsAtom(dData), [onChangeDT]), |
|||
); |
|||
|
|||
useEffect(() => { |
|||
if (certLogsData && (certLogsData as any)?.log) { |
|||
setShowTextInfo((prev) => prev + (certLogsData as any).log.toString()); |
|||
} |
|||
}, [certLogsData]); |
|||
|
|||
useEffect(() => { |
|||
if (props.applyingDrawerVisible) { |
|||
setShowTextInfo(""); |
|||
setOnChangeDT((prev) => prev + 1); |
|||
} |
|||
let timer: number | undefined; |
|||
if (props.applyingDrawerVisible) { |
|||
timer = window.setInterval(() => { |
|||
setDdata((prevState) => ({ |
|||
...prevState, |
|||
log_id: props.id, |
|||
log_pos: (certLogsData as any)?.log_pos || 0, |
|||
})); |
|||
// console.log("update drawer");
|
|||
setOnChangeDT((prev) => prev + 1); |
|||
}, 5000); |
|||
} else { |
|||
setDdata((prevState) => ({ |
|||
...prevState, |
|||
log_id: props.id, |
|||
log_pos: 0, |
|||
})); |
|||
window.clearInterval(timer); |
|||
} |
|||
|
|||
return () => window.clearInterval(timer); |
|||
}, [props.applyingDrawerVisible]); |
|||
|
|||
return ( |
|||
<Drawer |
|||
title="证书申请中..." |
|||
width={window.innerWidth / 2} |
|||
placement="right" |
|||
onClose={props.onCloseApplyingDrawer} |
|||
visible={props.applyingDrawerVisible} |
|||
bodyStyle={{ display: "flex", flexDirection: "column", height: "100%" }} |
|||
> |
|||
<TextArea value={showTextInfo} readOnly style={{ flex: 1 }} /> |
|||
</Drawer> |
|||
); |
|||
}; |
|||
|
|||
const CertificateManagement: React.FC = () => { |
|||
const [dlDrawerVisible, setDLDrawerVisible] = useState(false); |
|||
const [editDrawerVisible, setEditDrawerVisible] = useState(false); |
|||
const [detailsVisible, setDetailsVisible] = useState(false); |
|||
const navigate = useNavigate(); |
|||
const dData: Req_CertList = { order: "", prop: "", page: 1, pageSize: 10 }; |
|||
const [updataList, setUpdataList] = useState(0); |
|||
const { data: certListData, isFetching: certListFetching } = useAtomValue( |
|||
useMemo(() => certListAtom(dData), [updataList]), |
|||
); |
|||
|
|||
const [applyingDrawerVisible, setApplyingDrawerVisible] = useState(false); |
|||
const [certApplyingId, setCertApplyingId] = useState(0); |
|||
const [currentDLInfo, setCurrentDLInfo] = useState({}); |
|||
const { mutate: editCertUpdate } = useAtomValue(editCertificateAtom); |
|||
const [remakeTextInfo, setRemakeTextInfo] = useState(""); |
|||
|
|||
useEffect(() => { |
|||
window.setInterval(() => { |
|||
// console.log("update drawer");
|
|||
setUpdataList((prev) => prev + 1); |
|||
}, 5000); |
|||
}, []); |
|||
|
|||
const showDLDrawer = (record) => { |
|||
setCurrentDLInfo(record); |
|||
setDLDrawerVisible(true); |
|||
}; |
|||
|
|||
const closeDLDrawer = () => { |
|||
setDLDrawerVisible(false); |
|||
}; |
|||
|
|||
const showEditDrawer = (record) => { |
|||
setCurrentDLInfo(record); |
|||
setEditDrawerVisible(true); |
|||
}; |
|||
const enterEditDrawer = (record) => { |
|||
const data: Req_UpdateCert = { |
|||
id: record.id, |
|||
remark: remakeTextInfo, |
|||
}; |
|||
editCertUpdate(data); |
|||
setEditDrawerVisible(false); |
|||
}; |
|||
const closeEditDrawer = () => { |
|||
setEditDrawerVisible(false); |
|||
}; |
|||
const openDetails = () => { |
|||
setDetailsVisible(true); |
|||
}; |
|||
|
|||
const closeDetails = () => { |
|||
setDetailsVisible(false); |
|||
}; |
|||
|
|||
const showApplyingDrawer = () => { |
|||
setApplyingDrawerVisible(true); |
|||
}; |
|||
|
|||
const onCloseApplyingDrawer = () => { |
|||
setApplyingDrawerVisible(false); |
|||
}; |
|||
|
|||
const applyCertClick = () => { |
|||
navigate({ to: `/client/cert/apply` }); // 确保路径正确
|
|||
}; |
|||
|
|||
const clickStateTag = (id: number) => { |
|||
setCertApplyingId(id); |
|||
showApplyingDrawer(); |
|||
}; |
|||
|
|||
const handleRemarkChange = (e) => { |
|||
setRemakeTextInfo(e.target.value); |
|||
}; |
|||
|
|||
const clickDownLoad = (record: any) => { |
|||
const cert_format: string = record.certificateFormat.toString().split("/")[0].replace(/\s+/g, ""); |
|||
// const data: Req_DownloadCert = {
|
|||
// id: (currentDLInfo as any)?.id,
|
|||
// cert_format: cert_format,
|
|||
// };
|
|||
// websitesServ.cert.downloadCertificate(data);
|
|||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|||
const token = getToken(); |
|||
const certUrl = "http://127.0.0.1:8000/api/v1/cert"; |
|||
const url = `${certUrl}/apply/download?id=${(currentDLInfo as any)?.id}&cert_format=${cert_format}&token=${token}`; |
|||
window.open(url, "_blank"); |
|||
}; |
|||
|
|||
const clickDelete = (id: number) => { |
|||
confirm({ |
|||
title: "提示", |
|||
content: "是否要删除证书?", |
|||
okText: "确定", |
|||
okType: "danger", |
|||
cancelText: "取消", |
|||
onOk() { |
|||
const data: Req_DeletesCert = { ids: [id] }; |
|||
websitesServ.cert.deletesCertificate(data); |
|||
}, |
|||
onCancel() { |
|||
console.log("取消删除证书"); |
|||
}, |
|||
}); |
|||
}; |
|||
|
|||
const drawerColumns = [ |
|||
{ |
|||
title: "服务器类型", |
|||
dataIndex: "serverType", |
|||
key: "serverType", |
|||
}, |
|||
{ |
|||
title: "证书格式", |
|||
dataIndex: "certificateFormat", |
|||
key: "certificateFormat", |
|||
render: (text: any) => ( |
|||
<Space> |
|||
{text.split("/").map((format: string) => ( |
|||
<Button key={format} size="small" icon={<CopyOutlined />} iconPosition={"end"}> |
|||
{format.trim()} |
|||
</Button> |
|||
))} |
|||
</Space> |
|||
), |
|||
}, |
|||
{ |
|||
title: "操作", |
|||
key: "canDownLoad", |
|||
render: (text: string, record: any) => ( |
|||
<Space size="middle"> |
|||
<Button type="link">帮助</Button> |
|||
<Button type="link" disabled={!record.canDownLoad} onClick={() => clickDownLoad(record)}> |
|||
下载 |
|||
</Button> |
|||
</Space> |
|||
), |
|||
}, |
|||
]; |
|||
|
|||
const drawerDataSource = [ |
|||
{ |
|||
key: "1", |
|||
serverType: "Nginx", |
|||
certificateFormat: "pem / key", |
|||
canDownLoad: true, |
|||
}, |
|||
{ |
|||
key: "2", |
|||
serverType: "Tomcat", |
|||
certificateFormat: "pfx", |
|||
canDownLoad: false, |
|||
}, |
|||
{ |
|||
key: "3", |
|||
serverType: "Apache", |
|||
certificateFormat: "crt / key", |
|||
canDownLoad: true, |
|||
}, |
|||
{ |
|||
key: "4", |
|||
serverType: "IIS", |
|||
certificateFormat: "pfx", |
|||
canDownLoad: false, |
|||
}, |
|||
{ |
|||
key: "5", |
|||
serverType: "JKS", |
|||
certificateFormat: "jks", |
|||
canDownLoad: true, |
|||
}, |
|||
{ |
|||
key: "6", |
|||
serverType: "宝塔", |
|||
certificateFormat: "pem / key", |
|||
canDownLoad: true, |
|||
}, |
|||
{ |
|||
key: "7", |
|||
serverType: "其他", |
|||
certificateFormat: "pem / key", |
|||
canDownLoad: true, |
|||
}, |
|||
]; |
|||
|
|||
const columns = [ |
|||
{ |
|||
title: "域名", |
|||
dataIndex: "primary_domain", |
|||
key: "primary_domain", |
|||
render: (text: string) => ( |
|||
<Tooltip title={`点击查看详情`} color={"orange"}> |
|||
<Button type="link" onClick={openDetails}> |
|||
{text} |
|||
</Button> |
|||
</Tooltip> |
|||
), |
|||
}, |
|||
{ |
|||
title: "域名验证", |
|||
dataIndex: "domainVerification", |
|||
key: "domainVerification", |
|||
render: () => <CheckCircleFilled style={{ color: "green" }} />, |
|||
}, |
|||
{ |
|||
title: "证书品牌", |
|||
dataIndex: "acme_type", |
|||
key: "acme_type", |
|||
render: (text) => ( |
|||
<Tooltip title={`证书名字`}> |
|||
<span>{text}</span> |
|||
{/*<img src="https://placehold.co/20x20?text=Logo" alt="Let's Encrypt Logo" />{" "}*/} |
|||
</Tooltip> |
|||
), |
|||
}, |
|||
{ |
|||
/* |
|||
const ( |
|||
SSLSuccess CertStatus = "success" // 成功
|
|||
SSLInit CertStatus = "init" // 初始化
|
|||
SSLError CertStatus = "error" // 错误
|
|||
SSLReady CertStatus = "ready" // 准备中
|
|||
SSLApply CertStatus = "applying" // 申请中
|
|||
SSLApplyError CertStatus = "applyError" // 申请失败
|
|||
) |
|||
*/ |
|||
title: "有效期(天)", |
|||
dataIndex: "validityDays", |
|||
key: "validityDays", |
|||
render: (text, record: { created_at: string; updated_at: string; expiredate: string }) => { |
|||
if (record) { |
|||
// const twoColors: ProgressProps["strokeColor"] = {
|
|||
// "0%": "#00ff59",
|
|||
// "100%": "#ff0000",
|
|||
// };
|
|||
record.created_at = !record?.created_at ? format(new Date(), "yyyy-MM-dd HH:mm:ss") : record.created_at; |
|||
record.expiredate = !record?.expiredate ? format(new Date(), "yyyy-MM-dd HH:mm:ss") : record.expiredate; |
|||
const createdDate = new Date(record.created_at.replace(/-/g, "/")); |
|||
const expireDateObj = new Date(record.expiredate.replace(/-/g, "/")); |
|||
const currentDate = new Date(); |
|||
const totalValidTime = expireDateObj.getTime() - createdDate.getTime(); |
|||
const remainingTime = expireDateObj.getTime() - currentDate.getTime(); |
|||
const percentage = (remainingTime / totalValidTime) * 100; |
|||
//const percentage = 60;
|
|||
let cColor = "#000000"; |
|||
|
|||
if (percentage < 50) { |
|||
cColor = "#00ff59"; |
|||
} else if (percentage >= 50 && percentage <= 75) { |
|||
cColor = "#00641a"; |
|||
} else if (percentage >= 75 && percentage <= 90) { |
|||
cColor = "#ffdd00"; |
|||
} else { |
|||
cColor = "#ff0000"; |
|||
} |
|||
return ( |
|||
<Space> |
|||
<Tooltip |
|||
title={ |
|||
<div> |
|||
创建时间:{record.created_at} |
|||
<br /> |
|||
到期时间:{record.expiredate} |
|||
</div> |
|||
} |
|||
> |
|||
<Progress |
|||
style={{ width: 200 }} |
|||
percent={percentage} |
|||
strokeColor={cColor} |
|||
size="small" |
|||
showInfo={false} |
|||
/> |
|||
<span>{Math.round(percentage)}/100</span> |
|||
</Tooltip> |
|||
</Space> |
|||
); |
|||
} |
|||
}, |
|||
}, |
|||
|
|||
{ |
|||
title: "加密方式", |
|||
dataIndex: "keytype", |
|||
key: "keytype", |
|||
render: (encryptionMethod: string) => <Tag color="green">{encryptionMethod}</Tag>, |
|||
}, |
|||
{ |
|||
title: "状态", |
|||
dataIndex: "cert_status", |
|||
key: "cert_status", |
|||
render: (state, record: any) => { |
|||
const { id } = record; |
|||
if (state === "applying") { |
|||
return ( |
|||
<Tag color="blue" onClick={() => clickStateTag(id)} style={{ cursor: "pointer" }}> |
|||
{state} |
|||
</Tag> |
|||
); |
|||
} else { |
|||
return ( |
|||
<Tag color="green" onClick={() => clickStateTag(id)} style={{ cursor: "pointer" }}> |
|||
{state} |
|||
</Tag> |
|||
); |
|||
} |
|||
}, |
|||
}, |
|||
{ |
|||
title: "备注", |
|||
dataIndex: "remark", |
|||
key: "remark", |
|||
}, |
|||
{ |
|||
title: "操作", |
|||
key: "action", |
|||
render: (record: any) => ( |
|||
<Space size="middle"> |
|||
<a onClick={() => showEditDrawer(record)}>编辑</a> |
|||
<a onClick={() => showDLDrawer(record)}>下载</a> |
|||
<a onClick={() => clickDelete(record.id)}>删除</a> |
|||
</Space> |
|||
), |
|||
}, |
|||
]; |
|||
|
|||
const detailsColumns = [ |
|||
{ |
|||
title: "状态", |
|||
dataIndex: "status", |
|||
key: "status", |
|||
render: (text: string) => <span style={{ color: "#52c41a" }}>{text}</span>, |
|||
}, |
|||
{ |
|||
title: "域名", |
|||
dataIndex: "domain", |
|||
key: "domain", |
|||
}, |
|||
{ |
|||
title: "服务商", |
|||
dataIndex: "provider", |
|||
key: "provider", |
|||
render: (text: string) => <span style={{ color: "#1890ff" }}>{text}</span>, |
|||
}, |
|||
{ |
|||
title: "主机记录", |
|||
dataIndex: "hostRecord", |
|||
key: "hostRecord", |
|||
}, |
|||
{ |
|||
title: "记录类型", |
|||
dataIndex: "recordType", |
|||
key: "recordType", |
|||
render: (text: string) => <span style={{ color: "red" }}>{text}</span>, |
|||
}, |
|||
{ |
|||
title: "记录值", |
|||
dataIndex: "recordValue", |
|||
key: "recordValue", |
|||
}, |
|||
{ |
|||
title: "操作", |
|||
dataIndex: "action", |
|||
key: "action", |
|||
render: (text: string) => <Link href="#">{text}</Link>, |
|||
}, |
|||
]; |
|||
|
|||
const detailsData = [ |
|||
{ |
|||
key: "1", |
|||
status: "通过", |
|||
domain: "*.bidiy2.com", |
|||
provider: "Cloudflare", |
|||
hostRecord: "_acme-challenge", |
|||
recordType: "CNAME", |
|||
recordValue: "8cb0d498cecabd92.httpsok.com", |
|||
action: "检测命令", |
|||
}, |
|||
]; |
|||
|
|||
return ( |
|||
<div> |
|||
<ListPageLayout title={t(`${i18nPrefix}.apply.title`, "证书申请")}> |
|||
<Space style={{ marginBottom: 16 }}> |
|||
<Input addonBefore={<SearchOutlined />} placeholder="域名 | 备注" style={{ width: 200 }} /> |
|||
<Button>搜索</Button> |
|||
<Button type="primary" onClick={applyCertClick}> |
|||
免费申请证书 |
|||
</Button> |
|||
</Space> |
|||
<Table columns={columns} dataSource={(certListData as any)?.rows} pagination={false} /> |
|||
</ListPageLayout> |
|||
|
|||
<CertApplyingDrawer |
|||
id={certApplyingId} |
|||
onCloseApplyingDrawer={onCloseApplyingDrawer} |
|||
applyingDrawerVisible={applyingDrawerVisible} |
|||
/> |
|||
|
|||
<Drawer title="下载证书" placement="right" onClose={closeDLDrawer} visible={dlDrawerVisible} width={600}> |
|||
<Text>请根据您的服务器类型选择证书下载:</Text> |
|||
<Table dataSource={drawerDataSource} columns={drawerColumns} pagination={false} style={{ marginTop: 16 }} /> |
|||
<div style={{ backgroundColor: "#f6ffed", padding: 16, marginTop: 16 }}> |
|||
<Text>加入SSL证书交流群</Text> |
|||
<br /> |
|||
<Text>产品咨询 问题反馈</Text> |
|||
<br /> |
|||
<Link href="https://httpsok">请备注 httpsok</Link> |
|||
</div> |
|||
</Drawer> |
|||
|
|||
<Drawer |
|||
title="编辑" |
|||
placement="right" |
|||
closable={false} |
|||
onClose={closeEditDrawer} |
|||
visible={editDrawerVisible} |
|||
style={{ textAlign: "center" }} |
|||
bodyStyle={{ display: "flex", justifyContent: "center", alignItems: "center" }} |
|||
> |
|||
<Form layout="vertical" style={{ maxWidth: "400px", width: "100%" }}> |
|||
<Form.Item label="域名"> |
|||
<span>{(currentDLInfo as any)?.primary_domain}</span> |
|||
</Form.Item> |
|||
<Form.Item label="证书品牌"> |
|||
<span>{(currentDLInfo as any)?.acme_type}</span> |
|||
</Form.Item> |
|||
<Form.Item label="失效时间"> |
|||
<span>{(currentDLInfo as any)?.expiredate}</span> |
|||
</Form.Item> |
|||
<Form.Item label="创建时间"> |
|||
<span>{(currentDLInfo as any)?.created_at}</span> |
|||
</Form.Item> |
|||
<Form.Item label="备注"> |
|||
<TextArea |
|||
rows={4} |
|||
maxLength={100} |
|||
defaultValue={(currentDLInfo as any)?.remark} |
|||
onChange={handleRemarkChange} |
|||
/> |
|||
</Form.Item> |
|||
<Form.Item> |
|||
<Button onClick={closeEditDrawer} style={{ marginRight: 8 }}> |
|||
取消 |
|||
</Button> |
|||
<Button type="primary" onClick={() => enterEditDrawer(currentDLInfo)}> |
|||
确定 |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
</Drawer> |
|||
|
|||
<Modal |
|||
title="域名验证" |
|||
visible={detailsVisible} |
|||
onOk={openDetails} |
|||
onCancel={closeDetails} |
|||
width={1200} // 设置宽度为800px
|
|||
footer={[ |
|||
<Button key="ok" type="primary" onClick={openDetails}> |
|||
确定 |
|||
</Button>, |
|||
]} |
|||
> |
|||
<Text> |
|||
请您添加以下DNS解析记录 <Link href="#">参考文档</Link> |
|||
</Text> |
|||
<br /> |
|||
<Text>1. 只需要添加一次即可,添加后请勿删除记录。</Text> |
|||
<br /> |
|||
<Text> |
|||
2. 需等待1-2分钟。(<Text type="danger">如果长时间没通过,请联系客服解决</Text>) |
|||
</Text> |
|||
<Table columns={detailsColumns} dataSource={detailsData} pagination={false} /> |
|||
</Modal> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default CertificateManagement; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue