mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-19 10:50:13 +00:00
add tencent cdn deployer
This commit is contained in:
parent
1861e73531
commit
1e11b23fdc
43
README.md
43
README.md
@ -15,22 +15,21 @@ Certimate 是一个开源的 SSL 证书管理工具,具有以下特点:
|
||||
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。
|
||||
|
||||
- [Certimate](#certimate)
|
||||
- [安装](#安装)
|
||||
- [二进制文件](#二进制文件)
|
||||
- [Docker 安装](#docker-安装)
|
||||
- [默认账号:](#默认账号)
|
||||
- [概念](#概念)
|
||||
- [域名](#域名)
|
||||
- [dns 服务商授权信息](#dns-服务商授权信息)
|
||||
- [部署服务商授权信息](#部署服务商授权信息)
|
||||
- [使用](#使用)
|
||||
- [一、安装](#一安装)
|
||||
- [1. 二进制文件](#1-二进制文件)
|
||||
- [2. Docker 安装](#2-docker-安装)
|
||||
- [二、概念](#二概念)
|
||||
- [1. 域名](#1-域名)
|
||||
- [2. dns 服务商授权信息](#2-dns-服务商授权信息)
|
||||
- [3. 部署服务商授权信息](#3-部署服务商授权信息)
|
||||
- [三、使用](#三使用)
|
||||
- [许可证](#许可证)
|
||||
|
||||
|
||||
|
||||
## 安装
|
||||
## 一、安装
|
||||
|
||||
### 二进制文件
|
||||
### 1. 二进制文件
|
||||
|
||||
你可以直接从[Releases 页](https://github.com/usual2970/certimate/releases)下载预先编译好的二进制文件,解压后执行:
|
||||
|
||||
@ -39,7 +38,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
|
||||
```
|
||||
|
||||
|
||||
### Docker 安装
|
||||
### 2. Docker 安装
|
||||
|
||||
```bash
|
||||
|
||||
@ -49,29 +48,27 @@ git clone git@github.com:usual2970/certimate.git && cd certimate/docker && docke
|
||||
|
||||
然后在浏览器中访问 http://127.0.0.1:8090 即可访问 Certimate 管理页面。
|
||||
|
||||
### 默认账号:
|
||||
|
||||
```bash
|
||||
用户名:admin@certimate.fun
|
||||
密码:1234567890
|
||||
```
|
||||
|
||||
## 概念
|
||||
## 二、概念
|
||||
|
||||
Certimate 的工作流程如下:
|
||||
|
||||
1. 用户通过 Certimate 管理页面填写申请证书的信息,包括域名、dns 服务商的授权信息、以及要部署到的服务商的授权信息。
|
||||
2. Certimate 向证书场商的 API 发起申请请求,获取 SSL 证书。
|
||||
3. Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
|
||||
4. Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
|
||||
* 用户通过 Certimate 管理页面填写申请证书的信息,包括域名、dns 服务商的授权信息、以及要部署到的服务商的授权信息。
|
||||
* Certimate 向证书场商的 API 发起申请请求,获取 SSL 证书。
|
||||
* Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
|
||||
* Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
|
||||
|
||||
这就涉及域名、dns 服务商的授权信息、部署服务商的授权信息等。
|
||||
|
||||
### 域名
|
||||
### 1. 域名
|
||||
|
||||
就是要申请证书的域名。
|
||||
|
||||
### dns 服务商授权信息
|
||||
### 2. dns 服务商授权信息
|
||||
|
||||
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 记录。
|
||||
|
||||
@ -91,13 +88,13 @@ secretId: xxx
|
||||
secretKey: TOKEN
|
||||
```
|
||||
|
||||
### 部署服务商授权信息
|
||||
### 3. 部署服务商授权信息
|
||||
|
||||
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDN 这时你需要填写阿里云的授权信息。Certimate 会根据你填写的授权信息及域名找到对应的 CDN 服务,并将证书部署到对应的 CDN 服务上。
|
||||
|
||||
部署服务商授权信息和 dns 服务商授权信息一致,区别在于 dns 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
|
||||
|
||||
## 使用
|
||||
## 三、使用
|
||||
|
||||

|
||||
|
||||
|
4
go.mod
4
go.mod
@ -16,6 +16,9 @@ require (
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pocketbase/dbx v1.10.1
|
||||
github.com/pocketbase/pocketbase v0.22.18
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.992
|
||||
golang.org/x/crypto v0.26.0
|
||||
)
|
||||
|
||||
@ -90,7 +93,6 @@ require (
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 // indirect
|
||||
github.com/tjfoc/gmsm v1.3.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
|
7
go.sum
7
go.sum
@ -300,10 +300,15 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 h1:ERwcXqhc94L9cFxtiI0pvt7IJtlHl/p/Jayl3mLw+ms=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992 h1:266lOve+E8vzhnrb/Mr05Ee+oxXD9C82JiusY/AZqXw=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 h1:LoYv5u+gUoFpU/AmIuTRG/2KiEkdm9gCC0dTvk8WITQ=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898/go.mod h1:c1j6YQ+vCbeA8kJ59Im4UnMd1GxovlpPBDhGZoewfn8=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992 h1:A6O89OlCJQUpNxGqC/E5By04UNKBryIt5olQIGOx8mg=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992/go.mod h1:BcvC7ZPdSlhRggVq4J1ToJlgv8bmODIAuSo0naFZOLo=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.992 h1:ttCM2rrkGipHMFTavrPExKCWcfNjT7AMQ5ERrPExdI4=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.992/go.mod h1:WtzarrflM+eoyD8vcRuIPd8fT5UXD4IhUry6iSAUnxc=
|
||||
github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||
|
@ -1,17 +1,13 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||
)
|
||||
|
||||
type aliyunAccess struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type aliyun struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
@ -24,7 +20,7 @@ func NewAliyun(option *ApplyOption) Applicant {
|
||||
|
||||
func (a *aliyun) Apply() (*Certificate, error) {
|
||||
|
||||
access := &aliyunAccess{}
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)
|
||||
|
@ -1,17 +1,13 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||
)
|
||||
|
||||
type tencentAccess struct {
|
||||
SecretId string `json:"secretId"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type tencent struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
@ -24,7 +20,7 @@ func NewTencent(option *ApplyOption) Applicant {
|
||||
|
||||
func (t *tencent) Apply() (*Certificate, error) {
|
||||
|
||||
access := &tencentAccess{}
|
||||
access := &domain.TencentAccess{}
|
||||
json.Unmarshal([]byte(t.option.Access), access)
|
||||
|
||||
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)
|
||||
|
@ -2,6 +2,7 @@ package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/rand"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@ -15,18 +16,13 @@ import (
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
type aliyunAccess struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type aliyun struct {
|
||||
client *cas20200407.Client
|
||||
option *DeployerOption
|
||||
}
|
||||
|
||||
func NewAliyun(option *DeployerOption) (Deployer, error) {
|
||||
access := &aliyunAccess{}
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
a := &aliyun{
|
||||
option: option,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@ -17,7 +18,7 @@ type AliyunCdn struct {
|
||||
}
|
||||
|
||||
func NewAliyunCdn(option *DeployerOption) (*AliyunCdn, error) {
|
||||
access := &aliyunAccess{}
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
a := &AliyunCdn{
|
||||
option: option,
|
||||
|
@ -18,6 +18,7 @@ const (
|
||||
targetAliyunCdn = "aliyun-cdn"
|
||||
targetSSH = "ssh"
|
||||
targetWebhook = "webhook"
|
||||
targetTencentCdn = "tencent-cdn"
|
||||
)
|
||||
|
||||
type DeployerOption struct {
|
||||
@ -58,6 +59,8 @@ func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
|
||||
return NewSSH(option)
|
||||
case targetWebhook:
|
||||
return NewWebhook(option)
|
||||
case targetTencentCdn:
|
||||
return NewTencentCdn(option)
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
161
internal/deployer/tencent_cdn.go
Normal file
161
internal/deployer/tencent_cdn.go
Normal file
@ -0,0 +1,161 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||
tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813"
|
||||
)
|
||||
|
||||
type tencentCdn struct {
|
||||
option *DeployerOption
|
||||
credential *common.Credential
|
||||
}
|
||||
|
||||
func NewTencentCdn(option *DeployerOption) (Deployer, error) {
|
||||
|
||||
access := &domain.TencentAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
|
||||
}
|
||||
|
||||
credential := common.NewCredential(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
)
|
||||
|
||||
return &tencentCdn{
|
||||
option: option,
|
||||
credential: credential,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *tencentCdn) Deploy(ctx context.Context) error {
|
||||
|
||||
// 查询有没有对应的资源
|
||||
resource, err := t.resource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get resource: %w", err)
|
||||
}
|
||||
|
||||
// 上传证书
|
||||
certId, err := t.uploadCert()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
|
||||
if err := t.deploy(resource, certId); err != nil {
|
||||
return fmt.Errorf("failed to deploy: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tencentCdn) uploadCert() (string, error) {
|
||||
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
|
||||
client, _ := ssl.NewClient(t.credential, "", cpf)
|
||||
|
||||
request := ssl.NewUploadCertificateRequest()
|
||||
|
||||
request.CertificatePublicKey = common.StringPtr(t.option.Certificate.Certificate)
|
||||
request.CertificatePrivateKey = common.StringPtr(t.option.Certificate.PrivateKey)
|
||||
request.Alias = common.StringPtr(t.option.Domain)
|
||||
request.Repeatable = common.BoolPtr(true)
|
||||
|
||||
response, err := client.UploadCertificate(request)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
|
||||
return *response.Response.CertificateId, nil
|
||||
}
|
||||
|
||||
func (t *tencentCdn) deploy(resource *tag.ResourceTagMapping, certId string) error {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
// 实例化要请求产品的client对象,clientProfile是可选的
|
||||
client, _ := ssl.NewClient(t.credential, "", cpf)
|
||||
|
||||
resourceId, err := getResourceId(resource)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get resource id: %w", err)
|
||||
}
|
||||
|
||||
// 实例化一个请求对象,每个接口都会对应一个request对象
|
||||
request := ssl.NewDeployCertificateInstanceRequest()
|
||||
|
||||
request.CertificateId = common.StringPtr(certId)
|
||||
request.InstanceIdList = common.StringPtrs([]string{resourceId})
|
||||
request.ResourceType = common.StringPtr("cdn")
|
||||
request.Status = common.Int64Ptr(1)
|
||||
|
||||
// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
|
||||
_, err = client.DeployCertificateInstance(request)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy certificate: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tencentCdn) resource() (*tag.ResourceTagMapping, error) {
|
||||
request := tag.NewGetResourcesRequest()
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "tag.tencentcloudapi.com"
|
||||
|
||||
client, err := tag.NewClient(t.credential, "", cpf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
response, err := client.GetResources(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get resources: %w", err)
|
||||
}
|
||||
|
||||
for _, resource := range response.Response.ResourceTagMappingList {
|
||||
if t.compare(resource) {
|
||||
return resource, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("no resource found")
|
||||
|
||||
}
|
||||
|
||||
func (t *tencentCdn) compare(resource *tag.ResourceTagMapping) bool {
|
||||
slices := strings.Split(*resource.Resource, "/")
|
||||
if len(slices) != 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
typeSlices := strings.Split(slices[0], "::")
|
||||
if len(typeSlices) != 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
if typeSlices[1] != "cdn" || slices[2] != t.option.Domain {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
func getResourceId(resource *tag.ResourceTagMapping) (string, error) {
|
||||
slices := strings.Split(*resource.Resource, "/")
|
||||
if len(slices) != 3 {
|
||||
return "", errors.New("invalid resource")
|
||||
}
|
||||
return slices[2], nil
|
||||
}
|
13
internal/domain/access.go
Normal file
13
internal/domain/access.go
Normal file
@ -0,0 +1,13 @@
|
||||
package domain
|
||||
|
||||
type AliyunAccess struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
|
||||
|
||||
type TencentAccess struct {
|
||||
SecretId string `json:"secretId"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
File diff suppressed because one or more lines are too long
2
ui/dist/index.html
vendored
2
ui/dist/index.html
vendored
@ -5,7 +5,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Certimate - Your Trusted SSL Automation Partner</title>
|
||||
<script type="module" crossorigin src="/assets/index-D-MCicNE.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CGKfLY3n.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-VYJgHfoP.css">
|
||||
</head>
|
||||
<body class="bg-background">
|
||||
|
@ -30,6 +30,7 @@ export const getLastDeployment = (domain: Domain): Deployment | undefined => {
|
||||
export const targetTypeMap: Map<string, [string, string]> = new Map([
|
||||
["aliyun-cdn", ["阿里云-CDN", "/imgs/providers/aliyun.svg"]],
|
||||
["aliyun-oss", ["阿里云-OSS", "/imgs/providers/aliyun.svg"]],
|
||||
["tencent-cdn", ["腾讯云-CDN", "/imgs/providers/tencent.svg"]],
|
||||
["ssh", ["SSH部署", "/imgs/providers/ssh.png"]],
|
||||
["webhook", ["Webhook", "/imgs/providers/webhook.svg"]],
|
||||
]);
|
||||
|
@ -187,7 +187,7 @@ export default function Dashboard() {
|
||||
href="https://github.com/usual2970/certimate/releases"
|
||||
target="_blank"
|
||||
>
|
||||
Certimate v0.0.5
|
||||
Certimate v0.0.6
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,9 +34,6 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Plus } from "lucide-react";
|
||||
import { AccessEdit } from "@/components/certimate/AccessEdit";
|
||||
import { accessTypeMap } from "@/domain/access";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Edit = () => {
|
||||
const {
|
||||
@ -151,10 +148,6 @@ const Edit = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getOptionCls = (val: string) => {
|
||||
return form.getValues().targetType == val ? "border-primary" : "";
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="">
|
||||
@ -241,37 +234,33 @@ const Edit = () => {
|
||||
<FormItem>
|
||||
<FormLabel>部署服务类型</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
className="flex mt-3 space-x-2"
|
||||
onValueChange={(val: string) => {
|
||||
setTargetType(val);
|
||||
form.setValue("targetType", val);
|
||||
}}
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
setTargetType(value);
|
||||
form.setValue("targetType", value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择部署服务类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>部署服务类型</SelectLabel>
|
||||
{targetTypeKeys.map((key) => (
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
key={key}
|
||||
>
|
||||
<Label>
|
||||
<RadioGroupItem value={key} id={key} hidden />
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center space-x-2 border p-2 rounded cursor-pointer",
|
||||
getOptionCls(key)
|
||||
)}
|
||||
>
|
||||
<SelectItem key={key} value={key}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img
|
||||
className="w-6"
|
||||
src={targetTypeMap.get(key)?.[1]}
|
||||
className="h-6"
|
||||
/>
|
||||
<div>{targetTypeMap.get(key)?.[0]}</div>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
Loading…
x
Reference in New Issue
Block a user