2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00

feat: 项目管理阶段2

This commit is contained in:
2026-01-11 00:34:20 +08:00
parent 035046449d
commit 6ea1295f86
7 changed files with 261 additions and 14 deletions

View File

@@ -17,7 +17,7 @@ type Project struct {
}
type ProjectRepo interface {
List(page, limit uint) ([]*types.ProjectDetail, int64, error)
List(typ types.ProjectType, page, limit uint) ([]*types.ProjectDetail, int64, error)
Get(id uint) (*types.ProjectDetail, error)
Create(req *request.ProjectCreate) (*types.ProjectDetail, error)
Update(req *request.ProjectUpdate) error

View File

@@ -21,27 +21,30 @@ import (
)
type projectRepo struct {
systemdDir string
t *gotext.Locale
db *gorm.DB
t *gotext.Locale
db *gorm.DB
}
func NewProjectRepo(t *gotext.Locale, db *gorm.DB) biz.ProjectRepo {
return &projectRepo{
systemdDir: "/etc/systemd/system",
t: t,
db: db,
t: t,
db: db,
}
}
func (r *projectRepo) List(page, limit uint) ([]*types.ProjectDetail, int64, error) {
func (r *projectRepo) List(typ types.ProjectType, page, limit uint) ([]*types.ProjectDetail, int64, error) {
var projects []*biz.Project
var total int64
if err := r.db.Model(&biz.Project{}).Count(&total).Error; err != nil {
query := r.db.Model(&biz.Project{})
if typ != "" {
query = query.Where("type = ?", typ)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := r.db.Offset(int((page - 1) * limit)).Limit(int(limit)).Order("id desc").Find(&projects).Error; err != nil {
if err := query.Offset(int((page - 1) * limit)).Limit(int(limit)).Order("id desc").Find(&projects).Error; err != nil {
return nil, 0, err
}
@@ -148,7 +151,7 @@ func (r *projectRepo) Delete(id uint) error {
// unitFilePath 返回 systemd unit 文件路径
func (r *projectRepo) unitFilePath(name string) string {
return filepath.Join(r.systemdDir, fmt.Sprintf("acepanel-project-%s.service", name))
return filepath.Join("/etc/systemd/system", fmt.Sprintf("%s.service", name))
}
// parseProjectDetail 从数据库记录和 systemd unit 文件解析项目详情

View File

@@ -7,6 +7,7 @@ import (
"github.com/acepanel/panel/internal/biz"
"github.com/acepanel/panel/internal/http/request"
"github.com/acepanel/panel/pkg/types"
)
type ProjectService struct {
@@ -26,7 +27,8 @@ func (s *ProjectService) List(w http.ResponseWriter, r *http.Request) {
return
}
projects, total, err := s.projectRepo.List(req.Page, req.Limit)
typ := types.ProjectType(r.URL.Query().Get("type"))
projects, total, err := s.projectRepo.List(typ, req.Page, req.Limit)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return

View File

@@ -0,0 +1,15 @@
import { http } from '@/utils'
export default {
// 获取项目列表
list: (type: string, page: number, limit: number): any =>
http.Get('/project', { params: { type, page, limit } }),
// 获取项目详情
get: (id: number): any => http.Get(`/project/${id}`),
// 创建项目
create: (data: any): any => http.Post('/project', data),
// 更新项目
update: (id: number, data: any): any => http.Put(`/project/${id}`, data),
// 删除项目
delete: (id: number): any => http.Delete(`/project/${id}`)
}

View File

@@ -0,0 +1,8 @@
<script setup lang="ts">
const show = defineModel<boolean>('show', { type: Boolean, required: true })
const type = defineModel<string>('type', { type: String, required: true }) // 项目类型
</script>
<template></template>
<style scoped lang="scss"></style>

View File

@@ -3,20 +3,28 @@ defineOptions({
name: 'project-index'
})
const currentTab = ref('general')
import CreateModal from '@/views/project/CreateModal.vue'
import ListView from '@/views/project/ListView.vue'
const currentTab = ref('all')
const createModal = ref(false)
</script>
<template>
<common-page show-header show-footer>
<template #tabbar>
<n-tabs v-model:value="currentTab" animated>
<n-tab name="all" :tab="$gettext('All')" />
<n-tab name="general" :tab="$gettext('General')" />
<n-tab name="php" :tab="$gettext('PHP')" />
<n-tab name="java" :tab="$gettext('Java')" />
<n-tab name="go" :tab="$gettext('go')" />
<n-tab name="go" :tab="$gettext('Go')" />
<n-tab name="python" :tab="$gettext('Python')" />
<n-tab name="nodejs" :tab="$gettext('Node.js')" />
</n-tabs>
</template>
<list-view v-model:type="currentTab" v-model:createModal="createModal" />
<create-modal v-model:show="createModal" v-model:type="currentTab" />
</common-page>
</template>

View File

@@ -0,0 +1,211 @@
<script lang="ts" setup>
import { NButton, NDataTable, NFlex, NPopconfirm, NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import project from '@/api/panel/project'
import systemctl from '@/api/panel/systemctl'
const type = defineModel<string>('type', { type: String, required: true })
const createModal = defineModel<boolean>('createModal', { type: Boolean, required: true })
const { $gettext } = useGettext()
const router = useRouter()
const selectedRowKeys = ref<any>([])
const typeMap: Record<string, string> = {
general: $gettext('General'),
php: 'PHP',
java: 'Java',
go: 'Go',
python: 'Python',
nodejs: 'Node.js'
}
const columns: any = [
{ type: 'selection', fixed: 'left' },
{
title: $gettext('Project Name'),
key: 'name',
width: 200,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: $gettext('Type'),
key: 'type',
width: 120,
render(row: any) {
return h(NTag, { type: 'info' }, { default: () => typeMap[row.type] || row.type })
}
},
{
title: $gettext('Status'),
key: 'status',
width: 100,
render(row: any) {
return h(
NTag,
{ type: row.status === 'running' ? 'success' : 'default' },
{ default: () => (row.status === 'running' ? $gettext('Running') : $gettext('Stopped')) }
)
}
},
{
title: $gettext('Directory'),
key: 'root_dir',
minWidth: 200,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: $gettext('Actions'),
key: 'actions',
width: 280,
hideInExcel: true,
render(row: any) {
return [
h(
NButton,
{
size: 'small',
type: row.status === 'running' ? 'warning' : 'success',
onClick: () => handleToggleStatus(row)
},
{ default: () => (row.status === 'running' ? $gettext('Stop') : $gettext('Start')) }
),
h(
NButton,
{
size: 'small',
type: 'primary',
style: 'margin-left: 10px;',
onClick: () => handleEdit(row)
},
{ default: () => $gettext('Edit') }
),
h(
NPopconfirm,
{
showIcon: false,
onPositiveClick: () => handleDelete(row.id)
},
{
default: () =>
$gettext('Are you sure you want to delete project %{ name }?', { name: row.name }),
trigger: () =>
h(
NButton,
{
size: 'small',
type: 'error',
style: 'margin-left: 10px;'
},
{ default: () => $gettext('Delete') }
)
}
)
]
}
}
]
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
(page, pageSize) => project.list(type.value, page, pageSize),
{
initialData: { total: 0, list: [] },
initialPageSize: 20,
total: (res: any) => res.total,
data: (res: any) => res.items
}
)
const handleToggleStatus = (row: any) => {
if (row.status === 'running') {
useRequest(systemctl.stop(row.name)).onSuccess(() => {
row.status = 'stopped'
window.$message.success($gettext('Stopped successfully'))
})
} else {
useRequest(systemctl.start(row.name)).onSuccess(() => {
row.status = 'running'
window.$message.success($gettext('Started successfully'))
})
}
}
const handleEdit = (row: any) => {
router.push({
name: 'project-edit',
params: { id: row.id }
})
}
const handleDelete = (id: number) => {
useRequest(project.delete(id)).onSuccess(() => {
refresh()
window.$message.success($gettext('Deleted successfully'))
})
}
const bulkDelete = async () => {
if (selectedRowKeys.value.length === 0) {
window.$message.info($gettext('Please select the projects to delete'))
return
}
const promises = selectedRowKeys.value.map((id: any) => project.delete(id))
await Promise.all(promises)
selectedRowKeys.value = []
refresh()
window.$message.success($gettext('Deleted successfully'))
}
onMounted(() => {
refresh()
window.$bus.on('project:refresh', refresh)
})
watch(type, () => {
refresh()
})
</script>
<template>
<n-flex vertical>
<n-flex>
<n-button type="primary" @click="createModal = true">
{{ $gettext('Create Project') }}
</n-button>
<n-popconfirm @positive-click="bulkDelete">
<template #trigger>
<n-button type="error">
{{ $gettext('Batch Delete') }}
</n-button>
</template>
{{ $gettext('Are you sure you want to delete the selected projects?') }}
</n-popconfirm>
</n-flex>
<n-data-table
striped
remote
:loading="loading"
:scroll-x="900"
:columns="columns"
:data="data"
:row-key="(row: any) => row.id"
v-model:checked-row-keys="selectedRowKeys"
v-model:page="page"
v-model:pageSize="pageSize"
:pagination="{
page: page,
pageCount: pageCount,
pageSize: pageSize,
itemCount: total,
showQuickJumper: true,
showSizePicker: true,
pageSizes: [20, 50, 100, 200]
}"
/>
</n-flex>
</template>