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:
@@ -34,6 +34,7 @@ const platformDisplayList = computed(() =>
|
||||
const handleInstall = (plugin) => {
|
||||
emit("install", plugin);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -123,6 +124,7 @@ const handleInstall = (plugin) => {
|
||||
v-if="plugin?.social_link"
|
||||
:href="plugin.social_link"
|
||||
target="_blank"
|
||||
@click.stop
|
||||
class="text-subtitle-2 font-weight-medium"
|
||||
style="
|
||||
text-decoration: none;
|
||||
@@ -213,7 +215,10 @@ const handleInstall = (plugin) => {
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions style="gap: 6px; padding: 8px 12px; padding-top: 0">
|
||||
<v-card-actions
|
||||
style="gap: 6px; padding: 8px 12px; padding-top: 0"
|
||||
@click.stop
|
||||
>
|
||||
<v-chip
|
||||
v-for="tag in plugin.tags?.slice(0, 2)"
|
||||
:key="tag"
|
||||
@@ -248,22 +253,24 @@ const handleInstall = (plugin) => {
|
||||
<v-btn
|
||||
v-if="plugin?.repo"
|
||||
color="secondary"
|
||||
size="x-small"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="market-action-btn"
|
||||
:href="plugin.repo"
|
||||
target="_blank"
|
||||
style="height: 24px"
|
||||
style="height: 32px"
|
||||
>
|
||||
<v-icon icon="mdi-github" start size="x-small"></v-icon>
|
||||
<v-icon icon="mdi-github" start size="small"></v-icon>
|
||||
{{ tm("buttons.viewRepo") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="!plugin?.installed"
|
||||
color="primary"
|
||||
size="x-small"
|
||||
size="small"
|
||||
@click="handleInstall(plugin)"
|
||||
variant="flat"
|
||||
style="height: 24px"
|
||||
class="market-action-btn"
|
||||
style="height: 32px"
|
||||
>
|
||||
{{ tm("buttons.install") }}
|
||||
</v-btn>
|
||||
@@ -306,4 +313,9 @@ const handleInstall = (plugin) => {
|
||||
.plugin-description::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(var(--v-theme-primary-rgb), 0.6);
|
||||
}
|
||||
|
||||
.market-action-btn {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"handlersOperation": "Manage Handlers",
|
||||
"market": "AstrBot Plugin Market"
|
||||
},
|
||||
"titles": {
|
||||
"installedAstrBotPlugins": "Installed AstrBot Plugins"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search extensions...",
|
||||
"marketPlaceholder": "Search market extensions..."
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"skills": "Skills",
|
||||
"handlersOperation": "管理行为"
|
||||
},
|
||||
"titles": {
|
||||
"installedAstrBotPlugins": "已安装的 AstrBot 插件"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索插件...",
|
||||
"marketPlaceholder": "搜索市场插件..."
|
||||
|
||||
+250
-2183
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,639 @@
|
||||
<script setup>
|
||||
import ExtensionCard from "@/components/shared/ExtensionCard.vue";
|
||||
import StyledMenu from "@/components/shared/StyledMenu.vue";
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
|
||||
const props = defineProps({
|
||||
state: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
commonStore,
|
||||
t,
|
||||
tm,
|
||||
router,
|
||||
route,
|
||||
getSelectedGitHubProxy,
|
||||
conflictDialog,
|
||||
checkAndPromptConflicts,
|
||||
handleConflictConfirm,
|
||||
fileInput,
|
||||
activeTab,
|
||||
validTabs,
|
||||
isValidTab,
|
||||
getLocationHash,
|
||||
extractTabFromHash,
|
||||
syncTabFromHash,
|
||||
extension_data,
|
||||
getInitialShowReserved,
|
||||
showReserved,
|
||||
snack_message,
|
||||
snack_show,
|
||||
snack_success,
|
||||
configDialog,
|
||||
extension_config,
|
||||
pluginMarketData,
|
||||
loadingDialog,
|
||||
showPluginInfoDialog,
|
||||
selectedPlugin,
|
||||
curr_namespace,
|
||||
updatingAll,
|
||||
readmeDialog,
|
||||
forceUpdateDialog,
|
||||
updateAllConfirmDialog,
|
||||
changelogDialog,
|
||||
getInitialListViewMode,
|
||||
isListView,
|
||||
pluginSearch,
|
||||
loading_,
|
||||
currentPage,
|
||||
dangerConfirmDialog,
|
||||
selectedDangerPlugin,
|
||||
selectedMarketInstallPlugin,
|
||||
installCompat,
|
||||
versionCompatibilityDialog,
|
||||
showUninstallDialog,
|
||||
pluginToUninstall,
|
||||
showSourceDialog,
|
||||
showSourceManagerDialog,
|
||||
sourceName,
|
||||
sourceUrl,
|
||||
customSources,
|
||||
selectedSource,
|
||||
showRemoveSourceDialog,
|
||||
sourceToRemove,
|
||||
editingSource,
|
||||
originalSourceUrl,
|
||||
extension_url,
|
||||
dialog,
|
||||
upload_file,
|
||||
uploadTab,
|
||||
showPluginFullName,
|
||||
marketSearch,
|
||||
debouncedMarketSearch,
|
||||
refreshingMarket,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
randomPluginNames,
|
||||
normalizeStr,
|
||||
toPinyinText,
|
||||
toInitials,
|
||||
marketCustomFilter,
|
||||
plugin_handler_info_headers,
|
||||
pluginHeaders,
|
||||
filteredExtensions,
|
||||
filteredPlugins,
|
||||
filteredMarketPlugins,
|
||||
sortedPlugins,
|
||||
RANDOM_PLUGINS_COUNT,
|
||||
randomPlugins,
|
||||
shufflePlugins,
|
||||
refreshRandomPlugins,
|
||||
displayItemsPerPage,
|
||||
totalPages,
|
||||
paginatedPlugins,
|
||||
updatableExtensions,
|
||||
toggleShowReserved,
|
||||
toast,
|
||||
resetLoadingDialog,
|
||||
onLoadingDialogResult,
|
||||
failedPluginsDict,
|
||||
getExtensions,
|
||||
handleReloadAllFailed,
|
||||
checkUpdate,
|
||||
uninstallExtension,
|
||||
handleUninstallConfirm,
|
||||
updateExtension,
|
||||
showUpdateAllConfirm,
|
||||
confirmUpdateAll,
|
||||
cancelUpdateAll,
|
||||
confirmForceUpdate,
|
||||
updateAllExtensions,
|
||||
pluginOn,
|
||||
pluginOff,
|
||||
openExtensionConfig,
|
||||
updateConfig,
|
||||
showPluginInfo,
|
||||
reloadPlugin,
|
||||
viewReadme,
|
||||
viewChangelog,
|
||||
handleInstallPlugin,
|
||||
confirmDangerInstall,
|
||||
cancelDangerInstall,
|
||||
loadCustomSources,
|
||||
saveCustomSources,
|
||||
addCustomSource,
|
||||
openSourceManagerDialog,
|
||||
selectPluginSource,
|
||||
sourceSelectItems,
|
||||
editCustomSource,
|
||||
removeCustomSource,
|
||||
confirmRemoveSource,
|
||||
saveCustomSource,
|
||||
trimExtensionName,
|
||||
checkAlreadyInstalled,
|
||||
showVersionCompatibilityWarning,
|
||||
continueInstallIgnoringVersionWarning,
|
||||
cancelInstallOnVersionWarning,
|
||||
newExtension,
|
||||
normalizePlatformList,
|
||||
getPlatformDisplayList,
|
||||
resolveSelectedInstallPlugin,
|
||||
selectedInstallPlugin,
|
||||
checkInstallCompatibility,
|
||||
refreshPluginMarket,
|
||||
handleLocaleChange,
|
||||
searchDebounceTimer,
|
||||
} = props.state;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-tab-item v-show="activeTab === 'installed'">
|
||||
<div class="mb-4 pt-4 pb-4">
|
||||
<div class="d-flex align-center flex-wrap" style="gap: 12px">
|
||||
<h2 class="text-h2 mb-0">{{ tm("titles.installedAstrBotPlugins") }}</h2>
|
||||
|
||||
<div class="d-flex align-center flex-wrap ml-auto" style="gap: 8px">
|
||||
<v-text-field
|
||||
v-model="pluginSearch"
|
||||
density="compact"
|
||||
:label="tm('search.placeholder')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
hide-details
|
||||
single-line
|
||||
style="min-width: 220px; max-width: 340px"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-btn-toggle
|
||||
v-model="isListView"
|
||||
mandatory
|
||||
density="compact"
|
||||
color="primary"
|
||||
class="view-mode-toggle"
|
||||
>
|
||||
<v-btn :value="false" icon="mdi-view-grid"></v-btn>
|
||||
<v-btn :value="true" icon="mdi-view-list"></v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" class="d-flex align-center flex-wrap ga-2">
|
||||
<v-btn variant="tonal" @click="toggleShowReserved">
|
||||
<v-icon>{{
|
||||
showReserved ? "mdi-eye-off" : "mdi-eye"
|
||||
}}</v-icon>
|
||||
{{
|
||||
showReserved
|
||||
? tm("buttons.hideSystemPlugins")
|
||||
: tm("buttons.showSystemPlugins")
|
||||
}}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
:disabled="updatableExtensions.length === 0"
|
||||
:loading="updatingAll"
|
||||
@click="showUpdateAllConfirm"
|
||||
>
|
||||
<v-icon>mdi-update</v-icon>
|
||||
{{ tm("buttons.updateAll") }}
|
||||
</v-btn>
|
||||
|
||||
<v-dialog max-width="500px" v-if="extension_data.message">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon
|
||||
size="small"
|
||||
color="error"
|
||||
class="ml-auto"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon>mdi-alert-circle</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title class="headline d-flex align-center">
|
||||
<v-icon color="error" class="mr-2"
|
||||
>mdi-alert-circle</v-icon
|
||||
>
|
||||
{{ tm("dialogs.error.title") }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-1">
|
||||
{{ extension_data.message }}
|
||||
</p>
|
||||
<p class="text-caption mt-2">
|
||||
{{ tm("dialogs.error.checkConsole") }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-refresh"
|
||||
@click="handleReloadAllFailed"
|
||||
>
|
||||
尝试一键重载修复
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="isActive.value = false"
|
||||
>{{ tm("buttons.close") }}</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-fade-transition hide-on-leave>
|
||||
<!-- 表格视图 -->
|
||||
<div v-if="isListView">
|
||||
<v-card class="rounded-lg overflow-hidden elevation-0">
|
||||
<v-data-table
|
||||
:headers="pluginHeaders"
|
||||
:items="filteredPlugins"
|
||||
:loading="loading_"
|
||||
item-key="name"
|
||||
hover
|
||||
>
|
||||
<template v-slot:loader>
|
||||
<v-row class="py-8 d-flex align-center justify-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
></v-progress-circular>
|
||||
<span class="ml-2">{{ tm("status.loading") }}</span>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.name="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
<div
|
||||
v-if="item.logo"
|
||||
class="mr-3"
|
||||
style="flex-shrink: 0"
|
||||
>
|
||||
<img
|
||||
:src="item.logo"
|
||||
:alt="item.name"
|
||||
style="
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="mr-3" style="flex-shrink: 0">
|
||||
<img
|
||||
:src="defaultPluginIcon"
|
||||
:alt="item.name"
|
||||
style="
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-h5" style="font-family: inherit;">
|
||||
{{
|
||||
item.display_name && item.display_name.length
|
||||
? item.display_name
|
||||
: item.name
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.display_name && item.display_name.length"
|
||||
class="text-caption text-medium-emphasis mt-1"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.reserved"
|
||||
class="d-flex align-center mt-1"
|
||||
>
|
||||
<v-chip
|
||||
color="primary"
|
||||
size="x-small"
|
||||
class="font-weight-medium"
|
||||
>{{ tm("status.system") }}</v-chip
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.desc="{ item }">
|
||||
<div class="py-2">
|
||||
<div
|
||||
class="text-body-2 text-medium-emphasis"
|
||||
style="
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
"
|
||||
>
|
||||
{{ item.desc }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.support_platforms?.length"
|
||||
class="d-flex align-center flex-wrap mt-2"
|
||||
>
|
||||
<span class="text-caption text-medium-emphasis mr-2">
|
||||
{{ tm("card.status.supportPlatform") }}:
|
||||
</span>
|
||||
<v-chip
|
||||
v-for="platformId in item.support_platforms"
|
||||
:key="platformId"
|
||||
size="x-small"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
class="mr-1 mb-1"
|
||||
>
|
||||
{{ platformId }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.astrbot_version"
|
||||
class="d-flex align-center flex-wrap mt-1"
|
||||
>
|
||||
<span class="text-caption text-medium-emphasis mr-2">
|
||||
{{ tm("card.status.astrbotVersion") }}:
|
||||
</span>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
class="mr-1 mb-1"
|
||||
>
|
||||
{{ item.astrbot_version }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.version="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<span class="text-body-2">{{ item.version }}</span>
|
||||
<v-icon
|
||||
v-if="item.has_update"
|
||||
color="warning"
|
||||
size="small"
|
||||
class="ml-1"
|
||||
>mdi-alert</v-icon
|
||||
>
|
||||
<v-tooltip v-if="item.has_update" activator="parent">
|
||||
<span
|
||||
>{{ tm("messages.hasUpdate") }}
|
||||
{{ item.online_version }}</span
|
||||
>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.author="{ item }">
|
||||
<div class="text-body-2">{{ item.author }}</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div class="table-action-row d-flex align-center flex-nowrap ga-2 py-1">
|
||||
<v-btn
|
||||
v-if="!item.activated"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="success"
|
||||
class="table-action-btn"
|
||||
prepend-icon="mdi-play"
|
||||
@click="pluginOn(item)"
|
||||
>
|
||||
{{ tm("buttons.enable") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="error"
|
||||
class="table-action-btn"
|
||||
prepend-icon="mdi-pause"
|
||||
@click="pluginOff(item)"
|
||||
>
|
||||
{{ tm("buttons.disable") }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
class="table-action-btn"
|
||||
prepend-icon="mdi-refresh"
|
||||
@click="reloadPlugin(item.name)"
|
||||
>
|
||||
{{ tm("buttons.reload") }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
class="table-action-btn"
|
||||
prepend-icon="mdi-cog"
|
||||
@click="openExtensionConfig(item.name)"
|
||||
>
|
||||
{{ tm("buttons.configure") }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="info"
|
||||
class="table-action-btn"
|
||||
prepend-icon="mdi-book-open-page-variant"
|
||||
:disabled="!item.repo"
|
||||
@click="item.repo && viewReadme(item)"
|
||||
>
|
||||
{{ tm("buttons.viewDocs") }}
|
||||
</v-btn>
|
||||
|
||||
<StyledMenu location="bottom end" offset="8">
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
icon="mdi-dots-horizontal"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
class="table-action-btn"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
prepend-icon="mdi-information"
|
||||
@click="showPluginInfo(item)"
|
||||
>
|
||||
<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(item.name)"
|
||||
>
|
||||
<v-list-item-title>{{ tm("buttons.update") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
prepend-icon="mdi-delete"
|
||||
:disabled="item.reserved"
|
||||
@click="uninstallExtension(item.name)"
|
||||
>
|
||||
<v-list-item-title>{{ tm("buttons.uninstall") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:no-data>
|
||||
<div class="text-center pa-8">
|
||||
<v-icon size="64" color="info" class="mb-4"
|
||||
>mdi-puzzle-outline</v-icon
|
||||
>
|
||||
<div class="text-h5 mb-2">
|
||||
{{ tm("empty.noPlugins") }}
|
||||
</div>
|
||||
<div class="text-body-1 mb-4">
|
||||
{{ tm("empty.noPluginsDesc") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<!-- 卡片视图 -->
|
||||
<div v-else>
|
||||
<v-row v-if="filteredPlugins.length === 0" class="text-center">
|
||||
<v-col cols="12" class="pa-2">
|
||||
<v-icon size="64" color="info" class="mb-4"
|
||||
>mdi-puzzle-outline</v-icon
|
||||
>
|
||||
<div class="text-h5 mb-2">{{ tm("empty.noPlugins") }}</div>
|
||||
<div class="text-body-1 mb-4">
|
||||
{{ tm("empty.noPluginsDesc") }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
v-for="extension in filteredPlugins"
|
||||
:key="extension.name"
|
||||
class="pb-2"
|
||||
>
|
||||
<ExtensionCard
|
||||
:extension="extension"
|
||||
class="rounded-lg"
|
||||
style="background-color: rgb(var(--v-theme-mcpCardBg))"
|
||||
@configure="openExtensionConfig(extension.name)"
|
||||
@uninstall="
|
||||
(ext, options) => uninstallExtension(ext.name, options)
|
||||
"
|
||||
@update="updateExtension(extension.name)"
|
||||
@reload="reloadPlugin(extension.name)"
|
||||
@toggle-activation="
|
||||
extension.activated
|
||||
? pluginOff(extension)
|
||||
: pluginOn(extension)
|
||||
"
|
||||
@view-handlers="showPluginInfo(extension)"
|
||||
@view-readme="viewReadme(extension)"
|
||||
@view-changelog="viewChangelog(extension)"
|
||||
>
|
||||
</ExtensionCard>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
|
||||
<v-tooltip :text="tm('market.installPlugin')" location="left">
|
||||
<template v-slot:activator="{ props }">
|
||||
<button
|
||||
v-bind="props"
|
||||
type="button"
|
||||
class="v-btn v-btn--elevated v-btn--icon v-theme--PurpleThemeDark bg-darkprimary v-btn--density-default v-btn--size-x-large v-btn--variant-elevated fab-button"
|
||||
style="
|
||||
position: fixed;
|
||||
right: 52px;
|
||||
bottom: 52px;
|
||||
z-index: 10000;
|
||||
border-radius: 16px;
|
||||
"
|
||||
@click="dialog = true"
|
||||
>
|
||||
<span class="v-btn__overlay"></span>
|
||||
<span class="v-btn__underlay"></span>
|
||||
<span class="v-btn__content" data-no-activator="">
|
||||
<i
|
||||
class="mdi-plus mdi v-icon notranslate v-theme--PurpleThemeDark v-icon--size-default"
|
||||
aria-hidden="true"
|
||||
style="font-size: 32px"
|
||||
></i>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-tab-item>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.view-mode-toggle :deep(.v-btn) {
|
||||
min-width: 30px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.table-action-btn {
|
||||
min-height: 34px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-action-row {
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fab-button {
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.fab-button:hover {
|
||||
transform: translateY(-4px) scale(1.05);
|
||||
box-shadow: 0 12px 20px rgba(var(--v-theme-primary), 0.4);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,373 @@
|
||||
<script setup>
|
||||
import MarketPluginCard from "@/components/extension/MarketPluginCard.vue";
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
state: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
commonStore,
|
||||
t,
|
||||
tm,
|
||||
router,
|
||||
route,
|
||||
getSelectedGitHubProxy,
|
||||
conflictDialog,
|
||||
checkAndPromptConflicts,
|
||||
handleConflictConfirm,
|
||||
fileInput,
|
||||
activeTab,
|
||||
validTabs,
|
||||
isValidTab,
|
||||
getLocationHash,
|
||||
extractTabFromHash,
|
||||
syncTabFromHash,
|
||||
extension_data,
|
||||
getInitialShowReserved,
|
||||
showReserved,
|
||||
snack_message,
|
||||
snack_show,
|
||||
snack_success,
|
||||
configDialog,
|
||||
extension_config,
|
||||
pluginMarketData,
|
||||
loadingDialog,
|
||||
showPluginInfoDialog,
|
||||
selectedPlugin,
|
||||
curr_namespace,
|
||||
updatingAll,
|
||||
readmeDialog,
|
||||
forceUpdateDialog,
|
||||
updateAllConfirmDialog,
|
||||
changelogDialog,
|
||||
getInitialListViewMode,
|
||||
isListView,
|
||||
pluginSearch,
|
||||
loading_,
|
||||
currentPage,
|
||||
dangerConfirmDialog,
|
||||
selectedDangerPlugin,
|
||||
selectedMarketInstallPlugin,
|
||||
installCompat,
|
||||
versionCompatibilityDialog,
|
||||
showUninstallDialog,
|
||||
pluginToUninstall,
|
||||
showSourceDialog,
|
||||
showSourceManagerDialog,
|
||||
sourceName,
|
||||
sourceUrl,
|
||||
customSources,
|
||||
selectedSource,
|
||||
showRemoveSourceDialog,
|
||||
sourceToRemove,
|
||||
editingSource,
|
||||
originalSourceUrl,
|
||||
extension_url,
|
||||
dialog,
|
||||
upload_file,
|
||||
uploadTab,
|
||||
showPluginFullName,
|
||||
marketSearch,
|
||||
debouncedMarketSearch,
|
||||
refreshingMarket,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
randomPluginNames,
|
||||
normalizeStr,
|
||||
toPinyinText,
|
||||
toInitials,
|
||||
marketCustomFilter,
|
||||
plugin_handler_info_headers,
|
||||
pluginHeaders,
|
||||
filteredExtensions,
|
||||
filteredPlugins,
|
||||
filteredMarketPlugins,
|
||||
sortedPlugins,
|
||||
RANDOM_PLUGINS_COUNT,
|
||||
randomPlugins,
|
||||
shufflePlugins,
|
||||
refreshRandomPlugins,
|
||||
displayItemsPerPage,
|
||||
totalPages,
|
||||
paginatedPlugins,
|
||||
updatableExtensions,
|
||||
toggleShowReserved,
|
||||
toast,
|
||||
resetLoadingDialog,
|
||||
onLoadingDialogResult,
|
||||
failedPluginsDict,
|
||||
getExtensions,
|
||||
handleReloadAllFailed,
|
||||
checkUpdate,
|
||||
uninstallExtension,
|
||||
handleUninstallConfirm,
|
||||
updateExtension,
|
||||
showUpdateAllConfirm,
|
||||
confirmUpdateAll,
|
||||
cancelUpdateAll,
|
||||
confirmForceUpdate,
|
||||
updateAllExtensions,
|
||||
pluginOn,
|
||||
pluginOff,
|
||||
openExtensionConfig,
|
||||
updateConfig,
|
||||
showPluginInfo,
|
||||
reloadPlugin,
|
||||
viewReadme,
|
||||
viewChangelog,
|
||||
handleInstallPlugin,
|
||||
confirmDangerInstall,
|
||||
cancelDangerInstall,
|
||||
loadCustomSources,
|
||||
saveCustomSources,
|
||||
addCustomSource,
|
||||
openSourceManagerDialog,
|
||||
selectPluginSource,
|
||||
sourceSelectItems,
|
||||
editCustomSource,
|
||||
removeCustomSource,
|
||||
confirmRemoveSource,
|
||||
saveCustomSource,
|
||||
trimExtensionName,
|
||||
checkAlreadyInstalled,
|
||||
showVersionCompatibilityWarning,
|
||||
continueInstallIgnoringVersionWarning,
|
||||
cancelInstallOnVersionWarning,
|
||||
newExtension,
|
||||
normalizePlatformList,
|
||||
getPlatformDisplayList,
|
||||
resolveSelectedInstallPlugin,
|
||||
selectedInstallPlugin,
|
||||
checkInstallCompatibility,
|
||||
refreshPluginMarket,
|
||||
handleLocaleChange,
|
||||
searchDebounceTimer,
|
||||
} = props.state;
|
||||
|
||||
const currentSourceName = computed(() => {
|
||||
if (!selectedSource.value) {
|
||||
return tm("market.defaultSource");
|
||||
}
|
||||
const matched = customSources.value.find((s) => s.url === selectedSource.value);
|
||||
return matched?.name || tm("market.defaultSource");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-tab-item v-show="activeTab === 'market'">
|
||||
<div class="mb-6 pt-4 pb-4">
|
||||
<div class="d-flex align-center flex-wrap" style="gap: 12px">
|
||||
<h2 class="text-h2 mb-0">{{ tm("tabs.market") }}</h2>
|
||||
|
||||
<v-tooltip location="top" :text="tm('market.sourceManagement')">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
variant="tonal"
|
||||
rounded="md"
|
||||
color="primary"
|
||||
class="text-none px-2"
|
||||
@click="openSourceManagerDialog"
|
||||
>
|
||||
<v-icon size="18" class="mr-1">mdi-source-branch</v-icon>
|
||||
<span class="text-truncate" style="max-width: 180px">
|
||||
{{ currentSourceName }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-text-field
|
||||
v-model="marketSearch"
|
||||
density="compact"
|
||||
:label="tm('search.marketPlaceholder')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
hide-details
|
||||
single-line
|
||||
style="min-width: 220px; max-width: 340px"
|
||||
>
|
||||
</v-text-field>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="d-flex align-center text-caption text-medium-emphasis mt-2"
|
||||
style="color: grey; line-height: 1.4"
|
||||
>
|
||||
<v-icon size="16" class="mr-1">mdi-alert-outline</v-icon>
|
||||
<span>{{ tm("market.sourceSafetyWarning") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <small style="color: var(--v-theme-secondaryText);">每个插件都是作者无偿提供的的劳动成果。如果您喜欢某个插件,请 Star!</small> -->
|
||||
|
||||
<!-- FAB Button -->
|
||||
<v-tooltip :text="tm('market.installPlugin')" location="left">
|
||||
<template v-slot:activator="{ props }">
|
||||
<button
|
||||
v-bind="props"
|
||||
type="button"
|
||||
class="v-btn v-btn--elevated v-btn--icon v-theme--PurpleThemeDark bg-darkprimary v-btn--density-default v-btn--size-x-large v-btn--variant-elevated fab-button"
|
||||
style="
|
||||
position: fixed;
|
||||
right: 52px;
|
||||
bottom: 52px;
|
||||
z-index: 10000;
|
||||
border-radius: 16px;
|
||||
"
|
||||
@click="dialog = true"
|
||||
>
|
||||
<span class="v-btn__overlay"></span>
|
||||
<span class="v-btn__underlay"></span>
|
||||
<span class="v-btn__content" data-no-activator="">
|
||||
<i
|
||||
class="mdi-plus mdi v-icon notranslate v-theme--PurpleThemeDark v-icon--size-default"
|
||||
aria-hidden="true"
|
||||
style="font-size: 32px"
|
||||
></i>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<div class="mt-4">
|
||||
<div
|
||||
class="d-flex align-center mb-2"
|
||||
style="justify-content: space-between; flex-wrap: wrap; gap: 8px"
|
||||
>
|
||||
<h2>
|
||||
{{ tm("market.randomPlugins") }}
|
||||
</h2>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-shuffle-variant"
|
||||
:disabled="pluginMarketData.length === 0"
|
||||
@click="refreshRandomPlugins"
|
||||
>
|
||||
{{ tm("buttons.reshuffle") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-row class="mb-6" dense>
|
||||
<v-col
|
||||
v-for="plugin in randomPlugins"
|
||||
:key="`random-${plugin.name}`"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
class="pb-2"
|
||||
>
|
||||
<MarketPluginCard
|
||||
:plugin="plugin"
|
||||
:default-plugin-icon="defaultPluginIcon"
|
||||
:show-plugin-full-name="showPluginFullName"
|
||||
@install="handleInstallPlugin"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div
|
||||
class="d-flex align-center mb-2"
|
||||
style="
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
"
|
||||
>
|
||||
<div class="d-flex align-center" style="gap: 6px">
|
||||
<h2>
|
||||
{{ tm("market.allPlugins") }}({{
|
||||
filteredMarketPlugins.length
|
||||
}})
|
||||
</h2>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="refreshPluginMarket"
|
||||
:loading="refreshingMarket"
|
||||
>
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="d-flex align-center"
|
||||
style="gap: 8px; flex-wrap: wrap"
|
||||
>
|
||||
<v-select
|
||||
v-model="sortBy"
|
||||
:items="[
|
||||
{ title: tm('sort.default'), value: 'default' },
|
||||
{ title: tm('sort.stars'), value: 'stars' },
|
||||
{ title: tm('sort.author'), value: 'author' },
|
||||
{ title: tm('sort.updated'), value: 'updated' },
|
||||
]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template v-slot:prepend-inner>
|
||||
<v-icon size="small">mdi-sort</v-icon>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
v-if="sortBy !== 'default'"
|
||||
@click="sortOrder = sortOrder === 'desc' ? 'asc' : 'desc'"
|
||||
variant="text"
|
||||
density="compact"
|
||||
>
|
||||
<v-icon>{{
|
||||
sortOrder === "desc"
|
||||
? "mdi-sort-descending"
|
||||
: "mdi-sort-ascending"
|
||||
}}</v-icon>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{
|
||||
sortOrder === "desc"
|
||||
? tm("sort.descending")
|
||||
: tm("sort.ascending")
|
||||
}}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-row style="min-height: 26rem" dense>
|
||||
<v-col
|
||||
v-for="plugin in paginatedPlugins"
|
||||
:key="plugin.name"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
class="pb-2"
|
||||
>
|
||||
<MarketPluginCard
|
||||
:plugin="plugin"
|
||||
:default-plugin-icon="defaultPluginIcon"
|
||||
:show-plugin-full-name="showPluginFullName"
|
||||
@install="handleInstallPlugin"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="d-flex justify-center mt-4" v-if="totalPages > 1">
|
||||
<v-pagination
|
||||
v-model="currentPage"
|
||||
:length="totalPages"
|
||||
:total-visible="7"
|
||||
size="small"
|
||||
></v-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</v-tab-item>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user