mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-20 02:55:08 +08:00
* refactor(persona): replace local folder components with shared folder components * feat(webui): implement draggable reordering with animation for pinned plugins * refactor(webui): extract PinnedPluginItem into a standalone component
This commit is contained in:
183
dashboard/src/components/extension/PinnedPluginItem.vue
Normal file
183
dashboard/src/components/extension/PinnedPluginItem.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
|
||||
const props = defineProps({
|
||||
plugin: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isPinned: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tm: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
dragged: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'toggle-pin',
|
||||
'view-readme',
|
||||
'open-config',
|
||||
'reload',
|
||||
'update',
|
||||
'show-info',
|
||||
'uninstall',
|
||||
'dragstart',
|
||||
'dragover',
|
||||
'dragenter',
|
||||
'dragend',
|
||||
'drop'
|
||||
]);
|
||||
|
||||
const handlePinnedImgError = (e) => {
|
||||
e.target.src = defaultPluginIcon;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="pinned-item pinned-card-wrapper"
|
||||
:class="{ 'is-dragging': dragged }"
|
||||
style="position:relative;"
|
||||
draggable="true"
|
||||
@dragstart="$emit('dragstart')"
|
||||
@dragover.prevent="$emit('dragover', $event)"
|
||||
@dragenter.prevent="$emit('dragenter', $event)"
|
||||
@dragend="$emit('dragend', $event)"
|
||||
@drop="$emit('drop', $event)"
|
||||
>
|
||||
<v-menu offset-y>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-avatar
|
||||
v-bind="menuProps"
|
||||
size="72"
|
||||
class="pinned-avatar activator-avatar"
|
||||
:title="plugin.display_name || plugin.name"
|
||||
>
|
||||
<img
|
||||
:src="(typeof plugin.logo === 'string' && plugin.logo.trim()) ? plugin.logo : defaultPluginIcon"
|
||||
:alt="plugin.name"
|
||||
@error="handlePinnedImgError"
|
||||
/>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-text class="d-flex" style="gap:8px; padding:12px;">
|
||||
<v-tooltip location="top" :text="tm('buttons.viewDocs')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="info" @click.stop="$emit('view-readme', plugin)">
|
||||
<v-icon>mdi-book-open-page-variant</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" :text="tm('card.actions.pluginConfig')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="primary" @click.stop="$emit('open-config', plugin.name)">
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" :text="tm('card.actions.reloadPlugin')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="primary" @click.stop="$emit('reload', plugin.name)">
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" :text="tm('buttons.update')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="warning" @click.stop="$emit('update', plugin.name)">
|
||||
<v-icon>mdi-update</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" :text="tm('buttons.viewInfo')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="secondary" @click.stop="$emit('show-info', plugin)">
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" :text="tm('buttons.uninstall')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="error" @click.stop="$emit('uninstall', plugin.name)" v-if="!plugin.reserved">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
class="pinned-pin-btn"
|
||||
:color="isPinned ? 'primary' : 'secondary'"
|
||||
@click.stop="$emit('toggle-pin', plugin)"
|
||||
:title="isPinned ? tm('buttons.unpin') : tm('buttons.pin')"
|
||||
style="position:absolute; top:6px; right:6px; min-width:22px; width:22px; height:22px;"
|
||||
>
|
||||
<v-icon size="14">{{ isPinned ? 'mdi-pin' : 'mdi-pin-outline' }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pinned-avatar {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.pinned-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pinned-card-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.pinned-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.is-dragging {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
[draggable="true"] {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
[draggable="true"]:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
||||
@@ -18,26 +18,26 @@
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-folder-open</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.open }}</v-list-item-title>
|
||||
<v-list-item-title>{{ mergedLabels.open }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click.stop="$emit('rename')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-pencil</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.rename }}</v-list-item-title>
|
||||
<v-list-item-title>{{ mergedLabels.rename }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click.stop="$emit('move')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-folder-move</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.moveTo }}</v-list-item-title>
|
||||
<v-list-item-title>{{ mergedLabels.moveTo }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider class="my-1" />
|
||||
<v-list-item @click.stop="$emit('delete')" class="text-error">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small" color="error">mdi-delete</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.delete }}</v-list-item-title>
|
||||
<v-list-item-title>{{ mergedLabels.delete }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject, watch } from "vue";
|
||||
import { ref, computed, inject, watch, useAttrs } from "vue";
|
||||
import { useCustomizerStore } from "@/stores/customizer";
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
import { getPlatformDisplayName, getPlatformIcon } from "@/utils/platformUtils";
|
||||
@@ -13,6 +13,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
pinned: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
marketMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -31,6 +35,7 @@ const emit = defineEmits([
|
||||
"install",
|
||||
"uninstall",
|
||||
"toggle-activation",
|
||||
"toggle-pin",
|
||||
"view-handlers",
|
||||
"view-readme",
|
||||
"view-changelog",
|
||||
@@ -39,6 +44,8 @@ const emit = defineEmits([
|
||||
const reveal = ref(false);
|
||||
const showUninstallDialog = ref(false);
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
// 国际化
|
||||
const { tm } = useModuleI18n("features/extension");
|
||||
|
||||
@@ -114,6 +121,11 @@ 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);
|
||||
};
|
||||
@@ -130,6 +142,7 @@ const viewChangelog = () => {
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
v-bind="attrs"
|
||||
class="mx-auto d-flex flex-column h-100"
|
||||
elevation="0"
|
||||
height="100%"
|
||||
@@ -203,16 +216,34 @@ const viewChangelog = () => {
|
||||
<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>
|
||||
<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"
|
||||
></v-switch>
|
||||
</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")
|
||||
|
||||
Reference in New Issue
Block a user