From a4eff0b408fee1fc36ac5a19aba2d0f6eb28eef7 Mon Sep 17 00:00:00 2001
From: Fu Diwei <fudiwei@sina.com>
Date: Mon, 9 Dec 2024 19:42:56 +0800
Subject: [PATCH] feat(ui): enhance certificate downloading

---
 .../certificate/CertificateDetail.tsx         | 46 +++++++++++++++----
 ui/src/pages/dashboard/Dashboard.tsx          |  4 +-
 2 files changed, 38 insertions(+), 12 deletions(-)

diff --git a/ui/src/components/certificate/CertificateDetail.tsx b/ui/src/components/certificate/CertificateDetail.tsx
index c877ddd9..45e7ae51 100644
--- a/ui/src/components/certificate/CertificateDetail.tsx
+++ b/ui/src/components/certificate/CertificateDetail.tsx
@@ -1,7 +1,7 @@
 import { useTranslation } from "react-i18next";
-import { Button, Form, Input, message, Tooltip } from "antd";
+import { Button, Dropdown, Form, Input, message, Space, Tooltip } from "antd";
 import { CopyToClipboard } from "react-copy-to-clipboard";
-import { Clipboard as ClipboardIcon } from "lucide-react";
+import { ChevronDown as ChevronDownIcon, Clipboard as ClipboardIcon, ThumbsUp as ThumbsUpIcon } from "lucide-react";
 
 import { type Certificate } from "@/domain/certificate";
 import { saveFiles2Zip } from "@/utils/file";
@@ -15,8 +15,7 @@ const CertificateDetail = ({ data }: CertificateDetailProps) => {
 
   const [messageApi, MessageContextHolder] = message.useMessage();
 
-  const handleDownloadClick = async () => {
-    // TODO: 支持下载多种格式
+  const handleDownloadPEMClick = async () => {
     const zipName = `${data.id}-${data.san}.zip`;
     const files = [
       {
@@ -73,14 +72,41 @@ const CertificateDetail = ({ data }: CertificateDetailProps) => {
       </Form>
 
       <div className="flex items-center justify-end">
-        <Button
-          type="primary"
-          onClick={() => {
-            handleDownloadClick();
+        <Dropdown
+          menu={{
+            items: [
+              {
+                key: "PEM",
+                label: "PEM",
+                extra: <ThumbsUpIcon size="14" />,
+                onClick: () => handleDownloadPEMClick(),
+              },
+              {
+                key: "PFX",
+                label: "PFX",
+                onClick: () => {
+                  // TODO: 下载 PFX 格式证书
+                  alert("TODO");
+                },
+              },
+              {
+                key: "JKS",
+                label: "JKS",
+                onClick: () => {
+                  // TODO: 下载 JKS 格式证书
+                  alert("TODO");
+                },
+              },
+            ],
           }}
         >
-          {t("certificate.action.download")}
-        </Button>
+          <Button type="primary">
+            <Space>
+              <span>{t("certificate.action.download")}</span>
+              <ChevronDownIcon size={14} />
+            </Space>
+          </Button>
+        </Dropdown>
       </div>
     </div>
   );
diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx
index 60340cfc..69c9ff6c 100644
--- a/ui/src/pages/dashboard/Dashboard.tsx
+++ b/ui/src/pages/dashboard/Dashboard.tsx
@@ -65,7 +65,7 @@ const Dashboard = () => {
 
       <PageHeader title={t("dashboard.page.title")} />
 
-      <Row gutter={[16, 16]}>
+      <Row className="justify-stretch" gutter={[16, 16]}>
         <Col {...statisticGridSpans}>
           <StatisticCard
             icon={<SquareSigmaIcon size={48} strokeWidth={1} color={themeToken.colorInfo} />}
@@ -134,7 +134,7 @@ const StatisticCard = ({
   onClick?: () => void;
 }) => {
   return (
-    <Card className="overflow-hidden" bordered={false} hoverable onClick={onClick}>
+    <Card className="size-full overflow-hidden" bordered={false} hoverable onClick={onClick}>
       <Space size="middle">
         {icon}
         <Statistic