perf: significantly improve app store homepage loading speed (#11013)

This commit is contained in:
CityFun
2025-11-20 18:57:49 +08:00
committed by GitHub
parent 7498f3f66b
commit d83b673c90
14 changed files with 96 additions and 39 deletions
+27
View File
@@ -6,6 +6,8 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
// @Tags App
@@ -192,3 +194,28 @@ func (b *BaseApi) GetAppListUpdate(c *gin.Context) {
}
helper.SuccessWithData(c, res)
}
// @Tags App
// @Summary Get app icon by app_id
// @Accept json
// @Param appId path integer true "app id"
// @Success 200 {file} file "app icon"
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /apps/icon/:appId [get]
func (b *BaseApi) GetAppIcon(c *gin.Context) {
appID, err := helper.GetIntParamByKey(c, "appID")
if err != nil {
helper.BadRequest(c, err)
return
}
iconBytes, err := appService.GetAppIcon(appID)
if err != nil {
helper.InternalServer(c, err)
return
}
c.Header("Content-Type", "image/png")
c.Header("Cache-Control", "public, max-age=31536000, immutable")
c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
c.Data(http.StatusOK, "image/png", iconBytes)
}
+1 -7
View File
@@ -33,16 +33,10 @@ type AppItem struct {
Key string `json:"key"`
ID uint `json:"id"`
Description string `json:"description"`
Icon string `json:"icon"`
Type string `json:"type"`
Status string `json:"status"`
Resource string `json:"resource"`
Installed bool `json:"installed"`
Versions []string `json:"versions"`
Limit int `json:"limit"`
Tags []TagDTO `json:"tags"`
Github string `json:"github"`
Website string `json:"website"`
Tags []string `json:"tags"`
GpuSupport bool `json:"gpuSupport"`
Recommend int `json:"recommend"`
}
+19 -9
View File
@@ -37,7 +37,7 @@ type AppService struct {
}
type IAppService interface {
PageApp(ctx *gin.Context, req request.AppSearch) (interface{}, error)
PageApp(ctx *gin.Context, req request.AppSearch) (*response.AppRes, error)
GetAppTags(ctx *gin.Context) ([]response.TagDTO, error)
GetApp(ctx *gin.Context, key string) (*response.AppDTO, error)
GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error)
@@ -46,13 +46,14 @@ type IAppService interface {
GetAppUpdate() (*response.AppUpdateRes, error)
GetAppDetailByID(id uint) (*response.AppDetailDTO, error)
SyncAppListFromLocal(taskID string)
GetAppIcon(appID uint) ([]byte, error)
}
func NewIAppService() IAppService {
return &AppService{}
}
func (a AppService) PageApp(ctx *gin.Context, req request.AppSearch) (interface{}, error) {
func (a AppService) PageApp(ctx *gin.Context, req request.AppSearch) (*response.AppRes, error) {
var opts []repo.DBOption
opts = append(opts, appRepo.OrderByRecommend())
if req.Name != "" {
@@ -98,7 +99,7 @@ func (a AppService) PageApp(ctx *gin.Context, req request.AppSearch) (interface{
}
opts = append(opts, repo.WithByIDs(appIds))
}
var res response.AppRes
res := &response.AppRes{}
total, apps, err := appRepo.Page(req.Page, req.PageSize, opts...)
if err != nil {
@@ -120,12 +121,7 @@ func (a AppService) PageApp(ctx *gin.Context, req request.AppSearch) (interface{
ID: ap.ID,
Name: ap.Name,
Key: ap.Key,
Type: ap.Type,
Icon: ap.Icon,
Resource: ap.Resource,
Limit: ap.Limit,
Website: ap.Website,
Github: ap.Github,
GpuSupport: ap.GpuSupport,
Recommend: ap.Recommend,
Description: ap.GetDescription(ctx),
@@ -135,7 +131,9 @@ func (a AppService) PageApp(ctx *gin.Context, req request.AppSearch) (interface{
if err != nil {
continue
}
appDTO.Tags = tags
for _, tag := range tags {
appDTO.Tags = append(appDTO.Tags, tag.Name)
}
if ap.Type == constant.RuntimePHP || ap.Type == constant.RuntimeGo || ap.Type == constant.RuntimeNode || ap.Type == constant.RuntimePython || ap.Type == constant.RuntimeJava || ap.Type == constant.RuntimeDotNet {
details, _ := appDetailRepo.GetBy(appDetailRepo.WithAppId(ap.ID))
var ids []uint
@@ -1161,3 +1159,15 @@ func (a AppService) SyncAppListFromRemote(taskID string) (err error) {
return nil
}
func (a AppService) GetAppIcon(appID uint) ([]byte, error) {
app, err := appRepo.GetFirst(repo.WithByID(appID))
if err != nil {
return nil, err
}
iconBytes, err := base64.StdEncoding.DecodeString(app.Icon)
if err != nil {
return nil, err
}
return iconBytes, nil
}
-1
View File
@@ -1555,7 +1555,6 @@ func handleInstalled(appInstallList []model.AppInstall, updated bool, sync bool)
Message: installed.Message,
HttpPort: installed.HttpPort,
HttpsPort: installed.HttpsPort,
Icon: installed.App.Icon,
AppName: installed.App.Name,
AppKey: installed.App.Key,
AppType: installed.App.Type,
+1
View File
@@ -22,6 +22,7 @@ func (a *AppRouter) InitRouter(Router *gin.RouterGroup) {
appRouter.GET("/details/:id", baseApi.GetAppDetailByID)
appRouter.POST("/install", baseApi.InstallApp)
appRouter.GET("/tags", baseApi.GetAppTags)
appRouter.GET("/icon/:appID", baseApi.GetAppIcon)
appRouter.POST("/installed/check", baseApi.CheckAppInstalled)
appRouter.POST("/installed/loadport", baseApi.LoadPort)
+17 -4
View File
@@ -3,7 +3,7 @@ import { ReqPage, CommonModel } from '.';
export namespace App {
export interface App extends CommonModel {
name: string;
icon: string;
icon?: string;
key: string;
tags: Tag[];
shortDescZh: string;
@@ -14,8 +14,8 @@ export namespace App {
type: string;
status: string;
limit: number;
website: string;
github: string;
website?: string;
github?: string;
readme: string;
}
@@ -44,9 +44,22 @@ export namespace App {
sort: number;
}
export interface AppItem {
name: string;
key: string;
id: number;
description: string;
status: string;
installed: boolean;
limit: number;
tags: string[];
gpuSupport: boolean;
recommend: number;
}
export interface AppResPage {
total: number;
items: App.AppDTO[];
items: AppItem[];
}
export interface AppUpdateRes {
+6
View File
@@ -128,3 +128,9 @@ export const syncCutomAppStore = (req: App.AppStoreSync) => {
export const getCurrentNodeCustomAppConfig = () => {
return http.get<App.CustomAppStoreConfig>(`/custom/app/config`);
};
export function getAppIconUrl(appId: number, node?: string): string {
const baseURL = import.meta.env.VITE_API_URL as string;
const params = node ? `?operateNode=${node}` : '';
return `${baseURL}/apps/icon/${appId}${params}`;
}
@@ -3,7 +3,7 @@
<el-card>
<div class="app-wrapper" @click="openDetail(app.key)">
<div class="app-image">
<el-avatar shape="square" :size="60" :src="'data:image/png;base64,' + app.icon" />
<el-avatar shape="square" :size="60" :src="getAppIconUrl(app.id, currentNode)" />
</div>
<div class="app-content">
<div class="content-top">
@@ -23,7 +23,7 @@
<div class="app-tags">
<el-tag v-for="(tag, ind) in app.tags" :key="ind" type="info">
<span>
{{ tag.name }}
{{ tag }}
</span>
</el-tag>
<el-tag v-if="app.status === 'TakeDown'" class="p-mr-5">
@@ -48,6 +48,10 @@
</template>
<script lang="ts" setup>
import { getAppIconUrl } from '@/api/modules/app';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { currentNode } = useGlobalStore();
defineProps({
app: {
type: Object,
+1 -1
View File
@@ -107,7 +107,7 @@ const req = reactive({
showCurrentArch: false,
});
const apps = ref<App.AppDTO[]>([]);
const apps = ref<App.AppItem[]>([]);
const loading = ref(false);
const canUpdate = ref(false);
const syncing = ref(false);
@@ -3,7 +3,12 @@
<div class="brief" v-loading="loadingApp">
<div class="detail flex">
<div class="w-12 h-12 rounded p-1 shadow-md icon">
<img :src="app.icon" alt="App Icon" class="w-full h-full rounded" style="object-fit: contain" />
<img
:src="getAppIconUrl(app.id, currentNode)"
alt="App Icon"
class="w-full h-full rounded"
style="object-fit: contain"
/>
</div>
<div class="ml-4">
<div class="name mb-2">
@@ -66,17 +71,15 @@
</template>
<script lang="ts" setup>
import { getAppByKey, getAppDetail } from '@/api/modules/app';
import { getAppByKey, getAppDetail, getAppIconUrl } from '@/api/modules/app';
import MdEditor from 'md-editor-v3';
import { ref } from 'vue';
import Install from './install/index.vue';
import { GlobalStore } from '@/store';
import { computeSizeFromMB } from '@/utils/util';
import { storeToRefs } from 'pinia';
import { jumpToInstall } from '@/utils/app';
const globalStore = GlobalStore();
const { isDarkTheme } = storeToRefs(globalStore);
import { useGlobalStore } from '@/composables/useGlobalStore';
const { currentNode, isDarkTheme } = useGlobalStore();
const app = ref<any>({});
const appDetail = ref<any>({});
@@ -58,7 +58,7 @@
@click="openDetail(installed.appKey)"
shape="square"
:size="66"
:src="'data:image/png;base64,' + installed.icon"
:src="getAppIconUrl(installed.appID, currentNode)"
/>
</div>
</el-col>
@@ -417,7 +417,7 @@
</template>
<script lang="ts" setup>
import { searchAppInstalled, installedOp, appInstalledDeleteCheck } from '@/api/modules/app';
import { searchAppInstalled, installedOp, appInstalledDeleteCheck, getAppIconUrl } from '@/api/modules/app';
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { ElMessageBox } from 'element-plus';
@@ -444,8 +444,8 @@ import Tags from '@/views/app-store/components/tag.vue';
import SvgIcon from '@/components/svg-icon/svg-icon.vue';
import MainDiv from '@/components/main-div/index.vue';
import { routerToFileWithPath, routerToNameWithQuery } from '@/utils/router';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
import { useGlobalStore } from '@/composables/useGlobalStore';
const { currentNode, isMaster, currentNodeAddr } = useGlobalStore();
const data = ref<any>();
const loading = ref(false);
@@ -762,8 +762,8 @@ const getConfig = async () => {
defaultLink.value = res.data;
return;
}
if (!globalStore.isMaster || globalStore.currentNodeAddr != '127.0.0.1') {
defaultLink.value = globalStore.currentNodeAddr;
if (!isMaster.value || currentNodeAddr.value != '127.0.0.1') {
defaultLink.value = currentNodeAddr.value;
}
} catch (error) {}
};
@@ -51,7 +51,7 @@ const props = defineProps({
required: true,
},
});
const apps = ref<App.App[]>([]);
const apps = ref<App.AppItem[]>([]);
const appVersions = ref<string[]>([]);
const emit = defineEmits(['update:modelValue']);
const runtime = useVModel(props, 'modelValue', emit);
@@ -222,7 +222,7 @@ interface OperateRrops {
}
const open = ref(false);
const apps = ref<App.App[]>([]);
const apps = ref<App.AppItem[]>([]);
const runtimeForm = ref<FormInstance>();
const loading = ref(false);
const initParam = ref(false);
@@ -548,7 +548,7 @@ const appReq = reactive({
page: 1,
pageSize: 100,
});
const apps = ref<App.App[]>([]);
const apps = ref<App.AppItem[]>([]);
const preCheckRef = ref();
const staticPath = ref('');
const runtimeResource = ref('appstore');