mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
feat: 项目管理阶段2
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 文件解析项目详情
|
||||
|
||||
@@ -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
|
||||
|
||||
15
web/src/api/panel/project/index.ts
Normal file
15
web/src/api/panel/project/index.ts
Normal 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}`)
|
||||
}
|
||||
8
web/src/views/project/CreateModal.vue
Normal file
8
web/src/views/project/CreateModal.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
211
web/src/views/project/ListView.vue
Normal file
211
web/src/views/project/ListView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user