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() { 刷新 - @@ -255,10 +343,12 @@ export function DomainManager() {

- - + + + +
); } 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}`); },