From 0b1d2a570b75382e0482b2e55af3449dedfb1e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sun, 4 Jan 2026 17:53:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BA=94=E7=94=A8=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/app.go | 2 ++ internal/biz/cache.go | 6 +++-- internal/data/app.go | 27 ++++++++++++++++++++ internal/data/cache.go | 14 +++++++++++ internal/job/panel_task.go | 10 ++++++++ internal/route/http.go | 1 + internal/service/app.go | 16 ++++++++++++ internal/service/cli.go | 3 +++ pkg/types/app.go | 9 ++++--- web/src/api/panel/app/index.ts | 5 +++- web/src/views/app/AllView.vue | 45 ++++++++++++++++++++++++++++------ 11 files changed, 123 insertions(+), 15 deletions(-) diff --git a/internal/biz/app.go b/internal/biz/app.go index 5cf15ff3..b8382d1f 100644 --- a/internal/biz/app.go +++ b/internal/biz/app.go @@ -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 diff --git a/internal/biz/cache.go b/internal/biz/cache.go index 9bcabc9d..415fae90 100644 --- a/internal/biz/cache.go +++ b/internal/biz/cache.go @@ -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 } diff --git a/internal/data/app.go b/internal/data/app.go index 4e182243..45f26617 100644 --- a/internal/data/app.go +++ b/internal/data/app.go @@ -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 { diff --git a/internal/data/cache.go b/internal/data/cache.go index cb06ad38..a17bb272 100644 --- a/internal/data/cache.go +++ b/internal/data/cache.go @@ -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 { diff --git a/internal/job/panel_task.go b/internal/job/panel_task.go index a0243959..0b001b6e 100644 --- a/internal/job/panel_task.go +++ b/internal/job/panel_task.go @@ -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() { diff --git a/internal/route/http.go b/internal/route/http.go index fb180326..175a449c 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -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) diff --git a/internal/service/app.go b/internal/service/app.go index f0105a69..913bd1cb 100644 --- a/internal/service/app.go +++ b/internal/service/app.go @@ -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 diff --git a/internal/service/cli.go b/internal/service/cli.go index 6f496f57..0642f2f2 100644 --- a/internal/service/cli.go +++ b/internal/service/cli.go @@ -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)) } diff --git a/pkg/types/app.go b/pkg/types/app.go index 9a2deabc..21d9b6df 100644 --- a/pkg/types/app.go +++ b/pkg/types/app.go @@ -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"` diff --git a/web/src/api/panel/app/index.ts b/web/src/api/panel/app/index.ts index 416b925e..979a5943 100644 --- a/web/src/api/panel/app/index.ts +++ b/web/src/api/panel/app/index.ts @@ -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 }), diff --git a/web/src/views/app/AllView.vue b/web/src/views/app/AllView.vue index 06c0c9a7..5c116671 100644 --- a/web/src/views/app/AllView.vue +++ b/web/src/views/app/AllView.vue @@ -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({}) +// 当前选中的分类 +const selectedCategory = ref('') + 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(() => {