mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 08:00:53 +00:00
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>
This commit is contained in:
@@ -61,6 +61,7 @@ var (
|
||||
websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
|
||||
websiteSSLService = service.NewIWebsiteSSLService()
|
||||
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
|
||||
websiteTemplateService = service.NewIWebsiteTemplateService()
|
||||
|
||||
nginxService = service.NewINginxService()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -34,6 +34,8 @@ type WebsiteCreate struct {
|
||||
|
||||
SiteDir string `json:"siteDir"`
|
||||
|
||||
TemplateOutputID uint `json:"templateOutputID"`
|
||||
|
||||
RuntimeConfig
|
||||
FtpConfig
|
||||
DataBaseConfig
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ func InitAgentDB() {
|
||||
migrations.AddDatabaseUserTable,
|
||||
migrations.AddBackupRecordArgs,
|
||||
migrations.AddFtpIdentity,
|
||||
migrations.AddWebsiteTemplateTable,
|
||||
migrations.AddComposePinned,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,6 +16,7 @@ func commonGroups() []CommonRouter {
|
||||
&WebsiteDnsAccountRouter{},
|
||||
&WebsiteAcmeAccountRouter{},
|
||||
&WebsiteSSLRouter{},
|
||||
&WebsiteTemplateRouter{},
|
||||
&DatabaseRouter{},
|
||||
&NginxRouter{},
|
||||
&RuntimeRouter{},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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},
|
||||
|
||||
@@ -52,6 +52,7 @@ func Init() {
|
||||
migrations.AddAlertAuditUser,
|
||||
migrations.AddMenuAccordionSetting,
|
||||
migrations.AddAPITrustedProxiesSetting,
|
||||
migrations.AddWebsiteTemplateMenu,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
@@ -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")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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<string, string>;
|
||||
}
|
||||
|
||||
export interface PreviewReq {
|
||||
templateID: number;
|
||||
variableValues: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface PreviewDTO {
|
||||
html: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ResPage<Website.Template>>(`/websites/templates/search`, req);
|
||||
};
|
||||
|
||||
export const createTemplate = (req: Website.TemplateCreate) => {
|
||||
return http.post<any>(`/websites/templates`, req);
|
||||
};
|
||||
|
||||
export const updateTemplate = (req: Website.TemplateUpdate) => {
|
||||
return http.post<any>(`/websites/templates/update`, req);
|
||||
};
|
||||
|
||||
export const deleteTemplate = (params: { id: number }) => {
|
||||
return http.post<any>(`/websites/templates/del`, params);
|
||||
};
|
||||
|
||||
export const getTemplate = (id: number) => {
|
||||
return http.post<Website.Template>(`/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<Website.PreviewDTO>(`/websites/templates/preview`, req);
|
||||
};
|
||||
|
||||
export const searchTemplateOutputs = (req: Website.TemplateOutputSearch) => {
|
||||
return http.post<ResPage<Website.TemplateOutputDTO>>(`/websites/templates/outputs/search`, req);
|
||||
};
|
||||
|
||||
export const createTemplateOutput = (req: Website.TemplateOutputCreate) => {
|
||||
return http.post<any>(`/websites/templates/outputs`, req);
|
||||
};
|
||||
|
||||
export const deleteTemplateOutput = (params: { id: number }) => {
|
||||
return http.post<any>(`/websites/templates/outputs/del`, params);
|
||||
};
|
||||
|
||||
export const getTemplateOutput = (id: number) => {
|
||||
return http.post<Website.TemplateOutputDTO>(`/websites/templates/outputs/get`, { id });
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutContent :title="$t('menu.template')">
|
||||
<template #leftToolBar>
|
||||
<el-button v-permission type="primary" @click="openCreate()">
|
||||
{{ $t('template.create') }}
|
||||
</el-button>
|
||||
<el-button v-permission type="primary" plain @click="openOutputList()">
|
||||
{{ $t('template.outputList') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
<TableRefresh @search="search()" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:data="data"
|
||||
:pagination-config="paginationConfig"
|
||||
@search="search()"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-table-column :label="$t('template.name')" prop="name" min-width="120px" show-overflow-tooltip />
|
||||
<el-table-column :label="$t('template.type')" prop="type" width="150px">
|
||||
<template #default="{ row }">
|
||||
<el-tag>
|
||||
{{ row.type === 'single' ? $t('template.single') : $t('template.multi') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('template.variables')" width="100px">
|
||||
<template #default="{ row }">
|
||||
{{ getVariableCount(row.variables) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('template.remark')"
|
||||
prop="remark"
|
||||
min-width="150px"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('commons.table.date')"
|
||||
:formatter="dateFormat"
|
||||
width="180px"
|
||||
/>
|
||||
<fu-table-operations
|
||||
:ellipsis="3"
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
fix
|
||||
/>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<OpDialog ref="opRef" @search="search" />
|
||||
<TemplateOperate ref="operateRef" @search="search" />
|
||||
<OutputCreate ref="outputCreateRef" @search="search" />
|
||||
<OutputList ref="outputListRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Website } from '@/api/interface/website';
|
||||
import { deleteTemplate, searchTemplates } from '@/api/modules/website';
|
||||
import TemplateOperate from '@/views/website/template/operate/index.vue';
|
||||
import OutputCreate from '@/views/website/template/output/create.vue';
|
||||
import OutputList from '@/views/website/template/output/index.vue';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import i18n from '@/lang';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const data = ref<Website.Template[]>([]);
|
||||
const searchName = ref('');
|
||||
const opRef = ref();
|
||||
const operateRef = ref();
|
||||
const outputCreateRef = ref();
|
||||
const outputListRef = ref();
|
||||
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'website-template-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: (row: Website.Template) => {
|
||||
operateRef.value.acceptParams(row.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('template.generate'),
|
||||
click: (row: Website.Template) => {
|
||||
outputCreateRef.value.acceptParams(row.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: Website.Template) => {
|
||||
deleteTemplateRow(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getVariableCount = (variables: string) => {
|
||||
try {
|
||||
const arr = JSON.parse(variables || '[]');
|
||||
return Array.isArray(arr) ? arr.length : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await searchTemplates({
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
name: searchName.value,
|
||||
type: '',
|
||||
});
|
||||
data.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
operateRef.value.acceptParams();
|
||||
};
|
||||
|
||||
const openOutputList = () => {
|
||||
outputListRef.value.acceptParams();
|
||||
};
|
||||
|
||||
const deleteTemplateRow = (row: Website.Template) => {
|
||||
opRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.button.delete'),
|
||||
names: [row.name],
|
||||
msg: i18n.global.t('template.confirmDelete'),
|
||||
api: deleteTemplate,
|
||||
params: { id: row.id },
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
search();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<DrawerPro
|
||||
v-model="open"
|
||||
:header="isEdit ? $t('template.edit') : $t('template.create')"
|
||||
size="large"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" v-loading="loading">
|
||||
<el-form-item :label="$t('template.name')" prop="name">
|
||||
<el-input v-model.trim="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('template.type')" prop="type">
|
||||
<el-radio-group v-model="form.type" :disabled="isEdit">
|
||||
<el-radio value="single">{{ $t('template.single') }}</el-radio>
|
||||
<el-radio value="multi">{{ $t('template.multi') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.type === 'single'" :label="$t('template.content')" prop="content">
|
||||
<el-input v-model="form.content" type="textarea" :rows="12" :placeholder="contentPlaceholder" />
|
||||
<span class="input-help">{{ $t('template.autoDetectHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.type === 'multi'" :label="$t('template.upload')">
|
||||
<el-upload :show-file-list="false" :before-upload="handleUpload" accept=".zip" action="#">
|
||||
<el-button type="primary" plain>{{ $t('template.upload') }}</el-button>
|
||||
</el-upload>
|
||||
<span v-if="form.filePath" class="ml-2 text-xs text-gray-500">{{ form.filePath }}</span>
|
||||
<span class="input-help">{{ $t('template.autoDetectHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('template.remark')">
|
||||
<el-input v-model="form.remark" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('template.variables')">
|
||||
<el-button size="small" type="primary" plain @click="addVariable">
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
<el-table :data="variables" border class="mt-2">
|
||||
<el-table-column :label="$t('template.variableKey')" min-width="110px">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model.trim="row.key" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('template.variableLabel')" min-width="110px">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.label" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('template.variableType')" width="160px">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.type">
|
||||
<el-option
|
||||
v-for="item in variableTypes"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="$t('template.varType' + item.name)"
|
||||
>
|
||||
<div>
|
||||
<span>{{ $t('template.varType' + item.name) }}</span>
|
||||
<span class="ml-2 text-xs text-gray-400">
|
||||
{{ $t('template.varType' + item.name + 'Helper') }}
|
||||
</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('template.variableDefault')" min-width="110px">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.default" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('template.variableOptions')" min-width="130px">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.options" :disabled="row.type !== 'select'" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('template.variableRequired')" width="75px">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.required" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.operate')" width="85px">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="variables.splice($index, 1)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="onSubmit">
|
||||
{{ $t('commons.button.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Website } from '@/api/interface/website';
|
||||
import { createTemplate, getTemplate, updateTemplate, uploadTemplateZip } from '@/api/modules/website';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const contentPlaceholder = '<html><body><h1>{{title}}</h1></body></html>';
|
||||
|
||||
const variableTypes = [
|
||||
{ value: 'text', name: 'Text' },
|
||||
{ value: 'textarea', name: 'Textarea' },
|
||||
{ value: 'number', name: 'Number' },
|
||||
{ value: 'select', name: 'Select' },
|
||||
{ value: 'color', name: 'Color' },
|
||||
];
|
||||
|
||||
const initForm = () => ({
|
||||
id: 0,
|
||||
name: '',
|
||||
type: 'single',
|
||||
content: '',
|
||||
filePath: '',
|
||||
variables: '',
|
||||
remark: '',
|
||||
});
|
||||
const form = reactive(initForm());
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: i18n.global.t('commons.rule.requiredInput'), trigger: 'blur' }],
|
||||
type: [{ required: true, message: i18n.global.t('commons.rule.requiredSelect'), trigger: 'change' }],
|
||||
};
|
||||
|
||||
const variables = ref<Website.TemplateVariable[]>([]);
|
||||
|
||||
const addVariable = (key?: string) => {
|
||||
variables.value.push({
|
||||
key: typeof key === 'string' ? key : '',
|
||||
label: '',
|
||||
type: 'text',
|
||||
default: '',
|
||||
options: '',
|
||||
required: false,
|
||||
});
|
||||
};
|
||||
|
||||
const mergeDetectedKeys = (keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
if (!variables.value.some((v) => v.key === key)) {
|
||||
addVariable(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => form.content,
|
||||
(content) => {
|
||||
if (form.type !== 'single' || !content) return;
|
||||
const keys: string[] = [];
|
||||
for (const match of content.matchAll(/\{\{(\w+)\}\}/g)) {
|
||||
if (!keys.includes(match[1])) {
|
||||
keys.push(match[1]);
|
||||
}
|
||||
}
|
||||
mergeDetectedKeys(keys);
|
||||
},
|
||||
);
|
||||
|
||||
const handleUpload = async (file: globalThis.File) => {
|
||||
try {
|
||||
const res = await uploadTemplateZip(file);
|
||||
form.filePath = res.data.filePath;
|
||||
mergeDetectedKeys(res.data.variables || []);
|
||||
MsgSuccess(i18n.global.t('commons.msg.uploadSuccess'));
|
||||
} catch {}
|
||||
return false;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
await formRef.value.validate();
|
||||
form.variables = JSON.stringify(variables.value.filter((v) => v.key));
|
||||
loading.value = true;
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateTemplate(form);
|
||||
} else {
|
||||
await createTemplate(form);
|
||||
}
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
handleClose();
|
||||
em('search');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const em = defineEmits(['search']);
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
Object.assign(form, initForm());
|
||||
variables.value = [];
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
const acceptParams = async (id?: number) => {
|
||||
Object.assign(form, initForm());
|
||||
variables.value = [];
|
||||
isEdit.value = !!id;
|
||||
if (id) {
|
||||
const res = await getTemplate(id);
|
||||
const tpl = res.data;
|
||||
form.id = tpl.id;
|
||||
form.name = tpl.name;
|
||||
form.type = tpl.type;
|
||||
form.content = tpl.content;
|
||||
form.filePath = tpl.filePath;
|
||||
form.variables = tpl.variables;
|
||||
form.remark = tpl.remark;
|
||||
try {
|
||||
variables.value = JSON.parse(tpl.variables || '[]');
|
||||
} catch {
|
||||
variables.value = [];
|
||||
}
|
||||
}
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<DrawerPro v-model="open" :header="$t('template.outputCreate')" size="60%" @close="handleClose">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" v-loading="loading">
|
||||
<el-form-item :label="$t('template.selectTemplate')" prop="templateID">
|
||||
<el-select v-model="form.templateID" class="w-full" filterable @change="onTemplateChange">
|
||||
<el-option v-for="tpl in templates" :key="tpl.id" :value="tpl.id" :label="tpl.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('template.outputName')" prop="name">
|
||||
<el-input v-model.trim="form.name" />
|
||||
</el-form-item>
|
||||
<div v-if="currentTemplate">
|
||||
<el-divider content-position="left">
|
||||
<el-text type="info" size="small">{{ $t('template.fillVariables') }}</el-text>
|
||||
</el-divider>
|
||||
<span v-if="!templateVariables.length" class="input-help">{{ $t('template.noVariables') }}</span>
|
||||
<el-form-item
|
||||
v-for="variable in templateVariables"
|
||||
:key="variable.key"
|
||||
:label="variable.label || variable.key"
|
||||
:required="variable.required"
|
||||
>
|
||||
<el-input v-if="variable.type === 'text'" v-model="variableValues[variable.key]" />
|
||||
<el-input
|
||||
v-else-if="variable.type === 'textarea'"
|
||||
v-model="variableValues[variable.key]"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="variable.type === 'number'"
|
||||
v-model="variableValues[variable.key]"
|
||||
type="number"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="variable.type === 'select'"
|
||||
v-model="variableValues[variable.key]"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option v-for="opt in getOptions(variable)" :key="opt" :value="opt" :label="opt" />
|
||||
</el-select>
|
||||
<el-color-picker
|
||||
v-else-if="variable.type === 'color'"
|
||||
v-model="variableValues[variable.key]"
|
||||
/>
|
||||
<el-input v-else v-model="variableValues[variable.key]" />
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">
|
||||
<el-text type="info" size="small">{{ $t('template.preview') }}</el-text>
|
||||
</el-divider>
|
||||
<div class="preview-box">
|
||||
<iframe
|
||||
v-if="previewHTML"
|
||||
:srcdoc="previewHTML"
|
||||
class="preview-frame"
|
||||
sandbox="allow-same-origin"
|
||||
></iframe>
|
||||
<div v-else class="preview-empty">{{ $t('template.previewEmpty') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="onGenerate">
|
||||
{{ $t('template.generate') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Website } from '@/api/interface/website';
|
||||
import { createTemplateOutput, previewTemplate, searchTemplates } from '@/api/modules/website';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const formRef = ref();
|
||||
|
||||
const templates = ref<Website.Template[]>([]);
|
||||
const variableValues = reactive<Record<string, string>>({});
|
||||
const previewHTML = ref('');
|
||||
let previewTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const form = reactive({
|
||||
templateID: undefined as number | undefined,
|
||||
name: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
templateID: [{ required: true, message: i18n.global.t('commons.rule.requiredSelect'), trigger: 'change' }],
|
||||
name: [{ required: true, message: i18n.global.t('commons.rule.requiredInput'), trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const currentTemplate = computed(() => templates.value.find((tpl) => tpl.id === form.templateID));
|
||||
|
||||
const templateVariables = computed<Website.TemplateVariable[]>(() => {
|
||||
if (!currentTemplate.value) return [];
|
||||
try {
|
||||
const arr = JSON.parse(currentTemplate.value.variables || '[]');
|
||||
return Array.isArray(arr) ? arr : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const getOptions = (variable: Website.TemplateVariable) => {
|
||||
return (variable.options || '')
|
||||
.split(',')
|
||||
.map((opt) => opt.trim())
|
||||
.filter((opt) => opt);
|
||||
};
|
||||
|
||||
const onTemplateChange = () => {
|
||||
for (const key of Object.keys(variableValues)) {
|
||||
delete variableValues[key];
|
||||
}
|
||||
for (const variable of templateVariables.value) {
|
||||
variableValues[variable.key] = variable.default || '';
|
||||
}
|
||||
refreshPreview();
|
||||
};
|
||||
|
||||
const renderLocal = (content: string) => {
|
||||
let html = content;
|
||||
for (const key of Object.keys(variableValues)) {
|
||||
html = html.replace(new RegExp('\\{\\{' + key + '\\}\\}', 'g'), variableValues[key] || '');
|
||||
}
|
||||
return html.replace(/\{\{\w+\}\}/g, '');
|
||||
};
|
||||
|
||||
const refreshPreview = () => {
|
||||
if (!currentTemplate.value) {
|
||||
previewHTML.value = '';
|
||||
return;
|
||||
}
|
||||
if (currentTemplate.value.type === 'single') {
|
||||
previewHTML.value = renderLocal(currentTemplate.value.content || '');
|
||||
return;
|
||||
}
|
||||
if (previewTimer) {
|
||||
clearTimeout(previewTimer);
|
||||
}
|
||||
previewTimer = setTimeout(async () => {
|
||||
try {
|
||||
const res = await previewTemplate({
|
||||
templateID: form.templateID,
|
||||
variableValues: { ...variableValues },
|
||||
});
|
||||
previewHTML.value = res.data.html;
|
||||
} catch {
|
||||
previewHTML.value = '';
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
watch(variableValues, () => {
|
||||
refreshPreview();
|
||||
});
|
||||
|
||||
const onGenerate = async () => {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
await createTemplateOutput({
|
||||
templateID: form.templateID,
|
||||
name: form.name,
|
||||
variableValues: { ...variableValues },
|
||||
});
|
||||
MsgSuccess(i18n.global.t('template.outputSuccess'));
|
||||
handleClose();
|
||||
em('search');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const em = defineEmits(['search']);
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
form.templateID = undefined;
|
||||
form.name = '';
|
||||
for (const key of Object.keys(variableValues)) {
|
||||
delete variableValues[key];
|
||||
}
|
||||
previewHTML.value = '';
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
const acceptParams = async (templateID?: number) => {
|
||||
const res = await searchTemplates({ page: 1, pageSize: 1000, name: '', type: '' });
|
||||
templates.value = res.data.items || [];
|
||||
if (templateID && templates.value.some((tpl) => tpl.id === templateID)) {
|
||||
form.templateID = templateID;
|
||||
onTemplateChange();
|
||||
}
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview-box {
|
||||
width: 100%;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.preview-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<DrawerPro v-model="open" :header="$t('template.outputList')" size="60%" @close="handleClose">
|
||||
<template #buttons>
|
||||
<el-button type="primary" plain @click="openCreate()">
|
||||
{{ $t('template.outputCreate') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<ComplexTable :data="data" :pagination-config="paginationConfig" @search="search()" v-loading="loading">
|
||||
<el-table-column :label="$t('template.outputName')" prop="name" min-width="100px" show-overflow-tooltip />
|
||||
<el-table-column :label="$t('template.name')" prop="templateName" min-width="100px" show-overflow-tooltip />
|
||||
<el-table-column :label="$t('template.type')" width="110px">
|
||||
<template #default="{ row }">
|
||||
<el-tag>
|
||||
{{ row.templateType === 'single' ? $t('template.single') : $t('template.multi') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('template.filePath')"
|
||||
prop="outputPath"
|
||||
min-width="160px"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="createdAt" :label="$t('commons.table.date')" :formatter="dateFormat" width="165px" />
|
||||
<fu-table-operations :ellipsis="1" :buttons="buttons" :label="$t('commons.table.operate')" fixed="right" fix />
|
||||
</ComplexTable>
|
||||
<OpDialog ref="opRef" @search="search" />
|
||||
<OutputCreate ref="outputCreateRef" @search="search" />
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Website } from '@/api/interface/website';
|
||||
import { deleteTemplateOutput, searchTemplateOutputs } from '@/api/modules/website';
|
||||
import OutputCreate from '@/views/website/template/output/create.vue';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import i18n from '@/lang';
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const data = ref<Website.TemplateOutputDTO[]>([]);
|
||||
const opRef = ref();
|
||||
const outputCreateRef = ref();
|
||||
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'website-template-output-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: Website.TemplateOutputDTO) => {
|
||||
opRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.button.delete'),
|
||||
names: [row.name],
|
||||
msg: i18n.global.t('template.confirmDeleteOutput'),
|
||||
api: deleteTemplateOutput,
|
||||
params: { id: row.id },
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const search = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await searchTemplateOutputs({
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
templateID: 0,
|
||||
});
|
||||
data.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
outputCreateRef.value.acceptParams();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
const acceptParams = () => {
|
||||
open.value = true;
|
||||
search();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
@@ -271,6 +271,20 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<el-form-item
|
||||
:label="$t('template.importButton')"
|
||||
prop="templateOutputID"
|
||||
v-if="website.type === 'static'"
|
||||
>
|
||||
<el-select v-model="website.templateOutputID" clearable class="p-w-200">
|
||||
<el-option
|
||||
v-for="output in templateOutputs"
|
||||
:key="output.id"
|
||||
:label="output.name + ' (' + output.templateName + ')'"
|
||||
:value="output.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="enableFtp" v-if="website.type === 'static' || website.type === 'runtime'">
|
||||
<el-checkbox
|
||||
@change="random"
|
||||
@@ -443,6 +457,7 @@ import {
|
||||
preCheck,
|
||||
searchAcmeAccount,
|
||||
getDirConfig,
|
||||
searchTemplateOutputs,
|
||||
} from '@/api/modules/website';
|
||||
import { Rules, checkNumberRange } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
@@ -524,6 +539,7 @@ const initData = () => ({
|
||||
domains: [],
|
||||
parentWebsiteID: undefined,
|
||||
siteDir: '',
|
||||
templateOutputID: undefined,
|
||||
|
||||
streamPorts: '',
|
||||
udp: false,
|
||||
@@ -575,6 +591,7 @@ const appReq = reactive({
|
||||
const apps = ref<App.AppItem[]>([]);
|
||||
const preCheckRef = ref();
|
||||
const staticPath = ref('');
|
||||
const templateOutputs = ref<Website.TemplateOutputDTO[]>([]);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user