feat: 优化了插件卡片的 UI,插件卡片支持显示 logo

This commit is contained in:
Soulter
2025-03-08 17:13:36 +08:00
parent 52c868828c
commit 179eb5d847
5 changed files with 212 additions and 76 deletions

View File

@@ -207,7 +207,7 @@ class PluginManager:
await self._unbind_plugin(smd.name, specified_module_path)
await self.load(specified_module_path)
return await self.load(specified_module_path)
async def load(self, specified_module_path=None, specified_dir_name=None):
"""载入插件。

View File

@@ -1,32 +1,204 @@
<script setup lang="ts">
import { defineProps, defineEmits, ref, computed } from 'vue';
const props = defineProps({
title: String,
link: String,
logo: String,
has_update: Boolean,
activated: Boolean,
extension: {
type: Object,
required: true,
},
marketMode: {
type: Boolean,
default: false,
},
});
// 定义要发送到父组件的事件
const emit = defineEmits([
'configure',
'update',
'reload',
'install',
'uninstall',
'toggle-activation',
'view-handlers'
]);
const open = (link: string | undefined) => {
window.open(link, '_blank');
};
const reveal = ref(false);
// 检查是否有更新可用
const hasUpdate = computed(() => {
if (!props.extension.online_version || !props.extension.version) return false;
return props.extension.online_version !== props.extension.version;
});
// 操作函数
const configure = () => {
emit('configure', props.extension);
};
const updateExtension = () => {
emit('update', props.extension);
};
const reloadExtension = () => {
emit('reload', props.extension);
};
const uninstallExtension = () => {
emit('uninstall', props.extension);
};
const toggleActivation = () => {
emit('toggle-activation', props.extension);
};
const viewHandlers = () => {
emit('view-handlers', props.extension);
};
</script>
<template>
<v-card variant="outlined" elevation="0" class="withbg">
<v-card-item style="padding: 10px 12px">
<div class="d-sm-flex align-center justify-space-between">
<img v-if="logo" :src="logo" alt="logo" style="width: 40px; height: 40px; margin-right: 8px;">
<v-card-title style="font-size: 15px; max-width: 70%">{{ props.title }}</v-card-title>
<v-spacer></v-spacer>
<v-icon color="success" v-if="!activated">mdi-cancel</v-icon>
<v-icon color="success" v-if="has_update">mdi-arrow-up-bold</v-icon>
<v-btn size="small" text="Read" variant="flat" border @click="open(props.link)">帮助</v-btn>
<v-card class="mx-auto d-flex flex-column" :style="{ height: $vuetify.display.xs ? '250px' : '220px' }">
<v-card-text style="padding: 16px; padding-bottom: 0px; display: flex; justify-content: space-between;">
<div class="flex-grow-1">
<div>{{ extension.author }} /</div>
<p class="text-h3 font-weight-black" :class="{ 'text-h4': $vuetify.display.xs }">
{{ extension.name }}
<v-tooltip location="top" v-if="hasUpdate && !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>有新版本可用: {{ 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>该插件已经被禁用</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="hasUpdate" 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">
<v-icon icon="mdi-cogs" start></v-icon>
{{ extension.handlers?.length }}个行为
</v-chip>
</div>
<div class="text-medium-emphasis mt-2" :class="{ 'text-caption': $vuetify.display.xs }">
{{ extension.desc }}
</div>
</div>
<div class="extension-image-container" v-if="extension.logo">
<img
:src="extension.logo"
:style="{
height: $vuetify.display.xs ? '75px' : '100px',
width: $vuetify.display.xs ? '75px' : '100px',
borderRadius: '8px',
objectFit: 'cover',
objectPosition: 'center'
}"
alt="logo"
/>
</div>
</v-card-item>
<v-divider></v-divider>
<v-card-text style="padding: 16px;">
<slot />
</v-card-text>
<v-card-actions style="padding: 0px; margin-top: auto;">
<v-btn color="teal-accent-4" text="帮助" variant="text" @click="open(extension.repo)"></v-btn>
<v-btn v-if="!marketMode" color="teal-accent-4" text="操作" variant="text" @click="reveal = true"></v-btn>
<v-btn v-if="marketMode && !extension?.installed" color="teal-accent-4" text="安装" variant="text" @click="emit('install', extension)"></v-btn>
<v-btn v-if="marketMode && extension?.installed" color="teal-accent-4" text="已安装" variant="text" disabled></v-btn>
</v-card-actions>
<v-expand-transition v-if="!marketMode">
<v-card v-if="reveal" class="position-absolute w-100" height="100%"
style="bottom: 0; display: flex; flex-direction: column;">
<v-card-text style="overflow-y: auto;">
<div class="d-flex align-center mb-4">
<img
v-if="extension.logo"
:src="extension.logo"
style="height: 50px; width: 50px; border-radius: 8px; margin-right: 16px;"
alt="扩展图标"
/>
<h3>{{ extension.name }}</h3>
</div>
<div class="mt-4" :style="{
justifyContent: 'center',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: '8px',
flexDirection: $vuetify.display.xs ? 'column' : 'row'
}">
<v-btn prepend-icon="mdi-cog" color="primary" variant="tonal" @click="configure"
:block="$vuetify.display.xs">
插件配置
</v-btn>
<v-btn prepend-icon="mdi-delete" color="error" variant="tonal" @click="uninstallExtension"
:block="$vuetify.display.xs">
卸载插件
</v-btn>
<v-btn prepend-icon="mdi-reload" color="primary" variant="tonal" @click="reloadExtension"
:block="$vuetify.display.xs">
重载插件
</v-btn>
<v-btn :prepend-icon="extension.activated ? 'mdi-cancel' : 'mdi-check-circle'"
:color="extension.activated ? 'error' : 'success'" variant="tonal" @click="toggleActivation"
:block="$vuetify.display.xs">
{{ extension.activated ? '禁用' : '启用' }}插件
</v-btn>
<v-btn prepend-icon="mdi-cogs" color="info" variant="tonal" @click="viewHandlers"
:block="$vuetify.display.xs">
查看行为 ({{ extension.handlers.length }})
</v-btn>
<v-btn prepend-icon="mdi-update" color="primary" variant="tonal" :disabled="!hasUpdate"
@click="updateExtension" :block="$vuetify.display.xs">
更新到 {{ extension.online_version || extension.version }}
</v-btn>
</div>
</v-card-text>
<v-card-actions class="pt-0 d-flex justify-center">
<v-btn color="teal-accent-4" text="返回" variant="text" @click="reveal = false"></v-btn>
</v-card-actions>
</v-card>
</v-expand-transition>
</v-card>
</template>
<style scoped>
.extension-image-container {
display: flex;
align-items: center;
margin-left: 12px;
}
@media (max-width: 600px) {
.extension-image-container {
margin-left: 8px;
}
}
</style>

View File

@@ -52,7 +52,7 @@ import { useCommonStore } from '@/stores/common';
<template v-if="isListView">
<v-col cols="12" md="12">
<v-data-table :headers="pluginMarketHeaders" :items="pluginMarketData" item-key="name" :loading="loading_"
v-model:search="marketSearch" :filter-keys="['name', 'desc']">
v-model:search="marketSearch" :filter-keys="['name', 'desc', 'author']">
<template v-slot:item.name="{ item }">
<span v-if="item?.repo"><a :href="item?.repo"
style="color: #000; text-decoration:none">{{
@@ -79,21 +79,8 @@ import { useCommonStore } from '@/stores/common';
</template>
<template v-else>
<v-row style="margin: 8px;">
<v-col cols="12" md="6" lg="3" v-for="plugin in filteredPluginMarketData">
<ExtensionCard :key="plugin.name" :title="plugin.name" :link="plugin.repo"
style="margin-bottom: 4px;">
<div style="min-height: 130px; max-height: 130px; overflow: hidden;">
<p style="font-weight: bold;">By @{{ plugin.author }}</p>
{{ plugin.desc }}
</div>
<div class="d-flex align-center gap-2">
<v-btn v-if="!plugin.installed" class="text-none mr-2" size="small" text="Read"
variant="flat" border
@click="extension_url = plugin.repo; newExtension()">安装</v-btn>
<v-btn v-else class="text-none mr-2" size="small" text="Read" variant="flat" border
disabled>已安装</v-btn>
</div>
<v-col cols="12" md="6" lg="6" v-for="plugin in filteredPluginMarketData">
<ExtensionCard :extension="plugin" market-mode="true">
</ExtensionCard>
</v-col>
</v-row>

View File

@@ -267,40 +267,14 @@ onMounted(async () => {
</div>
</v-col>
<v-col cols="12" md="6" lg="3" v-for="extension in filteredExtensions" :key="extension.name">
<ExtensionCard :title="extension.name" :link="extension.repo" :logo="extension?.logo"
:has_update="extension.has_update" style="margin-bottom: 4px;" :activated="extension.activated">
<div style="min-height: 140px; max-height: 140px; overflow: auto;">
<div>
<span style="font-weight: bold;">By @{{ extension.author }}</span>
<span> | {{ extension.handlers.length }} 个行为</span>
</div>
<div>
当前: <v-chip size="small" color="primary">{{ extension.version }}</v-chip>
<span v-if="extension.online_version">
| 最新: <v-chip size="small" color="primary">{{ extension.online_version }}</v-chip>
</span>
<span v-if="extension.has_update" style="font-weight: bold;">有更新</span>
</div>
<p style="margin-top: 8px;">{{ extension.desc }}</p>
<a style="font-size: 12px; cursor: pointer; text-decoration: underline; color: #555;"
@click="reloadPlugin(extension.name)">重载插件</a>
</div>
<div class="d-flex align-center gap-2" style="overflow-x: auto;">
<v-btn v-if="!extension.reserved" class="text-none mr-2" size="small" variant="flat" border
@click="openExtensionConfig(extension.name)">配置</v-btn>
<v-btn v-if="!extension.reserved" class="text-none mr-2" size="small" variant="flat" border
@click="updateExtension(extension.name)">更新</v-btn>
<v-btn v-if="!extension.reserved" class="text-none mr-2" size="small" variant="flat" border
@click="uninstallExtension(extension.name)">卸载</v-btn>
<v-btn class="text-none mr-2" size="small" variant="flat" border v-if="extension.activated"
@click="pluginOff(extension)">禁用</v-btn>
<v-btn class="text-none mr-2" size="small" variant="flat" border v-else
@click="pluginOn(extension)">启用</v-btn>
<v-btn class="text-none mr-2" size="small" variant="flat" border
@click="showPluginInfo(extension)">行为</v-btn>
</div>
<v-col cols="10" md="6" lg="6" v-for="extension in filteredExtensions" :key="extension.name">
<ExtensionCard :extension="extension"
@configure="openExtensionConfig(extension.name)"
@uninstall="uninstallExtension(extension.name)"
@update="updateExtension(extension.name)"
@reload="reloadPlugin(extension.name)"
@toggle-activation="extension.activated ? pluginOff(extension) : pluginOn(extension)"
@view-handlers="showPluginInfo(extension)">
</ExtensionCard>
</v-col>
</v-row>

View File

@@ -71,16 +71,14 @@ class Main(star.Star):
dashboard_version = await get_dashboard_version()
msg = f"""AstrBot v{VERSION}(WebUI: {dashboard_version})
AstrBot 指令:
内置指令:
[System]
/plugin: 查看插件、插件帮助
/t2i: 开关文本转图片
/tts: 开关文本转语音
/sid: 获取会话 ID
/op <admin_id>: 授权管理员(op)
/deop <admin_id>: 取消管理员(op)
/wl <sid>: 添加白名单(op)
/dwl <sid>: 删除白名单(op)
/op: 管理员
/wl: 白名单
/dashboard_update: 更新管理面板(op)
/alter_cmd: 设置指令权限(op)
@@ -269,8 +267,11 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("op")
async def op(self, event: AstrMessageEvent, admin_id: str):
async def op(self, event: AstrMessageEvent, admin_id: str=None):
"""授权管理员。op <admin_id>"""
if admin_id is None:
event.set_result(MessageEventResult().message("使用方法: /op <id> 授权管理员;/deop <id> 取消管理员。可通过 /sid 获取 ID。"))
return
self.context.get_config()["admins_id"].append(admin_id)
self.context.get_config().save_config()
event.set_result(MessageEventResult().message("授权成功。"))
@@ -290,8 +291,10 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("wl")
async def wl(self, event: AstrMessageEvent, sid: str):
async def wl(self, event: AstrMessageEvent, sid: str=None):
"""添加白名单。wl <sid>"""
if sid is None:
event.set_result(MessageEventResult().message("使用方法: /wl <id> 添加白名单;/dwl <id> 删除白名单。可通过 /sid 获取 ID。"))
self.context.get_config()["platform_settings"]["id_whitelist"].append(sid)
self.context.get_config().save_config()
event.set_result(MessageEventResult().message("添加白名单成功。"))