mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 03:07:20 +08:00
feat: 添加项目默认目录配置及目录跳转功能 (#1233)
* Initial plan * feat: 添加项目默认目录配置及目录跳转功能 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: 为项目默认目录添加回退默认值 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>
This commit is contained in:
@@ -85,7 +85,7 @@ func initWeb() (*app.Web, error) {
|
||||
taskService := service.NewTaskService(taskRepo)
|
||||
websiteService := service.NewWebsiteService(websiteRepo, settingRepo)
|
||||
projectRepo := data.NewProjectRepo(locale, db, logger)
|
||||
projectService := service.NewProjectService(projectRepo)
|
||||
projectService := service.NewProjectService(projectRepo, settingRepo)
|
||||
databaseService := service.NewDatabaseService(databaseRepo)
|
||||
databaseServerService := service.NewDatabaseServerService(databaseServerRepo)
|
||||
databaseUserService := service.NewDatabaseUserService(databaseUserRepo)
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SettingKeyMonitorDays SettingKey = "monitor_days"
|
||||
SettingKeyBackupPath SettingKey = "backup_path"
|
||||
SettingKeyWebsitePath SettingKey = "website_path"
|
||||
SettingKeyProjectPath SettingKey = "project_path"
|
||||
SettingKeyWebsiteTLSVersions SettingKey = "website_tls_versions"
|
||||
SettingKeyWebsiteCipherSuites SettingKey = "website_tls_cipher_suites"
|
||||
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
|
||||
|
||||
@@ -225,6 +225,10 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projectPath, err := r.Get(biz.SettingKeyProjectPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hiddenMenu, err := r.GetSlice(biz.SettingHiddenMenu)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -261,6 +265,7 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
|
||||
BindUA: r.conf.HTTP.BindUA,
|
||||
WebsitePath: websitePath,
|
||||
BackupPath: backupPath,
|
||||
ProjectPath: projectPath,
|
||||
HiddenMenu: hiddenMenu,
|
||||
CustomLogo: customLogo,
|
||||
Port: r.conf.HTTP.Port,
|
||||
@@ -291,6 +296,9 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
|
||||
if err := r.Set(biz.SettingKeyBackupPath, req.BackupPath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.Set(biz.SettingKeyProjectPath, req.ProjectPath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.SetSlice(biz.SettingHiddenMenu, req.HiddenMenu); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ type ProjectCreate struct {
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Type types.ProjectType `form:"type" json:"type" validate:"required|in:general,php,java,go,python,nodejs"`
|
||||
Description string `form:"description" json:"description"`
|
||||
RootDir string `form:"root_dir" json:"root_dir" validate:"required"`
|
||||
RootDir string `form:"root_dir" json:"root_dir"`
|
||||
WorkingDir string `form:"working_dir" json:"working_dir"`
|
||||
ExecStart string `form:"exec_start" json:"exec_start"`
|
||||
User string `form:"user" json:"user"`
|
||||
|
||||
@@ -19,6 +19,7 @@ type SettingPanel struct {
|
||||
BindUA []string `json:"bind_ua"`
|
||||
WebsitePath string `json:"website_path" validate:"required"`
|
||||
BackupPath string `json:"backup_path" validate:"required"`
|
||||
ProjectPath string `json:"project_path" validate:"required"`
|
||||
HiddenMenu []string `json:"hidden_menu"` // 隐藏的菜单项
|
||||
CustomLogo string `json:"custom_logo" validate:"isFullURL"` // 自定义 Logo URL
|
||||
Port uint `json:"port" validate:"required|min:1|max:65535"`
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libtnb/chix"
|
||||
|
||||
@@ -12,11 +13,13 @@ import (
|
||||
|
||||
type ProjectService struct {
|
||||
projectRepo biz.ProjectRepo
|
||||
settingRepo biz.SettingRepo
|
||||
}
|
||||
|
||||
func NewProjectService(project biz.ProjectRepo) *ProjectService {
|
||||
func NewProjectService(project biz.ProjectRepo, setting biz.SettingRepo) *ProjectService {
|
||||
return &ProjectService{
|
||||
projectRepo: project,
|
||||
settingRepo: setting,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +66,11 @@ func (s *ProjectService) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.RootDir) == 0 {
|
||||
req.RootDir, _ = s.settingRepo.Get(biz.SettingKeyProjectPath, "/opt/ace/projects")
|
||||
req.RootDir = filepath.Join(req.RootDir, req.Name)
|
||||
}
|
||||
|
||||
project, err := s.projectRepo.Create(r.Context(), req)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -25,7 +25,7 @@ const phpFrameworks = [
|
||||
const createModel = ref({
|
||||
name: '',
|
||||
type: '',
|
||||
root_dir: '/opt/ace/projects',
|
||||
root_dir: '',
|
||||
working_dir: '',
|
||||
exec_start: '',
|
||||
user: 'www'
|
||||
@@ -98,7 +98,7 @@ const handleCreate = async () => {
|
||||
createModel.value = {
|
||||
name: '',
|
||||
type: '',
|
||||
root_dir: '/opt/ace/projects',
|
||||
root_dir: '',
|
||||
working_dir: '',
|
||||
exec_start: '',
|
||||
user: 'www'
|
||||
@@ -141,13 +141,17 @@ const modalTitle = computed(() => {
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="root_dir" :label="$gettext('Project Directory')" required>
|
||||
<n-form-item path="root_dir" :label="$gettext('Project Directory')">
|
||||
<n-input-group>
|
||||
<n-input
|
||||
v-model:value="createModel.root_dir"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('Project root directory')"
|
||||
:placeholder="
|
||||
$gettext(
|
||||
'Project root directory (if left empty, defaults to project directory/project name)'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<n-button @click="handleSelectPath">
|
||||
<template #icon>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useGettext } from 'vue3-gettext'
|
||||
import project from '@/api/panel/project'
|
||||
import systemctl from '@/api/panel/systemctl'
|
||||
import RealtimeLog from '@/components/common/RealtimeLog.vue'
|
||||
import { useFileStore } from '@/store'
|
||||
|
||||
const type = defineModel<string>('type', { type: String, required: true })
|
||||
const createModal = defineModel<boolean>('createModal', { type: Boolean, required: true })
|
||||
@@ -13,7 +14,9 @@ const editId = defineModel<number>('editId', { type: Number, required: true })
|
||||
const logModal = ref(false)
|
||||
const logService = ref('')
|
||||
|
||||
const fileStore = useFileStore()
|
||||
const { $gettext } = useGettext()
|
||||
const router = useRouter()
|
||||
const selectedRowKeys = ref<any>([])
|
||||
|
||||
const typeMap: Record<string, string> = {
|
||||
@@ -66,7 +69,20 @@ const columns: any = [
|
||||
key: 'root_dir',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
render(row: any) {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
class: 'cursor-pointer hover:opacity-60',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
fileStore.path = row.root_dir
|
||||
router.push({ name: 'file-index' })
|
||||
}
|
||||
},
|
||||
{ default: () => row.root_dir }
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Actions'),
|
||||
|
||||
@@ -37,6 +37,7 @@ const { data: model } = useRequest(setting.list, {
|
||||
bind_ua: [],
|
||||
website_path: '',
|
||||
backup_path: '',
|
||||
project_path: '',
|
||||
hidden_menu: [],
|
||||
custom_logo: '',
|
||||
https: false,
|
||||
|
||||
@@ -16,14 +16,17 @@ const model = defineModel<any>('model', { type: Object, required: true })
|
||||
// 目录选择器
|
||||
const showPathSelector = ref(false)
|
||||
const pathSelectorPath = ref('/opt/ace')
|
||||
const pathSelectorTarget = ref<'website' | 'backup'>('website')
|
||||
const pathSelectorTarget = ref<'website' | 'backup' | 'project'>('website')
|
||||
|
||||
const handleSelectPath = (target: 'website' | 'backup') => {
|
||||
const handleSelectPath = (target: 'website' | 'backup' | 'project') => {
|
||||
pathSelectorTarget.value = target
|
||||
pathSelectorPath.value =
|
||||
target === 'website'
|
||||
? model.value.website_path || '/opt/ace/sites'
|
||||
: model.value.backup_path || '/opt/ace/backup'
|
||||
if (target === 'website') {
|
||||
pathSelectorPath.value = model.value.website_path || '/opt/ace/sites'
|
||||
} else if (target === 'backup') {
|
||||
pathSelectorPath.value = model.value.backup_path || '/opt/ace/backup'
|
||||
} else {
|
||||
pathSelectorPath.value = model.value.project_path || '/opt/ace/projects'
|
||||
}
|
||||
showPathSelector.value = true
|
||||
}
|
||||
|
||||
@@ -31,8 +34,10 @@ watch(showPathSelector, (val) => {
|
||||
if (!val && pathSelectorPath.value) {
|
||||
if (pathSelectorTarget.value === 'website') {
|
||||
model.value.website_path = pathSelectorPath.value
|
||||
} else {
|
||||
} else if (pathSelectorTarget.value === 'backup') {
|
||||
model.value.backup_path = pathSelectorPath.value
|
||||
} else {
|
||||
model.value.project_path = pathSelectorPath.value
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -138,6 +143,19 @@ const menus = computed<TreeSelectOption[]>(() => {
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Default Project Directory')">
|
||||
<n-input-group>
|
||||
<n-input
|
||||
v-model:value="model.project_path"
|
||||
:placeholder="$gettext('/opt/ace/projects')"
|
||||
/>
|
||||
<n-button @click="handleSelectPath('project')">
|
||||
<template #icon>
|
||||
<i-mdi-folder-open />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Custom Logo')">
|
||||
<n-input
|
||||
v-model:value="model.custom_logo"
|
||||
|
||||
Reference in New Issue
Block a user