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

feat: app支持排序

This commit is contained in:
2026-01-12 22:17:18 +08:00
parent 1e5181c88e
commit bb28e5ef6d
7 changed files with 127 additions and 40 deletions

View File

@@ -32,4 +32,5 @@ type AppRepo interface {
UnInstall(slug string) error
Update(slug string) error
UpdateShow(slug string, show bool) error
UpdateOrder(slugs []string) error
}

View File

@@ -359,6 +359,15 @@ func (r *appRepo) UpdateShow(slug string, show bool) error {
return r.db.Save(item).Error
}
func (r *appRepo) UpdateOrder(slugs []string) error {
for i, slug := range slugs {
if err := r.db.Model(&biz.App{}).Where("slug = ?", slug).Update("show_order", i).Error; err != nil {
return err
}
}
return nil
}
func (r *appRepo) preCheck(app *api.App) error {
var apps []string
var installed []string

View File

@@ -17,3 +17,7 @@ type AppUpdateShow struct {
Slug string `json:"slug" form:"slug" validate:"required|exists:apps,slug"`
Show bool `json:"show" form:"show"`
}
type AppUpdateOrder struct {
Slugs []string `json:"slugs" form:"slugs" validate:"required"`
}

View File

@@ -288,6 +288,7 @@ func (route *Http) Register(r *chi.Mux) {
r.Post("/uninstall", route.app.Uninstall)
r.Post("/update", route.app.Update)
r.Post("/update_show", route.app.UpdateShow)
r.Post("/update_order", route.app.UpdateOrder)
r.Get("/is_installed", route.app.IsInstalled)
r.Get("/update_cache", route.app.UpdateCache)
})

View File

@@ -162,6 +162,21 @@ func (s *AppService) UpdateShow(w http.ResponseWriter, r *http.Request) {
Success(w, nil)
}
func (s *AppService) UpdateOrder(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.AppUpdateOrder](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.appRepo.UpdateOrder(req.Slugs); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
func (s *AppService) IsInstalled(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.AppSlugs](r)
if err != nil {

View File

@@ -15,6 +15,8 @@ export default {
update: (slug: string): any => http.Post('/app/update', { slug }),
// 设置首页显示
updateShow: (slug: string, show: boolean): any => http.Post('/app/update_show', { slug, show }),
// 更新首页显示排序
updateOrder: (slugs: string[]): any => http.Post('/app/update_order', { slugs }),
// 应用是否已安装
isInstalled: (slugs: string): any => http.Get('/app/is_installed', { params: { slugs } }),
// 更新缓存

View File

@@ -15,7 +15,9 @@ import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { NButton, NPopconfirm, useThemeVars } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import draggable from 'vuedraggable'
import app from '@/api/panel/app'
import home from '@/api/panel/home'
import TheIconLocal from '@/components/custom/TheIconLocal.vue'
import { router } from '@/router'
@@ -63,14 +65,14 @@ const { data: systemInfo } = useRequest(home.systemInfo, {
}
})
const { data: apps, loading: appLoading } = useRequest(home.apps, {
initialData: {
description: '',
icon: '',
name: '',
slug: '',
version: ''
}
initialData: []
})
const handleAppOrderChange = async () => {
const slugs = apps.value.map((item: { slug: string }) => item.slug)
await app.updateOrder(slugs)
window.$message.success($gettext('Order updated'))
}
const { data: countInfo } = useRequest(home.countInfo, {
initialData: {
website: 0,
@@ -715,41 +717,45 @@ if (import.meta.hot) {
<n-flex vertical>
<n-card :segmented="true" size="small" :title="$gettext('Quick Apps')" min-h-340>
<n-scrollbar max-h-270>
<n-grid
<draggable
v-if="!appLoading"
x-gap="12"
y-gap="12"
cols="4 s:1 m:2 l:3 xl:4 2xl:4"
item-responsive
responsive="screen"
p-10
v-model="apps"
item-key="slug"
handle=".drag-handle"
class="app-grid"
@end="handleAppOrderChange"
>
<n-gi v-for="item in apps" :key="item.name">
<n-card
:segmented="true"
size="small"
cursor-pointer
hover:card-shadow
@click="handleManageApp(item.slug)"
>
<n-flex>
<n-thing>
<template #avatar>
<div class="mt-8">
<the-icon-local type="app" :size="30" :icon="item.slug" />
</div>
</template>
<template #header>
{{ item.name }}
</template>
<template #description>
{{ item.version }}
</template>
</n-thing>
</n-flex>
</n-card>
</n-gi>
</n-grid>
<template #item="{ element: item }">
<div relative>
<n-card
:segmented="true"
size="small"
cursor-pointer
class="app-card"
@click="handleManageApp(item.slug)"
>
<div class="drag-handle" cursor-grab>
<the-icon icon="mdi:drag" :size="20" />
</div>
<n-flex>
<n-thing>
<template #avatar>
<div class="mt-8">
<the-icon-local type="app" :size="30" :icon="item.slug" />
</div>
</template>
<template #header>
{{ item.name }}
</template>
<template #description>
{{ item.version }}
</template>
</n-thing>
</n-flex>
</n-card>
</div>
</template>
</draggable>
</n-scrollbar>
<n-text v-if="!appLoading && !apps.length">
{{ $gettext('You have not set any apps to display here!') }}
@@ -890,3 +896,52 @@ if (import.meta.hot) {
</div>
</app-page>
</template>
<style scoped>
.app-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 10px;
}
@media (max-width: 1200px) {
.app-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.app-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.app-grid {
grid-template-columns: 1fr;
}
}
.app-card {
transition: box-shadow 0.3s ease;
}
.app-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.drag-handle {
position: absolute;
top: 8px;
right: 8px;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 10;
color: #999;
}
.app-card:hover .drag-handle {
opacity: 1;
}
</style>