mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-20 02:55:08 +08:00
feat: add useExtensionPage composable for managing plugin extensions
- Implemented a new composable `useExtensionPage` to handle various functionalities related to plugin management, including fetching extensions, handling updates, and managing UI states. - Added support for conflict checking, plugin installation, and custom source management. - Integrated search and filtering capabilities for plugins in the market. - Enhanced user experience with dialogs for confirmations and notifications. - Included pagination and sorting features for better plugin visibility.
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject } from "vue";
|
||||
import { ref, computed, inject, watch } from "vue";
|
||||
import { useCustomizerStore } from "@/stores/customizer";
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
import { getPlatformDisplayName, getPlatformIcon } from "@/utils/platformUtils";
|
||||
import UninstallConfirmDialog from "./UninstallConfirmDialog.vue";
|
||||
import PluginPlatformChip from "./PluginPlatformChip.vue";
|
||||
import StyledMenu from "./StyledMenu.vue";
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
|
||||
const props = defineProps({
|
||||
extension: {
|
||||
@@ -59,6 +61,25 @@ const astrbotVersionRequirement = computed(() => {
|
||||
: "";
|
||||
});
|
||||
|
||||
const logoLoadFailed = ref(false);
|
||||
|
||||
const logoSrc = computed(() => {
|
||||
const logo = props.extension?.logo;
|
||||
if (logoLoadFailed.value) {
|
||||
return defaultPluginIcon;
|
||||
}
|
||||
return typeof logo === "string" && logo.trim().length
|
||||
? logo
|
||||
: defaultPluginIcon;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.extension?.logo,
|
||||
() => {
|
||||
logoLoadFailed.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
// 操作函数
|
||||
const configure = () => {
|
||||
emit("configure", props.extension);
|
||||
@@ -104,6 +125,7 @@ const viewReadme = () => {
|
||||
const viewChangelog = () => {
|
||||
emit("view-changelog", props.extension);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -129,249 +151,292 @@ const viewChangelog = () => {
|
||||
style="
|
||||
padding: 16px;
|
||||
padding-bottom: 0px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<div v-if="extension?.logo">
|
||||
<img :src="extension.logo" :alt="extension.name" cover width="100" />
|
||||
</div>
|
||||
|
||||
<div style="overflow-x: auto">
|
||||
<!-- Top-right three-dot menu -->
|
||||
<div style="position: absolute; right: 8px; top: 8px; z-index: 5">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
aria-label="more"
|
||||
v-if="extension?.repo"
|
||||
:href="extension?.repo"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon icon="mdi-github"></v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-bind="menuProps" icon variant="text" aria-label="more">
|
||||
<v-icon icon="mdi-dots-vertical"></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item @click="viewReadme">
|
||||
<v-list-item-title
|
||||
>📄 {{ tm("buttons.viewDocs") }}</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="!marketMode" @click="viewChangelog">
|
||||
<v-list-item-title
|
||||
>📝 {{ tm("pluginChangelog.menuTitle") }}</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="marketMode && !extension?.installed"
|
||||
@click="installExtension"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ tm("buttons.install") }}</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="marketMode && extension?.installed">
|
||||
<v-list-item-title class="text--disabled">{{
|
||||
tm("status.installed")
|
||||
}}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Divider between market actions and plugin actions -->
|
||||
<v-divider v-if="!marketMode" />
|
||||
|
||||
<template v-if="!marketMode">
|
||||
<v-list-item @click="configure">
|
||||
<v-list-item-title>
|
||||
{{ tm("card.actions.pluginConfig") }}</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="uninstallExtension">
|
||||
<v-list-item-title class="text-error">{{
|
||||
tm("card.actions.uninstallPlugin")
|
||||
}}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="reloadExtension">
|
||||
<v-list-item-title>{{
|
||||
tm("card.actions.reloadPlugin")
|
||||
}}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="toggleActivation">
|
||||
<v-list-item-title>
|
||||
{{
|
||||
extension.activated
|
||||
? tm("buttons.disable")
|
||||
: tm("buttons.enable")
|
||||
}}{{ tm("card.actions.togglePlugin") }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="viewHandlers">
|
||||
<v-list-item-title
|
||||
>{{ tm("card.actions.viewHandlers") }} ({{
|
||||
extension.handlers.length
|
||||
}})</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="updateExtension">
|
||||
<v-list-item-title>
|
||||
{{
|
||||
extension.has_update
|
||||
? tm("card.actions.updateTo") +
|
||||
" " +
|
||||
extension.online_version
|
||||
: tm("card.actions.reinstall")
|
||||
}}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
|
||||
<div style="overflow-x: auto; width: 100%">
|
||||
<div style="width: 100%; margin-bottom: 24px">
|
||||
<!-- 最多一行 -->
|
||||
<div
|
||||
class="text-caption"
|
||||
style="
|
||||
color: gray;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 84px;
|
||||
"
|
||||
>
|
||||
{{ extension.author }} / {{ extension.name }}
|
||||
</div>
|
||||
<p
|
||||
class="text-h3 font-weight-black extension-title"
|
||||
:class="{ 'text-h4': $vuetify.display.xs }"
|
||||
>
|
||||
<span class="extension-title__text">{{
|
||||
extension.display_name?.length
|
||||
? extension.display_name
|
||||
: extension.name
|
||||
}}</span>
|
||||
<v-tooltip
|
||||
location="top"
|
||||
v-if="extension?.has_update && !marketMode"
|
||||
<div class="extension-title-row">
|
||||
<p
|
||||
class="text-h3 font-weight-black extension-title"
|
||||
:class="{ 'text-h4': $vuetify.display.xs }"
|
||||
>
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon
|
||||
v-bind="tooltipProps"
|
||||
color="warning"
|
||||
class="ml-2"
|
||||
icon="mdi-update"
|
||||
size="small"
|
||||
></v-icon>
|
||||
</template>
|
||||
<span
|
||||
>{{ tm("card.status.hasUpdate") }}:
|
||||
{{ extension.online_version }}</span
|
||||
<v-tooltip
|
||||
location="top"
|
||||
:text="
|
||||
extension.display_name?.length &&
|
||||
extension.display_name !== extension.name
|
||||
? `${extension.display_name} (${extension.name})`
|
||||
: extension.name
|
||||
"
|
||||
>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
location="top"
|
||||
v-if="!extension.activated && !marketMode"
|
||||
>
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon
|
||||
v-bind="tooltipProps"
|
||||
color="error"
|
||||
class="ml-2"
|
||||
icon="mdi-cancel"
|
||||
size="small"
|
||||
></v-icon>
|
||||
</template>
|
||||
<span>{{ tm("card.status.disabled") }}</span>
|
||||
</v-tooltip>
|
||||
</p>
|
||||
<template v-slot:activator="{ props: titleTooltipProps }">
|
||||
<span v-bind="titleTooltipProps" class="extension-title__text">{{
|
||||
extension.display_name?.length
|
||||
? extension.display_name
|
||||
: extension.name
|
||||
}}</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
location="top"
|
||||
v-if="extension?.has_update && !marketMode"
|
||||
>
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon
|
||||
v-bind="tooltipProps"
|
||||
color="warning"
|
||||
class="ml-2"
|
||||
icon="mdi-update"
|
||||
size="small"
|
||||
></v-icon>
|
||||
</template>
|
||||
<span
|
||||
>{{ tm("card.status.hasUpdate") }}:
|
||||
{{ extension.online_version }}</span
|
||||
>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
location="top"
|
||||
v-if="!extension.activated && !marketMode"
|
||||
>
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon
|
||||
v-bind="tooltipProps"
|
||||
color="error"
|
||||
class="ml-2"
|
||||
icon="mdi-cancel"
|
||||
size="small"
|
||||
></v-icon>
|
||||
</template>
|
||||
<span>{{ tm("card.status.disabled") }}</span>
|
||||
</v-tooltip>
|
||||
</p>
|
||||
|
||||
<div class="mt-1 d-flex flex-wrap">
|
||||
<v-chip color="primary" label size="small">
|
||||
<v-icon icon="mdi-source-branch" start></v-icon>
|
||||
{{ extension.version }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="extension?.has_update"
|
||||
color="warning"
|
||||
label
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
<v-icon icon="mdi-arrow-up-bold" start></v-icon>
|
||||
{{ extension.online_version }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
color="primary"
|
||||
label
|
||||
size="small"
|
||||
class="ml-2"
|
||||
v-if="extension.handlers?.length"
|
||||
@click="viewHandlers"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<v-icon icon="mdi-cogs" start></v-icon>
|
||||
{{ extension.handlers?.length
|
||||
}}{{ tm("card.status.handlersCount") }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-for="tag in extension.tags"
|
||||
:key="tag"
|
||||
:color="tag === 'danger' ? 'error' : 'primary'"
|
||||
label
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
<PluginPlatformChip
|
||||
:platforms="supportPlatforms"
|
||||
class="ml-2"
|
||||
/>
|
||||
<v-chip
|
||||
v-if="astrbotVersionRequirement"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
label
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
AstrBot: {{ astrbotVersionRequirement }}
|
||||
</v-chip>
|
||||
<template v-if="!marketMode">
|
||||
<v-tooltip location="left">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<div v-bind="tooltipProps" class="extension-switch-wrap" @click.stop>
|
||||
<v-switch
|
||||
:model-value="extension.activated"
|
||||
color="success"
|
||||
density="compact"
|
||||
hide-details
|
||||
inset
|
||||
@update:model-value="toggleActivation"
|
||||
></v-switch>
|
||||
</div>
|
||||
</template>
|
||||
<span>{{
|
||||
extension.activated ? tm("buttons.disable") : tm("buttons.enable")
|
||||
}}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="extension-market-menu-wrap">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
aria-label="more"
|
||||
v-if="extension?.repo"
|
||||
:href="extension?.repo"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon icon="mdi-github"></v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-bind="menuProps" icon variant="text" aria-label="more">
|
||||
<v-icon icon="mdi-dots-vertical"></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item @click="viewReadme">
|
||||
<v-list-item-title
|
||||
>📄 {{ tm("buttons.viewDocs") }}</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="marketMode && !extension?.installed"
|
||||
@click="installExtension"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ tm("buttons.install") }}</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="marketMode && extension?.installed">
|
||||
<v-list-item-title class="text--disabled">{{
|
||||
tm("status.installed")
|
||||
}}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-2"
|
||||
:class="{ 'text-caption': $vuetify.display.xs }"
|
||||
style="overflow-y: auto; height: 70px; font-size: 90%"
|
||||
>
|
||||
{{ extension.desc }}
|
||||
<div class="extension-content-row mt-2">
|
||||
<div class="extension-image-container">
|
||||
<img
|
||||
:src="logoSrc"
|
||||
:alt="extension.name"
|
||||
class="extension-logo"
|
||||
@error="logoLoadFailed = true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="extension-meta-group">
|
||||
<div class="extension-chip-group d-flex flex-wrap">
|
||||
<v-chip color="primary" label size="small">
|
||||
<v-icon icon="mdi-source-branch" start></v-icon>
|
||||
{{ extension.version }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="extension?.has_update"
|
||||
color="warning"
|
||||
label
|
||||
size="small"
|
||||
>
|
||||
<v-icon icon="mdi-arrow-up-bold" start></v-icon>
|
||||
{{ extension.online_version }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="extension.handlers?.length"
|
||||
color="primary"
|
||||
label
|
||||
size="small"
|
||||
@click="viewHandlers"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<v-icon icon="mdi-cogs" start></v-icon>
|
||||
{{ extension.handlers?.length
|
||||
}}{{ tm("card.status.handlersCount") }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-for="tag in extension.tags"
|
||||
:key="tag"
|
||||
:color="tag === 'danger' ? 'error' : 'primary'"
|
||||
label
|
||||
size="small"
|
||||
>
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
<PluginPlatformChip :platforms="supportPlatforms" />
|
||||
<v-chip
|
||||
v-if="astrbotVersionRequirement"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
label
|
||||
size="small"
|
||||
>
|
||||
AstrBot: {{ astrbotVersionRequirement }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="extension-desc"
|
||||
:class="{ 'text-caption': $vuetify.display.xs }"
|
||||
>
|
||||
{{ extension.desc }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="extension-actions">
|
||||
<v-btn color="primary" size="small" @click="viewReadme">
|
||||
{{ tm("buttons.viewDocs") }}
|
||||
</v-btn>
|
||||
<v-btn v-if="!marketMode" color="primary" size="small" @click="configure">
|
||||
{{ tm("card.actions.pluginConfig") }}
|
||||
</v-btn>
|
||||
<v-card-actions class="extension-actions" @click.stop>
|
||||
<template v-if="!marketMode">
|
||||
<v-spacer></v-spacer>
|
||||
<v-tooltip location="top" :text="tm('buttons.viewDocs')">
|
||||
<template v-slot:activator="{ props: actionProps }">
|
||||
<v-btn
|
||||
v-bind="actionProps"
|
||||
icon="mdi-book-open-page-variant"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="info"
|
||||
@click="viewReadme"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" :text="tm('card.actions.pluginConfig')">
|
||||
<template v-slot:activator="{ props: actionProps }">
|
||||
<v-btn
|
||||
v-bind="actionProps"
|
||||
icon="mdi-cog"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click="configure"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip v-if="extension?.repo" location="top" :text="tm('buttons.viewRepo')">
|
||||
<template v-slot:activator="{ props: actionProps }">
|
||||
<v-btn
|
||||
v-bind="actionProps"
|
||||
icon="mdi-github"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
:href="extension.repo"
|
||||
target="_blank"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" :text="tm('card.actions.reloadPlugin')">
|
||||
<template v-slot:activator="{ props: actionProps }">
|
||||
<v-btn
|
||||
v-bind="actionProps"
|
||||
icon="mdi-refresh"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click="reloadExtension"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<StyledMenu location="top end" offset="8">
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
icon="mdi-dots-horizontal"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<v-list-item class="styled-menu-item" prepend-icon="mdi-information" @click="viewHandlers">
|
||||
<v-list-item-title>{{ tm("buttons.viewInfo") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="styled-menu-item" prepend-icon="mdi-update" @click="updateExtension">
|
||||
<v-list-item-title>{{
|
||||
extension.has_update
|
||||
? tm("card.actions.updateTo") + " " + extension.online_version
|
||||
: tm("card.actions.reinstall")
|
||||
}}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="styled-menu-item" prepend-icon="mdi-delete" @click="uninstallExtension">
|
||||
<v-list-item-title class="text-error">{{ tm("card.actions.uninstallPlugin") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-btn color="primary" size="small" @click="viewReadme">
|
||||
{{ tm("buttons.viewDocs") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
@@ -385,13 +450,52 @@ const viewChangelog = () => {
|
||||
<style scoped>
|
||||
.extension-image-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
align-items: flex-start;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.extension-logo {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.extension-content-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.extension-meta-group {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.extension-chip-group {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.extension-desc {
|
||||
margin-top: 8px;
|
||||
font-size: 90%;
|
||||
overflow-y: auto;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.extension-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.extension-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.extension-title__text {
|
||||
@@ -399,17 +503,38 @@ const viewChangelog = () => {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.extension-switch-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.extension-switch-wrap :deep(.v-switch) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.extension-market-menu-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.extension-image-container {
|
||||
margin-left: 8px;
|
||||
.extension-content-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.extension-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.extension-actions {
|
||||
margin-top: auto;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user