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

feat: 应用支持分类筛选

This commit is contained in:
2026-01-04 17:53:45 +08:00
parent e6fcd48e32
commit 0b1d2a570b
11 changed files with 123 additions and 15 deletions

View File

@@ -4,6 +4,7 @@ import (
"time"
"github.com/acepanel/panel/pkg/api"
"github.com/acepanel/panel/pkg/types"
)
type App struct {
@@ -18,6 +19,7 @@ type App struct {
}
type AppRepo interface {
Categories() []types.LV
All() api.Apps
Get(slug string) (*api.App, error)
UpdateExist(slug string) bool

View File

@@ -5,8 +5,9 @@ import "time"
type CacheKey string
const (
CacheKeyApps CacheKey = "apps"
CacheKeyRewrites CacheKey = "rewrites"
CacheKeyCategories CacheKey = "categories"
CacheKeyApps CacheKey = "apps"
CacheKeyRewrites CacheKey = "rewrites"
)
type Cache struct {
@@ -19,6 +20,7 @@ type Cache struct {
type CacheRepo interface {
Get(key CacheKey, defaultValue ...string) (string, error)
Set(key CacheKey, value string) error
UpdateCategories() error
UpdateApps() error
UpdateRewrites() error
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/acepanel/panel/pkg/api"
"github.com/acepanel/panel/pkg/config"
"github.com/acepanel/panel/pkg/shell"
"github.com/acepanel/panel/pkg/types"
)
type appRepo struct {
@@ -42,6 +43,32 @@ func NewAppRepo(t *gotext.Locale, conf *config.Config, db *gorm.DB, log *slog.Lo
}
}
func (r *appRepo) Categories() []types.LV {
cached, err := r.cache.Get(biz.CacheKeyCategories)
if err != nil {
return nil
}
var categories api.Categories
if err = json.Unmarshal([]byte(cached), &categories); err != nil {
return nil
}
slices.SortFunc(categories, func(a, b *api.Category) int {
return a.Order - b.Order
})
result := make([]types.LV, 0)
for item := range slices.Values(categories) {
result = append(result, types.LV{
Label: item.Name,
Value: item.Slug,
})
}
return result
}
func (r *appRepo) All() api.Apps {
cached, err := r.cache.Get(biz.CacheKeyApps)
if err != nil {

View File

@@ -50,6 +50,20 @@ func (r *cacheRepo) Set(key biz.CacheKey, value string) error {
return r.db.Save(cache).Error
}
func (r *cacheRepo) UpdateCategories() error {
categories, err := r.api.Categories()
if err != nil {
return err
}
encoded, err := json.Marshal(categories)
if err != nil {
return err
}
return r.Set(biz.CacheKeyCategories, string(encoded))
}
func (r *cacheRepo) UpdateApps() error {
remote, err := r.api.Apps()
if err != nil {

View File

@@ -73,6 +73,7 @@ func (r *PanelTask) Run() {
// 非离线模式下任务
if offline, err := r.settingRepo.GetBool(biz.SettingKeyOfflineMode); err == nil && !offline {
r.updateCategories()
r.updateApps()
r.updateRewrites()
if autoUpdate, err := r.settingRepo.GetBool(biz.SettingKeyAutoUpdate); err == nil && autoUpdate {
@@ -87,6 +88,15 @@ func (r *PanelTask) Run() {
app.Status = app.StatusNormal
}
// 更新分类缓存
func (r *PanelTask) updateCategories() {
time.AfterFunc(time.Duration(rand.IntN(300))*time.Second, func() {
if err := r.cacheRepo.UpdateCategories(); err != nil {
r.log.Warn("[PanelTask] failed to update categories cache", slog.Any("err", err))
}
})
}
// 更新商店缓存
func (r *PanelTask) updateApps() {
time.AfterFunc(time.Duration(rand.IntN(300))*time.Second, func() {

View File

@@ -252,6 +252,7 @@ func (route *Http) Register(r *chi.Mux) {
})
r.Route("/app", func(r chi.Router) {
r.Get("/categories", route.app.Categories)
r.Get("/list", route.app.List)
r.Post("/install", route.app.Install)
r.Post("/uninstall", route.app.Uninstall)

View File

@@ -28,7 +28,15 @@ func NewAppService(t *gotext.Locale, app biz.AppRepo, cache biz.CacheRepo, setti
}
}
func (s *AppService) Categories(w http.ResponseWriter, r *http.Request) {
categories := s.appRepo.Categories()
Success(w, categories)
}
func (s *AppService) List(w http.ResponseWriter, r *http.Request) {
category := r.URL.Query().Get("category")
all := s.appRepo.All()
installedApps, err := s.appRepo.Installed()
if err != nil {
@@ -51,11 +59,15 @@ func (s *AppService) List(w http.ResponseWriter, r *http.Request) {
updateExist = s.appRepo.UpdateExist(item.Slug)
show = installedAppMap[item.Slug].Show
}
if category != "" && !strings.Contains(strings.Join(item.Categories, ","), category) {
continue
}
app := types.AppCenter{
Icon: item.Icon,
Name: item.Name,
Description: item.Description,
Categories: item.Categories,
Slug: item.Slug,
Installed: installed,
InstalledChannel: installedChannel,
@@ -181,6 +193,10 @@ func (s *AppService) UpdateCache(w http.ResponseWriter, r *http.Request) {
return
}
if err := s.cacheRepo.UpdateCategories(); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err := s.cacheRepo.UpdateApps(); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return

View File

@@ -110,6 +110,9 @@ func (s *CliService) Update(ctx context.Context, cmd *cli.Command) error {
}
func (s *CliService) Sync(ctx context.Context, cmd *cli.Command) error {
if err := s.cacheRepo.UpdateCategories(); err != nil {
return errors.New(s.t.Get("Failed to synchronize categories data: %v", err))
}
if err := s.cacheRepo.UpdateApps(); err != nil {
return errors.New(s.t.Get("Failed to synchronize app data: %v", err))
}

View File

@@ -9,10 +9,11 @@ type App interface {
// AppCenter 应用中心结构
type AppCenter struct {
Icon string `json:"icon"`
Name string `json:"name"`
Description string `json:"description"`
Slug string `json:"slug"`
Icon string `json:"icon"`
Name string `json:"name"`
Description string `json:"description"`
Categories []string `json:"categories"`
Slug string `json:"slug"`
Channels []struct {
Slug string `json:"slug"`
Name string `json:"name"`

View File

@@ -1,8 +1,11 @@
import { http } from '@/utils'
export default {
// 获取分类列表
categories: (): any => http.Get('/app/categories'),
// 获取应用列表
list: (page: number, limit: number): any => http.Get('/app/list', { params: { page, limit } }),
list: (page: number, limit: number, category?: string): any =>
http.Get('/app/list', { params: { page, limit, category } }),
// 安装应用
install: (slug: string, channel: string | null): any =>
http.Post('/app/install', { slug, channel }),

View File

@@ -3,7 +3,7 @@ defineOptions({
name: 'app-index'
})
import { NButton, NDataTable, NFlex, NPopconfirm, NSwitch } from 'naive-ui'
import { NButton, NDataTable, NFlex, NPopconfirm, NSwitch, NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import app from '@/api/panel/app'
@@ -17,6 +17,9 @@ const versionModalShow = ref(false)
const versionModalOperation = ref($gettext('Install'))
const versionModalInfo = ref<any>({})
// 当前选中的分类
const selectedCategory = ref<string>('')
const columns: any = [
{
key: 'icon',
@@ -155,16 +158,27 @@ const columns: any = [
}
]
const { data: categories } = useRequest(app.categories, {
initialData: []
})
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
(page, pageSize) => app.list(page, pageSize),
(page, pageSize) => app.list(page, pageSize, selectedCategory.value || undefined),
{
initialData: { total: 0, list: [] },
initialPageSize: 20,
total: (res: any) => res.total,
data: (res: any) => res.items
data: (res: any) => res.items,
watchingStates: [selectedCategory]
}
)
// 处理分类切换
const handleCategoryChange = (category: string) => {
selectedCategory.value = category
page.value = 1
}
const handleShowChange = (row: any) => {
useRequest(app.updateShow(row.slug, !row.show)).onSuccess(() => {
row.show = !row.show
@@ -199,11 +213,26 @@ onMounted(() => {
<template>
<n-flex vertical>
<n-alert type="warning">{{
$gettext(
'Before updating apps, it is strongly recommended to backup/snapshot first, so you can roll back immediately if there are any issues!'
)
}}</n-alert>
<n-flex>
<n-tag
:type="selectedCategory === '' ? 'primary' : 'default'"
:bordered="selectedCategory !== ''"
style="cursor: pointer"
@click="handleCategoryChange('')"
>
{{ $gettext('All') }}
</n-tag>
<n-tag
v-for="cat in categories"
:key="cat.value"
:type="selectedCategory === cat.value ? 'primary' : 'default'"
:bordered="selectedCategory !== cat.value"
style="cursor: pointer"
@click="handleCategoryChange(cat.value)"
>
{{ cat.label }}
</n-tag>
</n-flex>
<n-data-table
striped
remote