mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
647 lines
17 KiB
Vue
647 lines
17 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, inject, watch, useAttrs } 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: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
pinned: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
marketMode: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
highlight: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
// 定义要发送到父组件的事件
|
|
const emit = defineEmits([
|
|
"configure",
|
|
"update",
|
|
"reload",
|
|
"install",
|
|
"uninstall",
|
|
"toggle-activation",
|
|
"toggle-pin",
|
|
"view-handlers",
|
|
"view-readme",
|
|
"view-changelog",
|
|
]);
|
|
|
|
const reveal = ref(false);
|
|
const showUninstallDialog = ref(false);
|
|
|
|
const attrs = useAttrs();
|
|
|
|
// 国际化
|
|
const { tm } = useModuleI18n("features/extension");
|
|
|
|
const supportPlatforms = computed(() => {
|
|
const platforms = props.extension?.support_platforms;
|
|
if (!Array.isArray(platforms)) {
|
|
return [];
|
|
}
|
|
return platforms.filter((item) => typeof item === "string");
|
|
});
|
|
|
|
const supportPlatformDisplayNames = computed(() =>
|
|
supportPlatforms.value.map((platformId) => getPlatformDisplayName(platformId)),
|
|
);
|
|
|
|
const astrbotVersionRequirement = computed(() => {
|
|
const versionSpec = props.extension?.astrbot_version;
|
|
return typeof versionSpec === "string" && versionSpec.trim().length
|
|
? versionSpec.trim()
|
|
: "";
|
|
});
|
|
|
|
// 作者显示(兼容多种字段名)
|
|
const authorDisplay = computed(() => {
|
|
const ext = props.extension || {};
|
|
if (typeof ext.author === 'string' && ext.author.trim()) return ext.author;
|
|
if (Array.isArray(ext.authors) && ext.authors.length) return ext.authors.join(', ');
|
|
if (typeof ext.author_name === 'string' && ext.author_name.trim()) return ext.author_name;
|
|
if (typeof ext.owner === 'string' && ext.owner.trim()) return ext.owner;
|
|
if (ext.author && typeof ext.author === 'object' && ext.author.name) return ext.author.name;
|
|
return '';
|
|
});
|
|
|
|
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);
|
|
};
|
|
|
|
const updateExtension = () => {
|
|
emit("update", props.extension);
|
|
};
|
|
|
|
const reloadExtension = () => {
|
|
emit("reload", props.extension);
|
|
};
|
|
|
|
const $confirm = inject("$confirm");
|
|
|
|
const installExtension = async () => {
|
|
emit("install", props.extension);
|
|
};
|
|
|
|
const uninstallExtension = async () => {
|
|
showUninstallDialog.value = true;
|
|
};
|
|
|
|
const handleUninstallConfirm = (options: {
|
|
deleteConfig: boolean;
|
|
deleteData: boolean;
|
|
}) => {
|
|
emit("uninstall", props.extension, options);
|
|
};
|
|
|
|
const toggleActivation = () => {
|
|
emit("toggle-activation", props.extension);
|
|
};
|
|
|
|
const togglePin = (e?: Event) => {
|
|
if (e) e.stopPropagation();
|
|
emit("toggle-pin", props.extension);
|
|
};
|
|
|
|
const viewHandlers = () => {
|
|
emit("view-handlers", props.extension);
|
|
};
|
|
|
|
const viewReadme = () => {
|
|
emit("view-readme", props.extension);
|
|
};
|
|
|
|
const viewChangelog = () => {
|
|
emit("view-changelog", props.extension);
|
|
};
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<v-card
|
|
v-bind="attrs"
|
|
class="mx-auto d-flex flex-column h-100"
|
|
elevation="0"
|
|
height="100%"
|
|
:style="{
|
|
position: 'relative',
|
|
backgroundColor:
|
|
!useCustomizerStore().isDarkTheme
|
|
? marketMode
|
|
? '#f8f0dd'
|
|
: '#ffffff'
|
|
: '#282833',
|
|
color:
|
|
!useCustomizerStore().isDarkTheme
|
|
? '#000000dd'
|
|
: '#ffffff',
|
|
}"
|
|
>
|
|
<v-card-text
|
|
style="
|
|
padding: 16px;
|
|
padding-bottom: 0px;
|
|
width: 100%;
|
|
"
|
|
>
|
|
<div style="overflow-x: auto; width: 100%">
|
|
<div style="width: 100%; margin-bottom: 24px">
|
|
<div class="extension-title-row">
|
|
<p
|
|
class="text-h3 font-weight-black extension-title"
|
|
:class="{ 'text-h4': $vuetify.display.xs }"
|
|
>
|
|
<v-tooltip
|
|
location="top"
|
|
:text="
|
|
extension.display_name?.length &&
|
|
extension.display_name !== extension.name
|
|
? `${extension.display_name} (${extension.name})`
|
|
: extension.name
|
|
"
|
|
>
|
|
<template #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
|
|
v-if="extension?.has_update && !marketMode"
|
|
location="top"
|
|
>
|
|
<template #activator="{ props: tooltipProps }">
|
|
<v-icon
|
|
v-bind="tooltipProps"
|
|
color="warning"
|
|
class="ml-2"
|
|
icon="mdi-update"
|
|
size="small"
|
|
style="cursor: pointer"
|
|
@click.stop="updateExtension"
|
|
/>
|
|
</template>
|
|
<span>{{ tm("card.status.hasUpdate") }}:
|
|
{{ extension.online_version }}</span>
|
|
</v-tooltip>
|
|
</p>
|
|
|
|
<template v-if="!marketMode">
|
|
<v-tooltip location="left">
|
|
<template #activator="{ props: tooltipProps }">
|
|
<div
|
|
class="extension-switch-wrap"
|
|
@click.stop
|
|
>
|
|
<div
|
|
v-bind="tooltipProps"
|
|
style="display:inline-flex; align-items:center;"
|
|
>
|
|
<v-switch
|
|
:model-value="extension.activated"
|
|
color="success"
|
|
density="compact"
|
|
hide-details
|
|
inset
|
|
@update:model-value="toggleActivation"
|
|
/>
|
|
</div>
|
|
|
|
<v-tooltip
|
|
location="top"
|
|
:text="pinned ? tm('buttons.unpin') : tm('buttons.pin')"
|
|
>
|
|
<template #activator="{ props: pinProps }">
|
|
<v-btn
|
|
v-bind="pinProps"
|
|
icon
|
|
size="small"
|
|
variant="tonal"
|
|
:color="pinned ? 'primary' : 'secondary'"
|
|
class="ml-2"
|
|
@click.stop="togglePin"
|
|
>
|
|
<v-icon size="18">
|
|
{{ pinned ? 'mdi-pin' : 'mdi-pin-outline' }}
|
|
</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
</v-tooltip>
|
|
</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 #activator="{ props: menuProps }">
|
|
<v-btn
|
|
v-if="extension?.repo"
|
|
icon
|
|
variant="text"
|
|
aria-label="more"
|
|
:href="extension?.repo"
|
|
target="_blank"
|
|
>
|
|
<v-icon icon="mdi-github" />
|
|
</v-btn>
|
|
<v-btn
|
|
v-bind="menuProps"
|
|
icon
|
|
variant="text"
|
|
aria-label="more"
|
|
>
|
|
<v-icon icon="mdi-dots-vertical" />
|
|
</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="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
|
|
/>
|
|
{{ extension.version }}
|
|
</v-chip>
|
|
<v-chip
|
|
v-if="extension?.has_update"
|
|
color="warning"
|
|
label
|
|
size="small"
|
|
style="cursor: pointer"
|
|
@click="updateExtension"
|
|
>
|
|
<v-icon
|
|
icon="mdi-arrow-up-bold"
|
|
start
|
|
/>
|
|
{{ extension.online_version }}
|
|
</v-chip>
|
|
<v-chip
|
|
v-if="extension.handlers?.length"
|
|
color="primary"
|
|
label
|
|
size="small"
|
|
style="cursor: pointer"
|
|
@click="viewHandlers"
|
|
>
|
|
<v-icon
|
|
icon="mdi-cogs"
|
|
start
|
|
/>
|
|
{{ 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="authorDisplay" color="info" label size="small">
|
|
<v-icon icon="mdi-account" start></v-icon>
|
|
{{ authorDisplay }}
|
|
</v-chip>
|
|
<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"
|
|
@click.stop
|
|
>
|
|
<template v-if="!marketMode">
|
|
<v-spacer />
|
|
<v-tooltip
|
|
location="top"
|
|
:text="tm('buttons.viewDocs')"
|
|
>
|
|
<template #activator="{ props: actionProps }">
|
|
<v-btn
|
|
v-bind="actionProps"
|
|
icon="mdi-book-open-page-variant"
|
|
size="small"
|
|
variant="tonal"
|
|
color="info"
|
|
@click="viewReadme"
|
|
/>
|
|
</template>
|
|
</v-tooltip>
|
|
|
|
<v-tooltip
|
|
location="top"
|
|
:text="tm('card.actions.pluginConfig')"
|
|
>
|
|
<template #activator="{ props: actionProps }">
|
|
<v-btn
|
|
v-bind="actionProps"
|
|
icon="mdi-cog"
|
|
size="small"
|
|
variant="tonal"
|
|
color="primary"
|
|
@click="configure"
|
|
/>
|
|
</template>
|
|
</v-tooltip>
|
|
|
|
<v-tooltip
|
|
v-if="extension?.repo"
|
|
location="top"
|
|
:text="tm('buttons.viewRepo')"
|
|
>
|
|
<template #activator="{ props: actionProps }">
|
|
<v-btn
|
|
v-bind="actionProps"
|
|
icon="mdi-github"
|
|
size="small"
|
|
variant="tonal"
|
|
color="secondary"
|
|
:href="extension.repo"
|
|
target="_blank"
|
|
/>
|
|
</template>
|
|
</v-tooltip>
|
|
|
|
<v-tooltip
|
|
location="top"
|
|
:text="tm('card.actions.reloadPlugin')"
|
|
>
|
|
<template #activator="{ props: actionProps }">
|
|
<v-btn
|
|
v-bind="actionProps"
|
|
icon="mdi-refresh"
|
|
size="small"
|
|
variant="tonal"
|
|
color="primary"
|
|
@click="reloadExtension"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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>
|
|
|
|
<!-- 卸载确认对话框 -->
|
|
<UninstallConfirmDialog
|
|
v-model="showUninstallDialog"
|
|
@confirm="handleUninstallConfirm"
|
|
/>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.extension-image-container {
|
|
display: flex;
|
|
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 {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.extension-switch-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.extension-switch-wrap ::v-deep(.v-switch) {
|
|
margin: 0;
|
|
}
|
|
|
|
.extension-market-menu-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.extension-content-row {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.extension-logo {
|
|
width: 64px;
|
|
height: 64px;
|
|
}
|
|
}
|
|
|
|
.extension-actions {
|
|
margin-top: auto;
|
|
gap: 8px;
|
|
justify-content: flex-end;
|
|
}
|
|
</style>
|