feat: Optimize the structure of installed apps in the App Store

This commit is contained in:
zhengkunwang223 2025-12-17 10:43:27 +08:00
parent 15bc6cf884
commit b965b1357b
9 changed files with 439 additions and 395 deletions

View file

@ -626,10 +626,10 @@ func (w WebsiteService) GetWebsite(id uint) (response.WebsiteDTO, error) {
dName := d.GetName()
if _, ok := dto.LBAlgorithms[dName]; ok {
res.Algorithm = dName
break
}
}
res.Servers = getNginxUpstreamServers(up.UpstreamServers)
break
}
}
return res, nil

View file

@ -165,6 +165,9 @@ export namespace App {
webUI: string;
appKey?: string;
serviceName: string;
appStatus?: string;
appType?: string;
version?: string;
}
export interface AppInstalledInfo {

View file

@ -28,8 +28,8 @@ export interface SearchWithPage {
}
export interface CommonModel {
id: number;
CreatedAt?: string;
UpdatedAt?: string;
createdAt?: string;
updatedAt?: string;
}
export interface DescriptionUpdate {
id: number;

View file

@ -110,7 +110,6 @@ const getApp = async () => {
try {
const res = await getAppByKey(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 {

View file

@ -0,0 +1,78 @@
<template>
<div class="install-card">
<el-card class="e-card">
<el-row :gutter="10">
<el-col :xs="3" :sm="3" :md="3" :lg="4" :xl="3">
<AppIcon
@open-detail="$emit('openDetail')"
:appID="installed.appID"
:currentNode="currentNode"
></AppIcon>
</el-col>
<el-col :xs="21" :sm="21" :md="21" :lg="20" :xl="21">
<div class="a-detail">
<AppHeader
:installed="installed"
:mode="mode"
:defaultLink="defaultLink"
@open-backups="$emit('openBackups')"
@open-log="$emit('openLog')"
@open-terminal="$emit('openTerminal')"
@open-operate="$emit('openOperate')"
@favorite-install="$emit('favoriteInstall')"
@to-folder="$emit('toFolder')"
@open-uploads="$emit('openUploads')"
@ignore-app="$emit('ignoreApp')"
@to-container="$emit('toContainer')"
></AppHeader>
<AppInfo
:installed="installed"
:defaultLink="defaultLink"
@open-link="$emit('openLink')"
@to-link="$emit('toLink')"
@jump-to-path="$emit('jumpToPath', '/settings/panel')"
></AppInfo>
<div class="app-divider" />
<slot name="buttons"></slot>
</div>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script lang="ts" setup>
import AppIcon from '@/views/app-store/installed/app/icon.vue';
import AppHeader from '@/views/app-store/installed/app/header.vue';
import AppInfo from '@/views/app-store/installed/app/info.vue';
import { App } from '@/api/interface/app';
interface Props {
installed: App.AppInstalled;
mode: string;
defaultLink: string;
currentNode: string;
}
defineProps<Props>();
defineEmits([
'toFolder',
'openUploads',
'openDetail',
'openBackups',
'openLog',
'openTerminal',
'openOperate',
'favoriteInstall',
'openLink',
'toLink',
'jumpToPath',
'ignoreApp',
'toContainer',
]);
</script>
<style scoped lang="scss">
@use '@/views/app-store/index.scss';
</style>

View file

@ -0,0 +1,193 @@
<template>
<div class="d-name">
<div class="flex items-center justify-between">
<div class="min-w-50 flex items-center justify-start gap-1">
<el-button link type="info">
<el-tooltip :content="installed.name" placement="top">
<span class="name">{{ installed.name }}</span>
</el-tooltip>
</el-button>
<span class="status">
<Status :key="installed.status" :status="installed.status"></Status>
</span>
<span class="msg">
<el-popover
v-if="isAppErr(installed)"
placement="bottom"
:width="400"
trigger="hover"
:content="installed.message"
:popper-options="options"
>
<template #reference>
<el-button link type="danger">
<el-icon><Warning /></el-icon>
</el-button>
</template>
<div class="app-error">
{{ installed.message }}
</div>
</el-popover>
</span>
<span class="ml-1">
<el-tooltip effect="dark" :content="$t('app.toFolder')" placement="top">
<el-button type="primary" link @click="$emit('toFolder')" icon="FolderOpened"></el-button>
</el-tooltip>
</span>
<span class="ml-1">
<el-tooltip
v-if="mode !== 'upgrade'"
effect="dark"
:content="$t('commons.button.log')"
placement="top"
>
<el-button
type="primary"
link
@click="$emit('openLog')"
:disabled="installed.status === 'DownloadErr'"
>
<el-icon><Tickets /></el-icon>
</el-button>
</el-tooltip>
</span>
<span class="ml-1">
<el-tooltip v-if="mode !== 'upgrade'" effect="dark" :content="$t('menu.terminal')" placement="top">
<el-button
type="primary"
link
@click="$emit('openTerminal')"
:disabled="installed.status !== 'Running'"
>
<el-icon>
<SvgIcon iconName="p-terminal2" />
</el-icon>
</el-button>
</el-tooltip>
</span>
<span class="ml-1">
<el-tooltip v-if="mode !== 'upgrade'" effect="dark" :content="$t('menu.container')" placement="top">
<el-button type="primary" link @click="$emit('toContainer')">
<el-icon>
<SvgIcon iconName="p-docker" />
</el-icon>
</el-button>
</el-tooltip>
</span>
<span class="ml-1" v-if="mode === 'installed'">
<el-tooltip
effect="dark"
:content="$t('website.cancelFavorite')"
placement="top-start"
v-if="installed.favorite"
>
<el-button
link
size="large"
icon="StarFilled"
type="warning"
@click="$emit('favoriteInstall')"
></el-button>
</el-tooltip>
<el-tooltip effect="dark" :content="$t('website.favorite')" placement="top-start" v-else>
<el-button link icon="Star" type="info" @click="$emit('favoriteInstall')"></el-button>
</el-tooltip>
</span>
</div>
<div class="flex flex-wrap items-center justify-end gap-1">
<el-button
class="h-button"
plain
round
size="small"
@click="$emit('openUploads')"
v-if="mode === 'installed'"
>
{{ $t('database.loadBackup') }}
</el-button>
<el-button
class="h-button"
plain
round
size="small"
@click="$emit('openBackups')"
v-if="mode === 'installed'"
>
{{ $t('commons.button.backup') }}
</el-button>
<el-button
class="h-button"
plain
round
size="small"
:disabled="installed.status === 'Upgrading'"
@click="$emit('ignoreApp')"
v-if="mode === 'upgrade'"
>
{{ $t('commons.button.ignore') }}
</el-button>
<el-button
class="h-button"
plain
round
size="small"
:disabled="
(installed.status !== 'Running' && installed.status !== 'UpgradeErr') ||
installed.appStatus === 'TakeDown'
"
@click="$emit('openOperate')"
v-if="mode === 'upgrade'"
>
{{ $t('commons.button.upgrade') }}
</el-button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { App } from '@/api/interface/app';
interface Props {
installed: App.AppInstalled;
mode: string;
}
defineProps<Props>();
defineEmits([
'toFolder',
'openLog',
'openTerminal',
'toContainer',
'openBackups',
'openOperate',
'ignoreApp',
'openUploads',
'favoriteInstall',
]);
const options = {
modifiers: [
{
name: 'flip',
options: {
padding: 5,
fallbackPlacements: ['bottom-start', 'top-start', 'right', 'left'],
},
},
],
};
const isAppErr = (row: any) => {
return row.status.includes('Err') || row.status.includes('Error') || row.status.includes('UnHealthy');
};
</script>
<style scoped lang="scss">
@use '@/views/app-store/index.scss';
.svg-icon {
width: 100%;
height: 100%;
padding: 0;
}
</style>

View file

@ -0,0 +1,17 @@
<template>
<div class="icon">
<el-avatar @click="$emit('openDetail')" shape="square" :size="77" :src="getAppIconUrl(appID, currentNode)" />
</div>
</template>
<script lang="ts" setup>
import { getAppIconUrl } from '@/api/modules/app';
interface Props {
currentNode: string;
appID: number;
}
defineProps<Props>();
defineEmits(['openDetail']);
</script>

View file

@ -0,0 +1,98 @@
<template>
<div>
<div class="d-description flex flex-wrap items-center justify-start gap-1.5">
<el-button class="mr-1" plain size="small">
{{ $t('app.version') }}{{ $t('commons.colon') }}{{ installed.version }}
</el-button>
<el-button v-if="installed.httpPort > 0" class="mr-1" plain size="small">
{{ $t('commons.table.port') }}{{ $t('commons.colon') }}{{ installed.httpPort }}
</el-button>
<el-button v-if="installed.httpsPort > 0" plain size="small">
{{ $t('commons.table.port') }}{{ installed.httpsPort }}
</el-button>
<el-popover placement="top-start" trigger="hover" v-if="hasLinkButton(installed)" :width="400">
<template #reference>
<el-button plain icon="Promotion" size="small" @click="$emit('openLink')">
{{ $t('app.toLink') }}
</el-button>
</template>
<table>
<tbody>
<tr v-if="defaultLink != ''">
<td v-if="installed.httpPort > 0">
<el-button
type="primary"
link
@click="$emit('toLink', 'http://' + defaultLink + ':' + installed.httpPort)"
>
{{ 'http://' + defaultLink + ':' + installed.httpPort }}
</el-button>
</td>
</tr>
<tr v-if="defaultLink != ''">
<td v-if="installed.httpsPort > 0">
<el-button
type="primary"
link
@click="$emit('toLink', 'https://' + defaultLink + ':' + installed.httpsPort)"
>
{{ 'https://' + defaultLink + ':' + installed.httpsPort }}
</el-button>
</td>
</tr>
<tr v-if="installed.webUI != ''">
<td>
<el-button type="primary" link @click="$emit('toLink')">
{{ installed.webUI }}
</el-button>
</td>
</tr>
</tbody>
</table>
<span v-if="defaultLink == '' && installed.webUI == ''">
{{ $t('app.webUIConfig') }}
<el-link icon="Position" @click="$emit('jumpToPath')" type="primary">
{{ $t('firewall.quickJump') }}
</el-link>
</span>
</el-popover>
</div>
<div class="description">
<span>
{{ $t('app.alreadyRun') }}{{ $t('commons.colon') }}
{{ getAge(installed.createdAt) }}
</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import { getAge } from '@/utils/util';
interface Props {
installed: App.AppInstalled;
defaultLink: string;
}
defineProps<Props>();
defineEmits(['openLink', 'toLink', 'jumpToPath']);
const hasLinkButton = (installed: any) => {
return (
(installed.appType == 'website' || installed.appKey?.startsWith('local')) &&
(installed.httpPort > 0 || installed.httpsPort > 0 || installed.webUI != '')
);
};
</script>
<style scoped lang="scss">
@use '@/views/app-store/index.scss';
.d-description {
.el-button + .el-button {
margin-left: 0;
}
}
</style>

View file

@ -29,7 +29,6 @@
>
{{ $t('firewall.quickJump') }}
</el-link>
 
</span>
</template>
</el-alert>
@ -49,318 +48,26 @@
:lg="12"
:xl="12"
>
<div class="install-card">
<el-card class="e-card">
<el-row :gutter="10">
<el-col :xs="3" :sm="3" :md="3" :lg="4" :xl="4">
<div class="icon">
<el-avatar
@click="openDetail(installed.appKey)"
shape="square"
:size="66"
:src="getAppIconUrl(installed.appID, currentNode)"
/>
</div>
</el-col>
<el-col :xs="24" :sm="21" :md="21" :lg="20" :xl="20">
<div class="a-detail">
<div class="d-name">
<div class="flex items-center justify-between">
<div class="min-w-50 flex items-center justify-start gap-1">
<el-button link type="info">
<el-tooltip :content="installed.name" placement="top">
<span class="name">{{ installed.name }}</span>
</el-tooltip>
</el-button>
<span class="status">
<Status
:key="installed.status"
:status="installed.status"
></Status>
</span>
<span class="msg">
<el-popover
v-if="isAppErr(installed)"
placement="bottom"
:width="400"
trigger="hover"
:content="installed.message"
:popper-options="options"
<AppCard
:installed="installed"
:mode="mode"
:defaultLink="defaultLink"
:currentNode="currentNode"
@open-detail="openDetail(installed.appKey)"
@open-backups="openBackups(installed)"
@open-log="openLog(installed)"
@open-terminal="openTerminal(installed)"
@open-operate="openOperate(installed, 'upgrade')"
@favorite-install="favoriteInstall(installed)"
@to-folder="routerToFileWithPath(installed.path)"
@open-uploads="openUploads(installed.appKey, installed.name)"
@open-link="openLink(defaultLink, installed)"
@to-link="toLink(installed.webUI)"
@jump-to-path="jumpToPath(router, '/settings/panel')"
@to-container="toContainer(installed)"
@ignore-app="ignoreApp(installed)"
>
<template #reference>
<el-button link type="danger">
<el-icon><Warning /></el-icon>
</el-button>
</template>
<div class="app-error">
{{ installed.message }}
</div>
</el-popover>
</span>
<span class="ml-1">
<el-tooltip
effect="dark"
:content="$t('app.toFolder')"
placement="top"
>
<el-button
type="primary"
link
@click="routerToFileWithPath(installed.path)"
icon="FolderOpened"
></el-button>
</el-tooltip>
</span>
<span class="ml-1">
<el-tooltip
v-if="mode !== 'upgrade'"
effect="dark"
:content="$t('commons.button.log')"
placement="top"
>
<el-button
type="primary"
link
@click="openLog(installed)"
:disabled="installed.status === 'DownloadErr'"
>
<el-icon><Tickets /></el-icon>
</el-button>
</el-tooltip>
</span>
<span class="ml-1">
<el-tooltip
v-if="mode !== 'upgrade'"
effect="dark"
:content="$t('menu.terminal')"
placement="top"
>
<el-button
type="primary"
link
@click="openTerminal(installed)"
:disabled="installed.status !== 'Running'"
>
<el-icon>
<SvgIcon iconName="p-terminal2" />
</el-icon>
</el-button>
</el-tooltip>
</span>
<span class="ml-1">
<el-tooltip
v-if="mode !== 'upgrade'"
effect="dark"
:content="$t('menu.container')"
placement="top"
>
<el-button
type="primary"
link
@click="toContainer(installed)"
>
<el-icon>
<SvgIcon iconName="p-docker" />
</el-icon>
</el-button>
</el-tooltip>
</span>
<span class="ml-1" v-if="mode === 'installed'">
<el-tooltip
effect="dark"
:content="$t('website.cancelFavorite')"
placement="top-start"
v-if="installed.favorite"
>
<el-button
link
size="large"
icon="StarFilled"
type="warning"
@click="favoriteInstall(installed)"
></el-button>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('website.favorite')"
placement="top-start"
v-else
>
<el-button
link
icon="Star"
type="info"
@click="favoriteInstall(installed)"
></el-button>
</el-tooltip>
</span>
</div>
<div class="flex flex-wrap items-center justify-end gap-1">
<el-button
class="h-button"
plain
round
size="small"
@click="openUploads(installed.appKey, installed.name)"
v-if="mode === 'installed'"
>
{{ $t('database.loadBackup') }}
</el-button>
<el-button
class="h-button"
plain
round
size="small"
@click="openBackups(installed)"
v-if="mode === 'installed'"
>
{{ $t('commons.button.backup') }}
</el-button>
<el-button
class="h-button"
plain
round
size="small"
:disabled="installed.status === 'Upgrading'"
@click="ignoreApp(installed)"
v-if="mode === 'upgrade'"
>
{{ $t('commons.button.ignore') }}
</el-button>
<el-button
class="h-button"
plain
round
size="small"
:disabled="
(installed.status !== 'Running' &&
installed.status !== 'UpgradeErr') ||
installed.appStatus === 'TakeDown'
"
@click="openOperate(installed, 'upgrade')"
v-if="mode === 'upgrade'"
>
{{ $t('commons.button.upgrade') }}
</el-button>
</div>
</div>
</div>
<div
class="d-description flex flex-wrap items-center justify-start gap-1.5"
>
<el-button class="mr-1" plain size="small">
{{ $t('app.version') }}{{ $t('commons.colon')
}}{{ installed.version }}
</el-button>
<el-button
v-if="installed.httpPort > 0"
class="mr-1"
plain
size="small"
>
{{ $t('commons.table.port') }}{{ $t('commons.colon')
}}{{ installed.httpPort }}
</el-button>
<el-button v-if="installed.httpsPort > 0" plain size="small">
{{ $t('commons.table.port') }}{{ installed.httpsPort }}
</el-button>
<el-popover
placement="top-start"
trigger="hover"
v-if="hasLinkButton(installed)"
:width="400"
>
<template #reference>
<el-button
plain
icon="Promotion"
size="small"
@click="openLink(defaultLink, installed)"
>
{{ $t('app.toLink') }}
</el-button>
</template>
<table>
<tbody>
<tr v-if="defaultLink != ''">
<td v-if="installed.httpPort > 0">
<el-button
type="primary"
link
@click="
toLink(
'http://' +
defaultLink +
':' +
installed.httpPort,
)
"
>
{{
'http://' +
defaultLink +
':' +
installed.httpPort
}}
</el-button>
</td>
</tr>
<tr v-if="defaultLink != ''">
<td v-if="installed.httpsPort > 0">
<el-button
type="primary"
link
@click="
toLink(
'https://' +
defaultLink +
':' +
installed.httpsPort,
)
"
>
{{
'https://' +
defaultLink +
':' +
installed.httpsPort
}}
</el-button>
</td>
</tr>
<tr v-if="installed.webUI != ''">
<td>
<el-button
type="primary"
link
@click="toLink(installed.webUI)"
>
{{ installed.webUI }}
</el-button>
</td>
</tr>
</tbody>
</table>
<span v-if="defaultLink == '' && installed.webUI == ''">
{{ $t('app.webUIConfig') }}
<el-link
icon="Position"
@click="jumpToPath(router, '/settings/panel')"
type="primary"
>
{{ $t('firewall.quickJump') }}
</el-link>
</span>
</el-popover>
</div>
<div class="description">
<span>
{{ $t('app.alreadyRun') }}{{ $t('commons.colon') }}
{{ getAge(installed.createdAt) }}
</span>
</div>
<div class="app-divider" />
<template #buttons>
<div
class="d-button flex flex-wrap items-center justify-start gap-1.5"
v-if="mode === 'installed' && installed.status != 'Installing'"
@ -369,9 +76,7 @@
class="app-button"
v-for="(button, key) in buttons"
:key="key"
:type="
button.disabled && button.disabled(installed) ? 'info' : ''
"
:type="button.disabled && button.disabled(installed) ? 'info' : ''"
plain
round
size="small"
@ -381,11 +86,8 @@
{{ button.label }}
</el-button>
</div>
</div>
</el-col>
</el-row>
</el-card>
</div>
</template>
</AppCard>
</el-col>
</el-row>
</MainDiv>
@ -407,7 +109,6 @@
<AppDelete ref="deleteRef" @close="search" />
<AppParams ref="appParamRef" @close="search" />
<AppUpgrade ref="upgradeRef" @close="search" />
<PortJumpDialog ref="dialogPortJumpRef" />
<AppIgnore ref="ignoreRef" @close="search" />
<ComposeLogs ref="composeLogRef" />
<TerminalDialog ref="dialogTerminalRef" />
@ -417,30 +118,28 @@
</template>
<script lang="ts" setup>
import AppCard from '@/views/app-store/installed/app/card.vue';
import Backups from '@/components/backup/index.vue';
import Uploads from '@/components/upload/index.vue';
import PortJumpDialog from '@/components/port-jump/index.vue';
import AppResources from './check/index.vue';
import AppDelete from './delete/index.vue';
import AppParams from './detail/index.vue';
import AppUpgrade from './upgrade/index.vue';
import AppIgnore from './ignore/index.vue';
import Status from '@/components/status/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import Detail from '@/views/app-store/detail/index.vue';
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 ComposeLogs from '@/components/log/compose/index.vue';
import IgnoreApp from '@/views/app-store/installed/ignore/create/index.vue';
import TerminalDialog from '@/views/container/container/terminal/index.vue';
import { searchAppInstalled, installedOp, appInstalledDeleteCheck, getAppIconUrl } from '@/api/modules/app';
import { searchAppInstalled, installedOp, appInstalledDeleteCheck } from '@/api/modules/app';
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { ElMessageBox } from 'element-plus';
import { App } from '@/api/interface/app';
import { getAge, jumpToPath, toLink } from '@/utils/util';
import { jumpToPath, toLink } from '@/utils/util';
import { useRouter } from 'vue-router';
import { MsgSuccess } from '@/utils/message';
import { getAgentSettingByKey } from '@/api/modules/setting';
@ -472,7 +171,6 @@ const deleteRef = ref();
const appParamRef = ref();
const upgradeRef = ref();
const ignoreRef = ref();
const dialogPortJumpRef = ref();
const composeLogRef = ref();
const dialogTerminalRef = ref();
const taskLogRef = ref();
@ -491,18 +189,6 @@ const defaultLink = ref('');
const detailRef = ref();
const ignoreAppRef = ref();
const options = {
modifiers: [
{
name: 'flip',
options: {
padding: 5,
fallbackPlacements: ['bottom-start', 'top-start', 'right', 'left'],
},
},
],
};
const openDetail = (key: string) => {
detailRef.value.acceptParams(key, 'install');
};
@ -515,13 +201,6 @@ const changeTag = (key: string) => {
search();
};
const hasLinkButton = (installed: any) => {
return (
(installed.appType == 'website' || installed.appKey?.startsWith('local')) &&
(installed.httpPort > 0 || installed.httpsPort > 0 || installed.webUI != '')
);
};
const search = async () => {
searchReq.page = paginationConfig.currentPage;
searchReq.pageSize = paginationConfig.pageSize;
@ -579,10 +258,6 @@ const openIgnore = () => {
ignoreRef.value.acceptParams();
};
const ignoreApp = (row: App.AppInstalled) => {
ignoreAppRef.value.acceptParams(row);
};
const operate = async () => {
open.value = false;
loading.value = true;
@ -702,6 +377,10 @@ const buttons = [
},
];
const ignoreApp = (row: App.AppInstalled) => {
ignoreAppRef.value.acceptParams(row);
};
const toContainer = async (row: App.AppInstalled) => {
routerToNameWithQuery('ContainerItem', {
filters: 'com.docker.compose.project=' + row.serviceName,
@ -733,10 +412,6 @@ const openParam = (row: any) => {
appParamRef.value.acceptParams({ id: row.id });
};
const isAppErr = (row: any) => {
return row.status.includes('Err') || row.status.includes('Error') || row.status.includes('UnHealthy');
};
const openLog = (row: any) => {
switch (row.status) {
case 'Installing':
@ -819,28 +494,9 @@ onUnmounted(() => {
<style scoped lang="scss">
@use '../index';
.app-error {
max-height: 500px;
overflow-y: auto;
}
.d-name {
.el-button + .el-button {
margin-left: 0;
}
}
.d-button {
.el-button + .el-button {
margin-left: 0;
}
}
.d-description {
.el-button + .el-button {
margin-left: 0;
}
}
.svg-icon {
width: 100%;
height: 100%;
padding: 0;
}
</style>