mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 04:22:33 +08:00
feat: 为网站编辑添加自定义配置标签页 (#1235)
* Initial plan * feat: add custom config tab for website editing Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * chore: remove unused variables in EditView.vue Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: address code review comments for custom config Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: lint --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> Co-authored-by: 耗子 <haozi@loli.email>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -206,6 +207,10 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
|
|||||||
setting.Proxies = proxyVhost.Proxies()
|
setting.Proxies = proxyVhost.Proxies()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自定义配置
|
||||||
|
configDir := filepath.Join(app.Root, "sites", website.Name, "config")
|
||||||
|
setting.CustomConfigs = r.getCustomConfigs(configDir)
|
||||||
|
|
||||||
return setting, err
|
return setting, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,6 +652,12 @@ func (r *websiteRepo) Update(ctx context.Context, req *request.WebsiteUpdate) er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自定义配置
|
||||||
|
configDir := filepath.Join(app.Root, "sites", website.Name, "config")
|
||||||
|
if err = r.saveCustomConfigs(configDir, req.CustomConfigs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
if err = vhost.Save(); err != nil {
|
if err = vhost.Save(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -894,6 +905,141 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
|
|||||||
return r.cert.Deploy(newCert.ID, website.ID)
|
return r.cert.Deploy(newCert.ID, website.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// customConfigStartNum 自定义配置起始序号
|
||||||
|
const customConfigStartNum = 800
|
||||||
|
|
||||||
|
// customConfigEndNum 自定义配置结束序号
|
||||||
|
const customConfigEndNum = 999
|
||||||
|
|
||||||
|
// getCustomConfigs 获取网站自定义配置列表
|
||||||
|
func (r *websiteRepo) getCustomConfigs(configDir string) []types.WebsiteCustomConfig {
|
||||||
|
var configs []types.WebsiteCustomConfig
|
||||||
|
|
||||||
|
// 从 site 和 shared 目录读取自定义配置
|
||||||
|
for _, scope := range []string{"site", "shared"} {
|
||||||
|
scopeDir := filepath.Join(configDir, scope)
|
||||||
|
entries, err := os.ReadDir(scopeDir)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 匹配文件名格式: 800-999-name.conf
|
||||||
|
name := entry.Name()
|
||||||
|
if !strings.HasSuffix(name, ".conf") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 解析序号
|
||||||
|
parts := strings.SplitN(name, "-", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
num, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil || num < customConfigStartNum || num > customConfigEndNum {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 提取配置名称(去掉序号前缀和.conf后缀)
|
||||||
|
configName := strings.TrimSuffix(parts[1], ".conf")
|
||||||
|
if configName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 读取配置内容
|
||||||
|
content, err := io.Read(filepath.Join(scopeDir, name))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
configs = append(configs, types.WebsiteCustomConfig{
|
||||||
|
Name: configName,
|
||||||
|
Scope: scope,
|
||||||
|
Content: content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveCustomConfigs 保存网站自定义配置
|
||||||
|
func (r *websiteRepo) saveCustomConfigs(configDir string, configs []request.WebsiteCustomConfig) error {
|
||||||
|
if err := r.clearCustomConfigs(configDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分别跟踪 site 和 shared 目录的序号
|
||||||
|
siteNum := customConfigStartNum
|
||||||
|
sharedNum := customConfigStartNum
|
||||||
|
|
||||||
|
for _, cfg := range configs {
|
||||||
|
var num int
|
||||||
|
switch cfg.Scope {
|
||||||
|
case "site":
|
||||||
|
num = siteNum
|
||||||
|
siteNum++
|
||||||
|
case "shared":
|
||||||
|
num = sharedNum
|
||||||
|
sharedNum++
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid config scope: %s", cfg.Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
if num > customConfigEndNum {
|
||||||
|
return errors.New(r.t.Get("maximum number of custom configurations reached (limit: %d)", customConfigEndNum-customConfigStartNum+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := fmt.Sprintf("%03d-%s.conf", num, cfg.Name)
|
||||||
|
filePath := filepath.Join(configDir, cfg.Scope, fileName)
|
||||||
|
|
||||||
|
if err := io.Write(filePath, cfg.Content, 0600); err != nil {
|
||||||
|
return fmt.Errorf("failed to write custom config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearCustomConfigs 清除网站自定义配置文件
|
||||||
|
func (r *websiteRepo) clearCustomConfigs(configDir string) error {
|
||||||
|
for _, scope := range []string{"site", "shared"} {
|
||||||
|
scopeDir := filepath.Join(configDir, scope)
|
||||||
|
entries, err := os.ReadDir(scopeDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := entry.Name()
|
||||||
|
if !strings.HasSuffix(name, ".conf") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(name, "-", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
num, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil || num < customConfigStartNum || num > customConfigEndNum {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filePath := filepath.Join(scopeDir, name)
|
||||||
|
if err = os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to remove custom config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *websiteRepo) getVhost(website *biz.Website) (webservertypes.Vhost, error) {
|
func (r *websiteRepo) getVhost(website *biz.Website) (webservertypes.Vhost, error) {
|
||||||
webServer, err := r.setting.Get(biz.SettingKeyWebserver)
|
webServer, err := r.setting.Get(biz.SettingKeyWebserver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -66,6 +66,16 @@ type WebsiteUpdate struct {
|
|||||||
// 反向代理
|
// 反向代理
|
||||||
Upstreams []types.Upstream `json:"upstreams"`
|
Upstreams []types.Upstream `json:"upstreams"`
|
||||||
Proxies []types.Proxy `json:"proxies"`
|
Proxies []types.Proxy `json:"proxies"`
|
||||||
|
|
||||||
|
// 自定义配置
|
||||||
|
CustomConfigs []WebsiteCustomConfig `json:"custom_configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebsiteCustomConfig 网站自定义配置请求
|
||||||
|
type WebsiteCustomConfig struct {
|
||||||
|
Name string `json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 配置名称
|
||||||
|
Scope string `json:"scope" validate:"required|in:site,shared"` // 作用域: site(此网站), shared(全局)
|
||||||
|
Content string `json:"content"` // 配置内容
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsiteUpdateRemark struct {
|
type WebsiteUpdateRemark struct {
|
||||||
|
|||||||
@@ -46,4 +46,14 @@ type WebsiteSetting struct {
|
|||||||
// 反向代理
|
// 反向代理
|
||||||
Upstreams []types.Upstream `json:"upstreams"`
|
Upstreams []types.Upstream `json:"upstreams"`
|
||||||
Proxies []types.Proxy `json:"proxies"`
|
Proxies []types.Proxy `json:"proxies"`
|
||||||
|
|
||||||
|
// 自定义配置
|
||||||
|
CustomConfigs []WebsiteCustomConfig `json:"custom_configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebsiteCustomConfig 网站自定义配置
|
||||||
|
type WebsiteCustomConfig struct {
|
||||||
|
Name string `json:"name"` // 配置名称
|
||||||
|
Scope string `json:"scope"` // 作用域: site(此网站), shared(全局)
|
||||||
|
Content string `json:"content"` // 配置内容
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ const { data: setting, send: fetchSetting } = useRequest(website.config(Number(i
|
|||||||
rewrite: '',
|
rewrite: '',
|
||||||
open_basedir: false,
|
open_basedir: false,
|
||||||
upstreams: [],
|
upstreams: [],
|
||||||
proxies: []
|
proxies: [],
|
||||||
|
custom_configs: []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const { data: installedEnvironment } = useRequest(home.installedEnvironment, {
|
const { data: installedEnvironment } = useRequest(home.installedEnvironment, {
|
||||||
@@ -383,6 +384,33 @@ const updateTimeoutUnit = (proxy: any, unit: string) => {
|
|||||||
const parsed = parseDuration(proxy.resolver_timeout)
|
const parsed = parseDuration(proxy.resolver_timeout)
|
||||||
proxy.resolver_timeout = buildDuration(parsed.value, unit)
|
proxy.resolver_timeout = buildDuration(parsed.value, unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 自定义配置相关 ==========
|
||||||
|
// 作用域选项
|
||||||
|
const scopeOptions = [
|
||||||
|
{ label: $gettext('This Website'), value: 'site' },
|
||||||
|
{ label: $gettext('Global'), value: 'shared' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 添加自定义配置
|
||||||
|
const addCustomConfig = () => {
|
||||||
|
if (!setting.value.custom_configs) {
|
||||||
|
setting.value.custom_configs = []
|
||||||
|
}
|
||||||
|
const index = setting.value.custom_configs.length + 1
|
||||||
|
setting.value.custom_configs.push({
|
||||||
|
name: `custom_${index}`,
|
||||||
|
scope: 'site',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除自定义配置
|
||||||
|
const removeCustomConfig = (index: number) => {
|
||||||
|
if (setting.value.custom_configs) {
|
||||||
|
setting.value.custom_configs.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -666,7 +694,9 @@ const updateTimeoutUnit = (proxy: any, unit: string) => {
|
|||||||
<n-form-item-gi :span="12" :label="$gettext('Proxy Host')">
|
<n-form-item-gi :span="12" :label="$gettext('Proxy Host')">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="proxy.host"
|
v-model:value="proxy.host"
|
||||||
:placeholder="$gettext('Default: $proxy_host, or extracted from Proxy Pass')"
|
:placeholder="
|
||||||
|
$gettext('Default: $proxy_host, or extracted from Proxy Pass')
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="12" :label="$gettext('Proxy SNI')">
|
<n-form-item-gi :span="12" :label="$gettext('Proxy SNI')">
|
||||||
@@ -895,6 +925,64 @@ const updateTimeoutUnit = (proxy: any, unit: string) => {
|
|||||||
<common-editor v-if="setting" v-model:value="setting.rewrite" height="60vh" />
|
<common-editor v-if="setting" v-model:value="setting.rewrite" height="60vh" />
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="custom_configs" :tab="$gettext('Custom Configs')">
|
||||||
|
<n-flex vertical>
|
||||||
|
<!-- 自定义配置列表 -->
|
||||||
|
<draggable
|
||||||
|
v-model="setting.custom_configs"
|
||||||
|
item-key="name"
|
||||||
|
handle=".drag-handle"
|
||||||
|
:animation="200"
|
||||||
|
ghost-class="ghost-card"
|
||||||
|
>
|
||||||
|
<template #item="{ element: config, index }">
|
||||||
|
<n-card closable @close="removeCustomConfig(index)" style="margin-bottom: 16px">
|
||||||
|
<template #header>
|
||||||
|
<n-flex align="center" :size="8">
|
||||||
|
<!-- 拖拽手柄 -->
|
||||||
|
<div class="drag-handle" cursor-grab>
|
||||||
|
<the-icon icon="mdi:drag" :size="20" />
|
||||||
|
</div>
|
||||||
|
<span>{{ $gettext('Config') }} #{{ index + 1 }}</span>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
<n-form label-placement="left" label-width="100px">
|
||||||
|
<n-grid :cols="24" :x-gap="16">
|
||||||
|
<n-form-item-gi :span="12" :label="$gettext('Name')">
|
||||||
|
<n-input
|
||||||
|
v-model:value="config.name"
|
||||||
|
:placeholder="
|
||||||
|
$gettext('Config name (letters, numbers, underscore, hyphen)')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :span="12" :label="$gettext('Scope')">
|
||||||
|
<n-select v-model:value="config.scope" :options="scopeOptions" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
</n-grid>
|
||||||
|
<n-form-item :label="$gettext('Content')">
|
||||||
|
<common-editor
|
||||||
|
v-model:value="config.content"
|
||||||
|
height="30vh"
|
||||||
|
:lang="isNginx ? 'nginx' : 'apacheconf'"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<n-empty v-if="!setting.custom_configs || setting.custom_configs.length === 0">
|
||||||
|
{{ $gettext('No custom configs') }}
|
||||||
|
</n-empty>
|
||||||
|
|
||||||
|
<!-- 添加按钮 -->
|
||||||
|
<n-button type="primary" dashed @click="addCustomConfig" mb-20>
|
||||||
|
{{ $gettext('Add Custom Config') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-tab-pane>
|
||||||
<n-tab-pane name="log" :tab="$gettext('Access Log')">
|
<n-tab-pane name="log" :tab="$gettext('Access Log')">
|
||||||
<n-flex vertical>
|
<n-flex vertical>
|
||||||
<n-flex flex items-center>
|
<n-flex flex items-center>
|
||||||
|
|||||||
Reference in New Issue
Block a user