From e11dc5fadde31d073bcb65372a6c2364641fdfc8 Mon Sep 17 00:00:00 2001 From: BugPleaseGo Date: Fri, 31 Jul 2026 10:01:20 +0800 Subject: [PATCH] Add Website Template (#13400) * feat: Add Website Template * fix: Don't display the template list * feat: Add Mcp TopList * docs: Remove Mcp TopList * Add more languages --------- Co-authored-by: CityFun <31820853+zhengkunwang223@users.noreply.github.com> --- agent/app/api/v2/entry.go | 1 + agent/app/api/v2/website_template.go | 259 +++++++++++ agent/app/dto/request/website.go | 2 + agent/app/dto/request/website_template.go | 46 ++ agent/app/dto/response/website_template.go | 18 + agent/app/model/website_template.go | 28 ++ agent/app/repo/website_template.go | 131 ++++++ agent/app/service/entry.go | 14 +- agent/app/service/website.go | 15 + agent/app/service/website_template.go | 419 ++++++++++++++++++ agent/init/migration/migrate.go | 1 + agent/init/migration/migrations/init.go | 9 + agent/router/common.go | 1 + agent/router/ro_website_template.go | 28 ++ core/init/migration/helper/menu.go | 2 + core/init/migration/migrate.go | 1 + core/init/migration/migrations/init.go | 15 + frontend/src/api/interface/website.ts | 74 ++++ frontend/src/api/modules/website.ts | 46 ++ frontend/src/lang/modules/en.ts | 44 ++ frontend/src/lang/modules/es-es.ts | 44 ++ frontend/src/lang/modules/fa.ts | 44 ++ frontend/src/lang/modules/ja.ts | 44 ++ frontend/src/lang/modules/ko.ts | 44 ++ frontend/src/lang/modules/lo.ts | 44 ++ frontend/src/lang/modules/ms.ts | 44 ++ frontend/src/lang/modules/pt-br.ts | 44 ++ frontend/src/lang/modules/ru.ts | 44 ++ frontend/src/lang/modules/tr.ts | 44 ++ frontend/src/lang/modules/zh-Hant.ts | 44 ++ frontend/src/lang/modules/zh.ts | 44 ++ frontend/src/routers/modules/website.ts | 10 + frontend/src/views/website/template/index.vue | 157 +++++++ .../views/website/template/operate/index.vue | 237 ++++++++++ .../views/website/template/output/create.vue | 234 ++++++++++ .../views/website/template/output/index.vue | 99 +++++ .../views/website/website/create/index.vue | 27 ++ 37 files changed, 2396 insertions(+), 6 deletions(-) create mode 100644 agent/app/api/v2/website_template.go create mode 100644 agent/app/dto/request/website_template.go create mode 100644 agent/app/dto/response/website_template.go create mode 100644 agent/app/model/website_template.go create mode 100644 agent/app/repo/website_template.go create mode 100644 agent/app/service/website_template.go create mode 100644 agent/router/ro_website_template.go create mode 100644 frontend/src/views/website/template/index.vue create mode 100644 frontend/src/views/website/template/operate/index.vue create mode 100644 frontend/src/views/website/template/output/create.vue create mode 100644 frontend/src/views/website/template/output/index.vue diff --git a/agent/app/api/v2/entry.go b/agent/app/api/v2/entry.go index 74833593c..5f2122377 100644 --- a/agent/app/api/v2/entry.go +++ b/agent/app/api/v2/entry.go @@ -61,6 +61,7 @@ var ( websiteDnsAccountService = service.NewIWebsiteDnsAccountService() websiteSSLService = service.NewIWebsiteSSLService() websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService() + websiteTemplateService = service.NewIWebsiteTemplateService() nginxService = service.NewINginxService() diff --git a/agent/app/api/v2/website_template.go b/agent/app/api/v2/website_template.go new file mode 100644 index 000000000..2f13b5c22 --- /dev/null +++ b/agent/app/api/v2/website_template.go @@ -0,0 +1,259 @@ +package v2 + +import ( + "io" + + "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/gin-gonic/gin" +) + +// @Tags Website Template +// @Summary Page website templates +// @Accept json +// @Param request body request.WebsiteTemplateSearch true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/templates/search [post] +func (b *BaseApi) PageWebsiteTemplate(c *gin.Context) { + var req request.WebsiteTemplateSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, templates, err := websiteTemplateService.PageTemplate(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: templates, + }) +} + +// @Tags Website Template +// @Summary Create website template +// @Accept json +// @Param request body request.WebsiteTemplateCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/templates [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站模板 [name]","formatEN":"Create website template [name]"} +func (b *BaseApi) CreateWebsiteTemplate(c *gin.Context) { + var req request.WebsiteTemplateCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteTemplateService.CreateTemplate(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags Website Template +// @Summary Update website template +// @Accept json +// @Param request body request.WebsiteTemplateUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/templates/update [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新网站模板 [name]","formatEN":"Update website template [name]"} +func (b *BaseApi) UpdateWebsiteTemplate(c *gin.Context) { + var req request.WebsiteTemplateUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteTemplateService.UpdateTemplate(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags Website Template +// @Summary Delete website template +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/templates/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_templates","output_column":"name","output_value":"name"}],"formatZH":"删除网站模板 [name]","formatEN":"Delete website template [name]"} +func (b *BaseApi) DeleteWebsiteTemplate(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteTemplateService.DeleteTemplate(req.ID); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags Website Template +// @Summary Get website template +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Success 200 {object} response.WebsiteTemplateDTO +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/templates/get [post] +func (b *BaseApi) GetWebsiteTemplate(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + template, err := websiteTemplateService.GetTemplate(req.ID) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, template) +} + +// @Tags Website Template +// @Summary Upload website template zip +// @Accept multipart/form-data +// @Param file formData file true "file" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/templates/upload [post] +func (b *BaseApi) UploadTemplateZip(c *gin.Context) { + fileHeader, err := c.FormFile("file") + if err != nil { + helper.BadRequest(c, err) + return + } + file, err := fileHeader.Open() + if err != nil { + helper.InternalServer(c, err) + return + } + defer file.Close() + content, err := io.ReadAll(file) + if err != nil { + helper.InternalServer(c, err) + return + } + filePath, variables, err := websiteTemplateService.SaveUploadZip(fileHeader.Filename, content) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, gin.H{"filePath": filePath, "variables": variables}) +} + +// @Tags Website Template +// @Summary Preview website template +// @Accept json +// @Param request body request.WebsitePreviewReq true "request" +// @Success 200 {object} response.WebsitePreviewDTO +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/templates/preview [post] +func (b *BaseApi) PreviewWebsiteTemplate(c *gin.Context) { + var req request.WebsitePreviewReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + preview, err := websiteTemplateService.Preview(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, preview) +} + +// @Tags Website Template +// @Summary Page website template outputs +// @Accept json +// @Param request body request.WebsiteTemplateOutputSearch true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/templates/outputs/search [post] +func (b *BaseApi) PageWebsiteTemplateOutput(c *gin.Context) { + var req request.WebsiteTemplateOutputSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, outputs, err := websiteTemplateService.PageOutput(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: outputs, + }) +} + +// @Tags Website Template +// @Summary Create website template output +// @Accept json +// @Param request body request.WebsiteTemplateOutputCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/templates/outputs [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成模板产物 [name]","formatEN":"Generate template output [name]"} +func (b *BaseApi) CreateWebsiteTemplateOutput(c *gin.Context) { + var req request.WebsiteTemplateOutputCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteTemplateService.CreateOutput(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags Website Template +// @Summary Delete website template output +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/templates/outputs/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_template_outputs","output_column":"name","output_value":"name"}],"formatZH":"删除模板产物 [name]","formatEN":"Delete template output [name]"} +func (b *BaseApi) DeleteWebsiteTemplateOutput(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteTemplateService.DeleteOutput(req.ID); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags Website Template +// @Summary Get website template output +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Success 200 {object} response.WebsiteTemplateOutputDTO +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/templates/outputs/get [post] +func (b *BaseApi) GetWebsiteTemplateOutput(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + output, err := websiteTemplateService.GetOutput(req.ID) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, output) +} diff --git a/agent/app/dto/request/website.go b/agent/app/dto/request/website.go index 734d45223..397376a16 100644 --- a/agent/app/dto/request/website.go +++ b/agent/app/dto/request/website.go @@ -34,6 +34,8 @@ type WebsiteCreate struct { SiteDir string `json:"siteDir"` + TemplateOutputID uint `json:"templateOutputID"` + RuntimeConfig FtpConfig DataBaseConfig diff --git a/agent/app/dto/request/website_template.go b/agent/app/dto/request/website_template.go new file mode 100644 index 000000000..72a13db8e --- /dev/null +++ b/agent/app/dto/request/website_template.go @@ -0,0 +1,46 @@ +package request + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto" +) + +type WebsiteTemplateSearch struct { + dto.PageInfo + Name string `json:"name"` + Type string `json:"type"` +} + +type WebsiteTemplateCreate struct { + Name string `json:"name" validate:"required"` + Type string `json:"type" validate:"required,oneof=single multi"` + Content string `json:"content"` + FilePath string `json:"filePath"` + Variables string `json:"variables"` + Remark string `json:"remark"` +} + +type WebsiteTemplateUpdate struct { + ID uint `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Type string `json:"type" validate:"required,oneof=single multi"` + Content string `json:"content"` + FilePath string `json:"filePath"` + Variables string `json:"variables"` + Remark string `json:"remark"` +} + +type WebsiteTemplateOutputSearch struct { + dto.PageInfo + TemplateID uint `json:"templateID"` +} + +type WebsiteTemplateOutputCreate struct { + TemplateID uint `json:"templateID" validate:"required"` + Name string `json:"name" validate:"required"` + VariableValues map[string]string `json:"variableValues"` +} + +type WebsitePreviewReq struct { + TemplateID uint `json:"templateID" validate:"required"` + VariableValues map[string]string `json:"variableValues"` +} diff --git a/agent/app/dto/response/website_template.go b/agent/app/dto/response/website_template.go new file mode 100644 index 000000000..e1ab6ee85 --- /dev/null +++ b/agent/app/dto/response/website_template.go @@ -0,0 +1,18 @@ +package response + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +type WebsiteTemplateDTO struct { + model.WebsiteTemplate +} + +type WebsiteTemplateOutputDTO struct { + model.WebsiteTemplateOutput + TemplateName string `json:"templateName"` +} + +type WebsitePreviewDTO struct { + HTML string `json:"html"` +} diff --git a/agent/app/model/website_template.go b/agent/app/model/website_template.go new file mode 100644 index 000000000..f25ecfb42 --- /dev/null +++ b/agent/app/model/website_template.go @@ -0,0 +1,28 @@ +package model + +type WebsiteTemplate struct { + BaseModel + Name string `gorm:"not null" json:"name"` + Type string `gorm:"not null" json:"type"` // single | multi + Content string `gorm:"type:longtext" json:"content"` + FilePath string `json:"filePath"` + Variables string `gorm:"type:text" json:"variables"` + Remark string `json:"remark"` +} + +func (w WebsiteTemplate) TableName() string { + return "website_templates" +} + +type WebsiteTemplateOutput struct { + BaseModel + Name string `gorm:"not null" json:"name"` + TemplateID uint `gorm:"not null" json:"templateID"` + TemplateType string `json:"templateType"` + VariableValues string `gorm:"type:text" json:"variableValues"` + OutputPath string `json:"outputPath"` +} + +func (w WebsiteTemplateOutput) TableName() string { + return "website_template_outputs" +} diff --git a/agent/app/repo/website_template.go b/agent/app/repo/website_template.go new file mode 100644 index 000000000..c3ef78c0a --- /dev/null +++ b/agent/app/repo/website_template.go @@ -0,0 +1,131 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "gorm.io/gorm" +) + +type IWebsiteTemplateRepo interface { + Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplate, error) + GetFirst(opts ...DBOption) (*model.WebsiteTemplate, error) + List(opts ...DBOption) ([]model.WebsiteTemplate, error) + Create(template *model.WebsiteTemplate) error + Save(template *model.WebsiteTemplate) error + DeleteBy(opts ...DBOption) error + WithName(name string) DBOption + WithType(templateType string) DBOption +} + +func NewIWebsiteTemplateRepo() IWebsiteTemplateRepo { + return &WebsiteTemplateRepo{} +} + +type WebsiteTemplateRepo struct { +} + +func (w *WebsiteTemplateRepo) WithName(name string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("name like ?", "%"+name+"%") + } +} + +func (w *WebsiteTemplateRepo) WithType(templateType string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("type = ?", templateType) + } +} + +func (w *WebsiteTemplateRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplate, error) { + var templates []model.WebsiteTemplate + db := getDb(opts...).Model(&model.WebsiteTemplate{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&templates).Error + return count, templates, err +} + +func (w *WebsiteTemplateRepo) GetFirst(opts ...DBOption) (*model.WebsiteTemplate, error) { + var template model.WebsiteTemplate + db := getDb(opts...).Model(&model.WebsiteTemplate{}) + if err := db.First(&template).Error; err != nil { + return nil, err + } + return &template, nil +} + +func (w *WebsiteTemplateRepo) List(opts ...DBOption) ([]model.WebsiteTemplate, error) { + var templates []model.WebsiteTemplate + err := getDb(opts...).Model(&model.WebsiteTemplate{}).Find(&templates).Error + return templates, err +} + +func (w *WebsiteTemplateRepo) Create(template *model.WebsiteTemplate) error { + return getDb().Create(template).Error +} + +func (w *WebsiteTemplateRepo) Save(template *model.WebsiteTemplate) error { + return getDb().Save(template).Error +} + +func (w *WebsiteTemplateRepo) DeleteBy(opts ...DBOption) error { + return getDb(opts...).Delete(&model.WebsiteTemplate{}).Error +} + +type IWebsiteTemplateOutputRepo interface { + Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplateOutput, error) + GetFirst(opts ...DBOption) (*model.WebsiteTemplateOutput, error) + List(opts ...DBOption) ([]model.WebsiteTemplateOutput, error) + Create(output *model.WebsiteTemplateOutput) error + Save(output *model.WebsiteTemplateOutput) error + DeleteBy(opts ...DBOption) error + WithByTemplateID(templateID uint) DBOption +} + +func NewIWebsiteTemplateOutputRepo() IWebsiteTemplateOutputRepo { + return &WebsiteTemplateOutputRepo{} +} + +type WebsiteTemplateOutputRepo struct { +} + +func (w *WebsiteTemplateOutputRepo) WithByTemplateID(templateID uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("template_id = ?", templateID) + } +} + +func (w *WebsiteTemplateOutputRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplateOutput, error) { + var outputs []model.WebsiteTemplateOutput + db := getDb(opts...).Model(&model.WebsiteTemplateOutput{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&outputs).Error + return count, outputs, err +} + +func (w *WebsiteTemplateOutputRepo) GetFirst(opts ...DBOption) (*model.WebsiteTemplateOutput, error) { + var output model.WebsiteTemplateOutput + db := getDb(opts...).Model(&model.WebsiteTemplateOutput{}) + if err := db.First(&output).Error; err != nil { + return nil, err + } + return &output, nil +} + +func (w *WebsiteTemplateOutputRepo) List(opts ...DBOption) ([]model.WebsiteTemplateOutput, error) { + var outputs []model.WebsiteTemplateOutput + err := getDb(opts...).Model(&model.WebsiteTemplateOutput{}).Find(&outputs).Error + return outputs, err +} + +func (w *WebsiteTemplateOutputRepo) Create(output *model.WebsiteTemplateOutput) error { + return getDb().Create(output).Error +} + +func (w *WebsiteTemplateOutputRepo) Save(output *model.WebsiteTemplateOutput) error { + return getDb().Save(output).Error +} + +func (w *WebsiteTemplateOutputRepo) DeleteBy(opts ...DBOption) error { + return getDb(opts...).Delete(&model.WebsiteTemplateOutput{}).Error +} diff --git a/agent/app/service/entry.go b/agent/app/service/entry.go index fb91e430b..c3ba1186b 100644 --- a/agent/app/service/entry.go +++ b/agent/app/service/entry.go @@ -40,12 +40,14 @@ var ( settingRepo = repo.NewISettingRepo() backupRepo = repo.NewIBackupRepo() - websiteRepo = repo.NewIWebsiteRepo() - websiteDomainRepo = repo.NewIWebsiteDomainRepo() - websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo() - websiteSSLRepo = repo.NewISSLRepo() - websiteAcmeRepo = repo.NewIAcmeAccountRepo() - websiteCARepo = repo.NewIWebsiteCARepo() + websiteRepo = repo.NewIWebsiteRepo() + websiteDomainRepo = repo.NewIWebsiteDomainRepo() + websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo() + websiteSSLRepo = repo.NewISSLRepo() + websiteAcmeRepo = repo.NewIAcmeAccountRepo() + websiteCARepo = repo.NewIWebsiteCARepo() + websiteTemplateRepo = repo.NewIWebsiteTemplateRepo() + websiteTemplateOutputRepo = repo.NewIWebsiteTemplateOutputRepo() snapshotRepo = repo.NewISnapshotRepo() diff --git a/agent/app/service/website.go b/agent/app/service/website.go index 40c6a1aec..2911711de 100644 --- a/agent/app/service/website.go +++ b/agent/app/service/website.go @@ -488,6 +488,21 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig); err != nil { return err } + if create.Type == constant.Static && create.TemplateOutputID > 0 { + templateOutput, err := websiteTemplateOutputRepo.GetFirst(repo.WithByID(create.TemplateOutputID)) + if err != nil { + return err + } + if templateOutput.OutputPath == "" { + return buserr.New("ErrFileNotFound") + } + if _, err := os.Stat(templateOutput.OutputPath); err != nil { + return buserr.New("ErrFileNotFound") + } + if err := copyDir(templateOutput.OutputPath, GetSitePath(*website, SiteIndexDir)); err != nil { + return err + } + } if website.Type != constant.Stream { if err = createWafConfig(website, domains); err != nil { return err diff --git a/agent/app/service/website_template.go b/agent/app/service/website_template.go new file mode 100644 index 000000000..ce2ee1d15 --- /dev/null +++ b/agent/app/service/website_template.go @@ -0,0 +1,419 @@ +package service + +import ( + "archive/zip" + "encoding/json" + "fmt" + "io" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" +) + +type WebsiteTemplateService struct { +} + +type IWebsiteTemplateService interface { + PageTemplate(req request.WebsiteTemplateSearch) (int64, []response.WebsiteTemplateDTO, error) + CreateTemplate(req request.WebsiteTemplateCreate) error + UpdateTemplate(req request.WebsiteTemplateUpdate) error + DeleteTemplate(id uint) error + GetTemplate(id uint) (*response.WebsiteTemplateDTO, error) + SaveUploadZip(fileName string, content []byte) (string, []string, error) + PageOutput(req request.WebsiteTemplateOutputSearch) (int64, []response.WebsiteTemplateOutputDTO, error) + CreateOutput(req request.WebsiteTemplateOutputCreate) error + DeleteOutput(id uint) error + GetOutput(id uint) (*response.WebsiteTemplateOutputDTO, error) + Preview(req request.WebsitePreviewReq) (*response.WebsitePreviewDTO, error) +} + +func NewIWebsiteTemplateService() IWebsiteTemplateService { + return &WebsiteTemplateService{} +} + +func templateBaseDir() string { + return path.Join(global.Dir.DataDir, "templates") +} + +func templateFileDir() string { + return path.Join(templateBaseDir(), "files") +} + +func templateOutputDir(id uint) string { + return path.Join(templateBaseDir(), "outputs", fmt.Sprintf("%d", id)) +} + +func (w WebsiteTemplateService) PageTemplate(req request.WebsiteTemplateSearch) (int64, []response.WebsiteTemplateDTO, error) { + var opts []repo.DBOption + if req.Name != "" { + opts = append(opts, websiteTemplateRepo.WithName(req.Name)) + } + if req.Type != "" { + opts = append(opts, websiteTemplateRepo.WithType(req.Type)) + } + opts = append(opts, repo.WithOrderDesc("created_at")) + total, templates, err := websiteTemplateRepo.Page(req.Page, req.PageSize, opts...) + if err != nil { + return 0, nil, err + } + var dtos []response.WebsiteTemplateDTO + for _, t := range templates { + dtos = append(dtos, response.WebsiteTemplateDTO{WebsiteTemplate: t}) + } + return total, dtos, nil +} + +func (w WebsiteTemplateService) CreateTemplate(req request.WebsiteTemplateCreate) error { + if exist, _ := websiteTemplateRepo.GetFirst(repo.WithByName(req.Name)); exist != nil { + return buserr.New("ErrNameIsExist") + } + template := &model.WebsiteTemplate{ + Name: req.Name, + Type: req.Type, + Content: req.Content, + FilePath: req.FilePath, + Variables: req.Variables, + Remark: req.Remark, + } + return websiteTemplateRepo.Create(template) +} + +func (w WebsiteTemplateService) UpdateTemplate(req request.WebsiteTemplateUpdate) error { + template, err := websiteTemplateRepo.GetFirst(repo.WithByID(req.ID)) + if err != nil { + return err + } + if exist, _ := websiteTemplateRepo.GetFirst(repo.WithByName(req.Name), repo.WithByNOTID(req.ID)); exist != nil { + return buserr.New("ErrNameIsExist") + } + template.Name = req.Name + template.Type = req.Type + template.Content = req.Content + if req.FilePath != template.FilePath && template.FilePath != "" && strings.HasPrefix(template.FilePath, templateBaseDir()) { + _ = os.Remove(template.FilePath) + } + template.FilePath = req.FilePath + template.Variables = req.Variables + template.Remark = req.Remark + return websiteTemplateRepo.Save(template) +} + +func (w WebsiteTemplateService) DeleteTemplate(id uint) error { + template, err := websiteTemplateRepo.GetFirst(repo.WithByID(id)) + if err != nil { + return err + } + outputs, _ := websiteTemplateOutputRepo.List(websiteTemplateOutputRepo.WithByTemplateID(id)) + for _, output := range outputs { + if output.OutputPath != "" && strings.HasPrefix(output.OutputPath, templateBaseDir()) { + _ = os.RemoveAll(output.OutputPath) + } + } + if template.FilePath != "" && strings.HasPrefix(template.FilePath, templateBaseDir()) { + _ = os.Remove(template.FilePath) + } + if err := websiteTemplateOutputRepo.DeleteBy(websiteTemplateOutputRepo.WithByTemplateID(id)); err != nil { + return err + } + return websiteTemplateRepo.DeleteBy(repo.WithByID(id)) +} + +func (w WebsiteTemplateService) GetTemplate(id uint) (*response.WebsiteTemplateDTO, error) { + template, err := websiteTemplateRepo.GetFirst(repo.WithByID(id)) + if err != nil { + return nil, err + } + return &response.WebsiteTemplateDTO{WebsiteTemplate: *template}, nil +} + +func (w WebsiteTemplateService) SaveUploadZip(fileName string, content []byte) (string, []string, error) { + if !strings.HasSuffix(strings.ToLower(fileName), ".zip") { + return "", nil, buserr.WithName("ErrNotSupportType", fileName) + } + dir := templateFileDir() + if err := os.MkdirAll(dir, constant.DirPerm); err != nil { + return "", nil, err + } + filePath := path.Join(dir, fmt.Sprintf("%d_%s", time.Now().Unix(), filepath.Base(fileName))) + if err := os.WriteFile(filePath, content, constant.FilePerm); err != nil { + return "", nil, err + } + return filePath, scanZipVariables(filePath), nil +} + +func scanZipVariables(zipPath string) []string { + variables := []string{} + reader, err := zip.OpenReader(zipPath) + if err != nil { + return variables + } + defer reader.Close() + seen := make(map[string]struct{}) + for _, file := range reader.File { + if file.FileInfo().IsDir() || !isTextFile(file.Name) { + continue + } + rc, err := file.Open() + if err != nil { + continue + } + content, err := io.ReadAll(rc) + _ = rc.Close() + if err != nil { + continue + } + for _, match := range templateVarRegex.FindAllStringSubmatch(string(content), -1) { + if _, ok := seen[match[1]]; !ok { + seen[match[1]] = struct{}{} + variables = append(variables, match[1]) + } + } + } + return variables +} + +func (w WebsiteTemplateService) PageOutput(req request.WebsiteTemplateOutputSearch) (int64, []response.WebsiteTemplateOutputDTO, error) { + var opts []repo.DBOption + if req.TemplateID > 0 { + opts = append(opts, websiteTemplateOutputRepo.WithByTemplateID(req.TemplateID)) + } + opts = append(opts, repo.WithOrderDesc("created_at")) + total, outputs, err := websiteTemplateOutputRepo.Page(req.Page, req.PageSize, opts...) + if err != nil { + return 0, nil, err + } + var dtos []response.WebsiteTemplateOutputDTO + for _, output := range outputs { + item := response.WebsiteTemplateOutputDTO{WebsiteTemplateOutput: output} + if template, err := websiteTemplateRepo.GetFirst(repo.WithByID(output.TemplateID)); err == nil { + item.TemplateName = template.Name + } + dtos = append(dtos, item) + } + return total, dtos, nil +} + +func (w WebsiteTemplateService) CreateOutput(req request.WebsiteTemplateOutputCreate) error { + template, err := websiteTemplateRepo.GetFirst(repo.WithByID(req.TemplateID)) + if err != nil { + return err + } + valuesJSON, err := json.Marshal(req.VariableValues) + if err != nil { + return err + } + output := &model.WebsiteTemplateOutput{ + Name: req.Name, + TemplateID: template.ID, + TemplateType: template.Type, + VariableValues: string(valuesJSON), + } + if err := websiteTemplateOutputRepo.Create(output); err != nil { + return err + } + outputDir := templateOutputDir(output.ID) + if err := renderToDir(template, req.VariableValues, outputDir); err != nil { + _ = websiteTemplateOutputRepo.DeleteBy(repo.WithByID(output.ID)) + return err + } + output.OutputPath = outputDir + return websiteTemplateOutputRepo.Save(output) +} + +func (w WebsiteTemplateService) DeleteOutput(id uint) error { + output, err := websiteTemplateOutputRepo.GetFirst(repo.WithByID(id)) + if err != nil { + return err + } + if output.OutputPath != "" && strings.HasPrefix(output.OutputPath, templateBaseDir()) { + _ = os.RemoveAll(output.OutputPath) + } + return websiteTemplateOutputRepo.DeleteBy(repo.WithByID(id)) +} + +func (w WebsiteTemplateService) GetOutput(id uint) (*response.WebsiteTemplateOutputDTO, error) { + output, err := websiteTemplateOutputRepo.GetFirst(repo.WithByID(id)) + if err != nil { + return nil, err + } + item := &response.WebsiteTemplateOutputDTO{WebsiteTemplateOutput: *output} + if template, err := websiteTemplateRepo.GetFirst(repo.WithByID(output.TemplateID)); err == nil { + item.TemplateName = template.Name + } + return item, nil +} + +func (w WebsiteTemplateService) Preview(req request.WebsitePreviewReq) (*response.WebsitePreviewDTO, error) { + template, err := websiteTemplateRepo.GetFirst(repo.WithByID(req.TemplateID)) + if err != nil { + return nil, err + } + var html string + if template.Type == "single" { + html = renderContent(template.Content, req.VariableValues) + } else { + html, err = renderMainHTMLFromZip(template.FilePath, req.VariableValues) + if err != nil { + return nil, err + } + } + return &response.WebsitePreviewDTO{HTML: html}, nil +} + +var templateVarRegex = regexp.MustCompile(`\{\{(\w+)\}\}`) + +func renderContent(content string, values map[string]string) string { + rendered := templateVarRegex.ReplaceAllStringFunc(content, func(match string) string { + key := templateVarRegex.FindStringSubmatch(match)[1] + if val, ok := values[key]; ok { + return val + } + return "" + }) + return rendered +} + +func renderToDir(template *model.WebsiteTemplate, values map[string]string, outputDir string) error { + if err := os.MkdirAll(outputDir, constant.DirPerm); err != nil { + return err + } + if template.Type == "single" { + html := renderContent(template.Content, values) + return os.WriteFile(path.Join(outputDir, "index.html"), []byte(html), constant.FilePerm) + } + return unzipAndRender(template.FilePath, outputDir, values) +} + +func unzipAndRender(zipPath, outputDir string, values map[string]string) error { + if zipPath == "" { + return buserr.New("ErrFileNotFound") + } + reader, err := zip.OpenReader(zipPath) + if err != nil { + return err + } + defer reader.Close() + for _, file := range reader.File { + if err := extractAndRenderFile(file, outputDir, values); err != nil { + return err + } + } + return nil +} + +func extractAndRenderFile(file *zip.File, outputDir string, values map[string]string) error { + targetPath := filepath.Join(outputDir, file.Name) + if !strings.HasPrefix(targetPath, filepath.Clean(outputDir)+string(os.PathSeparator)) { + return fmt.Errorf("invalid file path in zip: %s", file.Name) + } + if file.FileInfo().IsDir() { + return os.MkdirAll(targetPath, constant.DirPerm) + } + if err := os.MkdirAll(filepath.Dir(targetPath), constant.DirPerm); err != nil { + return err + } + rc, err := file.Open() + if err != nil { + return err + } + defer rc.Close() + content, err := io.ReadAll(rc) + if err != nil { + return err + } + if isTextFile(file.Name) { + content = []byte(renderContent(string(content), values)) + } + return os.WriteFile(targetPath, content, constant.FilePerm) +} + +func isTextFile(name string) bool { + switch strings.ToLower(filepath.Ext(name)) { + case ".html", ".htm", ".css", ".js", ".json", ".xml", ".txt", ".svg", ".md": + return true + } + return false +} + +func renderMainHTMLFromZip(zipPath string, values map[string]string) (string, error) { + if zipPath == "" { + return "", buserr.New("ErrFileNotFound") + } + reader, err := zip.OpenReader(zipPath) + if err != nil { + return "", err + } + defer reader.Close() + var mainFile *zip.File + for _, file := range reader.File { + if file.FileInfo().IsDir() { + continue + } + name := strings.ToLower(filepath.Base(file.Name)) + if name == "index.html" || name == "index.htm" { + if mainFile == nil || len(file.Name) < len(mainFile.Name) { + mainFile = file + } + } + } + if mainFile == nil { + return "", buserr.New("ErrFileNotFound") + } + rc, err := mainFile.Open() + if err != nil { + return "", err + } + defer rc.Close() + content, err := io.ReadAll(rc) + if err != nil { + return "", err + } + return renderContent(string(content), values), nil +} + +func copyDir(src, dst string) error { + return filepath.Walk(src, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + relPath, err := filepath.Rel(src, p) + if err != nil { + return err + } + targetPath := filepath.Join(dst, relPath) + if info.IsDir() { + return os.MkdirAll(targetPath, constant.DirPerm) + } + return copyFile(p, targetPath) + }) +} + +func copyFile(src, dst string) error { + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + if err := os.MkdirAll(filepath.Dir(dst), constant.DirPerm); err != nil { + return err + } + dstFile, err := os.Create(dst) + if err != nil { + return err + } + defer dstFile.Close() + _, err = io.Copy(dstFile, srcFile) + return err +} + diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 7f887461e..38a75a24b 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -95,6 +95,7 @@ func InitAgentDB() { migrations.AddDatabaseUserTable, migrations.AddBackupRecordArgs, migrations.AddFtpIdentity, + migrations.AddWebsiteTemplateTable, migrations.AddComposePinned, }) if err := m.Migrate(); err != nil { diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 26a95f432..a1d105a50 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -73,6 +73,8 @@ var AddTable = &gormigrate.Migration{ &model.Website{}, &model.WebsiteAcmeAccount{}, &model.WebsiteCA{}, + &model.WebsiteTemplate{}, + &model.WebsiteTemplateOutput{}, &model.WebsiteDnsAccount{}, &model.WebsiteDomain{}, &model.WebsiteSSL{}, @@ -1699,6 +1701,13 @@ var AddFtpIdentity = &gormigrate.Migration{ }, } +var AddWebsiteTemplateTable = &gormigrate.Migration{ + ID: "20260728-add-website-template-table", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate( + &model.WebsiteTemplate{}, + &model.WebsiteTemplateOutput{}, + ) var AddComposePinned = &gormigrate.Migration{ ID: "20260729-add-compose-pinned", Migrate: func(tx *gorm.DB) error { diff --git a/agent/router/common.go b/agent/router/common.go index 500a10185..548ec931b 100644 --- a/agent/router/common.go +++ b/agent/router/common.go @@ -16,6 +16,7 @@ func commonGroups() []CommonRouter { &WebsiteDnsAccountRouter{}, &WebsiteAcmeAccountRouter{}, &WebsiteSSLRouter{}, + &WebsiteTemplateRouter{}, &DatabaseRouter{}, &NginxRouter{}, &RuntimeRouter{}, diff --git a/agent/router/ro_website_template.go b/agent/router/ro_website_template.go new file mode 100644 index 000000000..771c78b3f --- /dev/null +++ b/agent/router/ro_website_template.go @@ -0,0 +1,28 @@ +package router + +import ( + v2 "github.com/1Panel-dev/1Panel/agent/app/api/v2" + "github.com/gin-gonic/gin" +) + +type WebsiteTemplateRouter struct { +} + +func (a *WebsiteTemplateRouter) InitRouter(Router *gin.RouterGroup) { + groupRouter := Router.Group("websites/templates") + + baseApi := v2.ApiGroupApp.BaseApi + { + groupRouter.POST("/search", baseApi.PageWebsiteTemplate) + groupRouter.POST("", baseApi.CreateWebsiteTemplate) + groupRouter.POST("/update", baseApi.UpdateWebsiteTemplate) + groupRouter.POST("/del", baseApi.DeleteWebsiteTemplate) + groupRouter.POST("/get", baseApi.GetWebsiteTemplate) + groupRouter.POST("/upload", baseApi.UploadTemplateZip) + groupRouter.POST("/preview", baseApi.PreviewWebsiteTemplate) + groupRouter.POST("/outputs/search", baseApi.PageWebsiteTemplateOutput) + groupRouter.POST("/outputs", baseApi.CreateWebsiteTemplateOutput) + groupRouter.POST("/outputs/del", baseApi.DeleteWebsiteTemplateOutput) + groupRouter.POST("/outputs/get", baseApi.GetWebsiteTemplateOutput) + } +} diff --git a/core/init/migration/helper/menu.go b/core/init/migration/helper/menu.go index 9953c9be9..5e3333dbd 100644 --- a/core/init/migration/helper/menu.go +++ b/core/init/migration/helper/menu.go @@ -107,6 +107,7 @@ func LoadMenus() string { Children: []dto.ShowMenu{ {ID: "31", Disabled: false, Title: "menu.website", IsShow: true, Label: "Website", Path: "/websites", Sort: 100}, {ID: "32", Disabled: false, Title: "menu.ssl", IsShow: true, Label: "SSL", Path: "/websites/ssl", Sort: 200}, + {ID: "34", Disabled: false, Title: "menu.template", IsShow: true, Label: "WebsiteTemplate", Path: "/websites/templates", Sort: 250}, {ID: "33", Disabled: false, Title: "menu.runtime", IsShow: true, Label: "PHP", Path: "/websites/runtimes/php", Sort: 300}, }}, {ID: "5", Disabled: false, Title: "menu.database", IsShow: true, Label: "Database-Menu", Path: "/databases", Sort: 500}, @@ -222,6 +223,7 @@ func MenuSort() []dto.MenuLabelSort { {Label: "Website-Menu", Sort: 400}, {Label: "Website", Sort: 100}, {Label: "SSL", Sort: 200}, + {Label: "WebsiteTemplate", Sort: 250}, {Label: "PHP", Sort: 300}, {Label: "Database-Menu", Sort: 500}, {Label: "Container-Menu", Sort: 600}, diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index 31c085c0f..073dae812 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -52,6 +52,7 @@ func Init() { migrations.AddAlertAuditUser, migrations.AddMenuAccordionSetting, migrations.AddAPITrustedProxiesSetting, + migrations.AddWebsiteTemplateMenu, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index d18b1c837..b6bbd7a48 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -1299,3 +1299,18 @@ var AddAPITrustedProxiesSetting = &gormigrate.Migration{ return nil }, } + +var AddWebsiteTemplateMenu = &gormigrate.Migration{ + ID: "20260728-add-website-template-menu", + Migrate: func(tx *gorm.DB) error { + return helper.UpsertChildMenuByLabel(tx, "Website-Menu", dto.ShowMenu{ + ID: "34", + Disabled: false, + Title: "menu.template", + IsShow: true, + Label: "WebsiteTemplate", + Path: "/websites/templates", + Sort: 250, + }, "SSL") + }, +} diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index 9ec7445f7..e5141f54f 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -96,6 +96,7 @@ export namespace Website { ftpUser: string; ftpPassword: string; taskID: string; + templateOutputID?: number; SSLID?: number; enableSSL: boolean; createDB?: boolean; @@ -771,4 +772,77 @@ export namespace Website { ids: number[]; groupID: number; } + + export interface TemplateVariable { + key: string; + label: string; + type: 'text' | 'textarea' | 'number' | 'select' | 'color'; + default: string; + options: string; + required: boolean; + } + + export interface Template extends CommonModel { + name: string; + type: string; + content: string; + filePath: string; + variables: string; + remark: string; + } + + export interface TemplateSearch extends ReqPage { + name: string; + type: string; + } + + export interface TemplateCreate { + name: string; + type: string; + content: string; + filePath: string; + variables: string; + remark: string; + } + + export interface TemplateUpdate { + id: number; + name: string; + type: string; + content: string; + filePath: string; + variables: string; + remark: string; + } + + export interface TemplateOutput extends CommonModel { + name: string; + templateID: number; + templateType: string; + variableValues: string; + outputPath: string; + } + + export interface TemplateOutputDTO extends TemplateOutput { + templateName: string; + } + + export interface TemplateOutputSearch extends ReqPage { + templateID: number; + } + + export interface TemplateOutputCreate { + templateID: number; + name: string; + variableValues: Record; + } + + export interface PreviewReq { + templateID: number; + variableValues: Record; + } + + export interface PreviewDTO { + html: string; + } } diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index 006c960b1..bd3155207 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -401,3 +401,49 @@ export const updateWebsiteStream = (req: Website.WebsiteStreamUpdate) => { export const batchSetHttps = (req: Website.BatchSetHttps) => { return http.post(`/websites/batch/ssl`, req); }; + +export const searchTemplates = (req: Website.TemplateSearch) => { + return http.post>(`/websites/templates/search`, req); +}; + +export const createTemplate = (req: Website.TemplateCreate) => { + return http.post(`/websites/templates`, req); +}; + +export const updateTemplate = (req: Website.TemplateUpdate) => { + return http.post(`/websites/templates/update`, req); +}; + +export const deleteTemplate = (params: { id: number }) => { + return http.post(`/websites/templates/del`, params); +}; + +export const getTemplate = (id: number) => { + return http.post(`/websites/templates/get`, { id }); +}; + +export const uploadTemplateZip = (file: globalThis.File) => { + const formData = new FormData(); + formData.append('file', file); + return http.post<{ filePath: string; variables: string[] }>(`/websites/templates/upload`, formData, TimeoutEnum.T_5M); +}; + +export const previewTemplate = (req: Website.PreviewReq) => { + return http.post(`/websites/templates/preview`, req); +}; + +export const searchTemplateOutputs = (req: Website.TemplateOutputSearch) => { + return http.post>(`/websites/templates/outputs/search`, req); +}; + +export const createTemplateOutput = (req: Website.TemplateOutputCreate) => { + return http.post(`/websites/templates/outputs`, req); +}; + +export const deleteTemplateOutput = (params: { id: number }) => { + return http.post(`/websites/templates/outputs/del`, params); +}; + +export const getTemplateOutput = (id: number) => { + return http.post(`/websites/templates/outputs/get`, { id }); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index cc6b31486..160789362 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -426,6 +426,7 @@ const message = { app: 'Application', msgCenter: 'Tasks', disk: 'Disk', + template: 'Template', }, home: { recommend: 'Recommended', @@ -6389,6 +6390,49 @@ const message = { masterHostError: 'The master node IP cannot be 127.0.0.1', }, }, + template: { + create: 'Create Template', + edit: 'Edit Template', + name: 'Template Name', + type: 'Template Type', + single: 'Single File', + multi: 'Multi File (zip)', + content: 'Template Content', + variables: 'Variables', + remark: 'Remark', + filePath: 'File Path', + upload: 'Upload Zip', + preview: 'Preview', + previewEmpty: 'Select a template to preview in real time', + generate: 'Generate Output', + outputName: 'Output Name', + outputList: 'Output List', + outputCreate: 'Generate Output', + confirmDelete: 'Are you sure you want to delete this template? Associated outputs will also be deleted.', + confirmDeleteOutput: 'Are you sure you want to delete this output?', + variableKey: 'Variable Key', + variableLabel: 'Label', + variableType: 'Type', + variableDefault: 'Default', + variableOptions: 'Options (comma separated)', + variableRequired: 'Required', + varTypeText: 'Text', + varTypeTextHelper: 'Single-line text input', + varTypeTextarea: 'Textarea', + varTypeTextareaHelper: 'For paragraphs and long text', + varTypeNumber: 'Number', + varTypeNumberHelper: 'Numeric input only', + varTypeSelect: 'Select', + varTypeSelectHelper: 'Choose from preset options, options required', + varTypeColor: 'Color', + varTypeColorHelper: 'Color picker', + autoDetectHelper: 'Variables wrapped in double curly braces are auto-detected and added to the table', + selectTemplate: 'Select Template', + fillVariables: 'Fill Variables', + noVariables: 'No variables defined in this template', + outputSuccess: 'Output generated successfully', + importButton: 'Import from Template', + }, }; export default { diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 4738b95b3..da7e1348c 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -422,6 +422,7 @@ const message = { app: 'Aplicación', msgCenter: 'Tareas', disk: 'Disco', + template: 'Plantilla', filter: 'Filtro', }, home: { @@ -6447,6 +6448,49 @@ const message = { masterHostError: 'La IP del nodo maestro no puede ser 127.0.0.1', }, }, + template: { + create: 'Crear plantilla', + edit: 'Editar plantilla', + name: 'Nombre de plantilla', + type: 'Tipo de plantilla', + single: 'Archivo único', + multi: 'Varios archivos (zip)', + content: 'Contenido de la plantilla', + variables: 'Definición de variables', + remark: 'Observación', + filePath: 'Ruta del archivo', + upload: 'Subir zip', + preview: 'Vista previa', + previewEmpty: 'Seleccione una plantilla para previsualizar en tiempo real', + generate: 'Generar salida', + outputName: 'Nombre de la salida', + outputList: 'Lista de salidas', + outputCreate: 'Generar salida', + confirmDelete: '¿Está seguro de que desea eliminar esta plantilla? Las salidas asociadas también se eliminarán', + confirmDeleteOutput: '¿Está seguro de que desea eliminar esta salida?', + variableKey: 'Nombre de variable', + variableLabel: 'Etiqueta', + variableType: 'Tipo', + variableDefault: 'Valor predeterminado', + variableOptions: 'Opciones (separadas por comas)', + variableRequired: 'Obligatorio', + varTypeText: 'Texto de una línea', + varTypeTextHelper: 'Campo de texto normal', + varTypeTextarea: 'Texto multilínea', + varTypeTextareaHelper: 'Adecuado para párrafos y textos largos', + varTypeNumber: 'Número', + varTypeNumberHelper: 'Solo entrada numérica', + varTypeSelect: 'Selección desplegable', + varTypeSelectHelper: 'Elegir entre opciones predefinidas, se deben indicar las opciones', + varTypeColor: 'Color', + varTypeColorHelper: 'Selector de color', + autoDetectHelper: 'Las variables entre llaves dobles se detectan automáticamente y se añaden a la tabla', + selectTemplate: 'Seleccionar plantilla', + fillVariables: 'Completar variables', + noVariables: 'Esta plantilla no tiene variables definidas', + outputSuccess: 'Salida generada correctamente', + importButton: 'Importar desde plantilla', + }, }; export default { diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index 51d191758..d2ad6a539 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -415,6 +415,7 @@ const message = { app: 'برنامه', msgCenter: 'وظایف', disk: 'دیسک', + template: 'قالب', }, home: { recommend: 'پیشنهادی', @@ -6339,6 +6340,49 @@ const message = { masterHostError: 'IP گره اصلی نمی‌تواند 127.0.0.1 باشد', }, }, + template: { + create: 'ایجاد قالب', + edit: 'ویرایش قالب', + name: 'نام قالب', + type: 'نوع قالب', + single: 'تک فایل', + multi: 'چند فایل (zip)', + content: 'محتوای قالب', + variables: 'تعریف متغیرها', + remark: 'توضیحات', + filePath: 'مسیر فایل', + upload: 'بارگذاری zip', + preview: 'پیش‌نمایش', + previewEmpty: 'برای پیش‌نمایش زنده یک قالب انتخاب کنید', + generate: 'تولید خروجی', + outputName: 'نام خروجی', + outputList: 'فهرست خروجی‌ها', + outputCreate: 'تولید خروجی', + confirmDelete: 'آیا از حذف این قالب مطمئن هستید؟ خروجی‌های مرتبط نیز حذف خواهند شد', + confirmDeleteOutput: 'آیا از حذف این خروجی مطمئن هستید؟', + variableKey: 'نام متغیر', + variableLabel: 'برچسب', + variableType: 'نوع', + variableDefault: 'مقدار پیش‌فرض', + variableOptions: 'گزینه‌ها (جدا شده با کاما)', + variableRequired: 'الزامی', + varTypeText: 'متن تک‌خطی', + varTypeTextHelper: 'فیلد ورودی متن معمولی', + varTypeTextarea: 'متن چندخطی', + varTypeTextareaHelper: 'مناسب برای پاراگراف‌ها و متون طولانی', + varTypeNumber: 'عدد', + varTypeNumberHelper: 'فقط ورودی عددی', + varTypeSelect: 'انتخاب کشویی', + varTypeSelectHelper: 'انتخاب از گزینه‌های از پیش تعیین‌شده، وارد کردن گزینه‌ها الزامی است', + varTypeColor: 'رنگ', + varTypeColorHelper: 'انتخابگر رنگ', + autoDetectHelper: 'متغیرهای داخل آکولاد دوگانه به صورت خودکار شناسایی و به جدول اضافه می‌شوند', + selectTemplate: 'انتخاب قالب', + fillVariables: 'تکمیل متغیرها', + noVariables: 'در این قالب متغیری تعریف نشده است', + outputSuccess: 'خروجی با موفقیت تولید شد', + importButton: 'وارد کردن از قالب', + }, }; export default { diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 6ca5604ea..c57292414 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -420,6 +420,7 @@ const message = { app: 'アプリケーション', msgCenter: 'タスク', disk: 'ディスク', + template: 'テンプレート', filter: 'フィルター', }, home: { @@ -6392,6 +6393,49 @@ const message = { masterHostError: 'マスターノードのIPは127.0.0.1にできません', }, }, + template: { + create: 'テンプレート作成', + edit: 'テンプレート編集', + name: 'テンプレート名', + type: 'テンプレートタイプ', + single: '単一ファイル', + multi: '複数ファイル(zip)', + content: 'テンプレート内容', + variables: '変数定義', + remark: '備考', + filePath: 'ファイルパス', + upload: 'zip をアップロード', + preview: 'プレビュー', + previewEmpty: 'テンプレートを選択するとリアルタイムでプレビューできます', + generate: '成果物を生成', + outputName: '成果物名', + outputList: '成果物リスト', + outputCreate: '成果物を生成', + confirmDelete: 'このテンプレートを削除してもよろしいですか?関連する成果物も削除されます', + confirmDeleteOutput: 'この成果物を削除してもよろしいですか?', + variableKey: '変数名', + variableLabel: 'ラベル', + variableType: 'タイプ', + variableDefault: 'デフォルト値', + variableOptions: 'オプション(カンマ区切り)', + variableRequired: '必須', + varTypeText: '一行テキスト', + varTypeTextHelper: '通常のテキスト入力欄', + varTypeTextarea: '複数行テキスト', + varTypeTextareaHelper: '段落や長文に適しています', + varTypeNumber: '数値', + varTypeNumberHelper: '数値のみ入力可能', + varTypeSelect: 'ドロップダウン選択', + varTypeSelectHelper: 'プリセットオプションから選択、オプションの入力が必要', + varTypeColor: 'カラー', + varTypeColorHelper: 'カラーピッカー', + autoDetectHelper: '内容中の二重中括弧で囲まれた変数を自動認識し、変数定義テーブルに追加します', + selectTemplate: 'テンプレートを選択', + fillVariables: '変数を入力', + noVariables: 'このテンプレートには変数が定義されていません', + outputSuccess: '成果物の生成に成功しました', + importButton: 'テンプレートからインポート', + }, }; export default { ...getFuLocaleMessage('ja'), diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 34b019e2b..9c11ecd61 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -416,6 +416,7 @@ const message = { app: '애플리케이션', msgCenter: '작업', disk: '디스크', + template: '템플릿', filter: '필터', }, home: { @@ -6267,6 +6268,49 @@ const message = { masterHostError: '마스터 노드의 IP는 127.0.0.1이 될 수 없습니다', }, }, + template: { + create: '템플릿 생성', + edit: '템플릿 편집', + name: '템플릿 이름', + type: '템플릿 유형', + single: '단일 파일', + multi: '다중 파일(zip)', + content: '템플릿 내용', + variables: '변수 정의', + remark: '비고', + filePath: '파일 경로', + upload: 'zip 업로드', + preview: '미리보기', + previewEmpty: '템플릿을 선택하면 실시간으로 미리볼 수 있습니다', + generate: '산출물 생성', + outputName: '산출물 이름', + outputList: '산출물 목록', + outputCreate: '산출물 생성', + confirmDelete: '이 템플릿을 삭제하시겠습니까? 연관된 산출물도 함께 삭제됩니다', + confirmDeleteOutput: '이 산출물을 삭제하시겠습니까?', + variableKey: '변수명', + variableLabel: '레이블', + variableType: '유형', + variableDefault: '기본값', + variableOptions: '옵션(쉼표로 구분)', + variableRequired: '필수', + varTypeText: '한 줄 텍스트', + varTypeTextHelper: '일반 텍스트 입력란', + varTypeTextarea: '여러 줄 텍스트', + varTypeTextareaHelper: '문단이나 긴 텍스트에 적합', + varTypeNumber: '숫자', + varTypeNumberHelper: '숫자만 입력 가능', + varTypeSelect: '드롭다운 선택', + varTypeSelectHelper: '사전 설정된 옵션에서 선택, 옵션 입력 필요', + varTypeColor: '색상', + varTypeColorHelper: '색상 선택기', + autoDetectHelper: '내용에서 이중 중괄호로 감싸진 변수를 자동 인식하여 변수 정의 테이블에 추가합니다', + selectTemplate: '템플릿 선택', + fillVariables: '변수 입력', + noVariables: '이 템플릿에는 정의된 변수가 없습니다', + outputSuccess: '산출물이 성공적으로 생성되었습니다', + importButton: '템플릿에서 가져오기', + }, }; export default { diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 5191f603d..c2bfdb689 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -424,6 +424,7 @@ const message = { app: 'ແອັບພລິເຄຊັນ', msgCenter: 'ໜ້າວຽກ', disk: 'ດິສກ໌', + template: 'ແມ່ແບບ', }, home: { recommend: 'ແນະນຳ', @@ -6188,6 +6189,49 @@ const message = { masterHostError: 'IP ຂອງໂນດຫຼັກຫ້າມເປັນ 127.0.0.1', }, }, + template: { + create: 'ສ້າງແມ່ແບບ', + edit: 'ແກ້ໄຂແມ່ແບບ', + name: 'ຊື່ແມ່ແບບ', + type: 'ປະເພດແມ່ແບບ', + single: 'ໄຟລ໌ດ່ຽວ', + multi: 'ຫລາຍໄຟລ໌ (zip)', + content: 'ເນື້ອຫາແມ່ແບບ', + variables: 'ການກຳນົດຕົວປ່ຽນ', + remark: 'ໝາຍເຫດ', + filePath: 'ເສັ້ນທາງໄຟລ໌', + upload: 'ອັບໂຫລດ zip', + preview: 'ເບິ່ງຕົວຍ່າງ', + previewEmpty: 'ເລືອກແມ່ແບບເພື່ອເບິ່ງຕົວຍ່າງແບບທັນທີ', + generate: 'ສ້າງຜົນງານ', + outputName: 'ຊື່ຜົນງານ', + outputList: 'ລາຍການຜົນງານ', + outputCreate: 'ສ້າງຜົນງານ', + confirmDelete: 'ທ່ານແນ່ໃຈບໍ່ທີ່ຈະລຶບແມ່ແບບນີ້? ຜົນງານທີ່ກ່ຽວຂ້ອງຈະຖືກລຶບນຳ', + confirmDeleteOutput: 'ທ່ານແນ່ໃຈບໍ່ທີ່ຈະລຶບຜົນງານນີ້?', + variableKey: 'ຊື່ຕົວປ່ຽນ', + variableLabel: 'ປ້າຍກຳກັບ', + variableType: 'ປະເພດ', + variableDefault: 'ຄ່າເລີ່ມຕົ້ນ', + variableOptions: 'ຕົວເລືອກ (ແຍກດ້ວຍເຄື່ອງໝາຍຈຸດ)', + variableRequired: 'ຈຳເປັນ', + varTypeText: 'ຂໍ້ຄວາມແຖວດຽວ', + varTypeTextHelper: 'ຊ່ອງປ້ອນຂໍ້ຄວາມທົ່ວໄປ', + varTypeTextarea: 'ຂໍ້ຄວາມຫລາຍແຖວ', + varTypeTextareaHelper: 'ເໝາະສົມສຳລັບຫຍໍ້ໜ້າ ແລະ ຂໍ້ຄວາມຍາວ', + varTypeNumber: 'ຕົວເລກ', + varTypeNumberHelper: 'ປ້ອນໄດ້ພຽງຕົວເລກເທົ່ານັ້ນ', + varTypeSelect: 'ເລືອກຈາກລາຍການ', + varTypeSelectHelper: 'ເລືອກຈາກຕົວເລືອກທີ່ກຳນົດໄວ້, ຕ້ອງປ້ອນຕົວເລືອກ', + varTypeColor: 'ສີ', + varTypeColorHelper: 'ເຄື່ອງມືເລືອກສີ', + autoDetectHelper: 'ຕົວປ່ຽນທີ່ຫໍ່ດ້ວຍວົງເລັບຄູ່ຈະຖືກກວດພົບໂດຍອັດຕະໂນມັດ ແລະ ເພີ່ມໃສ່ຕາຕະລາງ', + selectTemplate: 'ເລືອກແມ່ແບບ', + fillVariables: 'ປ້ອນຕົວປ່ຽນ', + noVariables: 'ແມ່ແບບນີ້ບໍ່ມີຕົວປ່ຽນທີ່ກຳນົດໄວ້', + outputSuccess: 'ສ້າງຜົນງານສຳເລັດແລ້ວ', + importButton: 'ນຳເຂົ້າຈາກແມ່ແບບ', + }, }; export default { diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index d86b9d599..0886c2ed8 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -425,6 +425,7 @@ const message = { app: 'Aplikasi', msgCenter: 'Tugas', disk: 'Disk', + template: 'Templat', filter: 'Penapis', }, home: { @@ -6477,6 +6478,49 @@ const message = { masterHostError: 'IP nod utama tidak boleh 127.0.0.1', }, }, + template: { + create: 'Cipta Templat', + edit: 'Sunting Templat', + name: 'Nama Templat', + type: 'Jenis Templat', + single: 'Fail Tunggal', + multi: 'Berbilang Fail (zip)', + content: 'Kandungan Templat', + variables: 'Definisi Pemboleh Ubah', + remark: 'Catatan', + filePath: 'Laluan Fail', + upload: 'Muat Naik Zip', + preview: 'Pratonton', + previewEmpty: 'Pilih templat untuk pratonton masa nyata', + generate: 'Jana Output', + outputName: 'Nama Output', + outputList: 'Senarai Output', + outputCreate: 'Jana Output', + confirmDelete: 'Adakah anda pasti mahu memadam templat ini? Output yang berkaitan juga akan dipadam', + confirmDeleteOutput: 'Adakah anda pasti mahu memadam output ini?', + variableKey: 'Nama Pemboleh Ubah', + variableLabel: 'Label', + variableType: 'Jenis', + variableDefault: 'Nilai Lalai', + variableOptions: 'Pilihan (dipisahkan koma)', + variableRequired: 'Wajib', + varTypeText: 'Teks Satu Baris', + varTypeTextHelper: 'Medan input teks biasa', + varTypeTextarea: 'Teks Berbilang Baris', + varTypeTextareaHelper: 'Sesuai untuk perenggan dan teks panjang', + varTypeNumber: 'Nombor', + varTypeNumberHelper: 'Input nombor sahaja', + varTypeSelect: 'Pilihan Juntai Bawah', + varTypeSelectHelper: 'Pilih daripada pilihan pratetap, pilihan perlu diisi', + varTypeColor: 'Warna', + varTypeColorHelper: 'Pemilih warna', + autoDetectHelper: 'Pemboleh ubah dalam kurungan kerinting berganda dikesan secara automatik dan ditambah ke jadual', + selectTemplate: 'Pilih Templat', + fillVariables: 'Isi Pemboleh Ubah', + noVariables: 'Tiada pemboleh ubah ditakrifkan dalam templat ini', + outputSuccess: 'Output berjaya dijana', + importButton: 'Import daripada Templat', + }, }; export default { diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index fe9ae3599..05c547dd1 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -419,6 +419,7 @@ const message = { app: 'Aplicativo', msgCenter: 'Tarefas', disk: 'Disco', + template: 'Modelo', filter: 'Filtro', }, home: { @@ -6631,6 +6632,49 @@ const message = { masterHostError: 'O IP do nó mestre não pode ser 127.0.0.1', }, }, + template: { + create: 'Criar modelo', + edit: 'Editar modelo', + name: 'Nome do modelo', + type: 'Tipo de modelo', + single: 'Arquivo único', + multi: 'Múltiplos arquivos (zip)', + content: 'Conteúdo do modelo', + variables: 'Definição de variáveis', + remark: 'Observação', + filePath: 'Caminho do arquivo', + upload: 'Enviar zip', + preview: 'Pré-visualização', + previewEmpty: 'Selecione um modelo para pré-visualizar em tempo real', + generate: 'Gerar saída', + outputName: 'Nome da saída', + outputList: 'Lista de saídas', + outputCreate: 'Gerar saída', + confirmDelete: 'Tem certeza de que deseja excluir este modelo? As saídas associadas também serão excluídas', + confirmDeleteOutput: 'Tem certeza de que deseja excluir esta saída?', + variableKey: 'Nome da variável', + variableLabel: 'Rótulo', + variableType: 'Tipo', + variableDefault: 'Valor padrão', + variableOptions: 'Opções (separadas por vírgula)', + variableRequired: 'Obrigatório', + varTypeText: 'Texto de linha única', + varTypeTextHelper: 'Campo de texto comum', + varTypeTextarea: 'Texto de múltiplas linhas', + varTypeTextareaHelper: 'Adequado para parágrafos e textos longos', + varTypeNumber: 'Número', + varTypeNumberHelper: 'Somente entrada numérica', + varTypeSelect: 'Seleção suspensa', + varTypeSelectHelper: 'Escolher entre opções predefinidas, as opções devem ser informadas', + varTypeColor: 'Cor', + varTypeColorHelper: 'Seletor de cores', + autoDetectHelper: 'Variáveis entre chaves duplas são detectadas automaticamente e adicionadas à tabela', + selectTemplate: 'Selecionar modelo', + fillVariables: 'Preencher variáveis', + noVariables: 'Este modelo não possui variáveis definidas', + outputSuccess: 'Saída gerada com sucesso', + importButton: 'Importar do modelo', + }, }; export default { diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index e08c34130..6d7360b42 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -417,6 +417,7 @@ const message = { app: 'Приложение', msgCenter: 'Задачи', disk: 'Диск', + template: 'Шаблон', filter: 'Фильтр', }, home: { @@ -6477,6 +6478,49 @@ const message = { masterHostError: 'IP главного узла не может быть 127.0.0.1', }, }, + template: { + create: 'Создать шаблон', + edit: 'Редактировать шаблон', + name: 'Имя шаблона', + type: 'Тип шаблона', + single: 'Один файл', + multi: 'Несколько файлов (zip)', + content: 'Содержимое шаблона', + variables: 'Определение переменных', + remark: 'Примечание', + filePath: 'Путь к файлу', + upload: 'Загрузить zip', + preview: 'Предпросмотр', + previewEmpty: 'Выберите шаблон для предпросмотра в реальном времени', + generate: 'Сгенерировать результат', + outputName: 'Имя результата', + outputList: 'Список результатов', + outputCreate: 'Сгенерировать результат', + confirmDelete: 'Вы уверены, что хотите удалить этот шаблон? Связанные результаты также будут удалены', + confirmDeleteOutput: 'Вы уверены, что хотите удалить этот результат?', + variableKey: 'Имя переменной', + variableLabel: 'Метка', + variableType: 'Тип', + variableDefault: 'Значение по умолчанию', + variableOptions: 'Варианты (через запятую)', + variableRequired: 'Обязательно', + varTypeText: 'Однострочный текст', + varTypeTextHelper: 'Обычное текстовое поле', + varTypeTextarea: 'Многострочный текст', + varTypeTextareaHelper: 'Подходит для абзацев и длинного текста', + varTypeNumber: 'Число', + varTypeNumberHelper: 'Только числовой ввод', + varTypeSelect: 'Выпадающий список', + varTypeSelectHelper: 'Выбор из заданных вариантов, требуется указать варианты', + varTypeColor: 'Цвет', + varTypeColorHelper: 'Палитра цветов', + autoDetectHelper: 'Переменные в двойных фигурных скобках распознаются автоматически и добавляются в таблицу', + selectTemplate: 'Выбрать шаблон', + fillVariables: 'Заполнить переменные', + noVariables: 'В этом шаблоне не определены переменные', + outputSuccess: 'Результат успешно сгенерирован', + importButton: 'Импорт из шаблона', + }, }; export default { diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 9a4daf853..eee8d3fae 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -422,6 +422,7 @@ const message = { msgCenter: 'Görevler', filter: 'Filtre', disk: 'Disk', + template: 'Şablon', }, home: { recommend: 'Önerilen', @@ -6468,6 +6469,49 @@ const message = { masterHostError: "Ana düğüm IP'si 127.0.0.1 olamaz", }, }, + template: { + create: 'Şablon Oluştur', + edit: 'Şablonu Düzenle', + name: 'Şablon Adı', + type: 'Şablon Türü', + single: 'Tek Dosya', + multi: 'Çoklu Dosya (zip)', + content: 'Şablon İçeriği', + variables: 'Değişken Tanımları', + remark: 'Açıklama', + filePath: 'Dosya Yolu', + upload: 'Zip Yükle', + preview: 'Önizleme', + previewEmpty: 'Gerçek zamanlı önizleme için bir şablon seçin', + generate: 'Çıktı Oluştur', + outputName: 'Çıktı Adı', + outputList: 'Çıktı Listesi', + outputCreate: 'Çıktı Oluştur', + confirmDelete: 'Bu şablonu silmek istediğinizden emin misiniz? İlişkili çıktılar da silinecektir', + confirmDeleteOutput: 'Bu çıktıyı silmek istediğinizden emin misiniz?', + variableKey: 'Değişken Adı', + variableLabel: 'Etiket', + variableType: 'Tür', + variableDefault: 'Varsayılan', + variableOptions: 'Seçenekler (virgülle ayrılmış)', + variableRequired: 'Zorunlu', + varTypeText: 'Tek Satır Metin', + varTypeTextHelper: 'Normal metin giriş alanı', + varTypeTextarea: 'Çok Satırlı Metin', + varTypeTextareaHelper: 'Paragraflar ve uzun metinler için uygundur', + varTypeNumber: 'Sayı', + varTypeNumberHelper: 'Yalnızca sayısal giriş', + varTypeSelect: 'Açılır Liste', + varTypeSelectHelper: 'Önceden tanımlanan seçeneklerden seçim, seçenekler girilmelidir', + varTypeColor: 'Renk', + varTypeColorHelper: 'Renk seçici', + autoDetectHelper: 'Çift süslü parantez içindeki değişkenler otomatik algılanır ve tabloya eklenir', + selectTemplate: 'Şablon Seç', + fillVariables: 'Değişkenleri Doldur', + noVariables: 'Bu şablonda tanımlanmış değişken yok', + outputSuccess: 'Çıktı başarıyla oluşturuldu', + importButton: 'Şablondan İçe Aktar', + }, }; export default { diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index ffb5f4ff0..c42be2ae4 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -403,6 +403,7 @@ const message = { app: '應用', msgCenter: '任務中心', disk: '磁碟管理', + template: '模板', filter: '篩選器', }, home: { @@ -5940,6 +5941,49 @@ const message = { masterHostError: '主節點 IP 不能為 127.0.0.1', }, }, + template: { + create: '建立模板', + edit: '編輯模板', + name: '模板名稱', + type: '模板類型', + single: '單檔案', + multi: '多檔案(zip)', + content: '模板內容', + variables: '變數定義', + remark: '備註', + filePath: '檔案路徑', + upload: '上傳 zip', + preview: '預覽', + previewEmpty: '選擇模板後可即時預覽', + generate: '生成產物', + outputName: '產物名稱', + outputList: '產物列表', + outputCreate: '生成產物', + confirmDelete: '確定刪除此模板嗎?關聯的產物也將被刪除', + confirmDeleteOutput: '確定刪除此產物嗎?', + variableKey: '變數名', + variableLabel: '標籤', + variableType: '類型', + variableDefault: '預設值', + variableOptions: '選項(逗號分隔)', + variableRequired: '必填', + varTypeText: '單行文字', + varTypeTextHelper: '普通文字輸入框', + varTypeTextarea: '多行文字', + varTypeTextareaHelper: '適用於段落、長文字', + varTypeNumber: '數字', + varTypeNumberHelper: '僅允許輸入數字', + varTypeSelect: '下拉選擇', + varTypeSelectHelper: '從預設選項中選擇,需填寫選項', + varTypeColor: '顏色', + varTypeColorHelper: '顏色選擇器', + autoDetectHelper: '自動識別內容中雙花括號包裹的變數並加入變數定義表格', + selectTemplate: '選擇模板', + fillVariables: '填寫變數', + noVariables: '此模板沒有定義變數', + outputSuccess: '產物生成成功', + importButton: '從模板匯入', + }, }; export default { ...getFuLocaleMessage('zh-Hant'), diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 800479afd..c04851517 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -393,6 +393,7 @@ const message = { app: '应用', msgCenter: '任务中心', disk: '磁盘管理', + template: '模板', }, home: { dir: '目录', @@ -5940,6 +5941,49 @@ const message = { masterHostError: '主节点 IP 不能为 127.0.0.1', }, }, + template: { + create: '创建模板', + edit: '编辑模板', + name: '模板名称', + type: '模板类型', + single: '单文件', + multi: '多文件(zip)', + content: '模板内容', + variables: '变量定义', + remark: '备注', + filePath: '文件路径', + upload: '上传 zip', + preview: '预览', + previewEmpty: '选择模板后可实时预览', + generate: '生成产物', + outputName: '产物名称', + outputList: '产物列表', + outputCreate: '生成产物', + confirmDelete: '确定删除此模板吗?关联的产物也将被删除', + confirmDeleteOutput: '确定删除此产物吗?', + variableKey: '变量名', + variableLabel: '标签', + variableType: '类型', + variableDefault: '默认值', + variableOptions: '选项(逗号分隔)', + variableRequired: '必填', + varTypeText: '单行文本', + varTypeTextHelper: '普通文本输入框', + varTypeTextarea: '多行文本', + varTypeTextareaHelper: '适用于段落、长文本', + varTypeNumber: '数字', + varTypeNumberHelper: '仅允许输入数字', + varTypeSelect: '下拉选择', + varTypeSelectHelper: '从预设选项中选择,需填写选项', + varTypeColor: '颜色', + varTypeColorHelper: '颜色选择器', + autoDetectHelper: '自动识别内容中双花括号包裹的变量并加入变量定义表格', + selectTemplate: '选择模板', + fillVariables: '填写变量', + noVariables: '此模板没有定义变量', + outputSuccess: '产物生成成功', + importButton: '从模板导入', + }, }; export default { ...getFuLocaleMessage('zh'), diff --git a/frontend/src/routers/modules/website.ts b/frontend/src/routers/modules/website.ts index ef061d71b..4b8bd09e6 100644 --- a/frontend/src/routers/modules/website.ts +++ b/frontend/src/routers/modules/website.ts @@ -43,6 +43,16 @@ const webSiteRouter = { permission: 'website_cert_view', }, }, + { + path: '/websites/templates', + name: 'WebsiteTemplate', + component: () => import('@/views/website/template/index.vue'), + meta: { + icon: 'p-file-html', + title: 'menu.template', + permission: 'website_view', + }, + }, { path: '/websites/runtimes/php', name: 'PHP', diff --git a/frontend/src/views/website/template/index.vue b/frontend/src/views/website/template/index.vue new file mode 100644 index 000000000..932cbd4fe --- /dev/null +++ b/frontend/src/views/website/template/index.vue @@ -0,0 +1,157 @@ + + + diff --git a/frontend/src/views/website/template/operate/index.vue b/frontend/src/views/website/template/operate/index.vue new file mode 100644 index 000000000..0fa6f2767 --- /dev/null +++ b/frontend/src/views/website/template/operate/index.vue @@ -0,0 +1,237 @@ + + + diff --git a/frontend/src/views/website/template/output/create.vue b/frontend/src/views/website/template/output/create.vue new file mode 100644 index 000000000..05ae45871 --- /dev/null +++ b/frontend/src/views/website/template/output/create.vue @@ -0,0 +1,234 @@ + + + + + diff --git a/frontend/src/views/website/template/output/index.vue b/frontend/src/views/website/template/output/index.vue new file mode 100644 index 000000000..85edc1a37 --- /dev/null +++ b/frontend/src/views/website/template/output/index.vue @@ -0,0 +1,99 @@ + + + diff --git a/frontend/src/views/website/website/create/index.vue b/frontend/src/views/website/website/create/index.vue index bb5bcf7d4..58fac95aa 100644 --- a/frontend/src/views/website/website/create/index.vue +++ b/frontend/src/views/website/website/create/index.vue @@ -271,6 +271,20 @@ + + + + + ({ domains: [], parentWebsiteID: undefined, siteDir: '', + templateOutputID: undefined, streamPorts: '', udp: false, @@ -575,6 +591,7 @@ const appReq = reactive({ const apps = ref([]); const preCheckRef = ref(); const staticPath = ref(''); +const templateOutputs = ref([]); const runtimeResource = ref('appstore'); const initRuntimeReq = () => ({ page: 1, @@ -791,6 +808,7 @@ const acceptParams = async (openrestyVersion: string) => { runtimeResource.value = 'appstore'; runtimeReq.value = initRuntimeReq(); listAcmeAccount(); + listTemplateOutputs(); steamConfig.value = initLbForm(); @@ -815,6 +833,12 @@ const listAcmeAccount = () => { }); }; +const listTemplateOutputs = () => { + searchTemplateOutputs({ page: 1, pageSize: 1000, templateID: 0 }).then((res) => { + templateOutputs.value = res.data.items || []; + }); +}; + const changeSSl = (sslid?: number) => { if (!sslid) { websiteSSL.value = undefined; @@ -1029,6 +1053,9 @@ const submit = async (formEl: FormInstance | undefined) => { website.value.algorithm = steamConfig.value.algorithm; website.value.servers = steamConfig.value.servers; } + if (website.value.type !== 'static' || !website.value.templateOutputID) { + website.value.templateOutputID = undefined; + } const taskID = uuidv4(); website.value.taskID = taskID; await createWebsite(website.value);