Merge branch 'Soulter:master' into master
This commit is contained in:
@@ -499,6 +499,23 @@ class SimpleGewechatClient:
|
||||
json_blob = await resp.json()
|
||||
logger.debug(f"发送图片结果: {json_blob}")
|
||||
|
||||
async def post_video(
|
||||
self, to_wxid, video_url: str, thumb_url: str, video_duration: int
|
||||
):
|
||||
payload = {
|
||||
"appId": self.appid,
|
||||
"toWxid": to_wxid,
|
||||
"videoUrl": video_url,
|
||||
"thumbUrl": thumb_url,
|
||||
"videoDuration": video_duration,
|
||||
}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{self.base_url}/message/postVideo", headers=self.headers, json=payload
|
||||
) as resp:
|
||||
json_blob = await resp.json()
|
||||
logger.debug(f"发送视频结果: {json_blob}")
|
||||
|
||||
async def post_voice(self, to_wxid, voice_url: str, voice_duration: int):
|
||||
"""发送语音信息
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@ import wave
|
||||
import uuid
|
||||
import traceback
|
||||
import os
|
||||
|
||||
from astrbot.core.utils.io import save_temp_img, download_file
|
||||
from astrbot.core.utils.tencent_record_helper import wav_to_tencent_silk
|
||||
from astrbot.api import logger
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.platform import AstrBotMessage, PlatformMetadata, Group, MessageMember
|
||||
from astrbot.api.message_components import Plain, Image, Record, At, File
|
||||
from astrbot.api.message_components import Plain, Image, Record, At, File, Video
|
||||
from .client import SimpleGewechatClient
|
||||
|
||||
|
||||
@@ -82,6 +83,56 @@ class GewechatPlatformEvent(AstrMessageEvent):
|
||||
img_url = f"{client.file_server_url}/{file_id}"
|
||||
logger.debug(f"gewe callback img url: {img_url}")
|
||||
await client.post_image(to_wxid, img_url)
|
||||
elif isinstance(comp, Video):
|
||||
try:
|
||||
from pyffmpeg import FFmpeg
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
logger.error(
|
||||
"需要安装 pyffmpeg 库才能发送视频: pip install pyffmpeg"
|
||||
)
|
||||
raise ModuleNotFoundError(
|
||||
"需要安装 pyffmpeg 库才能发送视频: pip install pyffmpeg"
|
||||
)
|
||||
|
||||
video_url = comp.file
|
||||
# 根据 url 下载视频
|
||||
video_filename = f"{uuid.uuid4()}.mp4"
|
||||
video_path = f"data/temp/{video_filename}"
|
||||
await download_file(video_url, video_path)
|
||||
|
||||
# 获取视频第一帧
|
||||
thumb_path = f"data/temp/{uuid.uuid4()}.jpg"
|
||||
try:
|
||||
ff = FFmpeg()
|
||||
command = f'-i "{video_path}" -ss 0 -vframes 1 "{thumb_path}"'
|
||||
ff.options(command)
|
||||
thumb_file_id = os.path.basename(thumb_path)
|
||||
thumb_url = f"{client.file_server_url}/{thumb_file_id}"
|
||||
except Exception as e:
|
||||
logger.error(f"获取视频第一帧失败: {e}")
|
||||
# 获取视频时长
|
||||
try:
|
||||
from pyffmpeg import FFprobe
|
||||
|
||||
# 创建 FFprobe 实例
|
||||
ffprobe = FFprobe(video_url)
|
||||
# 获取时长字符串
|
||||
duration_str = ffprobe.duration
|
||||
# 处理时长字符串
|
||||
video_duration = float(duration_str.replace(":", ""))
|
||||
except Exception as e:
|
||||
logger.error(f"获取时长失败: {e}")
|
||||
video_duration = 10
|
||||
|
||||
file_id = os.path.basename(video_path)
|
||||
video_url = f"{client.file_server_url}/{file_id}"
|
||||
await client.post_video(to_wxid, video_url, thumb_url, video_duration)
|
||||
|
||||
# 删除临时视频和缩略图文件
|
||||
if os.path.exists(video_path):
|
||||
os.remove(video_path)
|
||||
if os.path.exists(thumb_path):
|
||||
os.remove(thumb_path)
|
||||
elif isinstance(comp, Record):
|
||||
# 默认已经存在 data/temp 中
|
||||
record_url = comp.file
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import traceback
|
||||
import psutil
|
||||
import time
|
||||
import threading
|
||||
from .route import Route, Response, RouteContext
|
||||
from astrbot.core import logger
|
||||
from quart import request
|
||||
@@ -64,6 +65,25 @@ class StatRoute(Route):
|
||||
|
||||
stat_dict = stat.__dict__
|
||||
|
||||
# 获取CPU使用率 - 修复CPU始终为0的问题
|
||||
process = psutil.Process()
|
||||
# 获取系统CPU使用率而不是进程CPU使用率
|
||||
cpu_percent = psutil.cpu_percent(interval=0.5)
|
||||
|
||||
# 获取线程数
|
||||
thread_count = threading.active_count()
|
||||
|
||||
# 获取插件信息
|
||||
plugins = self.core_lifecycle.star_context.get_all_stars()
|
||||
plugin_info = []
|
||||
for plugin in plugins:
|
||||
info = {
|
||||
"name": getattr(plugin, "name", plugin.__class__.__name__),
|
||||
"version": getattr(plugin, "version", "1.0.0"),
|
||||
"is_enabled": True
|
||||
}
|
||||
plugin_info.append(info)
|
||||
|
||||
stat_dict.update(
|
||||
{
|
||||
"platform": self.db_helper.get_grouped_base_stats(
|
||||
@@ -73,9 +93,8 @@ class StatRoute(Route):
|
||||
"platform_count": len(
|
||||
self.core_lifecycle.platform_manager.get_insts()
|
||||
),
|
||||
"plugin_count": len(
|
||||
self.core_lifecycle.star_context.get_all_stars()
|
||||
),
|
||||
"plugin_count": len(plugins),
|
||||
"plugins": plugin_info,
|
||||
"message_time_series": message_time_based_stats,
|
||||
"running": self.format_sec(
|
||||
int(time.time()) - self.core_lifecycle.start_time
|
||||
@@ -84,6 +103,9 @@ class StatRoute(Route):
|
||||
"process": psutil.Process().memory_info().rss >> 20,
|
||||
"system": psutil.virtual_memory().total >> 20,
|
||||
},
|
||||
"cpu_percent": round(cpu_percent, 1),
|
||||
"thread_count": thread_count,
|
||||
"start_time": self.core_lifecycle.start_time,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,156 +1,374 @@
|
||||
<template>
|
||||
<div style="margin-bottom: 6px;" v-if="iterable && metadata[metadataKey]?.type === 'object'">
|
||||
<v-list-item-title style="font-weight: bold;">
|
||||
{{ metadata[metadataKey]?.description }} ({{ metadataKey }})
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle style="font-size: 12px;">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint"
|
||||
style="opacity: 1.0;">‼️</span>
|
||||
{{ metadata[metadataKey]?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
<div class="config-section" v-if="iterable && metadata[metadataKey]?.type === 'object'">
|
||||
<v-list-item-title class="config-title">
|
||||
{{ metadata[metadataKey]?.description }} <span class="metadata-key">({{ metadataKey }})</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="config-hint">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint">‼️</span>
|
||||
{{ metadata[metadataKey]?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
|
||||
<v-card-text class="px-0 py-1">
|
||||
<!-- Object Type Configuration -->
|
||||
<div v-if="metadata[metadataKey]?.type === 'object' || metadata[metadataKey]?.config_template" class="object-config">
|
||||
<div v-for="(val, key, index) in iterable" :key="key" class="config-item">
|
||||
<!-- Nested Object -->
|
||||
<div v-if="metadata[metadataKey].items[key]?.type === 'object'" class="nested-object">
|
||||
<div v-if="metadata[metadataKey].items[key] && !metadata[metadataKey].items[key]?.invisible" class="nested-container">
|
||||
<v-expand-transition>
|
||||
<AstrBotConfig :metadata="metadata[metadataKey].items" :iterable="iterable[key]" :metadataKey="key">
|
||||
</AstrBotConfig>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Regular Property -->
|
||||
<template v-else>
|
||||
<v-row v-if="!metadata[metadataKey].items[key]?.invisible" class="config-row">
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
<span v-if="metadata[metadataKey].items[key]?.description">
|
||||
{{ metadata[metadataKey].items[key]?.description }}
|
||||
<span class="property-key">({{ key }})</span>
|
||||
</span>
|
||||
<span v-else>{{ key }}</span>
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint"
|
||||
class="important-hint">‼️</span>
|
||||
{{ metadata[metadataKey].items[key]?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="1" class="d-flex align-center type-indicator">
|
||||
<v-chip v-if="!metadata[metadataKey].items[key]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
size="x-small"
|
||||
variant="flat">
|
||||
{{ metadata[metadataKey].items[key]?.type || 'string' }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="5" class="config-input">
|
||||
<div v-if="metadata[metadataKey].items[key]" class="w-100">
|
||||
<!-- Select input -->
|
||||
<v-select
|
||||
v-if="metadata[metadataKey].items[key]?.options && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]"
|
||||
:items="metadata[metadataKey].items[key]?.options"
|
||||
:disabled="metadata[metadataKey].items[key]?.readonly"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-select>
|
||||
|
||||
<!-- String input -->
|
||||
<v-text-field
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'string' && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
<!-- Numeric input -->
|
||||
<v-text-field
|
||||
v-else-if="(metadata[metadataKey].items[key]?.type === 'int' || metadata[metadataKey].items[key]?.type === 'float') && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[key]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
<!-- Text area -->
|
||||
<v-textarea
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'text' && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]"
|
||||
variant="outlined"
|
||||
auto-grow
|
||||
rows="3"
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-textarea>
|
||||
|
||||
<!-- Boolean switch -->
|
||||
<v-switch
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'bool' && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]"
|
||||
color="primary"
|
||||
inset
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-switch>
|
||||
|
||||
<!-- List item -->
|
||||
<ListConfigItem
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'list' && !metadata[metadataKey].items[key]?.invisible"
|
||||
:value="iterable[key]"
|
||||
class="config-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Fallback for unknown metadata -->
|
||||
<div v-else class="w-100">
|
||||
<v-text-field
|
||||
v-model="iterable[key]"
|
||||
:label="key"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider
|
||||
v-if="index !== Object.keys(iterable).length - 1 && !metadata[metadataKey].items[key]?.invisible"
|
||||
class="config-divider"
|
||||
></v-divider>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Simple Value Configuration -->
|
||||
<div v-else class="simple-config">
|
||||
<v-row class="config-row">
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
{{ metadata[metadataKey]?.description }}
|
||||
<span class="property-key">({{ metadataKey }})</span>
|
||||
</v-list-item-title>
|
||||
|
||||
<v-card-text style="padding: 0px;">
|
||||
<div v-for="(val, key, index) in iterable" :key="key" style="margin-bottom: 0.5px;"
|
||||
v-if="metadata[metadataKey]?.type === 'object' || metadata[metadataKey]?.config_template">
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint">‼️</span>
|
||||
{{ metadata[metadataKey]?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
|
||||
<div v-if="metadata[metadataKey].items[key]?.type === 'object'" style="padding-left: 16px;">
|
||||
<div v-if="metadata[metadataKey].items[key] && !metadata[metadataKey].items[key]?.invisible"
|
||||
style="border: 1px solid #e0e0e0; padding: 8px; margin-bottom: 16px; border-radius: 10px; margin-top: 16px">
|
||||
<AstrBotConfig :metadata="metadata[metadataKey].items" :iterable="iterable[key]" :metadataKey=key>
|
||||
</AstrBotConfig>
|
||||
</div>
|
||||
</div>
|
||||
<v-col cols="12" sm="1" class="d-flex align-center type-indicator">
|
||||
<v-chip v-if="!metadata[metadataKey]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
size="x-small"
|
||||
variant="flat">
|
||||
{{ metadata[metadataKey]?.type }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-row v-else style="margin: 0; align-items: center;">
|
||||
<v-col cols="6" style="padding: 0px;">
|
||||
<v-list-item>
|
||||
<v-list-item-title style="font-size: 14px; font-weight: bold;">
|
||||
<span v-if="metadata[metadataKey].items[key]?.description">{{ metadata[metadataKey].items[key]?.description + '(' + key + ')' }}</span>
|
||||
<span v-else>{{ key }}</span>
|
||||
</v-list-item-title>
|
||||
<v-col cols="12" sm="5" class="config-input">
|
||||
<div class="w-100">
|
||||
<!-- Select input -->
|
||||
<v-select
|
||||
v-if="metadata[metadataKey]?.options && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]"
|
||||
:items="metadata[metadataKey]?.options"
|
||||
:disabled="metadata[metadataKey]?.readonly"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-select>
|
||||
|
||||
<!-- String input -->
|
||||
<v-text-field
|
||||
v-else-if="metadata[metadataKey]?.type === 'string' && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
<!-- Numeric input -->
|
||||
<v-text-field
|
||||
v-else-if="(metadata[metadataKey]?.type === 'int' || metadata[metadataKey]?.type === 'float') && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
<!-- Text area -->
|
||||
<v-textarea
|
||||
v-else-if="metadata[metadataKey]?.type === 'text' && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]"
|
||||
variant="outlined"
|
||||
auto-grow
|
||||
rows="3"
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-textarea>
|
||||
|
||||
<!-- Boolean switch -->
|
||||
<v-switch
|
||||
v-else-if="metadata[metadataKey]?.type === 'bool' && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]"
|
||||
color="primary"
|
||||
inset
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-switch>
|
||||
|
||||
<!-- List item -->
|
||||
<ListConfigItem
|
||||
v-else-if="metadata[metadataKey]?.type === 'list' && !metadata[metadataKey]?.invisible"
|
||||
:value="iterable[metadataKey]"
|
||||
class="config-field"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-list-item-subtitle style="font-size: 12px;">
|
||||
<span
|
||||
v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint"
|
||||
style="opacity: 1.0;">‼️</span>
|
||||
{{ metadata[metadataKey].items[key]?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="1">
|
||||
<v-chip v-if="!metadata[metadataKey].items[key]?.invisible" color="primary" label size="x-small"
|
||||
class="mb-1">{{
|
||||
metadata[metadataKey].items[key]?.type || 'string' }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="5">
|
||||
<div style="width: 100%;" v-if="metadata[metadataKey].items[key]">
|
||||
<v-select
|
||||
v-if="metadata[metadataKey].items[key]?.options && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]" variant="outlined"
|
||||
:items="metadata[metadataKey].items[key]?.options" dense
|
||||
:disabled="metadata[metadataKey].items[key]?.readonly" density="compact" flat hide-details
|
||||
single-line></v-select>
|
||||
<v-text-field
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'string' && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]" variant="outlined" dense density="compact" flat hide-details
|
||||
single-line></v-text-field>
|
||||
<v-text-field
|
||||
v-else-if="(metadata[metadataKey].items[key]?.type === 'int' || metadata[metadataKey].items[key]?.type === 'float') && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]" variant="outlined" dense density="compact" flat hide-details
|
||||
single-line></v-text-field>
|
||||
<v-textarea
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'text' && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]" variant="outlined" dense flat hide-details single-line></v-textarea>
|
||||
<v-switch
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'bool' && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]" color="primary" hide-details></v-switch>
|
||||
<ListConfigItem
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'list' && !metadata[metadataKey].items[key]?.invisible"
|
||||
:value="iterable[key]" />
|
||||
</div>
|
||||
<div style="width: 100%;" v-else>
|
||||
<!-- 在 metadata 中没有 key -->
|
||||
<v-text-field v-model="iterable[key]" :label="key" variant="outlined" dense density="compact" flat hide-details
|
||||
single-line></v-text-field>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
<v-divider style="border-color: #ccc;" v-if="index !== Object.keys(iterable).length - 1 && !metadata[metadataKey].items[key]?.invisible "></v-divider>
|
||||
|
||||
</div>
|
||||
<div v-else>
|
||||
|
||||
<v-row style="margin: 0; align-items: center;">
|
||||
<v-col cols="6" style="padding: 0px;">
|
||||
<v-list-item>
|
||||
<v-list-item-title style="font-size: 14px; font-weight: bold">
|
||||
{{ metadata[metadataKey]?.description + '(' + metadataKey + ')' }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle style="font-size: 12px;">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint">‼️</span>
|
||||
{{ metadata[metadataKey]?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="1">
|
||||
<v-chip v-if="!metadata[metadataKey]?.invisible" color="primary" label size="x-small"
|
||||
class="mb-1">{{
|
||||
metadata[metadataKey]?.type }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="5">
|
||||
<div style="width: 100%;">
|
||||
<v-select v-if="metadata[metadataKey]?.options && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]" variant="outlined" :items="metadata[metadataKey]?.options"
|
||||
dense :disabled="metadata[metadataKey]?.readonly" density="compact" flat hide-details
|
||||
single-line></v-select>
|
||||
<v-text-field
|
||||
v-else-if="metadata[metadataKey]?.type === 'string' && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]" variant="outlined" dense density="compact" flat hide-details
|
||||
single-line></v-text-field>
|
||||
<v-text-field
|
||||
v-else-if="(metadata[metadataKey]?.type === 'int' || metadata[metadataKey]?.type === 'float') && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]" variant="outlined" dense density="compact" flat hide-details
|
||||
single-line></v-text-field>
|
||||
<v-textarea
|
||||
v-else-if="metadata[metadataKey]?.type === 'text' && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]" variant="outlined" dense density="compact" flat hide-details
|
||||
single-line></v-textarea>
|
||||
<v-switch
|
||||
v-else-if="metadata[metadataKey]?.type === 'bool' && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]" color="primary" hide-details></v-switch>
|
||||
<ListConfigItem
|
||||
v-else-if="metadata[metadataKey]?.type === 'list' && !metadata[metadataKey]?.invisible"
|
||||
:value="iterable[metadataKey]" />
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider style="border-color: #ddd;"></v-divider>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-divider class="my-2 config-divider"></v-divider>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListConfigItem from './ListConfigItem.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ListConfigItem
|
||||
name: 'AstrBotConfig',
|
||||
components: {
|
||||
ListConfigItem
|
||||
},
|
||||
props: {
|
||||
metadata: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
props: {
|
||||
metadata: Object,
|
||||
iterable: Object,
|
||||
metadataKey: String
|
||||
iterable: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
metadataKey: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.config-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.config-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--v-primary-darken1);
|
||||
}
|
||||
|
||||
.config-hint {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.metadata-key, .property-key {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.important-hint {
|
||||
opacity: 1;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.object-config, .simple-config {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.nested-object {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.nested-container {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.config-row {
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.config-row:hover {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.property-info {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.property-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
||||
.property-hint {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.type-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.config-input {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.config-field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.config-divider {
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.nested-object {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.config-row {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.property-info, .type-indicator, .config-input {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+519
-127
@@ -8,164 +8,181 @@ marked.setOptions({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="chat-page-card">
|
||||
<v-card-text class="chat-page-container">
|
||||
<div class="chat-layout">
|
||||
<!-- 左侧对话列表面板 -->
|
||||
<div class="sidebar-panel">
|
||||
<v-btn variant="tonal" rounded="xl" class="new-chat-btn" @click="newC"
|
||||
:disabled="!currCid">
|
||||
<v-icon class="mr-2">mdi-plus</v-icon>创建对话
|
||||
</v-btn>
|
||||
|
||||
<v-card style="margin-bottom: 16px; width: 100%; background-color: #fff; height: 100%;">
|
||||
<v-card-text style="width: 100%; height: calc(100vh - 120px);">
|
||||
<div style="height: 100%; display: flex; gap: 16px;">
|
||||
<div style="max-width: 200px;">
|
||||
<!-- conversation -->
|
||||
<v-btn variant="tonal" rounded="xl" style="margin-bottom: 16px; min-width: 200px;" @click="newC"
|
||||
:disabled="!currCid">+ 创建对话</v-btn>
|
||||
|
||||
<v-card class="mx-auto" min-width="200">
|
||||
<v-list dense nav v-if="conversations.length > 0" style="max-height: 500px; overflow-y: auto;"
|
||||
@update:selected="getConversationMessages">
|
||||
<v-card class="conversation-list-card" v-if="conversations.length > 0">
|
||||
<v-list density="compact" nav class="conversation-list" @update:selected="getConversationMessages">
|
||||
<v-list-item v-for="(item, i) in conversations" :key="item.cid" :value="item.cid"
|
||||
color="primary" rounded="xl">
|
||||
color="primary" rounded="xl" class="conversation-item">
|
||||
<v-list-item-title>新对话</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatDate(item.updated_at) }}</v-list-item-subtitle>
|
||||
|
||||
<v-list-item-subtitle class="timestamp">{{ formatDate(item.updated_at) }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
|
||||
<div>
|
||||
|
||||
<v-chip class="mt-4" color="primary" :append-icon="status?.llm_enabled ? 'mdi-check' : 'mdi-close'">
|
||||
<div class="status-chips">
|
||||
<v-chip class="status-chip" color="primary" :append-icon="status?.llm_enabled ? 'mdi-check' : 'mdi-close'">
|
||||
LLM
|
||||
</v-chip>
|
||||
|
||||
<v-chip class="mt-4 ml-2" color="success" :append-icon="status?.stt_enabled ? 'mdi-check' : 'mdi-close'">
|
||||
<v-chip class="status-chip" color="success" :append-icon="status?.stt_enabled ? 'mdi-check' : 'mdi-close'">
|
||||
语音转文本
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-btn variant="tonal" rounded="xl"
|
||||
style="position: fixed; bottom: 48px; margin-bottom: 16px; min-width: 200px;" v-if="currCid"
|
||||
@click="deleteConversation(currCid)" color="error">删除此对话</v-btn>
|
||||
<v-btn variant="tonal" rounded="xl" class="delete-chat-btn" v-if="currCid"
|
||||
@click="deleteConversation(currCid)" color="error">
|
||||
<v-icon class="mr-2">mdi-delete</v-icon>删除此对话
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div style="height: 100%; width: 100%;">
|
||||
<div style="height: calc(100% - 120px); overflow-y: auto; padding: 16px; " ref="messageContainer">
|
||||
<div class="fade-in" v-if="messages.length == 0"
|
||||
style="height: 100%; display: flex; justify-content: center; align-items: center; flex-direction: column;">
|
||||
<div>
|
||||
<span style="font-size: 28px;">Hello, I'm</span>
|
||||
<span style="font-weight: 1000; font-size: 28px; margin-left: 8px;">AstrBot ⭐</span>
|
||||
<!-- 右侧聊天内容区域 -->
|
||||
<div class="chat-content-panel">
|
||||
<div class="messages-container" ref="messageContainer">
|
||||
<!-- 空聊天欢迎页 -->
|
||||
<div class="welcome-container fade-in" v-if="messages.length == 0">
|
||||
<div class="welcome-title">
|
||||
<span>Hello, I'm</span>
|
||||
<span class="bot-name">AstrBot ⭐</span>
|
||||
</div>
|
||||
<div style="margin-top: 8px; color: #aaa;">
|
||||
<div class="welcome-hint">
|
||||
<span>输入</span>
|
||||
<span
|
||||
style="background-color: #eee; padding-left: 4px; padding-right: 4px; margin: 2px; border-radius: 4px;">help</span>
|
||||
<code>help</code>
|
||||
<span>获取帮助 😊</span>
|
||||
</div>
|
||||
<div style="margin-top: 8px; color: #aaa;">
|
||||
<div class="welcome-hint">
|
||||
<span>长按</span>
|
||||
<span
|
||||
style="background-color: #eee; padding-left: 4px; padding-right: 4px; margin: 2px; border-radius: 4px;">Ctrl</span>
|
||||
<code>Ctrl</code>
|
||||
<span>录制语音 🎤</span>
|
||||
</div>
|
||||
<div style="margin-top: 8px; color: #aaa;">
|
||||
<div class="welcome-hint">
|
||||
<span>按</span>
|
||||
<span
|
||||
style="background-color: #eee; padding-left: 4px; padding-right: 4px; margin: 2px; border-radius: 4px;">Ctrl + V</span>
|
||||
<code>Ctrl + V</code>
|
||||
<span>粘贴图片 🏞️</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-else style="max-height: 100%; padding: 16px; max-width: 700px; margin: 0 auto;">
|
||||
<div class="fade-in" v-for="(msg, index) in messages" :key="index"
|
||||
style="margin-bottom: 16px;">
|
||||
<div v-if="msg.type == 'user'" style="display: flex; justify-content: flex-end;">
|
||||
<div
|
||||
style="padding: 12px; border-radius: 8px; background-color: rgba(94, 53, 177, 0.15)">
|
||||
|
||||
<!-- 聊天消息列表 -->
|
||||
<div v-else class="message-list">
|
||||
<div class="message-item fade-in" v-for="(msg, index) in messages" :key="index">
|
||||
<!-- 用户消息 -->
|
||||
<div v-if="msg.type == 'user'" class="user-message">
|
||||
<div class="message-bubble user-bubble">
|
||||
<span>{{ msg.message }}</span>
|
||||
<div style="display: flex; gap: 8px; margin-top: 8px;"
|
||||
v-if="msg.image_url && msg.image_url.length > 0">
|
||||
<div v-for="(img, index) in msg.image_url" :key="index"
|
||||
style="position: relative; display: inline-block;">
|
||||
<img :src="img"
|
||||
style="width: 100px; height: 100px; border-radius: 8px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);" />
|
||||
|
||||
<!-- 图片附件 -->
|
||||
<div class="image-attachments" v-if="msg.image_url && msg.image_url.length > 0">
|
||||
<div v-for="(img, index) in msg.image_url" :key="index" class="image-attachment">
|
||||
<img :src="img" class="attached-image" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- audio -->
|
||||
<div>
|
||||
<audio controls v-if="msg.audio_url && msg.audio_url.length > 0">
|
||||
|
||||
<!-- 音频附件 -->
|
||||
<div class="audio-attachment" v-if="msg.audio_url && msg.audio_url.length > 0">
|
||||
<audio controls class="audio-player">
|
||||
<source :src="msg.audio_url" type="audio/wav">
|
||||
Your browser does not support the audio element.
|
||||
您的浏览器不支持音频播放。
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
<v-avatar class="user-avatar" color="deep-purple-lighten-3" size="36">
|
||||
<v-icon icon="mdi-account" />
|
||||
</v-avatar>
|
||||
</div>
|
||||
<div v-else style="display: flex; justify-content: flex-start; gap: 16px;">
|
||||
<span style="font-size: 32px;">✨</span>
|
||||
<div v-html="marked(msg.message)" class="mc" style="font-family: inherit;"></div>
|
||||
|
||||
<!-- 机器人消息 -->
|
||||
<div v-else class="bot-message">
|
||||
<v-avatar class="bot-avatar" color="deep-purple" size="36">
|
||||
<span class="text-h6">✨</span>
|
||||
</v-avatar>
|
||||
<div class="message-bubble bot-bubble">
|
||||
<div v-html="marked(msg.message)" class="markdown-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fade-in" style="bottom: 16px; width: 100%; padding: 8px; ">
|
||||
<!-- 输入区域 -->
|
||||
<div class="input-area fade-in">
|
||||
<v-text-field
|
||||
id="input-field"
|
||||
variant="outlined"
|
||||
v-model="prompt"
|
||||
:label="inputFieldLabel"
|
||||
placeholder="开始输入..."
|
||||
:loading="loadingChat"
|
||||
clear-icon="mdi-close-circle"
|
||||
clearable
|
||||
@click:clear="clearMessage"
|
||||
class="message-input"
|
||||
@keydown="handleInputKeyDown"
|
||||
hide-details
|
||||
>
|
||||
<template v-slot:loader>
|
||||
<v-progress-linear :active="loadingChat" height="3" color="deep-purple" indeterminate></v-progress-linear>
|
||||
</template>
|
||||
|
||||
<div
|
||||
style="width: 100%; justify-content: center; align-items: center; display: flex; flex-direction: column; margin-top: 8px;">
|
||||
<template v-slot:append>
|
||||
<v-tooltip text="发送">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
@click="sendMessage"
|
||||
class="send-btn"
|
||||
icon="mdi-send"
|
||||
variant="text"
|
||||
color="deep-purple"
|
||||
:disabled="!prompt && stagedImagesUrl.length === 0 && !stagedAudioUrl"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-text-field id="input-field" variant="outlined" v-model="prompt" :label="inputFieldLabel"
|
||||
placeholder="Start typing..." loading clear-icon="mdi-close-circle" clearable
|
||||
@click:clear="clearMessage" style="width: 100%; max-width: 850px;"
|
||||
@keydown="handleInputKeyDown">
|
||||
<template v-slot:loader>
|
||||
<v-progress-linear :active="loadingChat" height="6"
|
||||
indeterminate></v-progress-linear>
|
||||
</template>
|
||||
<v-tooltip text="语音输入">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
@click="isRecording ? stopRecording() : startRecording()"
|
||||
class="record-btn"
|
||||
:icon="isRecording ? 'mdi-stop-circle' : 'mdi-microphone'"
|
||||
variant="text"
|
||||
:color="isRecording ? 'error' : 'deep-purple'"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-tooltip text="发送">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" @click="sendMessage" size="35"
|
||||
icon="mdi-arrow-up-circle" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
|
||||
<v-tooltip text="语音输入">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon :color="isRecording ? 'error' : ''" v-bind="props"
|
||||
@click="isRecording ? stopRecording() : startRecording()" size="35"
|
||||
icon="mdi-record-circle" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<div style="display: flex; gap: 8px; margin-top: -8px;">
|
||||
<div v-for="(img, index) in stagedImagesUrl" :key="index"
|
||||
style="position: relative; display: inline-block;">
|
||||
<img :src="img"
|
||||
style="width: 50px; height: 50px; border-radius: 8px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);" />
|
||||
<v-icon @click="removeImage(index)" size="20" color="red"
|
||||
style="position: absolute; top: 0; right: 0; cursor: pointer;">mdi-close-circle</v-icon>
|
||||
</div>
|
||||
<div style="display: inline-block; width: 50px; height: 50px;">
|
||||
<div v-if="stagedAudioUrl"
|
||||
style="position: relative; padding: 6px; border-radius: 8px; background-color: rgba(94, 53, 177, 0.15); display: inline-block;">
|
||||
新录音
|
||||
<v-icon @click="removeAudio" size="20" color="red"
|
||||
style="position: absolute; top: 0; right: 0; cursor: pointer;">mdi-close-circle</v-icon>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- 附件预览区 -->
|
||||
<div class="attachments-preview" v-if="stagedImagesUrl.length > 0 || stagedAudioUrl">
|
||||
<div v-for="(img, index) in stagedImagesUrl" :key="index" class="image-preview">
|
||||
<img :src="img" class="preview-image" />
|
||||
<v-btn @click="removeImage(index)" class="remove-attachment-btn" icon="mdi-close" size="small" color="error" variant="text" />
|
||||
</div>
|
||||
|
||||
<div v-if="stagedAudioUrl" class="audio-preview">
|
||||
<v-chip color="deep-purple-lighten-4" class="audio-chip">
|
||||
<v-icon start icon="mdi-microphone" size="small"></v-icon>
|
||||
新录音
|
||||
</v-chip>
|
||||
<v-btn @click="removeAudio" class="remove-attachment-btn" icon="mdi-close" size="small" color="error" variant="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChatPage',
|
||||
@@ -192,7 +209,7 @@ export default {
|
||||
|
||||
eventSource: null,
|
||||
|
||||
// 添加Ctrl键长按相关变量
|
||||
// Ctrl键长按相关变量
|
||||
ctrlKeyDown: false,
|
||||
ctrlKeyTimer: null,
|
||||
ctrlKeyLongPressThreshold: 300 // 长按阈值,单位毫秒
|
||||
@@ -574,40 +591,415 @@ export default {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 基础动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.mc h1,
|
||||
.mc h2,
|
||||
.mc h3,
|
||||
.mc h4,
|
||||
.mc h5,
|
||||
.mc h6 {
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(20px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 聊天页面布局 */
|
||||
.chat-page-card {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.chat-page-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 120px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.chat-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.sidebar-panel {
|
||||
max-width: 240px;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 8px;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.new-chat-btn {
|
||||
margin-bottom: 16px;
|
||||
min-width: 200px;
|
||||
background-color: #f5f0ff !important;
|
||||
color: #673ab7 !important;
|
||||
font-weight: 500;
|
||||
box-shadow: none !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.new-chat-btn:hover {
|
||||
background-color: #ede7f6 !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.conversation-list-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #f0f0f0;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
margin-bottom: 4px;
|
||||
border-radius: 8px !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.conversation-item:hover {
|
||||
background-color: #f5f0ff;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.status-chips {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.delete-chat-btn {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
margin-bottom: 16px;
|
||||
min-width: 200px;
|
||||
background-color: #feecec !important;
|
||||
color: #d32f2f !important;
|
||||
font-weight: 500;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.delete-chat-btn:hover {
|
||||
background-color: #ffebee !important;
|
||||
}
|
||||
|
||||
/* 聊天内容区域 */
|
||||
.chat-content-panel {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
height: calc(100% - 80px);
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 欢迎页样式 */
|
||||
.welcome-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 28px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.bot-name {
|
||||
font-weight: 700;
|
||||
margin-left: 8px;
|
||||
color: #673ab7;
|
||||
}
|
||||
|
||||
.welcome-hint {
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.welcome-hint code {
|
||||
background-color: #f5f0ff;
|
||||
padding: 2px 6px;
|
||||
margin: 0 4px;
|
||||
border-radius: 4px;
|
||||
color: #673ab7;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 消息列表样式 */
|
||||
.message-list {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 24px;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bot-message {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 12px 16px;
|
||||
border-radius: 18px;
|
||||
max-width: 80%;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-bubble {
|
||||
background-color: #f5f0ff;
|
||||
color: #333;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.bot-bubble {
|
||||
background-color: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
color: #333;
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
|
||||
.user-avatar, .bot-avatar {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
/* 附件样式 */
|
||||
.image-attachments {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.image-attachment {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.attached-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.attached-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.audio-attachment {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
/* 输入区域样式 */
|
||||
.input-area {
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
border-radius: 24px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.send-btn, .record-btn {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* 附件预览区 */
|
||||
.attachments-preview {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
max-width: 900px;
|
||||
margin: 8px auto 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.image-preview, .audio-preview {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.audio-chip {
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.remove-attachment-btn {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.remove-attachment-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Markdown内容样式 */
|
||||
.markdown-content {
|
||||
font-family: inherit;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3,
|
||||
.markdown-content h4,
|
||||
.markdown-content h5,
|
||||
.markdown-content h6 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.mc li {
|
||||
.markdown-content h1 {
|
||||
font-size: 1.8em;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-left: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
|
||||
.mc p {
|
||||
.markdown-content p {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background-color: #f8f8f8;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
background-color: #f5f0ff;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #673ab7;
|
||||
}
|
||||
|
||||
.markdown-content img {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid #673ab7;
|
||||
padding-left: 16px;
|
||||
color: #666;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-content th,
|
||||
.markdown-content td {
|
||||
border: 1px solid #eee;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-content th {
|
||||
background-color: #f5f0ff;
|
||||
}
|
||||
|
||||
/* 动画类 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +1,84 @@
|
||||
<template>
|
||||
<v-row style="margin: 2px;">
|
||||
<v-alert
|
||||
:type="noticeType"
|
||||
:text="noticeContent"
|
||||
:title="noticeTitle"
|
||||
v-if="noticeTitle && noticeContent"
|
||||
closable
|
||||
></v-alert>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<TotalMessage :stat="stat" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<OnlinePlatform :stat="stat" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<OnlineTime :stat="stat" />
|
||||
</v-col>
|
||||
<v-col cols="12" lg="8">
|
||||
<MessageStat :stat="stat" />
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4">
|
||||
<PlatformStat :stat="stat" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-header">
|
||||
<h1 class="dashboard-title">控制台</h1>
|
||||
<div class="dashboard-subtitle">实时监控和统计数据</div>
|
||||
</div>
|
||||
|
||||
<v-slide-y-transition>
|
||||
<v-row v-if="noticeTitle && noticeContent" class="notice-row">
|
||||
<v-alert
|
||||
:type="noticeType"
|
||||
:text="noticeContent"
|
||||
:title="noticeTitle"
|
||||
closable
|
||||
class="dashboard-alert"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
></v-alert>
|
||||
</v-row>
|
||||
</v-slide-y-transition>
|
||||
|
||||
<!-- 主指标卡片行 -->
|
||||
<v-row class="stats-row">
|
||||
<v-col cols="12" md="3">
|
||||
<v-slide-y-transition>
|
||||
<TotalMessage :stat="stat" />
|
||||
</v-slide-y-transition>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-slide-y-transition>
|
||||
<OnlinePlatform :stat="stat" />
|
||||
</v-slide-y-transition>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-slide-y-transition>
|
||||
<RunningTime :stat="stat" />
|
||||
</v-slide-y-transition>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-slide-y-transition>
|
||||
<MemoryUsage :stat="stat" />
|
||||
</v-slide-y-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 图表行 -->
|
||||
<v-row class="charts-row">
|
||||
<v-col cols="12" lg="8">
|
||||
<v-slide-y-transition>
|
||||
<MessageStat />
|
||||
</v-slide-y-transition>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4">
|
||||
<v-slide-y-transition>
|
||||
<PlatformStat :stat="stat" />
|
||||
</v-slide-y-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="dashboard-footer">
|
||||
<v-chip size="small" color="primary" variant="flat" prepend-icon="mdi-refresh">
|
||||
最后更新: {{ lastUpdated }}
|
||||
</v-chip>
|
||||
<v-btn
|
||||
icon="mdi-refresh"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="text"
|
||||
class="ml-2"
|
||||
@click="fetchData"
|
||||
:loading="isRefreshing"
|
||||
></v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import TotalMessage from './components/TotalMessage.vue';
|
||||
import OnlinePlatform from './components/OnlinePlatform.vue';
|
||||
import OnlineTime from './components/OnlineTime.vue';
|
||||
import RunningTime from './components/RunningTime.vue';
|
||||
import MemoryUsage from './components/MemoryUsage.vue';
|
||||
import MessageStat from './components/MessageStat.vue';
|
||||
import PlatformStat from './components/PlatformStat.vue';
|
||||
import axios from 'axios';
|
||||
@@ -41,7 +88,8 @@ export default {
|
||||
components: {
|
||||
TotalMessage,
|
||||
OnlinePlatform,
|
||||
OnlineTime,
|
||||
RunningTime,
|
||||
MemoryUsage,
|
||||
MessageStat,
|
||||
PlatformStat,
|
||||
},
|
||||
@@ -50,23 +98,142 @@ export default {
|
||||
noticeTitle: '',
|
||||
noticeContent: '',
|
||||
noticeType: '',
|
||||
lastUpdated: '加载中...',
|
||||
refreshInterval: null,
|
||||
isRefreshing: false
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
axios.get('/api/stat/get').then((res) => {
|
||||
this.stat = res.data.data;
|
||||
});
|
||||
|
||||
axios.get('https://api.soulter.top/astrbot-announcement').then((res) => {
|
||||
let data = res.data.data;
|
||||
// 如果 dashboard-notice 在其中
|
||||
if (data['dashboard-notice']) {
|
||||
this.noticeTitle = data['dashboard-notice'].title;
|
||||
this.noticeContent = data['dashboard-notice'].content;
|
||||
this.noticeType = data['dashboard-notice'].type;
|
||||
}
|
||||
});
|
||||
this.fetchData();
|
||||
this.fetchNotice();
|
||||
|
||||
// 设置自动刷新(每60秒)
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.fetchData();
|
||||
}, 60000);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
// 清除定时器
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchData() {
|
||||
this.isRefreshing = true;
|
||||
try {
|
||||
const res = await axios.get('/api/stat/get');
|
||||
this.stat = res.data.data;
|
||||
this.lastUpdated = new Date().toLocaleTimeString();
|
||||
console.log('Dashboard data:', this.stat);
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
},
|
||||
|
||||
fetchNotice() {
|
||||
axios.get('https://api.soulter.top/astrbot-announcement').then((res) => {
|
||||
let data = res.data.data;
|
||||
// 如果 dashboard-notice 在其中
|
||||
if (data['dashboard-notice']) {
|
||||
this.noticeTitle = data['dashboard-notice'].title;
|
||||
this.noticeContent = data['dashboard-notice'].content;
|
||||
this.noticeType = data['dashboard-notice'].type;
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('获取公告失败:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
padding: 16px;
|
||||
background-color: #f9fafc;
|
||||
min-height: calc(100vh - 64px);
|
||||
border-radius: 10px;
|
||||
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.notice-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dashboard-alert {
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.stats-row, .charts-row, .plugin-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.plugin-card {
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.plugin-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.plugin-subtitle {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.plugin-item {
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.plugin-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.plugin-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.plugin-version {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dashboard-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<v-card elevation="1" class="stat-card memory-card">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-start">
|
||||
<div class="icon-wrapper">
|
||||
<v-icon icon="mdi-memory" size="24"></v-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">内存占用</div>
|
||||
<div class="stat-value-wrapper">
|
||||
<h2 class="stat-value">{{ stat.memory?.process || 0 }} <span class="memory-unit">MiB / {{ stat.memory?.system || 0 }} MiB</span></h2>
|
||||
<v-chip :color="memoryStatus.color" size="x-small" class="status-chip">
|
||||
{{ memoryStatus.label }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metrics-container">
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">CPU 负载</div>
|
||||
<div class="metric-value">{{ stat.cpu_percent || '0' }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MemoryUsage',
|
||||
props: ['stat'],
|
||||
computed: {
|
||||
memoryPercentage() {
|
||||
if (!this.stat.memory || !this.stat.memory.process || !this.stat.memory.system) return 0;
|
||||
return Math.round((this.stat.memory.process / this.stat.memory.system) * 100);
|
||||
},
|
||||
memoryStatus() {
|
||||
const percentage = this.memoryPercentage;
|
||||
if (percentage < 30) {
|
||||
return { color: 'success', label: '良好' };
|
||||
} else if (percentage < 70) {
|
||||
return { color: 'warning', label: '正常' };
|
||||
} else {
|
||||
return { color: 'error', label: '偏高' };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.memory-card {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.memory-unit {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metrics-container {
|
||||
display: flex;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
margin-top: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +1,136 @@
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card elevation="0">
|
||||
<v-card variant="outlined">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="7">
|
||||
<span class="text-subtitle-2 text-disabled font-weight-bold">总消息趋势</span>
|
||||
<!-- <h3 class="text-h3 mt-1">{{ total_cnt }}</h3> -->
|
||||
</v-col>
|
||||
<v-col cols="12" sm="5">
|
||||
<v-select color="primary" variant="outlined" hide-details v-model="select" :items="items" item-title="state"
|
||||
item-value="abbr" label="Select" persistent-hint return-object single-line>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="mt-4">
|
||||
<apexchart type="area" height="280" :options="chartOptions1" :series="lineChart1.series" ref="rtchart">
|
||||
</apexchart>
|
||||
<v-card elevation="1" class="chart-card">
|
||||
<v-card-text>
|
||||
<div class="chart-header">
|
||||
<div>
|
||||
<div class="chart-title">消息趋势分析</div>
|
||||
<div class="chart-subtitle">跟踪消息数量随时间的变化</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-select
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
v-model="selectedTimeRange"
|
||||
:items="timeRanges"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
class="time-select"
|
||||
@update:model-value="fetchMessageSeries"
|
||||
return-object
|
||||
single-line
|
||||
>
|
||||
<template v-slot:selection="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon start size="small">mdi-calendar-range</v-icon>
|
||||
{{ item.raw.label }}
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
</div>
|
||||
|
||||
<div class="chart-stats">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">总消息数</div>
|
||||
<div class="stat-number">{{ totalMessages }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">平均每天</div>
|
||||
<div class="stat-number">{{ dailyAverage }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box" :class="{'trend-up': growthRate > 0, 'trend-down': growthRate < 0}">
|
||||
<div class="stat-label">增长率</div>
|
||||
<div class="stat-number">
|
||||
<v-icon size="small" :icon="growthRate > 0 ? 'mdi-arrow-up' : 'mdi-arrow-down'"></v-icon>
|
||||
{{ Math.abs(growthRate) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
<apexchart
|
||||
type="area"
|
||||
height="280"
|
||||
:options="chartOptions"
|
||||
:series="chartSeries"
|
||||
ref="chart"
|
||||
></apexchart>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'MessageStat',
|
||||
components: {
|
||||
},
|
||||
props: ['stat'],
|
||||
data: () => ({
|
||||
total_cnt: 0,
|
||||
select: { state: 'Today', abbr: 'FL' },
|
||||
items: [
|
||||
{ state: '过去 1 天', abbr: 'FL' },
|
||||
totalMessages: '0',
|
||||
dailyAverage: '0',
|
||||
growthRate: 0,
|
||||
loading: false,
|
||||
selectedTimeRange: { label: '过去 1 天', value: 86400 },
|
||||
timeRanges: [
|
||||
{ label: '过去 1 天', value: 86400 },
|
||||
{ label: '过去 3 天', value: 259200 },
|
||||
{ label: '过去 7 天', value: 604800 },
|
||||
{ label: '过去 30 天', value: 2592000 },
|
||||
],
|
||||
chartOptions1: {
|
||||
|
||||
chartOptions: {
|
||||
chart: {
|
||||
type: 'area',
|
||||
height: 400,
|
||||
fontFamily: `inherit`,
|
||||
foreColor: '#a1aab2',
|
||||
toolbar: {
|
||||
show: true,
|
||||
tools: {
|
||||
download: true,
|
||||
selection: false,
|
||||
zoom: true,
|
||||
zoomin: true,
|
||||
zoomout: true,
|
||||
pan: true,
|
||||
},
|
||||
},
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'easeinout',
|
||||
speed: 800,
|
||||
},
|
||||
},
|
||||
colors: ['#5e35b1'],
|
||||
fill: {
|
||||
type: 'solid',
|
||||
opacity: 0.3,
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 1
|
||||
width: 2
|
||||
},
|
||||
markers: {
|
||||
size: 3,
|
||||
strokeWidth: 2,
|
||||
hover: {
|
||||
size: 5,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
fixed: {
|
||||
enabled: false
|
||||
},
|
||||
theme: 'light',
|
||||
x: {
|
||||
show: true,
|
||||
format: 'yyyy-MM-dd HH:mm'
|
||||
},
|
||||
y: {
|
||||
@@ -75,45 +146,225 @@ export default {
|
||||
},
|
||||
labels: {
|
||||
formatter: function (value) {
|
||||
return new Date(value).toLocaleString();
|
||||
return new Date(value).toLocaleString('zh-CN', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: '消息条数'
|
||||
}
|
||||
},
|
||||
min: function(min) {
|
||||
return min < 10 ? 0 : Math.floor(min * 0.8);
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
show: true
|
||||
borderColor: '#f1f1f1',
|
||||
row: {
|
||||
colors: ['transparent', 'transparent'],
|
||||
opacity: 0.2
|
||||
},
|
||||
column: {
|
||||
colors: ['transparent', 'transparent'],
|
||||
},
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
lineChart1: {
|
||||
series: [
|
||||
{
|
||||
name: '消息条数',
|
||||
data: []
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
chartSeries: [
|
||||
{
|
||||
name: '消息条数',
|
||||
data: []
|
||||
}
|
||||
],
|
||||
|
||||
messageTimeSeries: []
|
||||
}),
|
||||
|
||||
watch: {
|
||||
stat: {
|
||||
handler: function (val, oldVal) {
|
||||
val = val.message_time_series
|
||||
// this.total_cnt = val.message_count
|
||||
// [[timestamp, cnt], ...]
|
||||
this.lineChart1.series[0].data = val.map((item) => {
|
||||
return [new Date(item[0]*1000).getTime(), item[1]];
|
||||
});
|
||||
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
mounted() {
|
||||
// 初始加载
|
||||
this.fetchMessageSeries();
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
methods: {
|
||||
formatNumber(num) {
|
||||
return new Intl.NumberFormat('zh-CN').format(num);
|
||||
},
|
||||
|
||||
async fetchMessageSeries() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/api/stat/get?offset_sec=${this.selectedTimeRange.value}`);
|
||||
const data = response.data.data;
|
||||
|
||||
if (data && data.message_time_series) {
|
||||
this.messageTimeSeries = data.message_time_series;
|
||||
this.processTimeSeriesData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取消息趋势数据失败:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
processTimeSeriesData() {
|
||||
// 转换数据为图表格式
|
||||
this.chartSeries[0].data = this.messageTimeSeries.map((item) => {
|
||||
return [new Date(item[0]*1000).getTime(), item[1]];
|
||||
});
|
||||
|
||||
// 计算总消息数
|
||||
let total = 0;
|
||||
this.messageTimeSeries.forEach(item => {
|
||||
total += item[1];
|
||||
});
|
||||
this.totalMessages = this.formatNumber(total);
|
||||
|
||||
// 计算日平均
|
||||
if (this.messageTimeSeries.length > 0) {
|
||||
const daysSpan = this.selectedTimeRange.value / 86400; // 将秒转换为天数
|
||||
this.dailyAverage = this.formatNumber(Math.round(total / daysSpan));
|
||||
}
|
||||
|
||||
// 计算增长率
|
||||
this.calculateGrowthRate();
|
||||
},
|
||||
|
||||
calculateGrowthRate() {
|
||||
if (this.messageTimeSeries.length < 4) {
|
||||
this.growthRate = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算前半部分和后半部分的消息总数
|
||||
const halfIndex = Math.floor(this.messageTimeSeries.length / 2);
|
||||
|
||||
const firstHalf = this.messageTimeSeries
|
||||
.slice(0, halfIndex)
|
||||
.reduce((sum, item) => sum + item[1], 0);
|
||||
|
||||
const secondHalf = this.messageTimeSeries
|
||||
.slice(halfIndex)
|
||||
.reduce((sum, item) => sum + item[1], 0);
|
||||
|
||||
// 计算增长率
|
||||
if (firstHalf > 0) {
|
||||
this.growthRate = Math.round(((secondHalf - firstHalf) / firstHalf) * 100);
|
||||
} else {
|
||||
this.growthRate = secondHalf > 0 ? 100 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-card {
|
||||
height: 100%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.chart-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chart-subtitle {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.time-select {
|
||||
max-width: 150px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chart-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
padding: 12px 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trend-up .stat-number {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.trend-down .stat-number {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +1,84 @@
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card elevation="0" class="bg-primary overflow-hidden bubble-shape bubble-primary-shape">
|
||||
<v-card elevation="1" class="stat-card platform-card">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-start mb-3">
|
||||
<v-btn icon rounded="sm" color="darkprimary" variant="flat">
|
||||
<v-icon icon="mdi-account-multiple-outline"></v-icon>
|
||||
</v-btn>
|
||||
<div class="d-flex align-start">
|
||||
<div class="icon-wrapper">
|
||||
<v-icon icon="mdi-server-network" size="24"></v-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">消息平台</div>
|
||||
<div class="stat-value-wrapper">
|
||||
<h2 class="stat-value">{{ stat.platform_count || 0 }}</h2>
|
||||
</div>
|
||||
<div class="stat-subtitle">已连接的消息平台数量</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<h2 class="text-h1 font-weight-medium">
|
||||
{{ stat.platform_count }}
|
||||
</h2>
|
||||
<span class="text-subtitle-1 text-medium-emphasis text-white">消息平台数</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'TotalSession',
|
||||
props: ['stat'],
|
||||
data: () => ({
|
||||
stat: {
|
||||
platform_count: 0
|
||||
}
|
||||
}),
|
||||
name: 'OnlinePlatform',
|
||||
props: ['stat']
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.platform-card {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.stat-subtitle {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +1,190 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-card elevation="0" class="bg-primary overflow-hidden bubble-shape-sm bubble-primary mb-6">
|
||||
<v-card-text class="pa-5">
|
||||
<div class="d-flex align-center gap-3">
|
||||
<v-btn color="darkprimary" icon rounded="sm" variant="flat">
|
||||
<v-icon icon="mdi-clock"></v-icon>
|
||||
</v-btn>
|
||||
<div>
|
||||
<h4 class="text-h4 font-weight-medium">{{ stat.running }}</h4>
|
||||
<span class="text-subtitle-2 text-medium-emphasis text-white">运行时间</span>
|
||||
<div class="stats-container">
|
||||
<v-card elevation="1" class="stat-card uptime-card mb-4">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center">
|
||||
<div class="icon-wrapper">
|
||||
<v-icon icon="mdi-clock-outline" size="24"></v-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">运行时间</div>
|
||||
<h3 class="uptime-value">{{ stat.running || '加载中...' }}</h3>
|
||||
</div>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<div class="uptime-status">
|
||||
<v-icon icon="mdi-circle" size="10" color="success" class="blink-animation"></v-icon>
|
||||
<span class="status-text">在线</span>
|
||||
</div>
|
||||
</div>
|
||||
<v-spacer></v-spacer>
|
||||
<div>
|
||||
<v-btn icon rounded="sm" variant="plain">
|
||||
<v-icon color="black" icon="mdi-stop" size="32"></v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card elevation="0" class="bubble-shape-sm overflow-hidden bubble-warning">
|
||||
<v-card-text class="pa-5">
|
||||
<div class="d-flex align-center gap-3">
|
||||
<v-btn color="lightwarning" icon rounded="sm" variant="flat">
|
||||
<v-icon icon="mdi-memory"></v-icon>
|
||||
</v-btn>
|
||||
<div>
|
||||
<h4 class="text-h4 font-weight-medium">{{ stat.memory?.process }} / {{ stat.memory?.system }} MiB</h4>
|
||||
|
||||
<span class="text-subtitle-2 text-disabled font-weight-medium">占用内存</span>
|
||||
<v-card elevation="1" class="stat-card memory-card">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center">
|
||||
<div class="icon-wrapper">
|
||||
<v-icon icon="mdi-memory" size="24"></v-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">内存占用</div>
|
||||
<div class="memory-values">
|
||||
<h3 class="memory-value">{{ stat.memory?.process || 0 }} <span class="memory-unit">MiB</span></h3>
|
||||
<span class="memory-separator">/</span>
|
||||
<h4 class="memory-total">{{ stat.memory?.system || 0 }} <span class="memory-unit">MiB</span></h4>
|
||||
</div>
|
||||
|
||||
<v-progress-linear
|
||||
:model-value="memoryPercentage"
|
||||
color="warning"
|
||||
height="4"
|
||||
class="mt-2"
|
||||
></v-progress-linear>
|
||||
|
||||
<div class="memory-percentage">{{ memoryPercentage }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'OnlineTime',
|
||||
components: {
|
||||
},
|
||||
props: ['stat'],
|
||||
watch: {
|
||||
},
|
||||
data: () => ({
|
||||
stat: {
|
||||
memory: "Loading",
|
||||
running: "Loading",
|
||||
memory: { process: 0, system: 0 },
|
||||
running: "加载中...",
|
||||
},
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
computed: {
|
||||
memoryPercentage() {
|
||||
if (!this.stat.memory || !this.stat.memory.process || !this.stat.memory.system) return 0;
|
||||
return Math.round((this.stat.memory.process / this.stat.memory.system) * 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.stats-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.uptime-card {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.memory-card {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.uptime-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.uptime-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.memory-values {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.memory-value {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.memory-separator {
|
||||
margin: 0 6px;
|
||||
font-weight: 300;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.memory-total {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.memory-unit {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.memory-percentage {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
text-align: right;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.blink-animation {
|
||||
animation: blink 1.5s infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -1,116 +1,253 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
// chart 1
|
||||
const chartOptions1 = computed(() => {
|
||||
return {
|
||||
chart: {
|
||||
type: 'area',
|
||||
height: 95,
|
||||
fontFamily: `inherit`,
|
||||
foreColor: '#a1aab2',
|
||||
sparkline: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
colors: ['#5e35b1'],
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 1
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'dark',
|
||||
fixed: {
|
||||
enabled: false
|
||||
},
|
||||
x: {
|
||||
show: false
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
formatter: () => '消息条数 '
|
||||
}
|
||||
},
|
||||
marker: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// chart 1
|
||||
const lineChart1 = {
|
||||
series: [
|
||||
{
|
||||
data: [0, 15, 10, 50, 30, 40, 25]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card elevation="0">
|
||||
<v-card variant="outlined">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center">
|
||||
<h4 class="text-h4 mt-1">各平台消息数</h4>
|
||||
<v-card elevation="1" class="platform-stat-card">
|
||||
<v-card-text>
|
||||
<div class="platform-header">
|
||||
<div>
|
||||
<div class="platform-title">平台消息统计</div>
|
||||
<div class="platform-subtitle">各平台消息数量分布</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<v-list lines="two" class="py-0" style="height: 270px;">
|
||||
</div>
|
||||
|
||||
<v-divider class="my-3"></v-divider>
|
||||
|
||||
<div v-if="platforms.length > 0" class="platform-list-container">
|
||||
<v-list class="platform-list" density="compact">
|
||||
<v-list-item
|
||||
v-for="(platform, i) in sortedPlatforms"
|
||||
:key="i"
|
||||
:value="platform"
|
||||
class="platform-item"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<div class="platform-rank" :class="{'top-rank': i < 3}">{{ i + 1 }}</div>
|
||||
</template>
|
||||
|
||||
<v-list-item v-for="(platform, i) in platforms" :key="i" :value="platform" color="secondary" rounded="sm">
|
||||
<div class="d-inline-flex align-center justify-space-between w-100">
|
||||
<div>
|
||||
<h6 class="text-subtitle-1 text-medium-emphasis font-weight-bold">
|
||||
{{ platform.name }}
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto text-subtitle-1 text-medium-emphasis font-weight-bold">{{ platform.count }} 条</div>
|
||||
<v-list-item-title class="platform-name">{{ platform.name }}</v-list-item-title>
|
||||
|
||||
<template v-slot:append>
|
||||
<div class="platform-count">
|
||||
<span class="count-value">{{ platform.count }}</span>
|
||||
<span class="count-label">条</span>
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<v-btn color="primary" variant="text"
|
||||
>详情
|
||||
<template v-slot:append>
|
||||
<ChevronRightIcon stroke-width="1.5" width="20" />
|
||||
</template>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div class="platform-stats-summary">
|
||||
<div class="platform-stat-item">
|
||||
<div class="stat-label">平台数</div>
|
||||
<div class="stat-value">{{ platforms.length }}</div>
|
||||
</div>
|
||||
<v-divider vertical></v-divider>
|
||||
<div class="platform-stat-item">
|
||||
<div class="stat-label">最活跃</div>
|
||||
<div class="stat-value">{{ mostActivePlatform }}</div>
|
||||
</div>
|
||||
<v-divider vertical></v-divider>
|
||||
<div class="platform-stat-item">
|
||||
<div class="stat-label">总消息占比</div>
|
||||
<div class="stat-value">{{ topPlatformPercentage }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div class="platform-chart">
|
||||
<v-progress-linear
|
||||
v-for="(platform, i) in sortedPlatforms.slice(0, 5)"
|
||||
:key="i"
|
||||
:model-value="getPercentage(platform.count)"
|
||||
height="8"
|
||||
rounded
|
||||
class="platform-progress"
|
||||
:color="i === 0 ? 'primary' : i === 1 ? 'info' : i === 2 ? 'success' : 'grey-lighten-1'"
|
||||
></v-progress-linear>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-data">
|
||||
<v-icon icon="mdi-information-outline" size="40" color="grey-lighten-1"></v-icon>
|
||||
<div class="no-data-text">暂无平台数据</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PlatformStat',
|
||||
components: {
|
||||
},
|
||||
props: ['stat'],
|
||||
data: () => ({
|
||||
platforms: []
|
||||
}),
|
||||
computed: {
|
||||
sortedPlatforms() {
|
||||
return [...this.platforms].sort((a, b) => b.count - a.count);
|
||||
},
|
||||
totalCount() {
|
||||
return this.platforms.reduce((sum, platform) => sum + platform.count, 0);
|
||||
},
|
||||
mostActivePlatform() {
|
||||
return this.sortedPlatforms.length > 0 ? this.sortedPlatforms[0].name : '-';
|
||||
},
|
||||
topPlatformPercentage() {
|
||||
if (this.totalCount === 0 || this.sortedPlatforms.length === 0) return 0;
|
||||
return Math.round((this.sortedPlatforms[0].count / this.totalCount) * 100);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
stat: {
|
||||
handler: function (val, oldVal) {
|
||||
this.platforms = val.platform
|
||||
handler: function (val) {
|
||||
if (val && val.platform) {
|
||||
this.platforms = val.platform;
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
platforms: [
|
||||
]
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
methods: {
|
||||
getPercentage(count) {
|
||||
return this.totalCount ? (count / this.totalCount) * 100 : 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.platform-stat-card {
|
||||
height: 100%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.platform-stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.platform-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.platform-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.platform-subtitle {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.platform-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.platform-list {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.platform-item {
|
||||
padding: 8px 16px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.platform-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.platform-rank {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: #f0f0f0;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.top-rank {
|
||||
background-color: #5e35b1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.platform-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.platform-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.count-value {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #5e35b1;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.count-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.platform-stats-summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.platform-stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.platform-chart {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.platform-progress {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
height: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.no-data-text {
|
||||
color: #999;
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<v-card elevation="1" class="stat-card uptime-card">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-start">
|
||||
<div class="icon-wrapper">
|
||||
<v-icon icon="mdi-clock-outline" size="24"></v-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">运行时间</div>
|
||||
<div class="stat-value-wrapper">
|
||||
<h2 class="stat-value">{{ formattedTime }}</h2>
|
||||
</div>
|
||||
<div class="stat-subtitle">AstrBot 运行时间</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RunningTime',
|
||||
props: ['stat'],
|
||||
computed: {
|
||||
formattedTime() {
|
||||
return this.stat?.running || '加载中...';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.uptime-card {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-subtitle {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -1,40 +1,97 @@
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card elevation="0" class="bg-secondary overflow-hidden bubble-shape bubble-secondary-shape">
|
||||
<v-card elevation="1" class="stat-card message-card">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-start mb-3">
|
||||
<v-btn icon rounded="sm" color="darksecondary" variant="flat">
|
||||
<v-icon icon="mdi-account-multiple-outline"></v-icon>
|
||||
</v-btn>
|
||||
<div class="d-flex align-start">
|
||||
<div class="icon-wrapper">
|
||||
<v-icon icon="mdi-message-text-outline" size="24"></v-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">消息总数</div>
|
||||
<div class="stat-value-wrapper">
|
||||
<h2 class="stat-value">{{ formattedCount }}</h2>
|
||||
<v-chip v-if="stat.daily_increase" class="trend-chip" size="x-small" color="success">
|
||||
+{{ stat.daily_increase }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="stat-subtitle">所有平台发送的消息总计</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<h2 class="text-h1 font-weight-medium">
|
||||
{{ stat.message_count }}
|
||||
</h2>
|
||||
<span class="text-subtitle-1 text-medium-emphasis text-white">消息总数</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'TotalMessage',
|
||||
props: ['stat'],
|
||||
data: () => ({
|
||||
stat: {
|
||||
message_count: 0
|
||||
computed: {
|
||||
formattedCount() {
|
||||
const count = this.stat?.message_count;
|
||||
return count ? count.toLocaleString() : '0';
|
||||
}
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.message-card {
|
||||
background-color: #5e35b1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.trend-chip {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-subtitle {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user