diff --git a/backend/internal/api/handlers/server.go b/backend/internal/api/handlers/server.go
index 5b3609d..c88084b 100644
--- a/backend/internal/api/handlers/server.go
+++ b/backend/internal/api/handlers/server.go
@@ -4,6 +4,7 @@ import (
"net/http"
"strconv"
"strings"
+ "time"
"ezops/internal/models"
@@ -616,8 +617,8 @@ func (h *DomainHandler) ListDomains(c *gin.Context) {
// @Success 200 {object} models.Response{data=models.Domain}
// @Router /domains [post]
func (h *DomainHandler) CreateDomain(c *gin.Context) {
- var domain models.Domain
- if err := c.ShouldBindJSON(&domain); err != nil {
+ var req updateDomainRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
@@ -625,6 +626,30 @@ func (h *DomainHandler) CreateDomain(c *gin.Context) {
return
}
+ if strings.TrimSpace(req.Name) == "" {
+ c.JSON(http.StatusBadRequest, models.Response{
+ Code: 400,
+ Message: "域名不能为空",
+ })
+ return
+ }
+
+ status := req.Status
+ if status == "" {
+ status = "active"
+ }
+
+ domain := models.Domain{
+ Name: strings.TrimSpace(req.Name),
+ Registrar: req.Registrar,
+ Status: status,
+ ExpireDate: parseDateStr(req.ExpireDate),
+ DNSProvider: req.DNSProvider,
+ RecordCount: req.RecordCount,
+ SSLEnabled: req.SSLEnabled,
+ SSLExpireDate: parseDateStr(req.SSLExpireDate),
+ }
+
if err := h.db.Create(&domain).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
@@ -640,6 +665,31 @@ func (h *DomainHandler) CreateDomain(c *gin.Context) {
})
}
+// updateDomainRequest 域名创建/更新请求(日期字段用字符串接收,避免 RFC3339 解析失败)
+type updateDomainRequest struct {
+ Name string `json:"name"`
+ Registrar string `json:"registrar"`
+ Status string `json:"status"`
+ ExpireDate string `json:"expire_date"`
+ DNSProvider string `json:"dns_provider"`
+ RecordCount int `json:"record_count"`
+ SSLEnabled bool `json:"ssl_enabled"`
+ SSLExpireDate string `json:"ssl_expire_date"`
+}
+
+// parseDateStr 将 YYYY-MM-DD 格式字符串解析为 *time.Time,空串返回 nil
+func parseDateStr(s string) *time.Time {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return nil
+ }
+ t, err := time.Parse("2006-01-02", s)
+ if err != nil {
+ return nil
+ }
+ return &t
+}
+
// DeleteDomain 删除域名
// @Summary 删除域名
// @Description 删除指定域名
@@ -665,3 +715,63 @@ func (h *DomainHandler) DeleteDomain(c *gin.Context) {
Message: "删除成功",
})
}
+
+// UpdateDomain 更新域名
+// @Summary 更新域名
+// @Description 更新指定域名信息
+// @Tags 域名
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param id path int true "域名ID"
+// @Param request body updateDomainRequest true "更新请求"
+// @Success 200 {object} models.Response{data=models.Domain}
+// @Router /domains/{id} [put]
+func (h *DomainHandler) UpdateDomain(c *gin.Context) {
+ id, _ := strconv.Atoi(c.Param("id"))
+ var req updateDomainRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, models.Response{
+ Code: 400,
+ Message: "请求参数错误: " + err.Error(),
+ })
+ return
+ }
+
+ var domain models.Domain
+ if err := h.db.First(&domain, id).Error; err != nil {
+ c.JSON(http.StatusNotFound, models.Response{
+ Code: 404,
+ Message: "域名不存在",
+ })
+ return
+ }
+
+ if strings.TrimSpace(req.Name) != "" {
+ domain.Name = strings.TrimSpace(req.Name)
+ }
+ domain.Registrar = req.Registrar
+ domain.Status = req.Status
+ if domain.Status == "" {
+ domain.Status = "active"
+ }
+ domain.ExpireDate = parseDateStr(req.ExpireDate)
+ domain.DNSProvider = req.DNSProvider
+ domain.RecordCount = req.RecordCount
+ domain.SSLEnabled = req.SSLEnabled
+ domain.SSLExpireDate = parseDateStr(req.SSLExpireDate)
+
+ if err := h.db.Save(&domain).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{
+ Code: 500,
+ Message: "更新失败",
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, models.Response{
+ Code: 200,
+ Message: "更新成功",
+ Data: domain,
+ })
+}
diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go
index f16891c..2d3da96 100644
--- a/backend/internal/api/routes/routes.go
+++ b/backend/internal/api/routes/routes.go
@@ -103,6 +103,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
{
domains.GET("", domainHandler.ListDomains)
domains.POST("", domainHandler.CreateDomain)
+ domains.PUT("/:id", domainHandler.UpdateDomain)
domains.DELETE("/:id", domainHandler.DeleteDomain)
}
diff --git a/frontend/src/components/cloud/DomainManager.tsx b/frontend/src/components/cloud/DomainManager.tsx
index 1f1d0a1..2f57a88 100644
--- a/frontend/src/components/cloud/DomainManager.tsx
+++ b/frontend/src/components/cloud/DomainManager.tsx
@@ -1,8 +1,17 @@
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
import {
Plus,
Search,
@@ -12,8 +21,8 @@ import {
CheckCircle2,
XCircle,
Clock,
- ExternalLink,
- RefreshCw
+ RefreshCw,
+ Loader2,
} from 'lucide-react';
import { cloudApi } from '@/services/api';
@@ -23,6 +32,85 @@ export function DomainManager() {
const [searchQuery, setSearchQuery] = useState('');
const [selectedStatus, setSelectedStatus] = useState