mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 04:22:33 +08:00
feat: 应用支持分类筛选
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user