From b110d38e73c75cad3a17c1ff5fc93dea1e7bbb87 Mon Sep 17 00:00:00 2001
From: attacker
Date: Sun, 14 Jun 2026 09:07:11 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9F=9F=E5=90=8D=E7=AE=A1=E7=90=86?=
=?UTF-8?q?=E5=A2=9E=E5=88=A0=E6=94=B9=E6=9F=A5=20=E2=80=94=20=E6=96=B0?=
=?UTF-8?q?=E5=A2=9EPUT=20/domains/:id=E7=BC=96=E8=BE=91=E7=AB=AF=E7=82=B9?=
=?UTF-8?q?=20+=20=E5=88=9B=E5=BB=BA/=E7=BC=96=E8=BE=91=E5=BC=B9=E7=AA=97?=
=?UTF-8?q?=20+=20=E6=97=A5=E6=9C=9F=E5=AD=97=E6=AE=B5=E5=AD=97=E7=AC=A6?=
=?UTF-8?q?=E4=B8=B2=E8=A7=A3=E6=9E=90=E4=BF=AE=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/internal/api/handlers/server.go | 114 ++++++++-
backend/internal/api/routes/routes.go | 1 +
.../src/components/cloud/DomainManager.tsx | 225 +++++++++++++++++-
frontend/src/services/api.ts | 3 +
4 files changed, 333 insertions(+), 10 deletions(-)
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('all');
+ // 创建/编辑域名表单状态
+ const [formOpen, setFormOpen] = useState(false);
+ const [submitting, setSubmitting] = useState(false);
+ const [editingDomain, setEditingDomain] = useState(null);
+ const [formData, setFormData] = useState({
+ name: '',
+ registrar: '',
+ status: 'active',
+ expire_date: '',
+ dns_provider: '',
+ record_count: '',
+ ssl_enabled: false,
+ ssl_expire_date: '',
+ });
+
+ const resetForm = () => {
+ setEditingDomain(null);
+ setFormData({
+ name: '',
+ registrar: '',
+ status: 'active',
+ expire_date: '',
+ dns_provider: '',
+ record_count: '',
+ ssl_enabled: false,
+ ssl_expire_date: '',
+ });
+ };
+
+ const handleOpenCreate = () => {
+ resetForm();
+ setFormOpen(true);
+ };
+
+ const handleOpenEdit = (domain: any) => {
+ setEditingDomain(domain);
+ // 后端返回 RFC3339 时间,截取 YYYY-MM-DD 部分给 date input
+ const toDateInput = (v: string) => (v ? v.substring(0, 10) : '');
+ setFormData({
+ name: domain.name || '',
+ registrar: domain.registrar || '',
+ status: domain.status || 'active',
+ expire_date: toDateInput(domain.expire_date || ''),
+ dns_provider: domain.dns_provider || domain.dnsProvider || '',
+ record_count: String(domain.record_count ?? domain.recordCount ?? ''),
+ ssl_enabled: domain.ssl_enabled ?? domain.sslEnabled ?? false,
+ ssl_expire_date: toDateInput(domain.ssl_expire_date || ''),
+ });
+ setFormOpen(true);
+ };
+
+ const handleSubmit = async () => {
+ if (!formData.name.trim()) return;
+ try {
+ setSubmitting(true);
+ const payload: any = {
+ name: formData.name.trim(),
+ registrar: formData.registrar.trim(),
+ status: formData.status,
+ expire_date: formData.expire_date,
+ dns_provider: formData.dns_provider.trim(),
+ record_count: Number(formData.record_count) || 0,
+ ssl_enabled: formData.ssl_enabled,
+ ssl_expire_date: formData.ssl_expire_date,
+ };
+ if (editingDomain) {
+ await cloudApi.updateDomain(editingDomain.id, payload);
+ } else {
+ await cloudApi.createDomain(payload);
+ }
+ setFormOpen(false);
+ loadData();
+ } catch (e) {
+ console.error(editingDomain ? '更新域名失败:' : '创建域名失败:', e);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
const loadData = async () => {
try {
setLoading(true);
@@ -83,7 +171,7 @@ export function DomainManager() {
刷新
-
-
-
-
-
+ handleOpenEdit(domain)}
+ >
)}
+
+ {/* 创建/编辑域名 Dialog */}
+
);
}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index f36dd69..76d7a33 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -339,6 +339,9 @@ export const cloudApi = {
createDomain: async (data: any) => {
return http.post('/domains', data);
},
+ updateDomain: async (id: number | string, data: any) => {
+ return http.put(`/domains/${id}`, data);
+ },
deleteDomain: async (id: number | string) => {
return http.delete(`/domains/${id}`);
},