mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-12-05 10:28:59 +08:00
feat: 应用详情增加架构信息 (#6204)
Refs https://github.com/1Panel-dev/1Panel/issues/3026
This commit is contained in:
parent
a44c4d96d8
commit
12b7f806f9
@ -94,6 +94,8 @@ type AppProperty struct {
|
||||
Website string `json:"website"`
|
||||
Github string `json:"github"`
|
||||
Document string `json:"document"`
|
||||
Architectures []string `json:"architectures"`
|
||||
MemoryLimit int `json:"memoryLimit"`
|
||||
}
|
||||
|
||||
type AppConfigVersion struct {
|
||||
|
@ -8,11 +8,12 @@ import (
|
||||
|
||||
type AppSearch struct {
|
||||
dto.PageInfo
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
Type string `json:"type"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Resource string `json:"resource"`
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
Type string `json:"type"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Resource string `json:"resource"`
|
||||
ShowCurrentArch bool `json:"showCurrentArch"`
|
||||
}
|
||||
|
||||
type AppInstallCreate struct {
|
||||
|
@ -42,6 +42,8 @@ type AppDto struct {
|
||||
Versions []string `json:"versions"`
|
||||
Limit int `json:"limit"`
|
||||
Tags []model.Tag `json:"tags"`
|
||||
Github string `json:"github"`
|
||||
Website string `json:"website"`
|
||||
}
|
||||
|
||||
type TagDTO struct {
|
||||
@ -65,10 +67,12 @@ type AppInstalledCheck struct {
|
||||
|
||||
type AppDetailDTO struct {
|
||||
model.AppDetail
|
||||
Enable bool `json:"enable"`
|
||||
Params interface{} `json:"params"`
|
||||
Image string `json:"image"`
|
||||
HostMode bool `json:"hostMode"`
|
||||
Enable bool `json:"enable"`
|
||||
Params interface{} `json:"params"`
|
||||
Image string `json:"image"`
|
||||
HostMode bool `json:"hostMode"`
|
||||
Architectures string `json:"architectures"`
|
||||
MemoryLimit int `json:"memoryLimit"`
|
||||
}
|
||||
|
||||
type IgnoredApp struct {
|
||||
|
@ -26,6 +26,8 @@ type App struct {
|
||||
Resource string `json:"resource" gorm:"not null;default:remote"`
|
||||
ReadMe string `json:"readMe"`
|
||||
LastModified int `json:"lastModified"`
|
||||
Architectures string `json:"architectures"`
|
||||
MemoryLimit int `json:"memoryLimit"`
|
||||
|
||||
Details []AppDetail `json:"-" gorm:"-:migration"`
|
||||
TagsKey []string `json:"tags" yaml:"tags" gorm:"-"`
|
||||
|
@ -2,6 +2,7 @@ package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"gorm.io/gorm"
|
||||
@ -26,6 +27,7 @@ type IAppRepo interface {
|
||||
Create(ctx context.Context, app *model.App) error
|
||||
Save(ctx context.Context, app *model.App) error
|
||||
BatchDelete(ctx context.Context, apps []model.App) error
|
||||
WithArch(arch string) DBOption
|
||||
}
|
||||
|
||||
func NewIAppRepo() IAppRepo {
|
||||
@ -71,6 +73,12 @@ func (a AppRepo) WithResource(resource string) DBOption {
|
||||
}
|
||||
}
|
||||
|
||||
func (a AppRepo) WithArch(arch string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("architectures like ?", fmt.Sprintf("%%%s%%", arch))
|
||||
}
|
||||
}
|
||||
|
||||
func (a AppRepo) Page(page, size int, opts ...DBOption) (int64, []model.App, error) {
|
||||
var apps []model.App
|
||||
db := getDb(opts...).Model(&model.App{})
|
||||
|
@ -66,6 +66,13 @@ func (a AppService) PageApp(req request.AppSearch) (interface{}, error) {
|
||||
if req.Resource != "" && req.Resource != "all" {
|
||||
opts = append(opts, appRepo.WithResource(req.Resource))
|
||||
}
|
||||
if req.ShowCurrentArch {
|
||||
info, err := NewIDashboardService().LoadOsInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts = append(opts, appRepo.WithArch(info.KernelArch))
|
||||
}
|
||||
if len(req.Tags) != 0 {
|
||||
tags, err := tagRepo.GetByKeys(req.Tags)
|
||||
if err != nil {
|
||||
@ -102,6 +109,8 @@ func (a AppService) PageApp(req request.AppSearch) (interface{}, error) {
|
||||
ShortDescEn: ap.ShortDescEn,
|
||||
Resource: ap.Resource,
|
||||
Limit: ap.Limit,
|
||||
Website: ap.Website,
|
||||
Github: ap.Github,
|
||||
}
|
||||
appDTOs = append(appDTOs, appDTO)
|
||||
appTags, err := appTagRepo.GetByAppId(ap.ID)
|
||||
@ -253,6 +262,8 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
||||
if err := checkLimit(app); err != nil {
|
||||
appDetailDTO.Enable = false
|
||||
}
|
||||
appDetailDTO.Architectures = app.Architectures
|
||||
appDetailDTO.MemoryLimit = app.MemoryLimit
|
||||
return appDetailDTO, nil
|
||||
}
|
||||
func (a AppService) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) {
|
||||
|
@ -1118,6 +1118,8 @@ func getApps(oldApps []model.App, items []dto.AppDefine) map[string]model.App {
|
||||
app.Status = constant.AppNormal
|
||||
app.LastModified = item.LastModified
|
||||
app.ReadMe = item.ReadMe
|
||||
app.MemoryLimit = config.MemoryLimit
|
||||
app.Architectures = strings.Join(config.Architectures, ",")
|
||||
apps[key] = app
|
||||
}
|
||||
return apps
|
||||
|
@ -18,6 +18,7 @@ func Init() {
|
||||
migrations.AddTask,
|
||||
migrations.UpdateWebsite,
|
||||
migrations.UpdateWebsiteDomain,
|
||||
migrations.UpdateApp,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
@ -234,3 +234,11 @@ var UpdateWebsiteDomain = &gormigrate.Migration{
|
||||
&model.WebsiteDomain{})
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateApp = &gormigrate.Migration{
|
||||
ID: "20240822-update-app",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&model.App{})
|
||||
},
|
||||
}
|
||||
|
@ -15,11 +15,13 @@ export namespace App {
|
||||
limit: number;
|
||||
website: string;
|
||||
github: string;
|
||||
readme: string;
|
||||
}
|
||||
|
||||
export interface AppDTO extends App {
|
||||
versions: string[];
|
||||
installed: boolean;
|
||||
architectures: string;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
@ -47,6 +49,8 @@ export namespace App {
|
||||
dockerCompose: string;
|
||||
image: string;
|
||||
hostMode?: boolean;
|
||||
memoryLimit: number;
|
||||
architectures: string;
|
||||
}
|
||||
|
||||
export interface AppReq extends ReqPage {
|
||||
|
@ -1800,7 +1800,7 @@ const message = {
|
||||
alreadyRun: 'Age',
|
||||
syncAppList: 'Sync',
|
||||
less1Minute: 'Less than 1 minute',
|
||||
appOfficeWebsite: 'Office website',
|
||||
appOfficeWebsite: 'Website',
|
||||
github: 'Github',
|
||||
document: 'Document',
|
||||
updatePrompt: 'The current application is the latest version',
|
||||
@ -1845,7 +1845,7 @@ const message = {
|
||||
syncAllAppHelper: 'All applications are about to be synchronized. Do you want to continue? ',
|
||||
hostModeHelper:
|
||||
'The current application network mode is host mode. If you need to open the port, please open it manually on the firewall page.',
|
||||
showLocal: 'Show Local Application',
|
||||
showLocal: 'Show Local App',
|
||||
reload: 'Reload',
|
||||
upgradeWarn:
|
||||
'Upgrading the application will replace the docker-compose.yml file. If there are any changes, you can click to view the file comparison',
|
||||
@ -1862,6 +1862,10 @@ const message = {
|
||||
'The left side is the old version, the right side is the new version. After editing, click to save the custom version',
|
||||
deleteImage: 'Delete Image',
|
||||
deleteImageHelper: 'Delete the image related to the application. The task will not stop if deletion fails',
|
||||
requireMemory: 'Memory',
|
||||
supportedArchitectures: 'Architectures',
|
||||
link: 'Link',
|
||||
showCurrentArch: 'Show current architecture App',
|
||||
},
|
||||
website: {
|
||||
website: 'Website',
|
||||
@ -2117,13 +2121,13 @@ const message = {
|
||||
'Only supports importing local backups, importing backups from other machines may cause recovery failure',
|
||||
ipWebsiteWarn: 'Websites with IP as domain names need to be set as default sites to be accessed normally',
|
||||
hstsHelper: 'Enabling HSTS can increase website security',
|
||||
defaultHtml: 'Default page',
|
||||
defaultHtml: 'Default Page',
|
||||
website404: 'Website 404 error page',
|
||||
domain404: 'Website page does not exist',
|
||||
indexHtml: 'Static website default page',
|
||||
stopHtml: 'Website stop page',
|
||||
indexPHP: 'PHP website default page',
|
||||
sslExpireDate: 'Certificate Expiry Date',
|
||||
sslExpireDate: 'SSL Expiry Date',
|
||||
website404Helper: 'Website 404 error page only supports PHP runtime environment websites and static websites',
|
||||
sni: 'Origin SNI',
|
||||
sniHelper:
|
||||
|
@ -1728,6 +1728,10 @@ const message = {
|
||||
diffHelper: '左側為舊版本,右側為新版,編輯之後點擊使用自訂版本保存',
|
||||
deleteImage: '刪除鏡像',
|
||||
deleteImageHelper: '刪除應用相關鏡像,刪除失敗任務不會終止',
|
||||
requireMemory: '最低內存',
|
||||
supportedArchitectures: '支持架構',
|
||||
link: '鏈接',
|
||||
showCurrentArch: '僅顯示當前架構',
|
||||
},
|
||||
website: {
|
||||
website: '網站',
|
||||
|
@ -1729,6 +1729,10 @@ const message = {
|
||||
diffHelper: '左侧为旧版本,右侧为新版,编辑之后点击使用自定义版本保存',
|
||||
deleteImage: '删除镜像',
|
||||
deleteImageHelper: '删除应用相关镜像,删除失败任务不会终止',
|
||||
requireMemory: '内存需求',
|
||||
supportedArchitectures: '支持架构',
|
||||
link: '链接',
|
||||
showCurrentArch: '仅显示当前服务器架构应用',
|
||||
},
|
||||
website: {
|
||||
website: '网站',
|
||||
|
@ -60,6 +60,9 @@
|
||||
</el-badge>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<el-checkbox class="!mr-2.5" v-model="req.showCurrentArch" @change="search(req)">
|
||||
{{ $t('app.showCurrentArch') }}
|
||||
</el-checkbox>
|
||||
<el-checkbox
|
||||
class="!mr-2.5"
|
||||
v-model="req.resource"
|
||||
@ -90,28 +93,12 @@
|
||||
<el-card>
|
||||
<div class="app-wrapper">
|
||||
<div class="app-image">
|
||||
<el-popover placement="top-start" :width="200" trigger="hover">
|
||||
<template #reference>
|
||||
<el-avatar
|
||||
shape="square"
|
||||
:size="60"
|
||||
:src="'data:image/png;base64,' + app.icon"
|
||||
/>
|
||||
</template>
|
||||
<div>
|
||||
<el-link @click="toLink(app.website)">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
<span>{{ $t('app.appOfficeWebsite') }}</span>
|
||||
</el-link>
|
||||
<el-link class="ml-5" @click="toLink(app.github)">
|
||||
<el-icon><Link /></el-icon>
|
||||
<span>{{ $t('app.github') }}</span>
|
||||
</el-link>
|
||||
<div class="mt-5">
|
||||
<el-text>推荐配置: 1c 1g</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
<el-avatar
|
||||
@click="openDetail(app.key)"
|
||||
shape="square"
|
||||
:size="60"
|
||||
:src="'data:image/png;base64,' + app.icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="app-content">
|
||||
<div class="content-top">
|
||||
@ -180,6 +167,7 @@
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<Install ref="installRef" />
|
||||
<Detail ref="detailRef" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -192,6 +180,7 @@ import router from '@/routers';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { GlobalStore } from '@/store';
|
||||
import { getLanguage } from '@/utils/util';
|
||||
import Detail from '../detail/index.vue';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
@ -214,6 +203,7 @@ const req = reactive({
|
||||
page: 1,
|
||||
pageSize: 60,
|
||||
resource: 'all',
|
||||
showCurrentArch: false,
|
||||
});
|
||||
|
||||
const apps = ref<App.AppDTO[]>([]);
|
||||
@ -227,6 +217,7 @@ const installRef = ref();
|
||||
const installKey = ref('');
|
||||
const moreTag = ref('');
|
||||
const mainHeight = ref(0);
|
||||
const detailRef = ref();
|
||||
|
||||
const search = async (req: App.AppReq) => {
|
||||
loading.value = true;
|
||||
@ -268,6 +259,10 @@ const openInstall = (app: App.App) => {
|
||||
}
|
||||
};
|
||||
|
||||
const openDetail = (key: string) => {
|
||||
detailRef.value.acceptParams(key, 'install');
|
||||
};
|
||||
|
||||
const sync = () => {
|
||||
syncing.value = true;
|
||||
SyncApp()
|
||||
@ -311,10 +306,6 @@ const searchByName = () => {
|
||||
search(req);
|
||||
};
|
||||
|
||||
const toLink = (link: string) => {
|
||||
window.open(link, '_blank');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (router.currentRoute.value.query.install) {
|
||||
installKey.value = String(router.currentRoute.value.query.install);
|
||||
|
202
frontend/src/views/app-store/detail/index.vue
Normal file
202
frontend/src/views/app-store/detail/index.vue
Normal file
@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="open"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
size="50%"
|
||||
>
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('app.detail')" :back="handleClose" />
|
||||
</template>
|
||||
<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" />
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="name mb-2">
|
||||
<span>{{ app.name }}</span>
|
||||
</div>
|
||||
<div class="description mb-4">
|
||||
<span>
|
||||
{{ language == 'zh' || language == 'tw' ? app.shortDescZh : app.shortDescEn }}
|
||||
</span>
|
||||
</div>
|
||||
<br />
|
||||
<div v-if="!loadingDetail" class="mb-2">
|
||||
<el-alert
|
||||
v-if="!appDetail.enable"
|
||||
:title="$t('app.limitHelper')"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
round
|
||||
v-if="appDetail.enable && operate === 'install'"
|
||||
@click="openInstall"
|
||||
type="primary"
|
||||
class="brief-button"
|
||||
>
|
||||
{{ $t('app.install') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="descriptions">
|
||||
<el-descriptions border size="large" direction="vertical">
|
||||
<el-descriptions-item :label="$t('app.appOfficeWebsite')">
|
||||
<el-link @click="toLink(app.website)">
|
||||
{{ $t('app.link') }}
|
||||
<el-icon class="ml-1.5"><Promotion /></el-icon>
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('app.github')">
|
||||
<el-link @click="toLink(app.github)">
|
||||
{{ $t('app.link') }}
|
||||
<el-icon class="ml-1.5"><Promotion /></el-icon>
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('app.requireMemory')" v-if="appDetail.memoryLimit > 0">
|
||||
<span>{{ appDetail.memoryLimit }} MB</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('app.supportedArchitectures')" v-if="architectures.length > 0">
|
||||
<el-tag v-for="(arch, index) in architectures" :key="index" class="mx-1">
|
||||
{{ arch }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
<MdEditor previewOnly v-model="app.readMe" :theme="isDarkTheme ? 'dark' : 'light'" />
|
||||
</el-drawer>
|
||||
<Install ref="installRef"></Install>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { GetApp, GetAppDetail } from '@/api/modules/app';
|
||||
import MdEditor from 'md-editor-v3';
|
||||
import { ref } from 'vue';
|
||||
import Install from './install/index.vue';
|
||||
import router from '@/routers';
|
||||
import { GlobalStore } from '@/store';
|
||||
import { getLanguage } from '@/utils/util';
|
||||
import { storeToRefs } from 'pinia';
|
||||
const globalStore = GlobalStore();
|
||||
const { isDarkTheme } = storeToRefs(globalStore);
|
||||
|
||||
const language = getLanguage();
|
||||
|
||||
const app = ref<any>({});
|
||||
const appDetail = ref<any>({});
|
||||
const version = ref('');
|
||||
const loadingDetail = ref(false);
|
||||
const loadingApp = ref(false);
|
||||
const installRef = ref();
|
||||
const open = ref(false);
|
||||
const appKey = ref();
|
||||
const operate = ref();
|
||||
const architectures = ref([]);
|
||||
|
||||
const acceptParams = async (key: string, op: string) => {
|
||||
appKey.value = key;
|
||||
operate.value = op;
|
||||
open.value = true;
|
||||
getApp();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
const getApp = async () => {
|
||||
loadingApp.value = true;
|
||||
try {
|
||||
const res = await GetApp(appKey.value);
|
||||
app.value = res.data;
|
||||
app.value.icon = 'data:image/png;base64,' + res.data.icon;
|
||||
version.value = app.value.versions[0];
|
||||
getDetail(app.value.id, version.value);
|
||||
} finally {
|
||||
loadingApp.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getDetail = async (id: number, version: string) => {
|
||||
loadingDetail.value = true;
|
||||
try {
|
||||
const res = await GetAppDetail(id, version, 'app');
|
||||
appDetail.value = res.data;
|
||||
if (appDetail.value.architectures != '') {
|
||||
architectures.value = appDetail.value.architectures.split(',');
|
||||
}
|
||||
} finally {
|
||||
loadingDetail.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toLink = (link: string) => {
|
||||
window.open(link, '_blank');
|
||||
};
|
||||
|
||||
const openInstall = () => {
|
||||
switch (app.value.type) {
|
||||
case 'php':
|
||||
router.push({ path: '/websites/runtimes/php' });
|
||||
break;
|
||||
case 'node':
|
||||
router.push({ path: '/websites/runtimes/node' });
|
||||
break;
|
||||
case 'java':
|
||||
router.push({ path: '/websites/runtimes/java' });
|
||||
break;
|
||||
case 'go':
|
||||
router.push({ path: '/websites/runtimes/go' });
|
||||
break;
|
||||
default:
|
||||
const params = {
|
||||
app: app.value,
|
||||
};
|
||||
installRef.value.acceptParams(params);
|
||||
open.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.brief {
|
||||
.name {
|
||||
span {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 10px;
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.version {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.descriptions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user