Merge branch 'master' into reply-bot-waking
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<p align="center">
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</p>
|
||||
|
||||
|
||||
@@ -32,31 +32,31 @@ class DingtalkMessageEvent(AstrMessageEvent):
|
||||
)
|
||||
elif isinstance(segment, Comp.Image):
|
||||
markdown_str = ""
|
||||
if segment.file and segment.file.startswith("file:///"):
|
||||
logger.warning(
|
||||
"dingtalk only support url image, not: " + segment.file
|
||||
)
|
||||
continue
|
||||
elif segment.file and segment.file.startswith("http"):
|
||||
markdown_str += f"\n\n"
|
||||
elif segment.file and segment.file.startswith("base64://"):
|
||||
logger.warning("dingtalk only support url image, not base64")
|
||||
continue
|
||||
else:
|
||||
logger.warning(
|
||||
"dingtalk only support url image, not: " + segment.file
|
||||
)
|
||||
continue
|
||||
|
||||
ret = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
client.reply_markdown,
|
||||
"😄",
|
||||
markdown_str,
|
||||
self.message_obj.raw_message,
|
||||
)
|
||||
logger.debug(f"send image: {ret}")
|
||||
try:
|
||||
if not segment.file:
|
||||
logger.warning("钉钉图片 segment 缺少 file 字段,跳过")
|
||||
continue
|
||||
if segment.file.startswith(("http://", "https://")):
|
||||
image_url = segment.file
|
||||
else:
|
||||
image_url = await segment.register_to_file_service()
|
||||
|
||||
markdown_str = f"\n\n"
|
||||
|
||||
ret = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
client.reply_markdown,
|
||||
"😄",
|
||||
markdown_str,
|
||||
self.message_obj.raw_message,
|
||||
)
|
||||
logger.debug(f"send image: {ret}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"钉钉图片处理失败: {e}")
|
||||
logger.warning(f"跳过图片发送: {image_path}")
|
||||
continue
|
||||
async def send(self, message: MessageChain):
|
||||
await self.send_with_client(self.client, message)
|
||||
await super().send(message)
|
||||
|
||||
@@ -652,7 +652,11 @@ class PluginManager:
|
||||
|
||||
plugin_info = None
|
||||
if plugin:
|
||||
plugin_info = {"repo": plugin.repo, "readme": cleaned_content}
|
||||
plugin_info = {
|
||||
"repo": plugin.repo,
|
||||
"readme": cleaned_content,
|
||||
"name": plugin.name,
|
||||
}
|
||||
|
||||
return plugin_info
|
||||
|
||||
@@ -847,6 +851,10 @@ class PluginManager:
|
||||
|
||||
plugin_info = None
|
||||
if plugin:
|
||||
plugin_info = {"repo": plugin.repo, "readme": readme_content}
|
||||
plugin_info = {
|
||||
"repo": plugin.repo,
|
||||
"readme": readme_content,
|
||||
"name": plugin.name,
|
||||
}
|
||||
|
||||
return plugin_info
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img width="110" src="@/assets/images/astrbot_logo_mini.webp" alt="AstrBot Logo">
|
||||
</div>
|
||||
<div class="logo-text">
|
||||
<h2 class="text-secondary">{{ title }}</h2>
|
||||
<h2 :style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'}">{{ title }}</h2>
|
||||
<!-- 父子组件传递css变量可能会出错,暂时使用十六进制颜色值 -->
|
||||
<h4 :style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000aa' : '#ffffffcc'}"
|
||||
class="hint-text">{{ subtitle }}</h4>
|
||||
|
||||
@@ -160,25 +160,23 @@ function endDrag() {
|
||||
width="220"
|
||||
:rail="customizer.mini_sidebar"
|
||||
>
|
||||
<v-list class="pa-4 listitem" style="height: auto;">
|
||||
<template v-for="(item, i) in sidebarMenu" :key="i">
|
||||
<NavItem :item="item" class="leftPadding" />
|
||||
</template>
|
||||
</v-list>
|
||||
<div style="position: absolute; bottom: 16px; width: 100%; font-size: 13px;" class="text-center">
|
||||
<v-btn style="margin-bottom: 8px;" size="small" variant="primary" v-if="!customizer.mini_sidebar" to="/settings">
|
||||
🔧 设置
|
||||
</v-btn>
|
||||
<br/>
|
||||
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" v-if="!customizer.mini_sidebar" @click="toggleIframe">
|
||||
官方文档
|
||||
</v-btn>
|
||||
<br/>
|
||||
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" v-if="!customizer.mini_sidebar" @click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
|
||||
GitHub
|
||||
</v-btn>
|
||||
<br/>
|
||||
|
||||
<div class="sidebar-container">
|
||||
<v-list class="pa-4 listitem flex-grow-1">
|
||||
<template v-for="(item, i) in sidebarMenu" :key="i">
|
||||
<NavItem :item="item" class="leftPadding" />
|
||||
</template>
|
||||
</v-list>
|
||||
<div class="sidebar-footer" v-if="!customizer.mini_sidebar">
|
||||
<v-btn style="margin-bottom: 8px;" size="small" variant="primary" to="/settings">
|
||||
🔧 设置
|
||||
</v-btn>
|
||||
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" @click="toggleIframe">
|
||||
官方文档
|
||||
</v-btn>
|
||||
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" @click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
|
||||
GitHub
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
|
||||
|
||||
@@ -41,22 +41,17 @@ const sidebarItem: menu[] = [
|
||||
to: '/config',
|
||||
},
|
||||
{
|
||||
title: '插件管理',
|
||||
title: '插件',
|
||||
icon: 'mdi-puzzle',
|
||||
to: '/extension'
|
||||
},
|
||||
{
|
||||
title: '插件市场',
|
||||
icon: 'mdi-storefront',
|
||||
to: '/extension-marketplace'
|
||||
},
|
||||
{
|
||||
title: '聊天',
|
||||
icon: 'mdi-chat',
|
||||
to: '/chat'
|
||||
},
|
||||
{
|
||||
title: '对话数据库',
|
||||
title: '对话数据',
|
||||
icon: 'mdi-database',
|
||||
to: '/conversation'
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ const MainRoutes = {
|
||||
{
|
||||
name: 'ExtensionMarketplace',
|
||||
path: '/extension-marketplace',
|
||||
component: () => import('@/views/ExtensionMarketplace.vue')
|
||||
component: () => import('@/views/ExtensionPage.vue')
|
||||
},
|
||||
{
|
||||
name: 'Platforms',
|
||||
|
||||
@@ -10,6 +10,69 @@
|
||||
.v-field__outline {
|
||||
color: rgb(var(--v-theme-inputBorder));
|
||||
}
|
||||
|
||||
// 亮色主题样式
|
||||
.v-theme--PurpleTheme .v-text-field .v-field__outline {
|
||||
--v-field-border-width: 2px !important;
|
||||
--v-field-border-opacity: 1 !important;
|
||||
color: rgba(149, 117, 205, 0.6) !important;
|
||||
border-color: rgba(149, 117, 205, 0.6) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleTheme .v-text-field:hover .v-field__outline {
|
||||
--v-field-border-width: 2px !important;
|
||||
--v-field-border-opacity: 1 !important;
|
||||
color: rgba(149, 117, 205, 0.6) !important;
|
||||
border-color: rgba(149, 117, 205, 0.6) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleTheme .v-text-field .v-field--focused .v-field__outline {
|
||||
--v-field-border-width: 2.5px !important;
|
||||
--v-field-border-opacity: 1 !important;
|
||||
color: rgba(149, 117, 205, 0.8) !important;
|
||||
border-color: rgba(149, 117, 205, 0.8) !important;
|
||||
}
|
||||
|
||||
// 深色主题样式
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field {
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field__outline {
|
||||
--v-field-border-width: 2px !important;
|
||||
--v-field-border-opacity: 1 !important;
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
border-color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field:hover .v-field__outline {
|
||||
--v-field-border-width: 2px !important;
|
||||
--v-field-border-opacity: 1 !important;
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
border-color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field--focused .v-field__outline {
|
||||
--v-field-border-width: 2.5px !important;
|
||||
--v-field-border-opacity: 1 !important;
|
||||
color: rgb(129, 102, 176) !important;
|
||||
border-color: rgb(126, 99, 171) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field input {
|
||||
color: #ffffff !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field__label {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field__prepend-inner .v-icon,
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field__append-inner .v-icon {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
}
|
||||
|
||||
.inputWithbg {
|
||||
.v-field--variant-outlined {
|
||||
background-color: rgba(0, 0, 0, 0.025);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.listitem {
|
||||
height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
.v-list {
|
||||
color: rgb(var(--v-theme-secondaryText));
|
||||
}
|
||||
@@ -16,6 +16,39 @@
|
||||
color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深色主题下的侧边栏悬停和选中样式
|
||||
.v-theme--PurpleThemeDark .leftSidebar {
|
||||
.listitem {
|
||||
.v-list-group__items .v-list-item,
|
||||
.v-list-item {
|
||||
&:hover {
|
||||
color: #b794f6 !important;
|
||||
|
||||
.v-list-item-title {
|
||||
color: #b794f6 !important;
|
||||
}
|
||||
|
||||
.v-icon {
|
||||
color: #b794f6 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 选中状态的样式
|
||||
&.v-list-item--active {
|
||||
color: #b794f6 !important;
|
||||
|
||||
.v-list-item-title {
|
||||
color: #b794f6 !important;
|
||||
}
|
||||
|
||||
.v-icon {
|
||||
color: #b794f6 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-list-item--density-default.v-list-item--one-line {
|
||||
min-height: 42px;
|
||||
}
|
||||
@@ -52,3 +85,34 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新的flex布局样式
|
||||
.leftSidebar {
|
||||
.sidebar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.flex-grow-1 {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 16px;
|
||||
background: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.v-btn {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<script setup>
|
||||
import ChatPage from './ChatPage.vue';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
const customizer = useCustomizerStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div id="container">
|
||||
<ChatPage chatbox-mode="true"></ChatPage>
|
||||
<v-app :theme="customizer.uiTheme" style="height: 100%; width: 100%;">
|
||||
<div
|
||||
style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div id="container">
|
||||
<ChatPage chatbox-mode="true"></ChatPage>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -4,6 +4,7 @@ import axios from 'axios';
|
||||
import { marked } from 'marked';
|
||||
import { ref } from 'vue';
|
||||
import { defineProps } from 'vue';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true
|
||||
@@ -39,13 +40,16 @@ const props = defineProps({
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px; padding-top: 8px;">
|
||||
<v-btn rounded="lg" class="new-chat-btn" @click="newC" :disabled="!currCid"
|
||||
v-if="!sidebarCollapsed" prepend-icon="mdi-plus">创建对话</v-btn>
|
||||
<v-btn block variant="text" class="new-chat-btn" @click="newC" :disabled="!currCid"
|
||||
v-if="!sidebarCollapsed" prepend-icon="mdi-plus" style="box-shadow: 0 1px 2px rgba(0,0,0,0.1); background-color: transparent !important; border-radius: 4px;">创建对话</v-btn>
|
||||
<v-btn icon="mdi-plus" rounded="lg" @click="newC" :disabled="!currCid" v-if="sidebarCollapsed"
|
||||
elevation="0"></v-btn>
|
||||
</div>
|
||||
<div v-if="!sidebarCollapsed">
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
</div>
|
||||
|
||||
<div style="overflow-y: auto;" :class="{ 'fade-in': sidebarHoverExpanded }"
|
||||
<div style="overflow-y: auto; flex-grow: 1;" class="sidebar-panel" :class="{ 'fade-in': sidebarHoverExpanded }"
|
||||
v-if="!sidebarCollapsed">
|
||||
<v-card class="conversation-list-card" v-if="conversations.length > 0" flat>
|
||||
<v-list density="compact" nav class="conversation-list"
|
||||
@@ -75,14 +79,17 @@ const props = defineProps({
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px; padding-bottom: 0px;" :class="{ 'fade-in': sidebarHoverExpanded }"
|
||||
<div v-if="!sidebarCollapsed">
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
</div>
|
||||
<div style="padding: 16px;" :class="{ 'fade-in': sidebarHoverExpanded }"
|
||||
v-if="!sidebarCollapsed">
|
||||
<div class="sidebar-section-title">
|
||||
系统状态
|
||||
</div>
|
||||
<div class="status-chips">
|
||||
<v-chip class="status-chip" :color="status?.llm_enabled ? 'primary' : 'grey-lighten-2'"
|
||||
variant="elevated" size="small">
|
||||
variant="outlined" size="small" rounded="sm">
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="status?.llm_enabled ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
size="x-small"></v-icon>
|
||||
@@ -91,7 +98,7 @@ const props = defineProps({
|
||||
</v-chip>
|
||||
|
||||
<v-chip class="status-chip" :color="status?.stt_enabled ? 'success' : 'grey-lighten-2'"
|
||||
variant="elevated" size="small">
|
||||
variant="outlined" size="small" rounded="sm">
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="status?.stt_enabled ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
size="x-small"></v-icon>
|
||||
@@ -100,11 +107,22 @@ const props = defineProps({
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-btn variant="tonal" rounded="lg" class="delete-chat-btn" v-if="currCid"
|
||||
@click="deleteConversation(currCid)" color="error" density="comfortable" size="small">
|
||||
<v-icon start size="small">mdi-delete</v-icon>
|
||||
删除此对话
|
||||
</v-btn>
|
||||
<transition
|
||||
name="expand"
|
||||
@before-enter="beforeEnter"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@before-leave="beforeLeave"
|
||||
@leave="leave"
|
||||
>
|
||||
<div v-if="currCid" class="delete-btn-container">
|
||||
<v-btn variant="outlined" rounded="sm" class="delete-chat-btn"
|
||||
@click="deleteConversation(currCid)" color="error" density="comfortable" size="small">
|
||||
<v-icon start size="small">mdi-delete</v-icon>
|
||||
删除此对话
|
||||
</v-btn>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -118,8 +136,27 @@ const props = defineProps({
|
||||
</div>
|
||||
<div class="conversation-header-actions">
|
||||
<!-- router 推送到 /chatbox -->
|
||||
<v-icon @click="router.push('/chatbox')" v-if="!props.chatboxMode"
|
||||
class="fullscreen-icon">mdi-fullscreen</v-icon>
|
||||
<v-tooltip text="全屏模式" v-if="!props.chatboxMode">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" @click="router.push(currCid ? `/chatbox/${currCid}` : '/chatbox')"
|
||||
class="fullscreen-icon">mdi-fullscreen</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<!-- 主题切换按钮 -->
|
||||
<v-tooltip :text="isDark ? '切换到日间模式' : '切换到夜间模式'" v-if="props.chatboxMode">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon @click="toggleTheme" class="theme-toggle-icon" variant="text">
|
||||
<v-icon>{{ isDark ? 'mdi-weather-night' : 'mdi-white-balance-sunny' }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<!-- router 推送到 /chat -->
|
||||
<v-tooltip text="退出全屏" v-if="props.chatboxMode">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" @click="router.push(currCid ? `/chat/${currCid}` : '/chat')"
|
||||
class="fullscreen-icon">mdi-fullscreen-exit</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider v-if="currCid && getCurrentConversation" class="conversation-divider"></v-divider>
|
||||
@@ -312,6 +349,9 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
isDark() {
|
||||
return useCustomizerStore().uiTheme === 'PurpleThemeDark';
|
||||
},
|
||||
// Get the current conversation from the conversations array
|
||||
getCurrentConversation() {
|
||||
if (!this.currCid) return null;
|
||||
@@ -360,6 +400,7 @@ export default {
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Theme is now handled globally by the customizer store.
|
||||
this.startListeningEvent();
|
||||
this.checkStatus();
|
||||
this.getConversations();
|
||||
@@ -401,6 +442,11 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleTheme() {
|
||||
const customizer = useCustomizerStore();
|
||||
const newTheme = customizer.uiTheme === 'PurpleTheme' ? 'PurpleThemeDark' : 'PurpleTheme';
|
||||
customizer.SET_UI_THEME(newTheme);
|
||||
},
|
||||
// 切换侧边栏折叠状态
|
||||
toggleSidebar() {
|
||||
if (this.sidebarHoverExpanded) {
|
||||
@@ -912,6 +958,23 @@ export default {
|
||||
});
|
||||
this.mediaCache = {};
|
||||
},
|
||||
|
||||
// For smooth height transition on delete button
|
||||
beforeEnter(el) {
|
||||
el.style.height = '0';
|
||||
},
|
||||
enter(el) {
|
||||
el.style.height = el.scrollHeight + 'px';
|
||||
},
|
||||
afterEnter(el) {
|
||||
el.style.height = 'auto';
|
||||
},
|
||||
beforeLeave(el) {
|
||||
el.style.height = el.scrollHeight + 'px';
|
||||
},
|
||||
leave(el) {
|
||||
el.style.height = '0';
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -967,12 +1030,18 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeInContent 0.2s ease-in forwards;
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
/* 聊天页面布局 */
|
||||
/* todo: 聊天页面背景颜色有问题 */
|
||||
.chat-page-card {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
@@ -1008,6 +1077,23 @@ export default {
|
||||
/* 防止内容溢出 */
|
||||
}
|
||||
|
||||
.sidebar-panel ::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.sidebar-panel ::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-panel ::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sidebar-panel ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 侧边栏折叠状态 */
|
||||
.sidebar-collapsed {
|
||||
max-width: 75px;
|
||||
@@ -1074,24 +1160,32 @@ export default {
|
||||
margin-bottom: 12px;
|
||||
padding-left: 4px;
|
||||
transition: opacity 0.25s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 8px;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.status-chips .v-chip {
|
||||
flex: 1 1 0;
|
||||
justify-content: center;
|
||||
opacity: 0.7; /* Make border and text slightly transparent */
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
font-size: 12px;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.delete-chat-btn {
|
||||
height: 32px !important;
|
||||
width: 100%;
|
||||
color: #d32f2f !important;
|
||||
color: rgb(var(--v-theme-error)) !important;
|
||||
font-weight: 500;
|
||||
box-shadow: none !important;
|
||||
margin-top: 8px;
|
||||
@@ -1100,10 +1194,21 @@ export default {
|
||||
font-size: 12px;
|
||||
line-height: 1.2em;
|
||||
transition: opacity 0.25s ease;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.delete-chat-btn:hover {
|
||||
background-color: rgba(211, 47, 47, 0.1) !important;
|
||||
background-color: rgba(var(--v-theme-error-rgb), 0.1) !important;
|
||||
}
|
||||
|
||||
.delete-btn-container {
|
||||
/* margin-top: -8px; */ /* Removed for better layout practices */
|
||||
}
|
||||
|
||||
.expand-enter-active,
|
||||
.expand-leave-active {
|
||||
transition: height 0.15s ease-in-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.no-conversations {
|
||||
|
||||
@@ -1,704 +0,0 @@
|
||||
<script setup>
|
||||
import ExtensionCard from '@/components/shared/ExtensionCard.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import axios from 'axios';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { marked } from 'marked';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github.css';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" md="12" v-if="announcement">
|
||||
<v-banner color="success" lines="one" :text="announcement" :stacked="false">
|
||||
</v-banner>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="12">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<div class="pl-2 pt-2 d-flex align-center pe-2">
|
||||
<h2>✨ 插件市场</h2>
|
||||
<v-btn icon size="small" style="margin-left: 8px" variant="plain" @click="jumpToPluginMarket()">
|
||||
<v-icon size="small">mdi-help</v-icon>
|
||||
<v-tooltip activator="parent" location="start" max-width="500" open-delay="500">
|
||||
<span>
|
||||
如无法显示,请单击此按钮跳转至插件市场,复制想安装插件对应的
|
||||
repo链接然后点击右下角 + 号安装,或打开链接下载压缩包安装。<br/>
|
||||
如果因为网络问题安装失败,点击设置页选择 GitHub 加速地址。或前往仓库下载压缩包然后本地上传。
|
||||
</span>
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="isListView = !isListView" size="small" style="margin-left: auto;"
|
||||
variant="plain">
|
||||
<v-icon>{{ isListView ? 'mdi-view-grid' : 'mdi-view-list' }}</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-text-field v-model="marketSearch" density="compact" label="Search"
|
||||
prepend-inner-icon="mdi-magnify" variant="solo-filled" flat hide-details
|
||||
single-line></v-text-field>
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
|
||||
<small style="color: var(--v-theme-secondaryText);">每个插件都是作者无偿提供的的劳动成果。如果您喜欢某个插件,请 Star!</small>
|
||||
<div v-if="pinnedPlugins.length > 0" class="mt-4">
|
||||
<h2>🥳 推荐</h2>
|
||||
|
||||
<v-row style="margin-top: 8px;">
|
||||
<v-col cols="12" md="6" lg="6" v-for="plugin in pinnedPlugins">
|
||||
<ExtensionCard :extension="plugin" class="h-120 rounded-lg"
|
||||
market-mode="true" :highlight="true"
|
||||
@install="extension_url=plugin.repo;
|
||||
newExtension()"
|
||||
@view-readme="open(plugin.repo)">
|
||||
</ExtensionCard>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div v-if="isListView" class="mt-4">
|
||||
<h2>📦 全部插件</h2>
|
||||
<v-switch
|
||||
v-model="showPluginFullName"
|
||||
label="显示完整名称"
|
||||
hide-details
|
||||
density="compact"
|
||||
style="margin-left: 12px"
|
||||
/>
|
||||
<v-col cols="12" md="12" style="padding: 0px;">
|
||||
<v-data-table :headers="pluginMarketHeaders" :items="pluginMarketData" item-key="name"
|
||||
:loading="loading_" v-model:search="marketSearch" :filter-keys="filterKeys">
|
||||
<template v-slot:item.name="{ item }">
|
||||
<div class="d-flex align-center" style="overflow-x: auto; scrollbar-width: thin; scrollbar-track-color: transparent;">
|
||||
<img v-if="item.logo" :src="item.logo"
|
||||
style="height: 80px; width: 80px; margin-right: 8px; border-radius: 8px; margin-top: 8px; margin-bottom: 8px;"
|
||||
alt="logo">
|
||||
<span v-if="item?.repo"><a :href="item?.repo"
|
||||
style="color: var(--v-theme-primaryText, #000); text-decoration:none">{{
|
||||
showPluginFullName ? item.name : item.trimmedName }}</a></span>
|
||||
<span v-else>{{ showPluginFullName ? item.name : item.trimmedName }}</span>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<template v-slot:item.desc="{ item }">
|
||||
<div style="font-size: 13px;">
|
||||
{{ item.desc }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:item.author="{ item }">
|
||||
<div style="font-size: 12px;">
|
||||
<span v-if="item?.social_link"><a :href="item?.social_link">{{ item.author
|
||||
}}</a></span>
|
||||
<span v-else>{{ item.author }}</span>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<template v-slot:item.stars="{ item }">
|
||||
<span>{{ item.stars }}</span>
|
||||
</template>
|
||||
<template v-slot:item.updated_at="{ item }">
|
||||
<!-- 2025-04-28T16:39:27Z -->
|
||||
<span>{{ new Date(item.updated_at).toLocaleString() }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.tags="{ item }">
|
||||
<span v-if="item.tags.length === 0">-</span>
|
||||
<v-chip v-for="tag in item.tags" :key="tag" color="primary" size="x-small">
|
||||
{{ tag }}</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn v-if="!item.installed" class="text-none mr-2" size="x-small"
|
||||
variant="flat" @click="extension_url = item.repo; newExtension()">
|
||||
<v-icon>mdi-download</v-icon></v-btn>
|
||||
<v-btn v-else class="text-none mr-2" size="x-small" variant="flat" border
|
||||
disabled><v-icon>mdi-check</v-icon></v-btn>
|
||||
<v-btn class="text-none mr-2" size="x-small" variant="flat" border
|
||||
@click="open(item.repo)"><v-icon>mdi-help</v-icon></v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</div>
|
||||
<div v-else class="mt-4">
|
||||
<h2>📦 全部插件</h2>
|
||||
<v-row style="margin-top: 16px;">
|
||||
<v-col cols="12" md="6" lg="6" v-for="plugin in filteredPluginMarketData">
|
||||
<ExtensionCard :extension="plugin" market-mode="true">
|
||||
</ExtensionCard>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
</v-card>
|
||||
|
||||
</v-col>
|
||||
|
||||
|
||||
<v-col style="margin-bottom: 16px;" cols="12" md="12">
|
||||
<small><a href="https://astrbot.app/dev/plugin.html">插件开发文档</a></small> |
|
||||
<small> <a href="https://github.com/Soulter/AstrBot_Plugins_Collection">提交插件仓库</a></small>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
<v-dialog v-model="dialog" width="700">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="mdi-plus" size="x-large" style="position: fixed; right: 52px; bottom: 52px;"
|
||||
color="darkprimary">
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="text-h5">安装插件</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<h3>从 GitHub 上在线下载</h3>
|
||||
<v-col cols="12">
|
||||
<small>请输入合法的 GitHub 仓库链接,当前仅支持
|
||||
GitHub。如:https://github.com/Soulter/astrbot_plugin_aiocqhttp</small>
|
||||
<v-text-field label="仓库链接" v-model="extension_url" variant="outlined"
|
||||
required></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<h3>从本机上传 .zip 压缩包</h3>
|
||||
<v-col cols="12">
|
||||
<small>请保证插件文件存在压缩包根目录中的第一个文件夹中(即类似于从 GitHub 仓库页上下载的 Zip 压缩包的格式)。</small>
|
||||
<v-file-input label="选择文件" v-model="upload_file" accept=".zip" outlined
|
||||
required></v-file-input>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<br>
|
||||
<small>{{ status }}</small>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue-darken-1" variant="text" @click="dialog = false">
|
||||
关闭
|
||||
</v-btn>
|
||||
<v-btn color="blue-darken-1" variant="text" :loading="loading_" @click="newExtension()">
|
||||
安装
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar :timeout="2000" elevation="24" :color="snack_success" v-model="snack_show">
|
||||
{{ snack_message }}
|
||||
</v-snackbar>
|
||||
|
||||
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
||||
|
||||
<!-- README Dialog -->
|
||||
<v-dialog v-model="readmeDialog.show" width="800" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h5">插件说明文档</span>
|
||||
<v-btn icon @click="readmeDialog.show = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="height: 70vh; overflow-y: auto;">
|
||||
<v-btn color="primary" prepend-icon="mdi-open-in-new" @click="openReadmeInNewTab()" class="mt-4">
|
||||
在GitHub中查看文档
|
||||
</v-btn>
|
||||
<div v-if="readmeDialog.content" class="markdown-body" v-html="renderMarkdown(readmeDialog.content)">
|
||||
</div>
|
||||
<div v-else-if="readmeDialog.error" class="d-flex flex-column align-center justify-center"
|
||||
style="height: 100%;">
|
||||
<v-icon size="64" color="error" class="mb-4">mdi-alert-circle-outline</v-icon>
|
||||
<p class="text-body-1 text-center mb-4">{{ readmeDialog.error }}</p>
|
||||
</div>
|
||||
<div v-else class="d-flex flex-column align-center justify-center" style="height: 100%;">
|
||||
<v-icon size="64" color="warning" class="mb-4">mdi-file-question-outline</v-icon>
|
||||
<p class="text-body-1 text-center mb-4">该插件未提供文档链接或GitHub仓库地址。<br>请查看插件市场或联系插件作者获取更多信息。</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" variant="tonal" @click="readmeDialog.show = false">
|
||||
关闭
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'ExtensionPage',
|
||||
components: {
|
||||
ExtensionCard,
|
||||
WaitingForRestart,
|
||||
ConsoleDisplayer,
|
||||
AstrBotConfig
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
extension_data: {
|
||||
"data": [],
|
||||
"message": ""
|
||||
},
|
||||
extension_url: "",
|
||||
status: "",
|
||||
dialog: false,
|
||||
snack_message: "",
|
||||
snack_show: false,
|
||||
snack_success: "success",
|
||||
loading_: false,
|
||||
upload_file: null,
|
||||
pluginMarketData: [],
|
||||
showPluginFullName: false,
|
||||
loadingDialog: {
|
||||
show: false,
|
||||
title: "加载中...",
|
||||
statusCode: 0, // 0: loading, 1: success, 2: error,
|
||||
result: ""
|
||||
},
|
||||
readmeDialog: {
|
||||
show: false,
|
||||
url: null,
|
||||
content: null,
|
||||
error: null
|
||||
},
|
||||
|
||||
announcement: "",
|
||||
isListView: true,
|
||||
pluginMarketHeaders: [
|
||||
{ title: '名称', key: 'name', maxWidth: '200px' },
|
||||
{ title: '描述', key: 'desc', maxWidth: '250px' },
|
||||
{ title: '作者', key: 'author', maxWidth: '90px' },
|
||||
{ title: 'Star数', key: 'stars', maxWidth: '80px' },
|
||||
{ title: '最近更新', key: 'updated_at', maxWidth: '100px' },
|
||||
{ title: '标签', key: 'tags', maxWidth: '100px' },
|
||||
{ title: '操作', key: 'actions', sortable: false }
|
||||
],
|
||||
marketSearch: "",
|
||||
|
||||
commonStore: useCommonStore(),
|
||||
|
||||
filterKeys: ['name', 'desc', 'author']
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredPluginMarketData() {
|
||||
if (!this.marketSearch) {
|
||||
return this.pluginMarketData;
|
||||
}
|
||||
const search = this.marketSearch.toLowerCase();
|
||||
return this.pluginMarketData.filter(plugin =>
|
||||
this.filterKeys.some(key =>
|
||||
plugin[key]?.toLowerCase().includes(search)
|
||||
));
|
||||
},
|
||||
pinnedPlugins() {
|
||||
return this.pluginMarketData.filter(plugin => plugin?.pinned);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 获取本地插件数据
|
||||
this.getExtensions();
|
||||
|
||||
// 获取插件市场数据
|
||||
this.loading_ = true
|
||||
this.commonStore.getPluginCollections().then((data) => {
|
||||
this.pluginMarketData = data;
|
||||
this.trimExtensionName();
|
||||
this.checkAlreadyInstalled();
|
||||
this.checkUpdate();
|
||||
this.loading_ = false
|
||||
}).catch((err) => {
|
||||
console.error("获取插件市场数据失败:", err);
|
||||
});
|
||||
|
||||
axios.get('https://api.soulter.top/astrbot-announcement-plugin-market').then((res) => {
|
||||
let data = res.data.data;
|
||||
this.announcement = data.text;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
open(link) {
|
||||
if (link) {
|
||||
window.open(link, '_blank');
|
||||
}
|
||||
},
|
||||
|
||||
jumpToPluginMarket() {
|
||||
window.open('https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json', '_blank');
|
||||
},
|
||||
toast(message, success) {
|
||||
this.snack_message = message;
|
||||
this.snack_show = true;
|
||||
this.snack_success = success;
|
||||
},
|
||||
resetLoadingDialog() {
|
||||
this.loadingDialog = {
|
||||
show: false,
|
||||
title: "加载中...",
|
||||
statusCode: 0,
|
||||
result: ""
|
||||
}
|
||||
},
|
||||
onLoadingDialogResult(statusCode, result, timeToClose = 2000) {
|
||||
this.loadingDialog.statusCode = statusCode;
|
||||
this.loadingDialog.result = result;
|
||||
if (timeToClose === -1) {
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.resetLoadingDialog()
|
||||
}, timeToClose);
|
||||
},
|
||||
getExtensions() {
|
||||
axios.get('/api/plugin/get').then((res) => {
|
||||
this.extension_data = res.data;
|
||||
this.trimExtensionName();
|
||||
this.checkAlreadyInstalled();
|
||||
this.checkUpdate()
|
||||
});
|
||||
},
|
||||
trimExtensionName() {
|
||||
this.pluginMarketData.forEach(plugin => {
|
||||
if (plugin.name) {
|
||||
let name = plugin.name.trim().toLowerCase();
|
||||
if (name.startsWith("astrbot_plugin_")) {
|
||||
plugin.trimmedName = name.substring(15);
|
||||
} else if (name.startsWith("astrbot_") || name.startsWith("astrbot-")) {
|
||||
plugin.trimmedName = name.substring(8);
|
||||
} else plugin.trimmedName = plugin.name;
|
||||
}
|
||||
});
|
||||
},
|
||||
checkUpdate() {
|
||||
// 创建在线插件的map
|
||||
const onlinePluginsMap = new Map();
|
||||
const onlinePluginsNameMap = new Map();
|
||||
|
||||
// 将在线插件信息存储到map中
|
||||
this.pluginMarketData.forEach(plugin => {
|
||||
if (plugin.repo) {
|
||||
onlinePluginsMap.set(plugin.repo.toLowerCase(), plugin);
|
||||
}
|
||||
onlinePluginsNameMap.set(plugin.name, plugin);
|
||||
});
|
||||
|
||||
// 遍历本地插件列表
|
||||
this.extension_data.data.forEach(extension => {
|
||||
// 通过repo或name查找在线版本
|
||||
const repoKey = extension.repo?.toLowerCase();
|
||||
const onlinePlugin = repoKey ? onlinePluginsMap.get(repoKey) : null;
|
||||
const onlinePluginByName = onlinePluginsNameMap.get(extension.name);
|
||||
const matchedPlugin = onlinePlugin || onlinePluginByName;
|
||||
|
||||
if (matchedPlugin) {
|
||||
extension.online_version = matchedPlugin.version;
|
||||
extension.has_update = extension.version !== matchedPlugin.version &&
|
||||
matchedPlugin.version !== "未知";
|
||||
} else {
|
||||
extension.has_update = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async getReadmeUrl(repoUrl) {
|
||||
// 去掉 repoUrl 末尾的斜杠
|
||||
repoUrl = repoUrl.replace(/\/+$/, '');
|
||||
|
||||
const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
||||
if (!match) {
|
||||
throw new Error("无效的 GitHub 仓库地址");
|
||||
}
|
||||
|
||||
const owner = match[1];
|
||||
const repo = match[2];
|
||||
|
||||
const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(apiUrl);
|
||||
const data = await res.json();
|
||||
|
||||
const branch = data?.default_branch || 'master';
|
||||
return `${repoUrl}/blob/${branch}/README.md`;
|
||||
} catch (error) {
|
||||
console.error("获取默认分支失败,使用 master 作为默认:", error);
|
||||
return `${repoUrl}/blob/master/README.md`;
|
||||
}
|
||||
},
|
||||
|
||||
async showReadmeDialog(res) {
|
||||
this.readmeDialog.content = null;
|
||||
this.readmeDialog.error = null;
|
||||
if (res?.data?.data?.repo) {
|
||||
this.readmeDialog.url = await this.getReadmeUrl(res.data.data.repo);
|
||||
if (res.data.data.readme) {
|
||||
this.readmeDialog.content = res.data.data.readme;
|
||||
} else {
|
||||
this.readmeDialog.error = "插件未提供README文档";
|
||||
}
|
||||
} else {
|
||||
this.readmeDialog.url = null;
|
||||
this.readmeDialog.error = "插件没有仓库信息或README文档";
|
||||
}
|
||||
this.readmeDialog.show = true;
|
||||
},
|
||||
|
||||
async newExtension() {
|
||||
if (this.extension_url === "" && this.upload_file === null) {
|
||||
this.toast("请填写插件链接或上传插件文件", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.extension_url !== "" && this.upload_file !== null) {
|
||||
this.toast("请不要同时填写插件链接和上传插件文件", "error");
|
||||
return;
|
||||
}
|
||||
this.loading_ = true;
|
||||
this.loadingDialog.show = true;
|
||||
if (this.upload_file !== null) {
|
||||
this.toast("正在从文件安装插件", "primary");
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.upload_file);
|
||||
axios.post('/api/plugin/install-upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}).then(async (res) => {
|
||||
this.loading_ = false;
|
||||
if (res.data.status === "error") {
|
||||
this.onLoadingDialogResult(2, res.data.message, -1);
|
||||
return;
|
||||
}
|
||||
this.extension_data = res.data;
|
||||
this.upload_file = "";
|
||||
this.onLoadingDialogResult(1, res.data.message);
|
||||
this.dialog = false;
|
||||
this.getExtensions();
|
||||
|
||||
await this.showReadmeDialog(res);
|
||||
}).catch((err) => {
|
||||
this.loading_ = false;
|
||||
this.onLoadingDialogResult(2, err, -1);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
this.toast("正在从链接 " + this.extension_url + " 安装插件...", "primary");
|
||||
axios.post('/api/plugin/install',
|
||||
{
|
||||
url: this.extension_url,
|
||||
proxy: localStorage.getItem('selectedGitHubProxy') || ""
|
||||
}).then(async (res) => {
|
||||
this.loading_ = false;
|
||||
this.toast(res.data.message, res.data.status === "ok" ? "success" : "error");
|
||||
if (res.data.status === "error") {
|
||||
this.onLoadingDialogResult(2, res.data.message, -1);
|
||||
return;
|
||||
}
|
||||
this.extension_data = res.data;
|
||||
this.extension_url = "";
|
||||
this.onLoadingDialogResult(1, res.data.message);
|
||||
this.dialog = false;
|
||||
this.getExtensions();
|
||||
await this.showReadmeDialog(res);
|
||||
}).catch((err) => {
|
||||
this.loading_ = false;
|
||||
this.toast("安装插件失败: " + err, "error");
|
||||
this.onLoadingDialogResult(2, err, -1);
|
||||
});
|
||||
}
|
||||
},
|
||||
checkAlreadyInstalled() {
|
||||
// 创建已安装插件的仓库和名称集合 统一格式
|
||||
const installedRepos = new Set(this.extension_data.data.map(ext => ext.repo?.toLowerCase()));
|
||||
const installedNames = new Set(this.extension_data.data.map(ext => ext.name));
|
||||
|
||||
// 遍历检查安装状态
|
||||
for (let i = 0; i < this.pluginMarketData.length; i++) {
|
||||
const plugin = this.pluginMarketData[i];
|
||||
plugin.installed = installedRepos.has(plugin.repo?.toLowerCase()) || installedNames.has(plugin.name);
|
||||
}
|
||||
|
||||
// 将已安装的插件移动到最后面
|
||||
let installed = [];
|
||||
let notInstalled = [];
|
||||
for (let i = 0; i < this.pluginMarketData.length; i++) {
|
||||
if (this.pluginMarketData[i].installed) {
|
||||
installed.push(this.pluginMarketData[i]);
|
||||
} else {
|
||||
notInstalled.push(this.pluginMarketData[i]);
|
||||
}
|
||||
}
|
||||
this.pluginMarketData = notInstalled.concat(installed);
|
||||
},
|
||||
openReadmeInNewTab() {
|
||||
if (this.readmeDialog.url) {
|
||||
window.open(this.readmeDialog.url, '_blank');
|
||||
}
|
||||
},
|
||||
renderMarkdown(content) {
|
||||
if (!content) return '';
|
||||
// Configure marked with highlight.js for syntax highlighting
|
||||
marked.setOptions({
|
||||
highlight: function (code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
},
|
||||
gfm: true, // GitHub Flavored Markdown
|
||||
breaks: true, // Convert \n to <br>
|
||||
headerIds: true, // Add id attributes to headers
|
||||
mangle: false // Don't mangle email addresses
|
||||
});
|
||||
return marked(content);
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.markdown-body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
padding: 8px 0;
|
||||
color: var(--v-theme-secondaryText);
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid var(--v-theme-border);
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--v-theme-border);
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-body p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
background-color: var(--v-theme-codeBg);
|
||||
border-radius: 3px;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
background-color: var(--v-theme-containerBg);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
padding-left: 2em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body img {
|
||||
max-width: 100%;
|
||||
margin: 8px 0;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--v-theme-background);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
padding: 0 1em;
|
||||
color: var(--v-theme-secondaryText);
|
||||
border-left: 0.25em solid var(--v-theme-border);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body a {
|
||||
color: var(--v-theme-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body table th,
|
||||
.markdown-body table td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid var(--v-theme-background);
|
||||
}
|
||||
|
||||
.markdown-body table tr {
|
||||
background-color: var(--v-theme-surface);
|
||||
border-top: 1px solid var(--v-theme-border);
|
||||
}
|
||||
|
||||
.markdown-body table tr:nth-child(2n) {
|
||||
background-color: var(--v-theme-background);
|
||||
}
|
||||
|
||||
.markdown-body hr {
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: var(--v-theme-containerBg);
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +1,16 @@
|
||||
<script setup>
|
||||
import ExtensionCard from '@/components/shared/ExtensionCard.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import ReadmeDialog from '@/components/shared/ReadmeDialog.vue';
|
||||
import axios from 'axios';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
|
||||
// 将所有状态和方法迁移到 setup 语法中
|
||||
import { ref, computed, onMounted, reactive } from 'vue';
|
||||
|
||||
|
||||
const commonStore = useCommonStore();
|
||||
const activeTab = ref('installed');
|
||||
const extension_data = reactive({
|
||||
data: [],
|
||||
message: ""
|
||||
@@ -34,7 +34,6 @@ const loadingDialog = reactive({
|
||||
const showPluginInfoDialog = ref(false);
|
||||
const selectedPlugin = ref({});
|
||||
const curr_namespace = ref("");
|
||||
const wfr = ref(null);
|
||||
|
||||
const readmeDialog = reactive({
|
||||
show: false,
|
||||
@@ -55,6 +54,14 @@ const isListView = ref(false);
|
||||
const pluginSearch = ref("");
|
||||
const loading_ = ref(false);
|
||||
|
||||
// 插件市场相关
|
||||
const extension_url = ref("");
|
||||
const dialog = ref(false);
|
||||
const upload_file = ref(null);
|
||||
const showPluginFullName = ref(false);
|
||||
const marketSearch = ref("");
|
||||
const filterKeys = ['name', 'desc', 'author'];
|
||||
|
||||
const plugin_handler_info_headers = [
|
||||
{ title: '行为类型', key: 'event_type_h' },
|
||||
{ title: '描述', key: 'desc', maxWidth: '250px' },
|
||||
@@ -72,6 +79,19 @@ const pluginHeaders = [
|
||||
{ title: '操作', key: 'actions', sortable: false, width: '220px' }
|
||||
];
|
||||
|
||||
|
||||
// 插件市场表头
|
||||
const pluginMarketHeaders = [
|
||||
{ title: '名称', key: 'name', maxWidth: '200px' },
|
||||
{ title: '描述', key: 'desc', maxWidth: '250px' },
|
||||
{ title: '作者', key: 'author', maxWidth: '90px' },
|
||||
{ title: 'Star数', key: 'stars', maxWidth: '80px' },
|
||||
{ title: '最近更新', key: 'updated_at', maxWidth: '100px' },
|
||||
{ title: '标签', key: 'tags', maxWidth: '100px' },
|
||||
{ title: '操作', key: 'actions', sortable: false }
|
||||
];
|
||||
|
||||
|
||||
// 过滤要显示的插件
|
||||
const filteredExtensions = computed(() => {
|
||||
if (!showReserved.value) {
|
||||
@@ -94,6 +114,10 @@ const filteredPlugins = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const pinnedPlugins = computed(() => {
|
||||
return pluginMarketData.value.filter(plugin => plugin?.pinned);
|
||||
});
|
||||
|
||||
// 方法
|
||||
const toggleShowReserved = () => {
|
||||
showReserved.value = !showReserved.value;
|
||||
@@ -385,6 +409,116 @@ const toggleAllPluginsForPlatform = (platformName) => {
|
||||
});
|
||||
};
|
||||
|
||||
const open = (link) => {
|
||||
if (link) {
|
||||
window.open(link, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
// 插件市场显示完整插件名称
|
||||
const trimExtensionName = () => {
|
||||
pluginMarketData.value.forEach(plugin => {
|
||||
if (plugin.name) {
|
||||
let name = plugin.name.trim().toLowerCase();
|
||||
if (name.startsWith("astrbot_plugin_")) {
|
||||
plugin.trimmedName = name.substring(15);
|
||||
} else if (name.startsWith("astrbot_") || name.startsWith("astrbot-")) {
|
||||
plugin.trimmedName = name.substring(8);
|
||||
} else plugin.trimmedName = plugin.name;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const checkAlreadyInstalled = () => {
|
||||
const installedRepos = new Set(extension_data.data.map(ext => ext.repo?.toLowerCase()));
|
||||
const installedNames = new Set(extension_data.data.map(ext => ext.name));
|
||||
|
||||
for (let i = 0; i < pluginMarketData.value.length; i++) {
|
||||
const plugin = pluginMarketData.value[i];
|
||||
plugin.installed = installedRepos.has(plugin.repo?.toLowerCase()) || installedNames.has(plugin.name);
|
||||
}
|
||||
|
||||
let installed = [];
|
||||
let notInstalled = [];
|
||||
for (let i = 0; i < pluginMarketData.value.length; i++) {
|
||||
if (pluginMarketData.value[i].installed) {
|
||||
installed.push(pluginMarketData.value[i]);
|
||||
} else {
|
||||
notInstalled.push(pluginMarketData.value[i]);
|
||||
}
|
||||
}
|
||||
pluginMarketData.value = notInstalled.concat(installed);
|
||||
};
|
||||
|
||||
const newExtension = async () => {
|
||||
if (extension_url.value === "" && upload_file.value === null) {
|
||||
toast("请填写插件链接或上传插件文件", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (extension_url.value !== "" && upload_file.value !== null) {
|
||||
toast("请不要同时填写插件链接和上传插件文件", "error");
|
||||
return;
|
||||
}
|
||||
loading_.value = true;
|
||||
loadingDialog.show = true;
|
||||
if (upload_file.value !== null) {
|
||||
toast("正在从文件安装插件", "primary");
|
||||
const formData = new FormData();
|
||||
formData.append('file', upload_file.value);
|
||||
axios.post('/api/plugin/install-upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}).then(async (res) => {
|
||||
loading_.value = false;
|
||||
if (res.data.status === "error") {
|
||||
onLoadingDialogResult(2, res.data.message, -1);
|
||||
return;
|
||||
}
|
||||
upload_file.value = null;
|
||||
onLoadingDialogResult(1, res.data.message);
|
||||
dialog.value = false;
|
||||
await getExtensions();
|
||||
|
||||
viewReadme({
|
||||
name: res.data.data.name,
|
||||
repo: res.data.data.repo || null
|
||||
});
|
||||
}).catch((err) => {
|
||||
loading_.value = false;
|
||||
onLoadingDialogResult(2, err, -1);
|
||||
});
|
||||
} else {
|
||||
toast("正在从链接 " + extension_url.value + " 安装插件...", "primary");
|
||||
axios.post('/api/plugin/install',
|
||||
{
|
||||
url: extension_url.value,
|
||||
proxy: localStorage.getItem('selectedGitHubProxy') || ""
|
||||
}).then(async (res) => {
|
||||
loading_.value = false;
|
||||
toast(res.data.message, res.data.status === "ok" ? "success" : "error");
|
||||
if (res.data.status === "error") {
|
||||
onLoadingDialogResult(2, res.data.message, -1);
|
||||
return;
|
||||
}
|
||||
extension_url.value = "";
|
||||
onLoadingDialogResult(1, res.data.message);
|
||||
dialog.value = false;
|
||||
await getExtensions();
|
||||
|
||||
viewReadme({
|
||||
name: res.data.data.name,
|
||||
repo: res.data.data.repo || null
|
||||
});
|
||||
}).catch((err) => {
|
||||
loading_.value = false;
|
||||
toast("安装插件失败: " + err, "error");
|
||||
onLoadingDialogResult(2, err, -1);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await getExtensions();
|
||||
@@ -399,11 +533,15 @@ onMounted(async () => {
|
||||
try {
|
||||
const data = await commonStore.getPluginCollections();
|
||||
pluginMarketData.value = data;
|
||||
trimExtensionName();
|
||||
checkAlreadyInstalled();
|
||||
checkUpdate();
|
||||
} catch (err) {
|
||||
console.error("获取插件市场数据失败:", err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -417,205 +555,300 @@ onMounted(async () => {
|
||||
</div>
|
||||
</template>
|
||||
<v-card-title class="text-h4 font-weight-bold">
|
||||
已安装的插件
|
||||
AstrBot 插件
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="text-subtitle-1 mt-1 text-medium-emphasis">
|
||||
管理已经安装的所有插件
|
||||
管理、安装 AstrBot 插件
|
||||
</v-card-subtitle>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text class="pt-2">
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" sm="6" md="6" class="d-flex align-center">
|
||||
<v-btn-group variant="outlined" density="comfortable" color="primary">
|
||||
<v-btn @click="isListView = false" :color="!isListView ? 'primary' : undefined"
|
||||
:variant="!isListView ? 'flat' : 'outlined'">
|
||||
<v-icon>mdi-view-grid</v-icon>
|
||||
</v-btn>
|
||||
<v-btn @click="isListView = true" :color="isListView ? 'primary' : undefined"
|
||||
:variant="isListView ? 'flat' : 'outlined'">
|
||||
<v-icon>mdi-view-list</v-icon>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
<!-- 标签页 -->
|
||||
<v-card-text>
|
||||
|
||||
<v-btn class="ml-2" @click="toggleShowReserved" prepend-icon="mdi-eye-settings-outline"
|
||||
:color="showReserved ? 'primary' : undefined" :variant="showReserved ? 'flat' : 'outlined'">
|
||||
{{ showReserved ? '隐藏系统插件' : '显示系统插件' }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn class="ml-2" prepend-icon="mdi-tune-vertical" color="primary" variant="outlined"
|
||||
@click="getPlatformEnableConfig">
|
||||
平台命令配置
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="auto" md="6" class="ml-auto">
|
||||
<div class="search-container rounded-lg">
|
||||
|
||||
<v-text-field v-model="pluginSearch" density="compact" label="Search" prepend-inner-icon="mdi-magnify"
|
||||
variant="solo-filled" flat hide-details single-line></v-text-field>
|
||||
</div>
|
||||
<div class="d-flex align-center mb-2" style="justify-content: space-between;">
|
||||
<v-tabs v-model="activeTab" color="primary" class="mb-4">
|
||||
<v-tab value="installed">
|
||||
<v-icon class="mr-2">mdi-puzzle</v-icon>
|
||||
已安装插件
|
||||
</v-tab>
|
||||
<v-tab value="market">
|
||||
<v-icon class="mr-2">mdi-store</v-icon>
|
||||
插件市场
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
|
||||
<v-text-field v-if="activeTab == 'market'" style="max-width: 300px;" v-model="marketSearch" density="compact"
|
||||
label="Search" prepend-inner-icon="mdi-magnify" variant="solo-filled" flat hide-details
|
||||
single-line></v-text-field>
|
||||
<v-text-field v-else style="max-width: 300px;" v-model="pluginSearch" density="compact" label="Search" prepend-inner-icon="mdi-magnify"
|
||||
variant="solo-filled" flat hide-details single-line></v-text-field>
|
||||
|
||||
<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-2" variant="tonal">
|
||||
<v-icon>mdi-alert-circle</v-icon>
|
||||
<v-badge dot color="error" floating></v-badge>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 已安装插件标签页内容 -->
|
||||
<v-tab-item v-show="activeTab === 'installed'">
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" sm="6" md="6" class="d-flex align-center">
|
||||
<v-btn-group variant="outlined" density="comfortable" color="primary">
|
||||
<v-btn @click="isListView = false" :color="!isListView ? 'primary' : undefined"
|
||||
:variant="!isListView ? 'flat' : 'outlined'">
|
||||
<v-icon>mdi-view-grid</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>
|
||||
错误信息
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-1">{{ extension_data.message }}</p>
|
||||
<p class="text-caption mt-2">详情请检查控制台</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" @click="isActive.value = false">关闭</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn @click="isListView = true" :color="isListView ? 'primary' : undefined"
|
||||
:variant="isListView ? 'flat' : 'outlined'">
|
||||
<v-icon>mdi-view-list</v-icon>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
|
||||
<v-fade-transition hide-on-leave>
|
||||
<!-- 表格视图 -->
|
||||
<div v-if="isListView">
|
||||
<v-card class="rounded-lg overflow-hidden elevation-1">
|
||||
<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">加载中...</span>
|
||||
</v-row>
|
||||
<v-btn class="ml-2" @click="toggleShowReserved" prepend-icon="mdi-eye-settings-outline"
|
||||
:color="showReserved ? 'primary' : undefined" :variant="showReserved ? 'flat' : 'outlined'">
|
||||
{{ showReserved ? '隐藏系统插件' : '显示系统插件' }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn class="ml-2" prepend-icon="mdi-tune-vertical" color="primary" variant="outlined"
|
||||
@click="getPlatformEnableConfig">
|
||||
平台命令配置
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="auto" md="6" class="ml-auto">
|
||||
<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-2" variant="tonal">
|
||||
<v-icon>mdi-alert-circle</v-icon>
|
||||
<v-badge dot color="error" floating></v-badge>
|
||||
</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>
|
||||
错误信息
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-1">{{ extension_data.message }}</p>
|
||||
<p class="text-caption mt-2">详情请检查控制台</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" @click="isActive.value = false">关闭</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<template v-slot:item.name="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">{{ 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">系统</v-chip>
|
||||
<v-fade-transition hide-on-leave>
|
||||
<!-- 表格视图 -->
|
||||
<div v-if="isListView">
|
||||
<v-card class="rounded-lg overflow-hidden elevation-1">
|
||||
<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">加载中...</span>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.name="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">{{ 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">系统</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.desc="{ item }">
|
||||
<div class="text-body-2 text-medium-emphasis">{{ item.desc }}</div>
|
||||
</template>
|
||||
<template v-slot:item.desc="{ item }">
|
||||
<div class="text-body-2 text-medium-emphasis">{{ item.desc }}</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>有新版本: {{ item.online_version }}</span>
|
||||
</v-tooltip>
|
||||
</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>有新版本: {{ 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.author="{ item }">
|
||||
<div class="text-body-2">{{ item.author }}</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<v-chip :color="item.activated ? 'success' : 'error'" size="small" class="font-weight-medium"
|
||||
:variant="item.activated ? 'flat' : 'outlined'">
|
||||
{{ item.activated ? '启用' : '禁用' }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.status="{ item }">
|
||||
<v-chip :color="item.activated ? 'success' : 'error'" size="small" class="font-weight-medium"
|
||||
:variant="item.activated ? 'flat' : 'outlined'">
|
||||
{{ item.activated ? '启用' : '禁用' }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-btn-group density="comfortable" variant="text" color="primary">
|
||||
<v-btn v-if="!item.activated" icon size="small" color="success" @click="pluginOn(item)">
|
||||
<v-icon>mdi-play</v-icon>
|
||||
<v-tooltip activator="parent" location="top">点击启用</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn v-else icon size="small" color="error" @click="pluginOff(item)">
|
||||
<v-icon>mdi-pause</v-icon>
|
||||
<v-tooltip activator="parent" location="top">点击禁用</v-tooltip>
|
||||
</v-btn>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-btn-group density="comfortable" variant="text" color="primary">
|
||||
<v-btn v-if="!item.activated" icon size="small" color="success" @click="pluginOn(item)">
|
||||
<v-icon>mdi-play</v-icon>
|
||||
<v-tooltip activator="parent" location="top">点击启用</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn v-else icon size="small" color="error" @click="pluginOff(item)">
|
||||
<v-icon>mdi-pause</v-icon>
|
||||
<v-tooltip activator="parent" location="top">点击禁用</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon size="small" color="info" @click="reloadPlugin(item.name)">
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
<v-tooltip activator="parent" location="top">重载</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn icon size="small" color="info" @click="reloadPlugin(item.name)">
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
<v-tooltip activator="parent" location="top">重载</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon size="small" @click="openExtensionConfig(item.name)">
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
<v-tooltip activator="parent" location="top">配置</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn icon size="small" @click="openExtensionConfig(item.name)">
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
<v-tooltip activator="parent" location="top">配置</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon size="small" @click="showPluginInfo(item)">
|
||||
<v-icon>mdi-information</v-icon>
|
||||
<v-tooltip activator="parent" location="top">行为</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn icon size="small" @click="showPluginInfo(item)">
|
||||
<v-icon>mdi-information</v-icon>
|
||||
<v-tooltip activator="parent" location="top">行为</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="item.repo" icon size="small" @click="viewReadme(item)">
|
||||
<v-icon>mdi-book-open-page-variant</v-icon>
|
||||
<v-tooltip activator="parent" location="top">文档</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn v-if="item.repo" icon size="small" @click="viewReadme(item)">
|
||||
<v-icon>mdi-book-open-page-variant</v-icon>
|
||||
<v-tooltip activator="parent" location="top">文档</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon size="small" color="warning"
|
||||
@click="updateExtension(item.name)" :v-show="item.has_update">
|
||||
<v-icon>mdi-update</v-icon>
|
||||
<v-tooltip activator="parent" location="top">更新</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn icon size="small" color="warning" @click="updateExtension(item.name)"
|
||||
:v-show="item.has_update">
|
||||
<v-icon>mdi-update</v-icon>
|
||||
<v-tooltip activator="parent" location="top">更新</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon size="small" color="error"
|
||||
@click="uninstallExtension(item.name)" :disabled="item.reserved">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
<v-tooltip activator="parent" location="top">卸载</v-tooltip>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
<v-btn icon size="small" color="error" @click="uninstallExtension(item.name)"
|
||||
:disabled="item.reserved">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
<v-tooltip activator="parent" location="top">卸载</v-tooltip>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</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">暂无插件</div>
|
||||
<div class="text-body-1 mb-4">尝试安装插件或者显示系统插件</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</div>
|
||||
<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">暂无插件</div>
|
||||
<div class="text-body-1 mb-4">尝试安装插件或者显示系统插件</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-8">
|
||||
<v-icon size="64" color="info" class="mb-4">mdi-puzzle-outline</v-icon>
|
||||
<div class="text-h5 mb-2">暂无插件</div>
|
||||
<div class="text-body-1 mb-4">尝试安装插件或者显示系统插件</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!-- 卡片视图 -->
|
||||
<div v-else>
|
||||
<v-row v-if="filteredPlugins.length === 0" class="text-center">
|
||||
<v-col cols="12" class="pa-8">
|
||||
<v-icon size="64" color="info" class="mb-4">mdi-puzzle-outline</v-icon>
|
||||
<div class="text-h5 mb-2">暂无插件</div>
|
||||
<div class="text-body-1 mb-4">尝试安装插件或者显示系统插件</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-4">
|
||||
<ExtensionCard :extension="extension" class="h-120 rounded-lg"
|
||||
@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)" @view-readme="viewReadme(extension)">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6" lg="4" v-for="extension in filteredPlugins" :key="extension.name"
|
||||
class="pb-4">
|
||||
<ExtensionCard :extension="extension" class="h-120 rounded-lg"
|
||||
@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)" @view-readme="viewReadme(extension)">
|
||||
</ExtensionCard>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- 插件市场标签页内容 -->
|
||||
<v-tab-item v-show="activeTab === 'market'">
|
||||
|
||||
<!-- <small style="color: var(--v-theme-secondaryText);">每个插件都是作者无偿提供的的劳动成果。如果您喜欢某个插件,请 Star!</small> -->
|
||||
|
||||
<div v-if="pinnedPlugins.length > 0" class="mt-4">
|
||||
<h2>🥳 推荐</h2>
|
||||
<v-row style="margin-top: 8px;">
|
||||
<v-col cols="12" md="6" lg="6" v-for="plugin in pinnedPlugins" :key="plugin.name">
|
||||
<ExtensionCard :extension="plugin" class="h-120 rounded-lg" market-mode="true" :highlight="true"
|
||||
@install="extension_url = plugin.repo; newExtension()" @view-readme="open(plugin.repo)">
|
||||
</ExtensionCard>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="d-flex align-center mb-2" style="justify-content: space-between;">
|
||||
<h2>📦 全部插件</h2>
|
||||
<v-switch v-model="showPluginFullName" label="完整名称" hide-details density="compact"
|
||||
style="margin-left: 12px" />
|
||||
</div>
|
||||
|
||||
<v-col cols="12" md="12" style="padding: 0px;">
|
||||
<v-data-table :headers="pluginMarketHeaders" :items="pluginMarketData" item-key="name"
|
||||
:loading="loading_" v-model:search="marketSearch" :filter-keys="filterKeys">
|
||||
<template v-slot:item.name="{ item }">
|
||||
<div class="d-flex align-center"
|
||||
style="overflow-x: auto; scrollbar-width: thin; scrollbar-track-color: transparent;">
|
||||
<img v-if="item.logo" :src="item.logo"
|
||||
style="height: 80px; width: 80px; margin-right: 8px; border-radius: 8px; margin-top: 8px; margin-bottom: 8px;"
|
||||
alt="logo">
|
||||
<span v-if="item?.repo"><a :href="item?.repo"
|
||||
style="color: var(--v-theme-primaryText, #000); text-decoration:none">{{
|
||||
showPluginFullName ? item.name : item.trimmedName }}</a></span>
|
||||
<span v-else>{{ showPluginFullName ? item.name : item.trimmedName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.desc="{ item }">
|
||||
<div style="font-size: 13px;">
|
||||
{{ item.desc }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:item.author="{ item }">
|
||||
<div style="font-size: 12px;">
|
||||
<span v-if="item?.social_link"><a :href="item?.social_link">{{ item.author }}</a></span>
|
||||
<span v-else>{{ item.author }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:item.stars="{ item }">
|
||||
<span>{{ item.stars }}</span>
|
||||
</template>
|
||||
<template v-slot:item.updated_at="{ item }">
|
||||
<span>{{ new Date(item.updated_at).toLocaleString() }}</span>
|
||||
</template>
|
||||
<template v-slot:item.tags="{ item }">
|
||||
<span v-if="item.tags.length === 0">-</span>
|
||||
<v-chip v-for="tag in item.tags" :key="tag" color="primary" size="x-small">
|
||||
{{ tag }}</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn v-if="!item.installed" class="text-none mr-2" size="x-small" variant="flat"
|
||||
@click="extension_url = item.repo; newExtension()">
|
||||
<v-icon>mdi-download</v-icon></v-btn>
|
||||
<v-btn v-else class="text-none mr-2" size="x-small" variant="flat" border
|
||||
disabled><v-icon>mdi-check</v-icon></v-btn>
|
||||
<v-btn class="text-none mr-2" size="x-small" variant="flat" border
|
||||
@click="open(item.repo)"><v-icon>mdi-help</v-icon></v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</div>
|
||||
</v-tab-item>
|
||||
|
||||
<v-row v-if="loading_">
|
||||
<v-col cols="12" class="d-flex justify-center">
|
||||
@@ -625,6 +858,11 @@ onMounted(async () => {
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="activeTab === 'market'" style="margin-bottom: 16px;" cols="12" md="12">
|
||||
<small><a href="https://astrbot.app/dev/plugin.html">插件开发文档</a></small> |
|
||||
<small> <a href="https://github.com/Soulter/AstrBot_Plugins_Collection">提交插件仓库</a></small>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 插件平台配置对话框 -->
|
||||
@@ -796,8 +1034,6 @@ onMounted(async () => {
|
||||
{{ snack_message }}
|
||||
</v-snackbar>
|
||||
|
||||
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
||||
|
||||
<ReadmeDialog v-model:show="readmeDialog.show" :plugin-name="readmeDialog.pluginName"
|
||||
:repo-url="readmeDialog.repoUrl" />
|
||||
</template>
|
||||
|
||||
@@ -9,6 +9,14 @@ import {useCustomizerStore} from "@/stores/customizer";
|
||||
const cardVisible = ref(false);
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const customizer = useCustomizerStore();
|
||||
|
||||
// 主题切换函数
|
||||
function toggleTheme() {
|
||||
customizer.SET_UI_THEME(
|
||||
customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark'
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查用户是否已登录,如果已登录则重定向
|
||||
@@ -27,6 +35,25 @@ onMounted(() => {
|
||||
<template>
|
||||
<div v-if="useCustomizerStore().uiTheme==='PurpleTheme'" class="login-page-container">
|
||||
<div class="login-background"></div>
|
||||
|
||||
<!-- 主题切换按钮 -->
|
||||
<div class="theme-toggle-container">
|
||||
<v-btn
|
||||
@click="toggleTheme"
|
||||
class="theme-toggle-btn"
|
||||
icon
|
||||
variant="flat"
|
||||
size="small"
|
||||
color="primary"
|
||||
elevation="2"
|
||||
>
|
||||
<v-icon size="20">mdi-weather-night</v-icon>
|
||||
<v-tooltip activator="parent" location="left">
|
||||
切换到深色主题
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-card
|
||||
variant="outlined"
|
||||
class="login-card"
|
||||
@@ -45,6 +72,25 @@ onMounted(() => {
|
||||
</div>
|
||||
<div v-else class="login-page-container-dark">
|
||||
<div class="login-background-dark"></div>
|
||||
|
||||
<!-- 主题切换按钮 -->
|
||||
<div class="theme-toggle-container">
|
||||
<v-btn
|
||||
@click="toggleTheme"
|
||||
class="theme-toggle-btn"
|
||||
icon
|
||||
variant="flat"
|
||||
size="small"
|
||||
color="secondary"
|
||||
elevation="2"
|
||||
>
|
||||
<v-icon size="20">mdi-white-balance-sunny</v-icon>
|
||||
<v-tooltip activator="parent" location="left">
|
||||
切换到浅色主题
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-card
|
||||
variant="outlined"
|
||||
class="login-card"
|
||||
@@ -71,7 +117,16 @@ onMounted(() => {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #ebf5fd 0%, #e0e9f8 100%);
|
||||
background:
|
||||
linear-gradient(-45deg,
|
||||
#faf9f7 0%,
|
||||
#f9f2f1 25%,
|
||||
#f1f9f9 50%,
|
||||
#f9f3f7 75%,
|
||||
#faf9f7 100%
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -82,17 +137,38 @@ onMounted(() => {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #1a1b1c 0%, #1d1e21 100%);
|
||||
background:
|
||||
linear-gradient(-45deg,
|
||||
#1e1f21 0%,
|
||||
#221e25 25%,
|
||||
#1e2225 50%,
|
||||
#221f23 75%,
|
||||
#1e1f21 100%
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.login-background {
|
||||
position: absolute;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
background: radial-gradient(circle, rgba(94, 53, 177, 0.03) 0%, rgba(30, 136, 229, 0.06) 70%);
|
||||
background: radial-gradient(circle, rgba(94, 53, 177, 0.02) 0%, rgba(94, 53, 177, 0.03) 70%);
|
||||
z-index: 0;
|
||||
animation: rotate 60s linear infinite;
|
||||
}
|
||||
@@ -103,7 +179,7 @@ onMounted(() => {
|
||||
height: 200%;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
background-color: var(--v-theme-surface);
|
||||
background: radial-gradient(circle, rgba(114, 46, 209, 0.03) 0%, rgba(114, 46, 209, 0.04) 70%);
|
||||
z-index: 0;
|
||||
animation: rotate 60s linear infinite;
|
||||
}
|
||||
@@ -117,23 +193,85 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.theme-toggle-container {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.login-card {
|
||||
max-width: 520px;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
color: var(--v-theme-primaryText) !important;
|
||||
border-radius: 12px !important;
|
||||
border-color: var(--v-theme-border) !important;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.07) !important;
|
||||
background-color: var(--v-theme-surface) !important;
|
||||
border-radius: 16px !important;
|
||||
border: 1px solid rgba(94, 53, 177, 0.15) !important;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08) !important;
|
||||
background: #f8f6fc !important;
|
||||
backdrop-filter: blur(10px);
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease;
|
||||
transition: transform 0.5s ease, opacity 0.5s ease, border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
z-index: 1;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: calc(100% + 4px);
|
||||
height: calc(100% + 4px);
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 18px;
|
||||
border: 2px solid rgba(94, 53, 177, 0);
|
||||
transition: border-color 0.3s ease;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&.card-visible {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(158, 126, 222, 0.99) !important;
|
||||
box-shadow: 0 12px 40px rgba(175, 145, 230, 0.741) !important;
|
||||
transform: translateY(-2px);
|
||||
|
||||
&::before {
|
||||
border-color: rgba(156, 114, 239, 0.907);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-page-container-dark .login-card {
|
||||
border: 1px solid rgba(110, 60, 180, 0.692) !important;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3) !important;
|
||||
background: #2a2733 !important;
|
||||
|
||||
&::before {
|
||||
border: 2px solid rgba(114, 46, 209, 0);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(160, 118, 219, 0.782) !important;
|
||||
box-shadow: 0 12px 40px rgba(99, 44, 175, 0.462) !important;
|
||||
transform: translateY(-2px);
|
||||
|
||||
&::before {
|
||||
border-color: rgba(114, 46, 209, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
@@ -145,8 +283,12 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.custom-divider {
|
||||
border-color: rgba(0, 0, 0, 0.05) !important;
|
||||
opacity: 0.8;
|
||||
border-color: rgba(94, 53, 177, 0.1) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.login-page-container-dark .custom-divider {
|
||||
border-color: rgba(114, 46, 209, 0.08) !important;
|
||||
}
|
||||
|
||||
.loginBox {
|
||||
|
||||
@@ -77,6 +77,7 @@ async function validate(values: any, { setErrors }: any) {
|
||||
:disabled="valid"
|
||||
type="submit"
|
||||
elevation="2"
|
||||
|
||||
>
|
||||
<span class="login-btn-text">登录</span>
|
||||
</v-btn>
|
||||
@@ -146,6 +147,7 @@ async function validate(values: any, { setErrors }: any) {
|
||||
height: 48px;
|
||||
transition: all 0.3s ease;
|
||||
letter-spacing: 0.5px;
|
||||
border-radius: 8px !important;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
|
||||
Reference in New Issue
Block a user