UI改版测试V0

This commit is contained in:
liuwei
2026-03-09 11:32:08 +08:00
parent 8b95fbc2a9
commit 14720f48a4
17 changed files with 3054 additions and 4559 deletions

View File

@@ -3,13 +3,29 @@
{% block title %}接口文档 - 机器人管理后台{% endblock %}
{% block content %}
<div class="iframe-page-container">
<el-card class="iframe-card">
<div slot="header">
<span>接口文档</span>
<div class="page-shell docs-page">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">Developer Workspace</div>
<h1>接口文档</h1>
<p>把文档入口纳入统一的控制台壳层,让开发工具页也具备一致的使用体验。</p>
</div>
<div class="iframe-container">
<iframe src="{{ src_url }}" frameborder="0"></iframe>
<div class="page-hero-actions">
<el-button type="primary" plain @click="reloadIframe"><i class="el-icon-refresh"></i> 刷新文档</el-button>
<el-button type="primary" @click="openInNewTab"><i class="el-icon-top-right"></i> 新窗口打开</el-button>
</div>
</div>
<el-card class="iframe-shell-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>API 文档面板</h3>
<p>在控制台中直接浏览接口说明,方便排查与对接。</p>
</div>
<div class="iframe-url">{{ src_url }}</div>
</div>
<div class="iframe-shell">
<iframe ref="docsFrame" src="{{ src_url }}" frameborder="0"></iframe>
</div>
</el-card>
</div>
@@ -23,48 +39,47 @@
data() {
return {
currentView: '13',
showTimeRangeSelector: false
showTimeRangeSelector: false,
frameUrl: '{{ src_url }}'
}
},
mounted() {
this.currentView = '13';
},
methods: {
reloadIframe() {
if (this.$refs.docsFrame) {
this.$refs.docsFrame.src = this.frameUrl;
}
},
openInNewTab() {
window.open(this.frameUrl, '_blank');
}
}
});
</script>
<style>
.iframe-page-container {
height: 100%;
display: flex;
flex-direction: column;
.page-shell { display: flex; flex-direction: column; gap: 16px; }
.page-hero {
display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; padding: 24px 26px; border-radius: 24px;
background: linear-gradient(135deg, rgba(79,70,229,0.10), rgba(59,130,246,0.08), rgba(255,255,255,0.9));
border: 1px solid rgba(148, 163, 184, 0.16); box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
.iframe-card {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
margin: 0;
}
.el-card__body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.iframe-container {
flex: 1;
overflow: auto;
min-height: 0;
}
.iframe-container iframe {
width: 100%;
height: 100%;
border: none;
display: block;
.page-hero-actions { display: flex; align-items: center; gap: 12px; }
.page-eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #6366f1; font-weight: 700; margin-bottom: 8px; }
.page-hero-copy h1 { font-size: 30px; line-height: 1.1; margin-bottom: 10px; color: #0f172a; }
.page-hero-copy p { color: #64748b; font-size: 14px; }
.workspace-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.workspace-header h3 { font-size: 18px; margin-bottom: 4px; }
.workspace-header p { font-size: 13px; color: #64748b; }
.iframe-url {
max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px;
color: #94a3b8; padding: 8px 12px; border-radius: 999px; background: rgba(248,250,252,0.9); border: 1px solid rgba(148,163,184,0.12);
}
.iframe-shell-card { height: calc(100vh - 230px); }
.iframe-shell-card .el-card__body { height: calc(100% - 73px); }
.iframe-shell { height: 100%; border-radius: 18px; overflow: hidden; border: 1px solid rgba(148,163,184,0.12); background: rgba(248,250,252,0.82); }
.iframe-shell iframe { width: 100%; height: 100%; border: none; display: block; background: #fff; }
</style>
{% endblock %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,144 +3,45 @@
{% block title %}错误日志 - 机器人管理后台{% endblock %}
{% block content %}
<!-- 错误日志 -->
<div>
<el-row :gutter="20">
<el-col :span="24">
<el-card shadow="hover">
<div slot="header">
<span>错误日志</span>
</div>
<el-table {% raw %}:data="errorLogs"{% endraw %} style="width: 100%" border>
<el-table-column prop="plugin_name" label="插件名称"></el-table-column>
<el-table-column prop="command" label="命令"></el-table-column>
<el-table-column prop="error_message" label="错误信息" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="created_at" label="时间"></el-table-column>
<el-table-column label="用户">
<template slot-scope="scope">
<span v-if="scope.row.user_id">
{% raw %}{{ scope.row.user_name || scope.row.user_id }} ({{ scope.row.user_id }}){% endraw %}
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="群组">
<template slot-scope="scope">
<span v-if="scope.row.group_id">
{% raw %}{{ scope.row.group_name || scope.row.group_id }} ({{ scope.row.group_id }}){% endraw %}
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="viewErrorDetail(scope.row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container" style="margin-top: 20px; text-align: right;">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="totalErrors">
</el-pagination>
</div>
</el-card>
</el-col>
<div class="page-shell logs-page">
<div class="page-hero"><div class="page-hero-copy"><div class="page-eyebrow">Error Center</div><h1>错误日志</h1><p>统一查看插件异常、用户上下文与错误详情,方便快速定位问题来源。</p></div></div>
<el-row :gutter="16" class="overview-grid">
<el-col :span="8"><el-card class="overview-card overview-card--primary"><div class="overview-label">错误总数</div><div class="overview-value">{% raw %}{{ totalErrors }}{% endraw %}</div><div class="overview-note">当前分页查询结果对应总量</div></el-card></el-col>
<el-col :span="8"><el-card class="overview-card"><div class="overview-label">当前页日志数</div><div class="overview-value">{% raw %}{{ errorLogs.length }}{% endraw %}</div><div class="overview-note">本页已加载的错误记录</div></el-card></el-col>
<el-col :span="8"><el-card class="overview-card overview-card--soft"><div class="overview-label">分页大小</div><div class="overview-value">{% raw %}{{ pageSize }}{% endraw %}</div><div class="overview-note">每页可查看的错误条目</div></el-card></el-col>
</el-row>
<!-- 错误详情对话框 -->
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header"><div><h3>错误列表</h3><p>查看插件、命令、错误摘要以及相关用户/群组上下文。</p></div></div>
<el-table :data="errorLogs" style="width:100%">
<el-table-column prop="plugin_name" label="插件名称" width="140"></el-table-column>
<el-table-column prop="command" label="命令" width="120"></el-table-column>
<el-table-column prop="error_message" label="错误信息" min-width="320" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="created_at" label="时间" width="170"></el-table-column>
<el-table-column label="用户" min-width="180"><template slot-scope="scope"><span v-if="scope.row.user_id">{% raw %}{{ scope.row.user_name || scope.row.user_id }} ({{ scope.row.user_id }}){% endraw %}</span><span v-else>-</span></template></el-table-column>
<el-table-column label="群组" min-width="180"><template slot-scope="scope"><span v-if="scope.row.group_id">{% raw %}{{ scope.row.group_name || scope.row.group_id }} ({{ scope.row.group_id }}){% endraw %}</span><span v-else>-</span></template></el-table-column>
<el-table-column label="操作" width="120" align="center"><template slot-scope="scope"><el-button size="mini" type="primary" plain @click="viewErrorDetail(scope.row)">查看详情</el-button></template></el-table-column>
</el-table>
<div class="pagination-container"><el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="currentPage" :page-sizes="[10,20,50,100]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="totalErrors"></el-pagination></div>
</el-card>
<el-dialog title="错误详情" :visible.sync="errorDetailVisible" width="70%">
<div>
<p><strong>插件名称:</strong> {% raw %}{{ errorDetail.plugin_name }}{% endraw %}</p>
<p><strong>命令:</strong> {% raw %}{{ errorDetail.command }}{% endraw %}</p>
<p><strong>用户ID:</strong> {% raw %}{{ errorDetail.user_id }}{% endraw %}</p>
<p><strong>群组ID:</strong> {% raw %}{{ errorDetail.group_id || '无' }}{% endraw %}</p>
<p><strong>时间:</strong> {% raw %}{{ errorDetail.created_at }}{% endraw %}</p>
<p><strong>错误信息:</strong> {% raw %}{{ errorDetail.error_message }}{% endraw %}</p>
<div v-if="errorDetail.stack_trace">
<p><strong>堆栈跟踪:</strong></p>
<pre>{% raw %}{{ errorDetail.stack_trace }}{% endraw %}</pre>
</div>
</div>
<el-descriptions :column="1" border>
<el-descriptions-item label="插件名称">{% raw %}{{ errorDetail.plugin_name }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="命令">{% raw %}{{ errorDetail.command }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="用户ID">{% raw %}{{ errorDetail.user_id }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="群组ID">{% raw %}{{ errorDetail.group_id || '无' }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="时间">{% raw %}{{ errorDetail.created_at }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="错误信息">{% raw %}{{ errorDetail.error_message }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="堆栈跟踪" v-if="errorDetail.stack_trace"><pre class="stack-trace">{% raw %}{{ errorDetail.stack_trace }}{% endraw %}</pre></el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
{% endblock %}
{% block scripts %}
<script>
new Vue({
el: '#app',
mixins: [baseApp],
data() {
return {
errorLogs: [],
errorDetail: {},
errorDetailVisible: false,
currentPage: 1,
pageSize: 20,
totalErrors: 0
}
},
mounted() {
this.currentView = '5';
this.loadData();
},
methods: {
loadData() {
const days = parseInt(this.timeRange);
this.loadErrorLogs(days);
},
loadErrorLogs(days) {
axios.get(`/api/error_logs?days=${days}&limit=${this.pageSize}&offset=${(this.currentPage - 1) * this.pageSize}`)
.then(response => {
if (response.data.success) {
this.errorLogs = response.data.data.logs || [];
this.totalErrors = response.data.data.total || 0;
}
})
.catch(error => {
console.error('加载错误日志出错:', error);
this.$message.error('加载错误日志出错');
});
},
viewErrorDetail(error) {
// 如果已有错误ID直接加载详情
if (error.id) {
this.loadErrorDetail(error.id);
} else {
// 否则直接显示当前行的数据
this.errorDetail = error;
this.errorDetailVisible = true;
}
},
loadErrorDetail(errorId) {
axios.get(`/api/error_detail/${errorId}`)
.then(response => {
if (response.data.success) {
this.errorDetail = response.data.data || {};
this.errorDetailVisible = true;
}
})
.catch(error => {
console.error('加载错误详情出错:', error);
this.$message.error('加载错误详情出错');
});
},
handleSizeChange(size) {
this.pageSize = size;
this.currentPage = 1; // 重置到第一页
this.loadData();
},
handleCurrentChange(page) {
this.currentPage = page;
this.loadData();
}
}
});
new Vue({ el:'#app', mixins:[baseApp], data(){ return { errorLogs:[], errorDetail:{}, errorDetailVisible:false, currentPage:1, pageSize:20, totalErrors:0 } }, mounted(){ this.currentView='5'; this.loadData(); }, methods:{ loadData(){ this.loadErrorLogs(parseInt(this.timeRange)); }, loadErrorLogs(days){ axios.get(`/api/error_logs?days=${days}&limit=${this.pageSize}&offset=${(this.currentPage - 1) * this.pageSize}`).then(r=>{ if(r.data.success){ this.errorLogs=r.data.data.logs||[]; this.totalErrors=r.data.data.total||0; } }).catch(e=>{ console.error('加载错误日志出错:',e); this.$message.error('加载错误日志出错'); }); }, viewErrorDetail(error){ if(error.id){ this.loadErrorDetail(error.id);} else { this.errorDetail=error; this.errorDetailVisible=true; } }, loadErrorDetail(id){ axios.get(`/api/error_detail/${id}`).then(r=>{ if(r.data.success){ this.errorDetail=r.data.data||{}; this.errorDetailVisible=true; } }).catch(e=>{ console.error('加载错误详情出错:',e); this.$message.error('加载错误详情出错'); }); }, handleSizeChange(size){ this.pageSize=size; this.currentPage=1; this.loadData(); }, handleCurrentChange(page){ this.currentPage=page; this.loadData(); } } });
</script>
{% endblock %}
<style>
.page-shell{display:flex;flex-direction:column;gap:16px}.page-hero{padding:24px 26px;border-radius:24px;background:linear-gradient(135deg, rgba(79,70,229,.10), rgba(59,130,246,.08), rgba(255,255,255,.9));border:1px solid rgba(148,163,184,.16);box-shadow:0 18px 40px rgba(15,23,42,.06)}.page-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#6366f1;font-weight:700;margin-bottom:8px}.page-hero-copy h1{font-size:30px;line-height:1.1;margin-bottom:10px;color:#0f172a}.page-hero-copy p{color:#64748b;font-size:14px}.overview-grid .el-col{margin-bottom:16px}.overview-card{min-height:112px}.overview-card--primary{background:linear-gradient(180deg, rgba(79,70,229,.10), rgba(255,255,255,.94)) !important}.overview-card--soft{background:linear-gradient(180deg, rgba(59,130,246,.08), rgba(255,255,255,.94)) !important}.overview-label{font-size:13px;color:#64748b;margin-bottom:14px}.overview-value{font-size:30px;font-weight:700;color:#0f172a;margin-bottom:10px}.overview-note{font-size:12px;color:#94a3b8}.workspace-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.workspace-header h3{font-size:18px;margin-bottom:4px}.workspace-header p{font-size:13px;color:#64748b}.pagination-container{margin-top:20px;text-align:right}.stack-trace{white-space:pre-wrap;word-break:break-word;background:rgba(248,250,252,.85);border:1px solid rgba(148,163,184,.12);border-radius:14px;padding:14px;color:#334155}
</style>
{% endblock %}

View File

@@ -3,82 +3,58 @@
{% block title %}文件浏览 - 机器人管理后台{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>文件浏览</span>
<div style="float: right;">
<el-input
v-model="currentPath"
placeholder="当前路径"
style="width: 300px; margin-right: 10px;"
readonly>
</el-input>
<el-button type="primary" size="small" @click="navigateUp">
<i class="el-icon-arrow-up"></i> 上级目录
</el-button>
</div>
</div>
<el-table
:data="fileList"
style="width: 100%"
v-loading="loading">
<el-table-column
prop="name"
label="名称"
min-width="200">
<template slot-scope="scope">
<el-link
:type="scope.row.is_dir ? 'primary' : 'info'"
@click="scope.row.is_dir ? navigateTo(scope.row.name) : null">
<i :class="scope.row.is_dir ? 'el-icon-folder' : 'el-icon-document'"></i>
[[ scope.row.name ]]
</el-link>
</template>
</el-table-column>
<el-table-column
prop="type"
label="类型"
width="100">
<template slot-scope="scope">
[[ scope.row.is_dir ? '目录' : '文件' ]]
</template>
</el-table-column>
<el-table-column
prop="size"
label="大小"
width="120">
<template slot-scope="scope">
[[ scope.row.is_dir ? '-' : formatFileSize(scope.row.size) ]]
</template>
</el-table-column>
<el-table-column
prop="modified"
label="修改时间"
width="180">
<template slot-scope="scope">
[[ formatDate(scope.row.modified) ]]
</template>
</el-table-column>
<el-table-column
label="操作"
width="120">
<template slot-scope="scope">
<el-button
v-if="!scope.row.is_dir"
type="primary"
size="mini"
@click="downloadFile(scope.row.name)">
下载
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<div class="page-shell file-browser-page">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">Tools Workspace</div>
<h1>文件浏览</h1>
<p>文件查看入口纳入同一套工具工作台,路径、导航和下载动作更清晰。</p>
</div>
<div class="page-hero-actions">
<div class="path-pill">{% raw %}{{ currentPath || '/' }}{% endraw %}</div>
<el-button type="primary" plain @click="loadFiles(currentPath === '/' ? '' : currentPath)"><i class="el-icon-refresh"></i> 刷新</el-button>
<el-button type="primary" @click="navigateUp"><i class="el-icon-arrow-up"></i> 上级目录</el-button>
</div>
</div>
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>文件列表</h3>
<p>支持目录切换、文件下载与修改时间查看。</p>
</div>
</div>
<el-table :data="fileList" style="width: 100%" v-loading="loading">
<el-table-column prop="name" label="名称" min-width="320">
<template slot-scope="scope">
<div class="entity-cell">
<div class="entity-avatar" :class="scope.row.is_dir ? 'entity-avatar--folder' : 'entity-avatar--file'">
<i :class="scope.row.is_dir ? 'el-icon-folder' : 'el-icon-document'"></i>
</div>
<div class="entity-copy">
<el-link :type="scope.row.is_dir ? 'primary' : 'info'" @click="scope.row.is_dir ? navigateTo(scope.row.name) : null">
[[ scope.row.name ]]
</el-link>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="110" align="center">
<template slot-scope="scope">[[ scope.row.is_dir ? '目录' : '文件' ]]</template>
</el-table-column>
<el-table-column prop="size" label="大小" width="120" align="center">
<template slot-scope="scope">[[ scope.row.is_dir ? '-' : formatFileSize(scope.row.size) ]]</template>
</el-table-column>
<el-table-column prop="modified" label="修改时间" width="190">
<template slot-scope="scope">[[ formatDate(scope.row.modified) ]]</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template slot-scope="scope">
<el-button v-if="!scope.row.is_dir" type="primary" size="mini" @click="downloadFile(scope.row.name)">下载</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
{% endblock %}
@@ -113,10 +89,7 @@
loadFiles(path) {
this.loading = true;
this.currentPath = path || '/';
axios.get('/api/list_files', {
params: { path: path }
})
axios.get('/api/list_files', { params: { path: path } })
.then(response => {
if (response.data.success) {
this.fileList = response.data.data.items;
@@ -137,10 +110,9 @@
this.loadFiles(newPath);
},
navigateUp() {
if (!this.currentPath) return;
if (!this.currentPath || this.currentPath === '/') return;
const lastSlashIndex = this.currentPath.lastIndexOf('/');
if (lastSlashIndex === -1) {
if (lastSlashIndex <= 0) {
this.loadFiles('');
} else {
this.loadFiles(this.currentPath.substring(0, lastSlashIndex));
@@ -153,4 +125,31 @@
}
});
</script>
{% endblock %}
<style>
.page-shell { display: flex; flex-direction: column; gap: 16px; }
.page-hero {
display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; padding: 24px 26px; border-radius: 24px;
background: linear-gradient(135deg, rgba(79,70,229,0.10), rgba(59,130,246,0.08), rgba(255,255,255,0.9));
border: 1px solid rgba(148, 163, 184, 0.16); box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
.page-hero-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.page-eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #6366f1; font-weight: 700; margin-bottom: 8px; }
.page-hero-copy h1 { font-size: 30px; line-height: 1.1; margin-bottom: 10px; color: #0f172a; }
.page-hero-copy p { color: #64748b; font-size: 14px; }
.path-pill {
max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px;
color: #475569; padding: 10px 14px; border-radius: 999px; background: rgba(255,255,255,0.82); border: 1px solid rgba(148,163,184,0.14);
}
.workspace-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.workspace-header h3 { font-size: 18px; margin-bottom: 4px; }
.workspace-header p { font-size: 13px; color: #64748b; }
.entity-cell { display: flex; align-items: center; gap: 12px; }
.entity-avatar {
width: 32px; height: 32px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
color: #4f46e5; background: rgba(79,70,229,0.10);
}
.entity-avatar--folder { color: #10b981; background: rgba(16,185,129,0.12); }
.entity-avatar--file { color: #3b82f6; background: rgba(59,130,246,0.10); }
</style>
{% endblock %}

View File

@@ -3,77 +3,59 @@
{% block title %}群组统计 - 机器人管理后台{% endblock %}
{% block content %}
<!-- 群组统计 -->
<div>
<el-row :gutter="20">
<el-col :span="24">
<el-card shadow="hover">
<div slot="header">
<span>群组活跃度排行</span>
</div>
<el-table {% raw %}:data="groupStats"{% endraw %} style="width: 100%" border>
<el-table-column label="群组信息">
<template slot-scope="scope">
{% raw %}{{ scope.row.group_name || scope.row.group_id }} ({{ scope.row.group_id }}){% endraw %}
</template>
</el-table-column>
<el-table-column prop="total_calls" label="调用次数" sortable></el-table-column>
<el-table-column prop="used_plugins" label="使用插件数" sortable></el-table-column>
<el-table-column prop="unique_users" label="唯一用户数" sortable></el-table-column>
<el-table-column prop="success_calls" label="成功次数" sortable></el-table-column>
<el-table-column prop="failed_calls" label="失败次数" sortable></el-table-column>
<el-table-column label="成功率" sortable>
<template slot-scope="scope">
{% raw %} {{ (scope.row.success_calls / scope.row.total_calls * 100).toFixed(2) }}{% endraw %}%
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="viewGroupDetail(scope.row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<div class="page-shell stats-page">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">Groups Analytics</div>
<h1>群组统计</h1>
<p>聚焦群组活跃度、成功率与用户覆盖,快速识别最重要的互动场景。</p>
</div>
</div>
<el-row :gutter="16" class="overview-grid">
<el-col :span="8"><el-card class="overview-card overview-card--primary"><div class="overview-label">群组总数</div><div class="overview-value">{% raw %}{{ groupStats.length }}{% endraw %}</div><div class="overview-note">当前排行中展示的群组</div></el-card></el-col>
<el-col :span="8"><el-card class="overview-card"><div class="overview-label">总调用次数</div><div class="overview-value">{% raw %}{{ totalCalls }}{% endraw %}</div><div class="overview-note">统计周期内所有群组调用总和</div></el-card></el-col>
<el-col :span="8"><el-card class="overview-card overview-card--soft"><div class="overview-label">平均成功率</div><div class="overview-value">{% raw %}{{ averageSuccessRate }}{% endraw %}%</div><div class="overview-note">群组整体调用成功表现</div></el-card></el-col>
</el-row>
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header"><div><h3>群组活跃度排行</h3><p>查看调用量、插件覆盖与用户活跃情况。</p></div></div>
<el-table :data="groupStats" style="width: 100%">
<el-table-column label="群组信息" min-width="300">
<template slot-scope="scope"><div class="entity-cell"><div class="entity-badge">{% raw %}{{ scope.$index + 1 }}{% endraw %}</div><div class="entity-copy"><div class="entity-title">{% raw %}{{ scope.row.group_name || scope.row.group_id }}{% endraw %}</div><div class="entity-subtitle">{% raw %}{{ scope.row.group_id }}{% endraw %}</div></div></div></template>
</el-table-column>
<el-table-column prop="total_calls" label="调用次数" sortable width="120"></el-table-column>
<el-table-column prop="used_plugins" label="使用插件数" sortable width="120"></el-table-column>
<el-table-column prop="unique_users" label="唯一用户数" sortable width="120"></el-table-column>
<el-table-column prop="success_calls" label="成功次数" sortable width="120"></el-table-column>
<el-table-column prop="failed_calls" label="失败次数" sortable width="120"></el-table-column>
<el-table-column label="成功率" sortable width="120">
<template slot-scope="scope">{% raw %}{{ scope.row.total_calls ? (scope.row.success_calls / scope.row.total_calls * 100).toFixed(2) : '0.00' }}{% endraw %}%</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center"><template slot-scope="scope"><el-button size="mini" type="primary" plain @click="viewGroupDetail(scope.row)">查看详情</el-button></template></el-table-column>
</el-table>
</el-card>
</div>
{% endblock %}
{% block scripts %}
<script>
new Vue({
el: '#app',
mixins: [baseApp],
data() {
return {
groupStats: []
}
},
mounted() {
this.currentView = '4';
this.loadData();
},
methods: {
loadData() {
const days = parseInt(this.timeRange);
this.loadGroupStats(days);
},
loadGroupStats(days) {
axios.get(`/api/group_stats?days=${days}&limit=20`)
.then(response => {
if (response.data.success) {
this.groupStats = response.data.data || [];
}
})
.catch(error => {
console.error('加载群组统计数据出错:', error);
this.$message.error('加载群组统计数据出错');
});
},
viewGroupDetail(group) {
this.$message.info('群组详情功能开发中');
}
}
});
new Vue({
el:'#app', mixins:[baseApp],
data(){ return { groupStats:[] } },
computed:{
totalCalls(){ return this.groupStats.reduce((s,i)=>s+(parseInt(i.total_calls)||0),0); },
averageSuccessRate(){ if(!this.groupStats.length) return '0.00'; const sum=this.groupStats.reduce((s,i)=>s+(i.total_calls?((i.success_calls/i.total_calls)*100):0),0); return (sum/this.groupStats.length).toFixed(2); }
},
mounted(){ this.currentView='4'; this.loadData(); },
methods:{
loadData(){ this.loadGroupStats(parseInt(this.timeRange)); },
loadGroupStats(days){ axios.get(`/api/group_stats?days=${days}&limit=20`).then(r=>{ if(r.data.success) this.groupStats=r.data.data||[]; }).catch(e=>{ console.error('加载群组统计数据出错:',e); this.$message.error('加载群组统计数据出错'); }); },
viewGroupDetail(){ this.$message.info('群组详情功能开发中'); }
}
});
</script>
{% endblock %}
<style>
.page-shell{display:flex;flex-direction:column;gap:16px}.page-hero{padding:24px 26px;border-radius:24px;background:linear-gradient(135deg, rgba(79,70,229,.10), rgba(59,130,246,.08), rgba(255,255,255,.9));border:1px solid rgba(148,163,184,.16);box-shadow:0 18px 40px rgba(15,23,42,.06)}.page-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#6366f1;font-weight:700;margin-bottom:8px}.page-hero-copy h1{font-size:30px;line-height:1.1;margin-bottom:10px;color:#0f172a}.page-hero-copy p{color:#64748b;font-size:14px}.overview-grid .el-col{margin-bottom:16px}.overview-card{min-height:112px}.overview-card--primary{background:linear-gradient(180deg, rgba(79,70,229,.10), rgba(255,255,255,.94)) !important}.overview-card--soft{background:linear-gradient(180deg, rgba(59,130,246,.08), rgba(255,255,255,.94)) !important}.overview-label{font-size:13px;color:#64748b;margin-bottom:14px}.overview-value{font-size:30px;font-weight:700;color:#0f172a;margin-bottom:10px}.overview-note{font-size:12px;color:#94a3b8}.workspace-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.workspace-header h3{font-size:18px;margin-bottom:4px}.workspace-header p{font-size:13px;color:#64748b}.entity-cell{display:flex;align-items:center;gap:12px}.entity-badge{width:30px;height:30px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:rgba(79,70,229,.10);color:#4f46e5;font-size:12px;font-weight:700;flex-shrink:0}.entity-title{font-size:14px;font-weight:600;color:#0f172a}.entity-subtitle{margin-top:4px;font-size:12px;color:#94a3b8}
</style>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -3,155 +3,271 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - A-BOT管理后台</title>
<!-- 添加favicon -->
<title>登录 - A-BOT 管理后台</title>
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon">
<!-- Element UI CSS -->
<link rel="stylesheet" href="/static/css/element-ui/theme-chalk/index.min.css">
<!-- 图表库 -->
<script src="/static/js/chart.js"></script>
<!-- Vue.js -->
<script src="/static/js/vue.js"></script>
<!-- Element UI JS -->
<script src="/static/js/element-ui/index.min.js"></script>
<!-- Axios -->
<script src="/static/js/axios.min.js"></script>
<style>
:root {
--bg: #f4f7fb;
--text: #0f172a;
--text-soft: #64748b;
--border: rgba(148, 163, 184, 0.18);
--primary: #4f46e5;
--primary-2: #6366f1;
--danger: #ef4444;
--shadow: 0 24px 48px rgba(15, 23, 42, 0.10);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
padding: 0;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
background: #fdfcfb;
height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(99, 102, 241, 0.14), transparent 30%),
radial-gradient(circle at bottom right, rgba(59, 130, 246, 0.10), transparent 26%),
linear-gradient(180deg, #f8fafc 0%, #f3f6fb 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
overflow: hidden;
}
.login-container {
width: 400px;
padding: 40px 36px 32px 36px;
background-color: #ffffff;
border: 1px solid #e5e0d9;
border-radius: 12px;
box-shadow: 0 12px 24px rgba(17, 24, 39, 0.08);
.login-shell {
width: min(1120px, calc(100vw - 48px));
min-height: 680px;
display: grid;
grid-template-columns: 1.1fr 0.9fr;
border-radius: 32px;
overflow: hidden;
background: rgba(255,255,255,0.72);
border: 1px solid rgba(148,163,184,0.14);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
box-shadow: var(--shadow);
}
.login-showcase {
padding: 56px 52px;
background: linear-gradient(145deg, rgba(79,70,229,0.96), rgba(59,130,246,0.88));
color: #fff;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
}
.login-showcase::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at 20% 20%, rgba(255,255,255,0.18), transparent 22%), radial-gradient(circle at 80% 70%, rgba(255,255,255,0.08), transparent 24%);
pointer-events: none;
}
.showcase-top, .showcase-bottom { position: relative; z-index: 1; }
.brand {
display: inline-flex;
align-items: center;
animation: fadeIn 0.8s;
gap: 12px;
padding: 10px 14px;
border-radius: 999px;
background: rgba(255,255,255,0.12);
border: 1px solid rgba(255,255,255,0.12);
width: fit-content;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-30px);}
to { opacity: 1; transform: translateY(0);}
}
.login-title {
text-align: center;
margin-bottom: 32px;
color: #f97316;
font-size: 28px;
font-weight: 700;
letter-spacing: 2px;
}
.login-form {
width: 100%;
margin-top: 10px;
}
.el-input__inner {
border-radius: 6px;
border: 1.5px solid #e5e0d9;
background: #fcfaf8;
color: #111827;
transition: border-color 0.2s;
}
.el-input__inner:focus {
border-color: #f97316;
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
}
.el-form-item__content {
margin-left: 0 !important;
}
.login-button {
width: 100%;
margin-top: 10px;
background: #f97316;
border: none;
color: #ffffff;
font-weight: 600;
border-radius: 6px;
box-shadow: 0 8px 16px rgba(17, 24, 39, 0.12);
transition: background 0.2s;
}
.login-button:hover, .login-button:focus {
background: #f59e0b;
}
.error-message {
color: #ef4444;
margin-top: 18px;
text-align: center;
font-size: 15px;
}
.login-logo {
width: 60px;
height: 60px;
margin-bottom: 18px;
border-radius: 50%;
box-shadow: 0 6px 14px rgba(17, 24, 39, 0.12);
background: #fcfaf8;
border: 1px solid #e5e0d9;
display: flex;
.brand-logo {
width: 40px;
height: 40px;
border-radius: 14px;
background: rgba(255,255,255,0.18);
display: inline-flex;
align-items: center;
justify-content: center;
}
.login-footer {
margin-top: 28px;
.brand-logo img { width: 26px; height: 26px; }
.brand-copy { display: flex; flex-direction: column; gap: 2px; }
.brand-copy strong { font-size: 15px; }
.brand-copy span { font-size: 12px; color: rgba(255,255,255,0.78); }
.hero-copy { margin-top: 36px; }
.hero-eyebrow {
font-size: 12px; text-transform: uppercase; letter-spacing: .12em; color: rgba(255,255,255,0.74); margin-bottom: 16px;
}
.hero-title {
font-size: 46px; line-height: 1.08; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 16px;
}
.hero-desc {
max-width: 460px; font-size: 16px; line-height: 1.75; color: rgba(255,255,255,0.86);
}
.showcase-metrics {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.metric-item {
padding: 16px 18px;
border-radius: 20px;
background: rgba(255,255,255,0.12);
border: 1px solid rgba(255,255,255,0.12);
}
.metric-label { font-size: 12px; color: rgba(255,255,255,0.72); margin-bottom: 10px; }
.metric-value { font-size: 24px; font-weight: 700; color: #fff; }
.login-panel {
padding: 48px 42px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, rgba(255,255,255,0.78), rgba(248,250,252,0.96));
}
.login-card {
width: 100%;
max-width: 420px;
}
.panel-eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
background: rgba(79,70,229,0.08);
color: var(--primary);
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
margin-bottom: 18px;
}
.panel-title { font-size: 30px; font-weight: 800; color: var(--text); margin-bottom: 10px; }
.panel-desc { color: var(--text-soft); line-height: 1.7; font-size: 14px; margin-bottom: 24px; }
.login-form { width: 100%; }
.el-form-item { margin-bottom: 18px; }
.el-input__inner {
height: 48px;
border-radius: 14px;
border: 1px solid rgba(148,163,184,0.22);
background: rgba(248,250,252,0.94);
color: var(--text);
transition: all .18s ease;
}
.el-input__inner:focus {
border-color: rgba(99,102,241,0.42);
box-shadow: 0 0 0 4px rgba(99,102,241,0.10);
background: #fff;
}
.el-form-item__content { margin-left: 0 !important; }
.login-button {
width: 100%;
height: 48px;
border: none;
border-radius: 14px;
background: linear-gradient(135deg, var(--primary), var(--primary-2));
color: #fff;
font-weight: 700;
font-size: 15px;
box-shadow: 0 14px 28px rgba(79,70,229,0.20);
}
.login-button:hover, .login-button:focus {
opacity: .96;
transform: translateY(-1px);
}
.error-message {
margin-top: 16px;
padding: 12px 14px;
border-radius: 14px;
background: rgba(239,68,68,0.08);
border: 1px solid rgba(239,68,68,0.12);
color: var(--danger);
font-size: 14px;
text-align: center;
color: #6b7280;
}
.panel-footer {
margin-top: 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--text-soft);
font-size: 13px;
letter-spacing: 1px;
}
.panel-footer .status {
display: inline-flex;
align-items: center;
gap: 8px;
}
.panel-footer .dot {
width: 8px; height: 8px; border-radius: 50%; background: #10b981; box-shadow: 0 0 0 4px rgba(16,185,129,0.12);
}
@media (max-width: 960px) {
.login-shell { grid-template-columns: 1fr; min-height: auto; }
.login-showcase { padding: 34px 28px; }
.hero-title { font-size: 34px; }
.showcase-metrics { grid-template-columns: 1fr; }
.login-panel { padding: 32px 24px; }
}
</style>
</head>
<body>
<div id="app">
<div class="login-container">
<div class="login-logo">
<img src="/static/logo.png" alt="Logo" style="width:38px;height:38px;">
</div>
<h2 class="login-title">A-BOT管理后台</h2>
<el-form class="login-form" ref="loginForm" :model="loginForm" :rules="rules" label-width="0px">
<el-form-item prop="username">
<el-input v-model="loginForm.username" prefix-icon="el-icon-user" placeholder="用户名" size="large"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" prefix-icon="el-icon-lock" placeholder="密码" type="password" size="large" @keyup.enter.native="submitForm"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" class="login-button" @click="submitForm" :loading="loading" size="large">登录</el-button>
</el-form-item>
</el-form>
<div class="error-message" v-if="errorMessage">{{ errorMessage }}</div>
<div class="login-footer">
© 2025 ABOT | 智能机器人管理平台
</div>
<div class="login-shell">
<section class="login-showcase">
<div class="showcase-top">
<div class="brand">
<div class="brand-logo"><img src="/static/logo.png" alt="Logo"></div>
<div class="brand-copy">
<strong>A-BOT 控制台</strong>
<span>Modern Operations Dashboard</span>
</div>
</div>
<div class="hero-copy">
<div class="hero-eyebrow">Welcome back</div>
<div class="hero-title">统一管理机器人、消息、插件与系统状态</div>
<div class="hero-desc">这不是传统后台的样子了。登录后你会进入一套更现代、更清晰的机器人运营控制台。</div>
</div>
</div>
<div class="showcase-bottom">
<div class="showcase-metrics">
<div class="metric-item"><div class="metric-label">Workspace</div><div class="metric-value">ABOT</div></div>
<div class="metric-item"><div class="metric-label">Focus</div><div class="metric-value">消息 / 群组</div></div>
<div class="metric-item"><div class="metric-label">Style</div><div class="metric-value">Modern UI</div></div>
</div>
</div>
</section>
<section class="login-panel">
<div class="login-card">
<div class="panel-eyebrow">Sign In</div>
<div class="panel-title">登录后台</div>
<div class="panel-desc">输入管理员账号信息,进入新版控制台继续处理机器人运营、统计与系统管理工作。</div>
<el-form class="login-form" ref="loginForm" :model="loginForm" :rules="rules" label-width="0px">
<el-form-item prop="username">
<el-input v-model="loginForm.username" prefix-icon="el-icon-user" placeholder="用户名" size="large"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" prefix-icon="el-icon-lock" placeholder="密码" type="password" size="large" @keyup.enter.native="submitForm"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" class="login-button" @click="submitForm" :loading="loading" size="large">登录进入控制台</el-button>
</el-form-item>
</el-form>
<div class="error-message" v-if="errorMessage">{{ errorMessage }}</div>
<div class="panel-footer">
<span>© 2026 ABOT</span>
<span class="status"><span class="dot"></span> 安全连接已就绪</span>
</div>
</div>
</section>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
loginForm: {
username: '',
password: ''
},
loginForm: { username: '', password: '' },
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
]
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
},
loading: false,
errorMessage: ''
@@ -162,35 +278,28 @@
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
// 创建表单数据
const formData = new FormData();
formData.append('username', this.loginForm.username);
formData.append('password', this.loginForm.password);
// 发送POST请求
fetch('/login', {
method: 'POST',
body: formData
})
.then(response => {
if (response.redirected) {
window.location.href = response.url;
} else {
return response.text();
}
})
.then(html => {
if (html) {
// 如果返回HTML说明登录失败
this.errorMessage = '用户名或密码错误';
fetch('/login', { method: 'POST', body: formData })
.then(response => {
if (response.redirected) {
window.location.href = response.url;
} else {
return response.text();
}
})
.then(html => {
if (html) {
this.errorMessage = '用户名或密码错误';
this.loading = false;
}
})
.catch(error => {
console.error('登录出错:', error);
this.errorMessage = '登录请求失败,请稍后重试';
this.loading = false;
}
})
.catch(error => {
console.error('登录出错:', error);
this.errorMessage = '登录请求失败,请稍后重试';
this.loading = false;
});
});
}
});
}

View File

@@ -3,155 +3,146 @@
{% block title %}消息列表{% endblock %}
{% block content %}
<!-- 消息列表 -->
<div>
<!-- 筛选条件 -->
<el-card class="filter-card">
<el-form {% raw %}:inline="true"{% endraw %} size="small">
<div class="page-shell message-page">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">Messages Center</div>
<h1>消息列表</h1>
<p>按群组、时间与内容快速回溯消息记录,把查看明细、媒体预览与分页统一到一个工作台里。</p>
</div>
</div>
<el-card class="filter-card workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>筛选条件</h3>
<p>组合筛选群组、日期与关键词,快速定位你要看的消息。</p>
</div>
<div class="filter-summary">
当前共 {% raw %}{{ pagination.total }}{% endraw %} 条消息
</div>
</div>
<el-form :inline="true" size="small" class="filter-form">
<el-form-item label="群组">
<el-select {% raw %}v-model="filter.groupId"{% endraw %} placeholder="选择群组" clearable>
<el-select v-model="filter.groupId" placeholder="选择群组" clearable>
<el-option
{% raw %}v-for="group in groups"
v-for="group in groups"
:key="group.id"
:label="group.name"
:value="group.id"{% endraw %}>
:value="group.id">
</el-option>
</el-select>
</el-form-item>
<!-- 其他筛选条件保持不变 -->
<el-form-item label="日期范围">
<el-date-picker
{% raw %}v-model="dateRange"
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
:picker-options="pickerOptions"{% endraw %}>
:picker-options="pickerOptions">
</el-date-picker>
</el-form-item>
<el-form-item label="搜索内容">
<el-input {% raw %}v-model="filter.searchText"{% endraw %} placeholder="搜索消息内容" clearable></el-input>
<el-input v-model="filter.searchText" placeholder="搜索消息内容" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" {% raw %}@click="searchMessages"{% endraw %}>搜索</el-button>
<el-button {% raw %}@click="resetFilter"{% endraw %}>重置</el-button>
<el-button type="primary" @click="searchMessages">搜索</el-button>
<el-button @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 消息列表内容保持不变 -->
<el-card class="message-card">
<el-card class="message-card workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>消息记录</h3>
<p>支持文本、图片、视频等多类型消息的统一查看。</p>
</div>
</div>
<el-table
{% raw %}:data="messages"
:data="messages"
style="width: 100%"
size="small"
v-loading="loading"{% endraw %}
border>
<!-- 表格列保持不变 -->
<el-table-column
prop="timestamp"
label="时间"
width="150">
</el-table-column>
<el-table-column
prop="group_name"
label="群组"
width="150">
</el-table-column>
<el-table-column
prop="sender_name"
label="发送者"
width="120">
</el-table-column>
<el-table-column
prop="content"
label="内容">
v-loading="loading">
<el-table-column prop="timestamp" label="时间" width="165"></el-table-column>
<el-table-column prop="group_name" label="群组" width="160"></el-table-column>
<el-table-column prop="sender_name" label="发送者" width="140"></el-table-column>
<el-table-column prop="content" label="内容" min-width="420">
<template slot-scope="scope">
<!-- 文本消息 -->
<div v-if="scope.row.message_type == 1">
{% raw %}{{ scope.row.content }}{% endraw %}
</div>
<!-- 图片消息 -->
<div v-else-if="scope.row.message_type == 3">
<div>【图片消息】</div>
<!-- 优先使用image_path显示图片如果没有则回退到message_thumb -->
<img v-if="scope.row.image_path" :src="getImageUrl(scope.row.image_path)" class="message-thumb" @click="showImage(scope.row)">
<img v-else-if="scope.row.message_thumb" :src="scope.row.message_thumb" class="message-thumb" @click="showImage(scope.row)">
</div>
<!-- 视频消息 -->
<div v-else-if="scope.row.message_type == 43">
<div>【视频消息】</div>
<img v-if="scope.row.message_thumb" :src="scope.row.message_thumb" class="message-thumb" @click="showVideo(scope.row)">
</div>
<!-- 其他类型消息 -->
<div v-else>
{% raw %}{{ scope.row.content || `【消息类型: ${scope.row.message_type}】` }}{% endraw %}
<div class="message-preview">
<div v-if="scope.row.message_type == 1" class="message-text-preview">
{% raw %}{{ scope.row.content }}{% endraw %}
</div>
<div v-else-if="scope.row.message_type == 3" class="message-media-preview">
<div class="message-media-label">【图片消息】</div>
<img v-if="scope.row.image_path" :src="getImageUrl(scope.row.image_path)" class="message-thumb" @click="showImage(scope.row)">
<img v-else-if="scope.row.message_thumb" :src="scope.row.message_thumb" class="message-thumb" @click="showImage(scope.row)">
</div>
<div v-else-if="scope.row.message_type == 43" class="message-media-preview">
<div class="message-media-label">【视频消息】</div>
<img v-if="scope.row.message_thumb" :src="scope.row.message_thumb" class="message-thumb" @click="showVideo(scope.row)">
</div>
<div v-else class="message-text-preview is-muted">
{% raw %}{{ scope.row.content || `【消息类型: ${scope.row.message_type}】` }}{% endraw %}
</div>
</div>
</template>
</el-table-column>
<el-table-column
label="操作"
width="100">
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button type="text" size="small" @click="showMessageDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
{% raw %}@size-change="handleSizeChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.page"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.pageSize"{% endraw %}
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
{% raw %}:total="pagination.total"{% endraw %}>
:total="pagination.total">
</el-pagination>
</div>
</el-card>
<!-- 对话框保持不变 -->
<!-- 消息详情对话框 -->
<el-dialog title="消息详情" {% raw %}:visible.sync="detailDialogVisible"{% endraw %} width="60%">
<div {% raw %}v-if="selectedMessage"{% endraw %}>
<el-descriptions {% raw %}:column="1"{% endraw %} border>
<el-dialog title="消息详情" :visible.sync="detailDialogVisible" width="60%">
<div v-if="selectedMessage">
<el-descriptions :column="1" border>
<el-descriptions-item label="时间">{% raw %}{{ selectedMessage.timestamp }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="群组">{% raw %}{{ selectedMessage.group_name }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="发送者">{% raw %}{{ selectedMessage.sender_name }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="消息类型">{% raw %}{{ getMessageTypeName(selectedMessage.message_type) }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="内容">{% raw %}{{ selectedMessage.content }}{% endraw %}</el-descriptions-item>
<!-- 图片或视频消息 -->
<el-descriptions-item {% raw %}v-if="selectedMessage.message_type == 3 || selectedMessage.message_type == 43"{% endraw %} label="媒体内容">
<img {% raw %}v-if="selectedMessage.message_type == 3 && selectedMessage.image_path" :src="getImageUrl(selectedMessage.image_path)"{% endraw %} style="max-width: 100%;">
<img {% raw %}v-else-if="selectedMessage.message_type == 3 && selectedMessage.message_thumb" :src="selectedMessage.message_thumb"{% endraw %} style="max-width: 100%;">
<video {% raw %}v-if="selectedMessage.message_type == 43 && selectedMessage.attachment_url" :src="selectedMessage.attachment_url"{% endraw %} controls style="max-width: 100%;"></video>
<el-descriptions-item v-if="selectedMessage.message_type == 3 || selectedMessage.message_type == 43" label="媒体内容">
<img v-if="selectedMessage.message_type == 3 && selectedMessage.image_path" :src="getImageUrl(selectedMessage.image_path)" style="max-width: 100%; border-radius: 16px;">
<img v-else-if="selectedMessage.message_type == 3 && selectedMessage.message_thumb" :src="selectedMessage.message_thumb" style="max-width: 100%; border-radius: 16px;">
<video v-if="selectedMessage.message_type == 43 && selectedMessage.attachment_url" :src="selectedMessage.attachment_url" controls style="max-width: 100%; border-radius: 16px;"></video>
</el-descriptions-item>
<el-descriptions-item label="原始XML" {% raw %}v-if="selectedMessage.message_xml"{% endraw %}>
<pre style="white-space: pre-wrap; word-break: break-all;">{% raw %}{{ selectedMessage.message_xml }}{% endraw %}</pre>
<el-descriptions-item label="原始XML" v-if="selectedMessage.message_xml">
<pre class="message-xml">{% raw %}{{ selectedMessage.message_xml }}{% endraw %}</pre>
</el-descriptions-item>
</el-descriptions>
</div>
</el-dialog>
<!-- 图片预览 -->
<el-dialog {% raw %}:visible.sync="imageDialogVisible"{% endraw %} append-to-body width="80%" class="image-dialog">
<img {% raw %}v-if="selectedMessage && selectedMessage.image_path" :src="getImageUrl(selectedMessage.image_path)"{% endraw %} style="max-width: 100%;">
<img {% raw %}v-else-if="selectedMessage && selectedMessage.message_thumb" :src="selectedMessage.message_thumb"{% endraw %} style="max-width: 100%;">
<el-dialog :visible.sync="imageDialogVisible" append-to-body width="80%" class="image-dialog">
<img v-if="selectedMessage && selectedMessage.image_path" :src="getImageUrl(selectedMessage.image_path)" style="max-width: 100%; border-radius: 18px;">
<img v-else-if="selectedMessage && selectedMessage.message_thumb" :src="selectedMessage.message_thumb" style="max-width: 100%; border-radius: 18px;">
</el-dialog>
</div>
{% endblock %}
@@ -211,18 +202,14 @@
}
},
mounted() {
this.currentView = '7'; // 设置当前菜单项
// 设置默认日期为今天
this.currentView = '7';
const today = new Date();
this.dateRange = [this.formatDate(today), this.formatDate(today)];
this.filter.startDate = this.formatDate(today);
this.filter.endDate = this.formatDate(today);
// 加载群组列表
this.loadGroups();
// 加载消息列表
this.loadMessages();
},
methods: {
@@ -249,29 +236,28 @@
},
loadMessages() {
this.loading = true;
// 构建查询参数
const params = {
page: this.pagination.page,
page_size: this.pagination.pageSize
};
if (this.filter.groupId) {
params.group_id = this.filter.groupId;
}
if (this.filter.startDate) {
params.start_date = this.filter.startDate;
}
if (this.filter.endDate) {
params.end_date = this.filter.endDate;
}
if (this.filter.searchText) {
params.search_text = this.filter.searchText;
}
axios.get('/api/messages', { params })
.then(response => {
this.messages = response.data.messages || [];
@@ -287,28 +273,23 @@
});
},
searchMessages() {
// 更新日期范围
if (this.dateRange && this.dateRange.length === 2) {
this.filter.startDate = this.dateRange[0];
this.filter.endDate = this.dateRange[1];
}
// 重置页码并加载消息
this.pagination.page = 1;
this.loadMessages();
},
resetFilter() {
// 重置筛选条件
this.filter.groupId = '';
this.filter.searchText = '';
// 设置日期为今天
const today = new Date();
this.dateRange = [this.formatDate(today), this.formatDate(today)];
this.filter.startDate = this.formatDate(today);
this.filter.endDate = this.formatDate(today);
// 重置页码并加载消息
this.pagination.page = 1;
this.loadMessages();
},
@@ -342,37 +323,29 @@
return typeMap[type] || `未知类型(${type})`;
},
getImageUrl(imagePath) {
// 如果路径为空,返回空字符串
if (!imagePath) return '';
// 如果已经是完整URL直接返回
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
return imagePath;
}
// 提取文件名和路径
const pathParts = imagePath.split(/[\/\\]/);
const fileName = pathParts[pathParts.length - 1];
// 如果路径包含群ID通常是倒数第二个部分
if (pathParts.length >= 2) {
const groupId = pathParts[pathParts.length - 2];
// 检查是否是群ID格式通常以@chatroom结尾
if (groupId.includes('@chatroom')) {
return `/static/images/${groupId}/${fileName}`;
}
}
// 检查路径中是否包含static/images
if (imagePath.includes('static/images') || imagePath.includes('static\\images')) {
// 提取static/images后面的部分
const parts = imagePath.split(/static[\/\\]images[\/\\]/);
if (parts.length > 1) {
return `/static/images/${parts[1]}`;
}
}
// 如果以上都不匹配,则直接使用文件名
return `/static/images/${fileName}`;
}
},
@@ -388,27 +361,119 @@
</script>
<style>
.filter-card {
margin-bottom: 20px;
.page-shell {
display: flex;
flex-direction: column;
gap: 16px;
}
.message-card {
margin-bottom: 20px;
.page-hero {
padding: 24px 26px;
border-radius: 24px;
background: linear-gradient(135deg, rgba(79,70,229,0.10), rgba(59,130,246,0.08), rgba(255,255,255,0.9));
border: 1px solid rgba(148, 163, 184, 0.16);
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
.page-eyebrow {
font-size: 12px;
text-transform: uppercase;
letter-spacing: .08em;
color: #6366f1;
font-weight: 700;
margin-bottom: 8px;
}
.page-hero-copy h1 {
font-size: 30px;
line-height: 1.1;
margin-bottom: 10px;
color: #0f172a;
}
.page-hero-copy p {
color: #64748b;
font-size: 14px;
}
.workspace-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.workspace-header h3 {
font-size: 18px;
margin-bottom: 4px;
}
.workspace-header p,
.filter-summary {
font-size: 13px;
color: #64748b;
}
.filter-form {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px 10px;
}
.message-preview {
display: flex;
flex-direction: column;
gap: 8px;
}
.message-text-preview {
color: #0f172a;
line-height: 1.6;
word-break: break-word;
}
.message-text-preview.is-muted {
color: #64748b;
}
.message-media-preview {
display: flex;
flex-direction: column;
gap: 8px;
}
.message-media-label {
font-size: 12px;
color: #64748b;
}
.message-thumb {
max-width: 120px;
max-height: 120px;
cursor: pointer;
border-radius: 14px;
border: 1px solid rgba(148,163,184,0.16);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
}
.pagination-container {
margin-top: 20px;
text-align: right;
}
.message-thumb {
max-width: 100px;
max-height: 100px;
cursor: pointer;
}
.image-dialog .el-dialog__body {
text-align: center;
}
.message-xml {
white-space: pre-wrap;
word-break: break-all;
background: rgba(248,250,252,0.85);
border: 1px solid rgba(148,163,184,0.12);
border-radius: 14px;
padding: 14px;
color: #334155;
}
</style>
{% endblock %}
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -3,253 +3,34 @@
{% block title %}插件统计 - 机器人管理后台{% endblock %}
{% block content %}
<!-- 插件统计 -->
<div>
<el-row {% raw %}:gutter="20"{% endraw %}>
<el-col {% raw %}:span="24"{% endraw %}>
<el-card shadow="hover">
<div slot="header">
<span>插件使用统计</span>
</div>
<el-table {% raw %}:data="pluginStats"{% endraw %} style="width: 100%" border>
<el-table-column prop="plugin_name" label="插件名称"></el-table-column>
<el-table-column prop="command" label="触发指令"></el-table-column>
<el-table-column prop="total_calls" label="调用次数" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.total_calls) || 0 }}{% endraw %}
</template>
</el-table-column>
<el-table-column prop="success_calls" label="成功次数" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.success_calls) || 0 }}{% endraw %}
</template>
</el-table-column>
<el-table-column prop="failed_calls" label="失败次数" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.failed_calls) || 0 }}{% endraw %}
</template>
</el-table-column>
<el-table-column label="成功率" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.total_calls) > 0 ? ((parseInt(scope.row.success_calls) / parseInt(scope.row.total_calls)) * 100).toFixed(2) : '100.00' }}{% endraw %}%
</template>
</el-table-column>
<el-table-column label="平均响应时间" sortable>
<template slot-scope="scope">
{% raw %}{{ (scope.row.avg_process_time || 0).toFixed(2) }}{% endraw %}ms
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" {% raw %}@click="viewPluginTrend(scope.row)"{% endraw %}>查看趋势</el-button>
</template>
</el-table-column>
</el-table>
<el-table {% raw %}:data="pluginUsers"{% endraw %} style="width: 100%" border v-if="showUserStats">
<el-table-column label="用户">
<template slot-scope="scope">
{% raw %}{{ scope.row.user_name || scope.row.user_id }} ({{ scope.row.user_id }}){% endraw %}
</template>
</el-table-column>
<el-table-column prop="total_calls" label="调用次数" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.total_calls) || 0 }}{% endraw %}
</template>
</el-table-column>
<el-table-column prop="success_calls" label="成功次数" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.success_calls) || 0 }}{% endraw %}
</template>
</el-table-column>
<el-table-column prop="failed_calls" label="失败次数" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.failed_calls) || 0 }}{% endraw %}
</template>
</el-table-column>
<el-table-column label="成功率" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.total_calls) > 0 ? ((parseInt(scope.row.success_calls) / parseInt(scope.row.total_calls)) * 100).toFixed(2) : '100.00' }}{% endraw %}%
</template>
</el-table-column>
<el-table-column label="平均响应时间" sortable>
<template slot-scope="scope">
{% raw %}{{ (scope.row.avg_process_time || 0).toFixed(2) }}{% endraw %}ms
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" {% raw %}@click="viewPluginTrend(scope.row)"{% endraw %}>查看趋势</el-button>
</template>
</el-table-column>
</el-table>
<el-table {% raw %}:data="pluginGroups"{% endraw %} style="width: 100%" border v-if="showGroupStats">
<el-table-column label="群组">
<template slot-scope="scope">
{% raw %}{{ scope.row.group_name || scope.row.group_id }} ({{ scope.row.group_id }}){% endraw %}
</template>
</el-table-column>
<el-table-column prop="total_calls" label="调用次数" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.total_calls) || 0 }}{% endraw %}
</template>
</el-table-column>
<el-table-column prop="success_calls" label="成功次数" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.success_calls) || 0 }}{% endraw %}
</template>
</el-table-column>
<el-table-column prop="failed_calls" label="失败次数" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.failed_calls) || 0 }}{% endraw %}
</template>
</el-table-column>
<el-table-column label="成功率" sortable>
<template slot-scope="scope">
{% raw %}{{ parseInt(scope.row.total_calls) > 0 ? ((parseInt(scope.row.success_calls) / parseInt(scope.row.total_calls)) * 100).toFixed(2) : '100.00' }}{% endraw %}%
</template>
</el-table-column>
<el-table-column label="平均响应时间" sortable>
<template slot-scope="scope">
{% raw %}{{ (scope.row.avg_process_time || 0).toFixed(2) }}{% endraw %}ms
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" {% raw %}@click="viewPluginTrend(scope.row)"{% endraw %}>查看趋势</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<div class="page-shell stats-page">
<div class="page-hero"><div class="page-hero-copy"><div class="page-eyebrow">Plugins Analytics</div><h1>插件统计</h1><p>查看插件调用规模、成功率与响应效率,快速定位高频能力模块。</p></div></div>
<el-row :gutter="16" class="overview-grid">
<el-col :span="8"><el-card class="overview-card overview-card--primary"><div class="overview-label">插件总数</div><div class="overview-value">{% raw %}{{ pluginStats.length }}{% endraw %}</div><div class="overview-note">当前统计周期内被触发的插件</div></el-card></el-col>
<el-col :span="8"><el-card class="overview-card"><div class="overview-label">总调用次数</div><div class="overview-value">{% raw %}{{ totalCalls }}{% endraw %}</div><div class="overview-note">统计周期内插件调用总和</div></el-card></el-col>
<el-col :span="8"><el-card class="overview-card overview-card--soft"><div class="overview-label">平均成功率</div><div class="overview-value">{% raw %}{{ averageSuccessRate }}{% endraw %}%</div><div class="overview-note">插件整体稳定性表现</div></el-card></el-col>
</el-row>
<!-- 插件趋势对话框 -->
<el-dialog title="插件使用趋势" {% raw %}:visible.sync="pluginTrendVisible"{% endraw %} width="70%">
<div class="chart-container">
<h3>{% raw %}{{ selectedPlugin ? selectedPlugin.plugin_name : '' }}{% endraw %} 使用趋势</h3>
<canvas id="pluginTrendChart" width="800" height="400"></canvas>
</div>
</el-dialog>
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header"><div><h3>插件使用统计</h3><p>按插件查看调用、成功、失败与平均响应时间。</p></div></div>
<el-table :data="pluginStats" style="width:100%">
<el-table-column label="插件信息" min-width="260"><template slot-scope="scope"><div class="entity-cell"><div class="entity-badge">{% raw %}{{ scope.$index + 1 }}{% endraw %}</div><div class="entity-copy"><div class="entity-title">{% raw %}{{ scope.row.plugin_name }}{% endraw %}</div><div class="entity-subtitle">指令:{% raw %}{{ scope.row.command }}{% endraw %}</div></div></div></template></el-table-column>
<el-table-column prop="total_calls" label="调用次数" sortable width="110"><template slot-scope="scope">{% raw %}{{ parseInt(scope.row.total_calls) || 0 }}{% endraw %}</template></el-table-column>
<el-table-column prop="success_calls" label="成功次数" sortable width="110"><template slot-scope="scope">{% raw %}{{ parseInt(scope.row.success_calls) || 0 }}{% endraw %}</template></el-table-column>
<el-table-column prop="failed_calls" label="失败次数" sortable width="110"><template slot-scope="scope">{% raw %}{{ parseInt(scope.row.failed_calls) || 0 }}{% endraw %}</template></el-table-column>
<el-table-column label="成功率" sortable width="110"><template slot-scope="scope">{% raw %}{{ parseInt(scope.row.total_calls) > 0 ? ((parseInt(scope.row.success_calls) / parseInt(scope.row.total_calls)) * 100).toFixed(2) : '100.00' }}{% endraw %}%</template></el-table-column>
<el-table-column label="平均响应时间" sortable width="140"><template slot-scope="scope">{% raw %}{{ (scope.row.avg_process_time || 0).toFixed(2) }}{% endraw %}ms</template></el-table-column>
<el-table-column label="操作" width="120" align="center"><template slot-scope="scope"><el-button size="mini" type="primary" plain @click="viewPluginTrend(scope.row)">查看趋势</el-button></template></el-table-column>
</el-table>
</el-card>
<el-dialog title="插件使用趋势" :visible.sync="pluginTrendVisible" width="70%"><div class="chart-shell"><div class="chart-heading">{% raw %}{{ selectedPlugin ? selectedPlugin.plugin_name : '' }}{% endraw %} 使用趋势</div><canvas id="pluginTrendChart" width="800" height="360"></canvas></div></el-dialog>
</div>
{% endblock %}
{% block scripts %}
<script>
new Vue({
el: '#app',
mixins: [baseApp],
data() {
return {
pluginStats: [],
pluginTrendVisible: false,
selectedPlugin: null
}
},
mounted() {
this.currentView = '2';
this.loadData();
},
methods: {
loadData() {
const days = parseInt(this.timeRange);
this.loadPluginStats(days);
},
loadPluginStats(days) {
axios.get(`/api/plugin_stats?days=${days}`)
.then(response => {
if (response.data.success) {
this.pluginStats = response.data.data || [];
}
})
.catch(error => {
console.error('加载插件统计数据出错:', error);
this.$message.error('加载插件统计数据出错');
});
},
viewPluginTrend(plugin) {
this.selectedPlugin = plugin;
this.pluginTrendVisible = true;
// 加载插件趋势数据
const days = parseInt(this.timeRange);
axios.get(`/api/plugin_trend?days=${days}&plugin_name=${plugin.plugin_name}`)
.then(response => {
if (response.data.success) {
const trendData = response.data.data || [];
this.$nextTick(() => {
this.renderPluginTrendChart(trendData, plugin.plugin_name);
});
}
})
.catch(error => {
console.error('加载插件趋势数据出错:', error);
this.$message.error('加载插件趋势数据出错');
});
},
renderPluginTrendChart(trendData, pluginName) {
const ctx = document.getElementById('pluginTrendChart').getContext('2d');
// 销毁旧图表
if (this.charts && this.charts.pluginTrendChart) {
this.charts.pluginTrendChart.destroy();
}
// 确保charts对象存在
if (!this.charts) {
this.charts = {};
}
// 准备数据
const labels = trendData.map(item => item.date || item.stat_date);
const totalData = trendData.map(item => parseInt(item.total_calls) || 0);
const successData = trendData.map(item => parseInt(item.success_calls) || 0);
const failedData = trendData.map(item => parseInt(item.failed_calls) || 0);
// 创建新图表
this.charts.pluginTrendChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: `${pluginName} 总调用`,
data: totalData,
fill: false,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
tension: 0.1
},
{
label: `${pluginName} 成功调用`,
data: successData,
fill: false,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
tension: 0.1
},
{
label: `${pluginName} 失败调用`,
data: failedData,
fill: false,
backgroundColor: 'rgba(255, 99, 132, 0.6)',
borderColor: 'rgba(255, 99, 132, 1)',
tension: 0.1
}
]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
}
});
new Vue({ el:'#app', mixins:[baseApp], data(){ return { pluginStats:[], pluginTrendVisible:false, selectedPlugin:null } }, computed:{ totalCalls(){ return this.pluginStats.reduce((s,i)=>s+(parseInt(i.total_calls)||0),0); }, averageSuccessRate(){ if(!this.pluginStats.length) return '0.00'; const sum=this.pluginStats.reduce((s,i)=>s+(parseInt(i.total_calls)>0?((parseInt(i.success_calls)/parseInt(i.total_calls))*100):100),0); return (sum/this.pluginStats.length).toFixed(2); } }, mounted(){ this.currentView='2'; this.loadData(); }, methods:{ loadData(){ this.loadPluginStats(parseInt(this.timeRange)); }, loadPluginStats(days){ axios.get(`/api/plugin_stats?days=${days}`).then(r=>{ if(r.data.success) this.pluginStats=r.data.data||[]; }).catch(e=>{ console.error('加载插件统计数据出错:',e); this.$message.error('加载插件统计数据出错'); }); }, viewPluginTrend(plugin){ this.selectedPlugin=plugin; this.pluginTrendVisible=true; const days=parseInt(this.timeRange); axios.get(`/api/plugin_trend?days=${days}&plugin_name=${plugin.plugin_name}`).then(r=>{ if(r.data.success){ const trendData=r.data.data||[]; this.$nextTick(()=>this.renderPluginTrendChart(trendData, plugin.plugin_name)); } }).catch(e=>{ console.error('加载插件趋势数据出错:',e); this.$message.error('加载插件趋势数据出错'); }); }, renderPluginTrendChart(trendData, pluginName){ const ctx=document.getElementById('pluginTrendChart').getContext('2d'); if(this.charts&&this.charts.pluginTrendChart)this.charts.pluginTrendChart.destroy(); if(!this.charts)this.charts={}; const labels=trendData.map(i=>i.date||i.stat_date); const totalData=trendData.map(i=>parseInt(i.total_calls)||0); const successData=trendData.map(i=>parseInt(i.success_calls)||0); const failedData=trendData.map(i=>parseInt(i.failed_calls)||0); this.charts.pluginTrendChart=new Chart(ctx,{ type:'line', data:{ labels, datasets:[ {label:`${pluginName} 总调用`,data:totalData,fill:false,backgroundColor:'rgba(79,70,229,.2)',borderColor:'rgba(79,70,229,1)',tension:.28,borderWidth:3,pointRadius:2},{label:`${pluginName} 成功调用`,data:successData,fill:false,backgroundColor:'rgba(16,185,129,.2)',borderColor:'rgba(16,185,129,1)',tension:.28,borderWidth:3,pointRadius:2},{label:`${pluginName} 失败调用`,data:failedData,fill:false,backgroundColor:'rgba(239,68,68,.2)',borderColor:'rgba(239,68,68,1)',tension:.28,borderWidth:3,pointRadius:2}]}, options:{ responsive:true, scales:{ y:{beginAtZero:true,grid:{color:'rgba(148,163,184,.12)'}}, x:{grid:{display:false}} } } }); } } });
</script>
{% endblock %}
<style>
.page-shell{display:flex;flex-direction:column;gap:16px}.page-hero{padding:24px 26px;border-radius:24px;background:linear-gradient(135deg, rgba(79,70,229,.10), rgba(59,130,246,.08), rgba(255,255,255,.9));border:1px solid rgba(148,163,184,.16);box-shadow:0 18px 40px rgba(15,23,42,.06)}.page-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#6366f1;font-weight:700;margin-bottom:8px}.page-hero-copy h1{font-size:30px;line-height:1.1;margin-bottom:10px;color:#0f172a}.page-hero-copy p{color:#64748b;font-size:14px}.overview-grid .el-col{margin-bottom:16px}.overview-card{min-height:112px}.overview-card--primary{background:linear-gradient(180deg, rgba(79,70,229,.10), rgba(255,255,255,.94)) !important}.overview-card--soft{background:linear-gradient(180deg, rgba(59,130,246,.08), rgba(255,255,255,.94)) !important}.overview-label{font-size:13px;color:#64748b;margin-bottom:14px}.overview-value{font-size:30px;font-weight:700;color:#0f172a;margin-bottom:10px}.overview-note{font-size:12px;color:#94a3b8}.workspace-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.workspace-header h3{font-size:18px;margin-bottom:4px}.workspace-header p{font-size:13px;color:#64748b}.entity-cell{display:flex;align-items:center;gap:12px}.entity-badge{width:30px;height:30px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:rgba(79,70,229,.10);color:#4f46e5;font-size:12px;font-weight:700;flex-shrink:0}.entity-title{font-size:14px;font-weight:600;color:#0f172a}.entity-subtitle{margin-top:4px;font-size:12px;color:#94a3b8}.chart-shell{padding:16px;border-radius:18px;background:linear-gradient(180deg, rgba(248,250,252,.78), rgba(255,255,255,.96));border:1px solid rgba(148,163,184,.12)}.chart-heading{margin-bottom:12px;font-size:14px;font-weight:600;color:#334155}
</style>
{% endblock %}

View File

@@ -3,67 +3,112 @@
{% block title %}插件管理 - 机器人管理后台{% endblock %}
{% block content %}
<!-- 插件管理 -->
<div>
<el-row {% raw %}:gutter="20"{% endraw %}>
<el-col {% raw %}:span="24"{% endraw %}>
<el-card shadow="hover">
<div slot="header">
<span>插件管理</span>
<el-button style="float: right; padding: 3px 0" type="text" {% raw %}@click="refreshPlugins"{% endraw %}>
<i class="el-icon-refresh"></i> 刷新
</el-button>
</div>
<el-table {% raw %}:data="plugins"{% endraw %} style="width: 100%" border>
<el-table-column prop="name" label="插件名称"></el-table-column>
<el-table-column prop="module_name" label="模块名称"></el-table-column>
<el-table-column prop="version" label="版本"></el-table-column>
<el-table-column prop="author" label="作者"></el-table-column>
<el-table-column prop="description" label="描述" show-overflow-tooltip></el-table-column>
<el-table-column label="状态">
<template slot-scope="scope">
<el-tag {% raw %}:type="scope.row.status === 'RUNNING' ? 'success' : 'danger'"{% endraw %}>
{% raw %}{{ scope.row.status === 'RUNNING' ? '已启用' : '已禁用' }}{% endraw %}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280">
<template slot-scope="scope">
<el-button
size="mini"
{% raw %}:type="scope.row.status === 'RUNNING' ? 'danger' : 'success'"{% endraw %}
{% raw %}@click="togglePluginStatus(scope.row)"{% endraw %}>
{% raw %}{{ scope.row.status === 'RUNNING' ? '禁用' : '启用' }}{% endraw %}
</el-button>
<el-button
size="mini"
type="primary"
{% raw %}@click="reloadPlugin(scope.row)"{% endraw %}>
重载
</el-button>
<el-button
size="mini"
type="info"
{% raw %}@click="showPluginInfo(scope.row)"{% endraw %}>
详情
</el-button>
</template>
</el-table-column>
</el-table>
<div class="page-shell plugins-page">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">Plugins Workspace</div>
<h1>插件管理</h1>
<p>统一查看插件状态、版本、说明与配置,减少传统后台式碎片操作。</p>
</div>
<div class="page-hero-actions">
<el-button type="primary" @click="refreshPlugins">
<i class="el-icon-refresh"></i> 刷新插件
</el-button>
</div>
</div>
<el-row :gutter="16" class="overview-grid">
<el-col :span="6">
<el-card class="overview-card overview-card--primary" shadow="hover">
<div class="overview-label">插件总数</div>
<div class="overview-value">{% raw %}{{ plugins.length }}{% endraw %}</div>
<div class="overview-note">当前已注册插件模块</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="overview-card" shadow="hover">
<div class="overview-label">运行中</div>
<div class="overview-value">{% raw %}{{ runningPluginsCount }}{% endraw %}</div>
<div class="overview-note">可正常提供能力的插件</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="overview-card" shadow="hover">
<div class="overview-label">已停用</div>
<div class="overview-value">{% raw %}{{ stoppedPluginsCount }}{% endraw %}</div>
<div class="overview-note">待启用或排查状态</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="overview-card overview-card--soft" shadow="hover">
<div class="overview-label">作者数量</div>
<div class="overview-value">{% raw %}{{ authorsCount }}{% endraw %}</div>
<div class="overview-note">参与维护的作者规模</div>
</el-card>
</el-col>
</el-row>
<!-- 插件详情对话框 -->
<el-dialog title="插件详情" {% raw %}:visible.sync="pluginInfoVisible"{% endraw %} width="60%" top="5vh">
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>插件列表</h3>
<p>优先关注状态和说明,再进入单个插件详情与配置编辑。</p>
</div>
</div>
<el-table :data="plugins" style="width: 100%" v-loading="loading">
<el-table-column label="插件信息" min-width="260">
<template slot-scope="scope">
<div class="entity-cell">
<div class="entity-badge">{% raw %}{{ scope.$index + 1 }}{% endraw %}</div>
<div class="entity-copy">
<div class="entity-title">{% raw %}{{ scope.row.name }}{% endraw %}</div>
<div class="entity-subtitle">模块:{% raw %}{{ scope.row.module_name }}{% endraw %}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="version" label="版本" width="120"></el-table-column>
<el-table-column prop="author" label="作者" width="140"></el-table-column>
<el-table-column prop="description" label="描述" min-width="280" show-overflow-tooltip></el-table-column>
<el-table-column label="状态" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'RUNNING' ? 'success' : 'info'">
{% raw %}{{ scope.row.status === 'RUNNING' ? '已启用' : '已禁用' }}{% endraw %}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="290">
<template slot-scope="scope">
<div class="action-row">
<el-button
size="mini"
:type="scope.row.status === 'RUNNING' ? 'danger' : 'success'"
@click="togglePluginStatus(scope.row)">
{% raw %}{{ scope.row.status === 'RUNNING' ? '禁用' : '启用' }}{% endraw %}
</el-button>
<el-button size="mini" type="primary" plain @click="reloadPlugin(scope.row)">
重载
</el-button>
<el-button size="mini" type="info" plain @click="showPluginInfo(scope.row)">
详情
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog title="插件详情" :visible.sync="pluginInfoVisible" width="64%" top="5vh">
<div v-if="selectedPlugin" class="plugin-detail-container">
<div class="dialog-intro">查看插件基础信息、命令列表与配置内容,需要时可直接在这里编辑配置。</div>
<el-descriptions border direction="vertical" :column="2" size="small" class="plugin-descriptions">
<el-descriptions-item label="插件名称" :span="1">{% raw %}{{ selectedPlugin.name }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="模块名称" :span="1">{% raw %}{{ selectedPlugin.module_name }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="版本" :span="1">{% raw %}{{ selectedPlugin.version }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="作者" :span="1">{% raw %}{{ selectedPlugin.author }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="状态" :span="1">
<el-tag {% raw %}:type="selectedPlugin.status === 'RUNNING' ? 'success' : 'danger'"{% endraw %} size="small">
<el-tag :type="selectedPlugin.status === 'RUNNING' ? 'success' : 'info'" size="small">
{% raw %}{{ selectedPlugin.status === 'RUNNING' ? '已启用' : '已禁用' }}{% endraw %}
</el-tag>
</el-descriptions-item>
@@ -73,7 +118,7 @@
<el-descriptions-item label="描述" :span="2">{% raw %}{{ selectedPlugin.description }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="命令列表" :span="2" v-if="selectedPlugin.commands && selectedPlugin.commands.length > 0">
<div class="command-tags">
<el-tag v-for="cmd in selectedPlugin.commands" :key="cmd" size="mini" style="margin-right: 5px; margin-bottom: 5px;">
<el-tag v-for="cmd in selectedPlugin.commands" :key="cmd" size="mini" class="command-tag">
{% raw %}{{ cmd }}{% endraw %}
</el-tag>
</div>
@@ -81,15 +126,9 @@
<el-descriptions-item label="配置信息" :span="2" v-if="selectedPlugin.config">
<div class="config-container">
<div class="config-actions">
<el-button type="primary" size="mini" @click="editConfig" :disabled="isEditingConfig">
编辑配置
</el-button>
<el-button type="success" size="mini" @click="saveConfig" v-if="isEditingConfig">
保存配置
</el-button>
<el-button type="info" size="mini" @click="cancelEditConfig" v-if="isEditingConfig">
取消
</el-button>
<el-button type="primary" size="mini" @click="editConfig" :disabled="isEditingConfig">编辑配置</el-button>
<el-button type="success" size="mini" @click="saveConfig" v-if="isEditingConfig">保存配置</el-button>
<el-button type="info" size="mini" @click="cancelEditConfig" v-if="isEditingConfig">取消</el-button>
</div>
<div v-if="!isEditingConfig">
<pre>{% raw %}{{ selectedPlugin.configText }}{% endraw %}</pre>
@@ -98,10 +137,10 @@
<el-input
type="textarea"
v-model="editedConfig"
:rows="10"
:rows="12"
placeholder="请输入TOML格式的配置"
class="config-editor"
></el-input>
class="config-editor">
</el-input>
<div class="config-error" v-if="configError">{% raw %}{{ configError }}{% endraw %}</div>
</div>
</div>
@@ -126,11 +165,22 @@
isEditingConfig: false,
editedConfig: '',
configError: '',
configFormat: 'toml' // 默认为toml格式
configFormat: 'toml'
}
},
computed: {
runningPluginsCount() {
return this.plugins.filter(plugin => plugin.status === 'RUNNING').length;
},
stoppedPluginsCount() {
return this.plugins.filter(plugin => plugin.status !== 'RUNNING').length;
},
authorsCount() {
return new Set((this.plugins || []).map(plugin => plugin.author).filter(Boolean)).size;
}
},
mounted() {
this.currentView = '11'; // 设置当前菜单项
this.currentView = '11';
this.loadPlugins();
},
methods: {
@@ -159,7 +209,7 @@
togglePluginStatus(plugin) {
const action = plugin.status === 'RUNNING' ? 'disable' : 'enable';
const actionText = plugin.status === 'RUNNING' ? '禁用' : '启用';
this.$confirm(`确定要${actionText}插件 "${plugin.name}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@@ -171,7 +221,7 @@
.then(response => {
if (response.data.success) {
this.$message.success(`${actionText}插件成功`);
this.loadPlugins(); // 重新加载插件列表
this.loadPlugins();
} else {
this.$message.error(response.data.message || `${actionText}插件失败`);
}
@@ -196,7 +246,7 @@
.then(response => {
if (response.data.success) {
this.$message.success('重载插件成功');
this.loadPlugins(); // 重新加载插件列表
this.loadPlugins();
} else {
this.$message.error(response.data.message || '重载插件失败');
}
@@ -209,27 +259,19 @@
this.$message.info('已取消操作');
});
},
// 编辑配置
editConfig() {
this.isEditingConfig = true;
this.editedConfig = this.selectedPlugin.configText || '';
this.configError = '';
},
// 取消编辑配置
cancelEditConfig() {
this.isEditingConfig = false;
this.editedConfig = '';
this.configError = '';
},
// 保存配置
saveConfig() {
try {
// 验证TOML格式
let configObj;
// 发送到后端保存
axios.post('/api/plugins/config/update', {
plugin_name: this.selectedPlugin.module_name,
config_text: this.editedConfig,
@@ -239,12 +281,8 @@
if (response.data.success) {
this.$message.success('配置保存成功');
this.isEditingConfig = false;
// 更新本地配置显示
this.selectedPlugin.configText = this.editedConfig;
this.selectedPlugin.config = configObj;
// 询问是否要重载插件以应用新配置
this.$confirm('配置已保存,是否要重载插件以应用新配置?', '提示', {
confirmButtonText: '重载插件',
cancelButtonText: '稍后手动重载',
@@ -266,15 +304,11 @@
this.configError = '处理配置时出错: ' + e.message;
}
},
// 修改现有的showPluginInfo方法获取原始配置文本
showPluginInfo(plugin) {
// 获取插件详细信息
axios.get(`/api/plugins/info?plugin_name=${plugin.module_name}`)
.then(response => {
if (response.data.success) {
this.selectedPlugin = response.data.data;
// 如果有配置文本,直接使用
if (this.selectedPlugin.configText) {
this.pluginInfoVisible = true;
this.isEditingConfig = false;
@@ -282,8 +316,7 @@
this.configError = '';
return;
}
// 如果没有配置文本,获取原始配置文件内容
axios.get(`/api/plugins/config/raw?plugin_name=${plugin.module_name}`)
.then(configResponse => {
if (configResponse.data.success) {
@@ -316,72 +349,77 @@
});
</script>
<style>
.plugin-detail-container {
max-height: 70vh;
overflow-y: auto;
}
.plugin-descriptions {
width: 100%;
}
.config-container {
max-height: 200px;
overflow-y: auto;
background-color: var(--tech-panel-2);
border: 1px solid var(--tech-border);
border-radius: 4px;
padding: 8px;
font-size: 12px;
color: var(--tech-text);
}
.config-container pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
.command-tags {
.page-shell {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 16px;
}
/* 自定义滚动条样式 */
.plugin-detail-container::-webkit-scrollbar,
.config-container::-webkit-scrollbar {
width: 6px;
height: 6px;
.page-hero {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
padding: 24px 26px;
border-radius: 24px;
background: linear-gradient(135deg, rgba(79,70,229,0.10), rgba(59,130,246,0.08), rgba(255,255,255,0.9));
border: 1px solid rgba(148, 163, 184, 0.16);
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
.plugin-detail-container::-webkit-scrollbar-thumb,
.config-container::-webkit-scrollbar-thumb {
background: #d7d1c8;
border-radius: 3px;
.page-eyebrow {
font-size: 12px;
text-transform: uppercase;
letter-spacing: .08em;
color: #6366f1;
font-weight: 700;
margin-bottom: 8px;
}
.plugin-detail-container::-webkit-scrollbar-track,
.config-container::-webkit-scrollbar-track {
background: #fcfaf8;
}
/* ... 现有样式保持不变 ... */
.config-actions {
.page-hero-copy h1 {
font-size: 30px;
line-height: 1.1;
margin-bottom: 10px;
display: flex;
gap: 10px;
color: #0f172a;
}
.config-editor {
font-family: monospace;
font-size: 12px;
.page-hero-copy p {
color: #64748b;
font-size: 14px;
}
.config-error {
color: var(--tech-danger);
font-size: 12px;
margin-top: 5px;
.overview-grid .el-col { margin-bottom: 16px; }
.overview-card { min-height: 112px; }
.overview-card--primary {
background: linear-gradient(180deg, rgba(79,70,229,0.10), rgba(255,255,255,0.94)) !important;
}
.overview-card--soft {
background: linear-gradient(180deg, rgba(59,130,246,0.08), rgba(255,255,255,0.94)) !important;
}
.overview-label { font-size: 13px; color: #64748b; margin-bottom: 14px; }
.overview-value { font-size: 30px; font-weight: 700; color: #0f172a; margin-bottom: 10px; }
.overview-note { font-size: 12px; color: #94a3b8; }
.workspace-header {
display: flex; align-items: center; justify-content: space-between; gap: 16px;
}
.workspace-header h3 { font-size: 18px; margin-bottom: 4px; }
.workspace-header p { font-size: 13px; color: #64748b; }
.entity-cell { display: flex; align-items: center; gap: 12px; }
.entity-badge {
width: 30px; height: 30px; border-radius: 50%; display: inline-flex; align-items: center;
justify-content: center; background: rgba(79,70,229,0.10); color: #4f46e5; font-size: 12px;
font-weight: 700; flex-shrink: 0;
}
.entity-title { font-size: 14px; font-weight: 600; color: #0f172a; }
.entity-subtitle { margin-top: 4px; font-size: 12px; color: #94a3b8; }
.action-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.plugin-detail-container { max-height: 70vh; overflow-y: auto; }
.plugin-descriptions { width: 100%; }
.dialog-intro { margin-bottom: 14px; color: #64748b; font-size: 13px; }
.config-container {
max-height: 320px; overflow-y: auto; background: rgba(248,250,252,0.82); border: 1px solid rgba(148,163,184,0.12);
border-radius: 14px; padding: 12px; font-size: 12px; color: #334155;
}
.config-container pre { margin: 0; white-space: pre-wrap; word-break: break-word; }
.command-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.command-tag { margin: 0 !important; }
.config-actions { margin-bottom: 10px; display: flex; gap: 10px; }
.config-editor { font-family: monospace; font-size: 12px; }
.config-error { color: #ef4444; font-size: 12px; margin-top: 5px; }
</style>
{% endblock %}

View File

@@ -3,129 +3,153 @@
{% block title %}群权限管理 - 机器人管理后台{% endblock %}
{% block content %}
<!-- 群机器人管理 -->
<div>
<el-row :gutter="20">
<el-col :span="24">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>群机器人管理</span>
<el-button
type="primary"
size="small"
style="float: right; margin-left: 10px;"
@click="showAddGroupDialog">
添加群组
</el-button>
<el-input
placeholder="搜索群组..."
v-model="searchQuery"
style="width: 200px; float: right"
clearable>
</el-input>
</div>
<!-- 群组列表 -->
<el-table
:data="filteredGroups"
style="width: 100%"
border
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="群组信息">
<template slot-scope="scope">
{% raw %}{{ scope.row.group_name || scope.row.group_id }} ({{ scope.row.group_id }}){% endraw %}
</template>
</el-table-column>
<el-table-column label="机器人状态" width="120">
<template slot-scope="scope">
<el-tag
:type="scope.row.robot_status === 'enabled' ? 'success' : 'danger'">
{% raw %}{{ scope.row.robot_status === 'enabled' ? '已启用' : '已关闭' }}{% endraw %}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280">
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
@click="viewGroupPermissions(scope.row)">
查看权限
</el-button>
<el-button
size="mini"
:type="scope.row.robot_status === 'enabled' ? 'danger' : 'success'"
@click="toggleRobotStatus(scope.row)">
{% raw %}{{ scope.row.robot_status === 'enabled' ? '关闭' : '启用' }}{% endraw %}
</el-button>
<el-button
size="mini"
type="info"
@click="viewMessageTrend(scope.row)">
消息趋势
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 批量操作按钮 -->
<div style="margin-top: 20px" v-if="selectedGroups.length > 0">
<el-alert
title="批量操作"
type="info"
:closable="false">
<span>已选择 {% raw %}{{ selectedGroups.length }}{% endraw %} 个群组</span>
</el-alert>
<div style="margin-top: 10px">
<el-button type="success" size="small" @click="batchEnableRobot">批量启用</el-button>
<el-button type="danger" size="small" @click="batchDisableRobot">批量关闭</el-button>
<el-button type="warning" size="small" @click="batchRemoveGroups">批量移除</el-button>
</div>
</div>
<div class="page-shell robot-page">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">Groups Workspace</div>
<h1>群权限与机器人管理</h1>
<p>集中管理群组启停、权限开关与消息趋势,减少传统后台式分散操作。</p>
</div>
<div class="page-hero-actions">
<el-input
placeholder="搜索群组名称 / ID"
v-model="searchQuery"
class="hero-search"
clearable>
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
<el-button type="primary" @click="showAddGroupDialog">
<i class="el-icon-plus"></i> 添加群组
</el-button>
</div>
</div>
<el-row :gutter="16" class="overview-grid">
<el-col :span="6">
<el-card class="overview-card overview-card--primary" shadow="hover">
<div class="overview-label">群组总数</div>
<div class="overview-value">{% raw %}{{ groups.length }}{% endraw %}</div>
<div class="overview-note">已纳入机器人管理的群组</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="overview-card" shadow="hover">
<div class="overview-label">已启用</div>
<div class="overview-value">{% raw %}{{ enabledGroupsCount }}{% endraw %}</div>
<div class="overview-note">机器人功能已开启</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="overview-card" shadow="hover">
<div class="overview-label">已关闭</div>
<div class="overview-value">{% raw %}{{ disabledGroupsCount }}{% endraw %}</div>
<div class="overview-note">等待重新启用或清理</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="overview-card overview-card--soft" shadow="hover">
<div class="overview-label">当前筛选结果</div>
<div class="overview-value">{% raw %}{{ filteredGroups.length }}{% endraw %}</div>
<div class="overview-note">按搜索条件即时过滤</div>
</el-card>
</el-col>
</el-row>
<!-- 群组权限管理对话框 -->
<el-dialog
:title="currentGroupName + ' 功能权限管理'"
:visible.sync="permissionDialogVisible"
width="70%">
<el-table :data="permissions" style="width: 100%" border>
<el-table-column prop="feature_id" label="功能ID" width="80"></el-table-column>
<el-table-column prop="feature_description" label="功能描述"></el-table-column>
<el-table-column label="状态" width="100">
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>群组列表</h3>
<p>先处理状态,再深入查看权限与趋势。</p>
</div>
<div class="workspace-actions" v-if="selectedGroups.length > 0">
<span class="selection-summary">已选择 {% raw %}{{ selectedGroups.length }}{% endraw %} 个群组</span>
<el-button type="success" size="small" @click="batchEnableRobot">批量启用</el-button>
<el-button type="danger" size="small" @click="batchDisableRobot">批量关闭</el-button>
<el-button type="warning" size="small" @click="batchRemoveGroups">批量移除</el-button>
</div>
</div>
<el-table
:data="filteredGroups"
style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="52"></el-table-column>
<el-table-column label="群组信息" min-width="300">
<template slot-scope="scope">
<el-tag
:type="scope.row.status === 'enabled' ? 'success' : 'danger'">
<div class="entity-cell">
<div class="entity-badge">{% raw %}{{ scope.$index + 1 }}{% endraw %}</div>
<div class="entity-copy">
<div class="entity-title">{% raw %}{{ scope.row.group_name || scope.row.group_id }}{% endraw %}</div>
<div class="entity-subtitle">Group ID: {% raw %}{{ scope.row.group_id }}{% endraw %}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="机器人状态" width="140" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.robot_status === 'enabled' ? 'success' : 'info'">
{% raw %}{{ scope.row.robot_status === 'enabled' ? '已启用' : '已关闭' }}{% endraw %}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="360">
<template slot-scope="scope">
<div class="action-row">
<el-button size="mini" type="primary" plain @click="viewGroupPermissions(scope.row)">
查看权限
</el-button>
<el-button
size="mini"
:type="scope.row.robot_status === 'enabled' ? 'danger' : 'success'"
@click="toggleRobotStatus(scope.row)">
{% raw %}{{ scope.row.robot_status === 'enabled' ? '关闭机器人' : '启用机器人' }}{% endraw %}
</el-button>
<el-button size="mini" type="info" plain @click="viewMessageTrend(scope.row)">
消息趋势
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog
:title="currentGroupName + ' 功能权限管理'"
:visible.sync="permissionDialogVisible"
width="72%">
<div class="dialog-intro">按功能维度快速启停,适合对群组做差异化权限控制。</div>
<el-table :data="permissions" style="width: 100%">
<el-table-column prop="feature_id" label="功能 ID" width="90"></el-table-column>
<el-table-column prop="feature_description" label="功能描述"></el-table-column>
<el-table-column label="当前状态" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'enabled' ? 'success' : 'info'">
{% raw %}{{ scope.row.status === 'enabled' ? '已启用' : '已关闭' }}{% endraw %}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<el-table-column label="切换" width="140" align="center">
<template slot-scope="scope">
<el-switch
v-model="scope.row.statusBool"
active-color="#13ce66"
inactive-color="#ff4949"
active-color="#10b981"
inactive-color="#cbd5e1"
@change="togglePermission(scope.row)">
</el-switch>
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button @click="enableAllPermissions" type="success">一键启用全部</el-button>
<el-button @click="disableAllPermissions" type="danger">一键关闭全部</el-button>
<div slot="footer" class="dialog-footer dialog-footer-split">
<div>
<el-button @click="enableAllPermissions" type="success">一键启用全部</el-button>
<el-button @click="disableAllPermissions" type="danger">一键关闭全部</el-button>
</div>
<el-button @click="permissionDialogVisible = false">关闭</el-button>
</div>
</el-dialog>
<!-- 添加群组对话框 -->
<el-dialog
title="添加群组"
:visible.sync="addGroupDialogVisible"
width="30%">
<el-dialog title="添加群组" :visible.sync="addGroupDialogVisible" width="34%">
<div class="dialog-intro">输入需要纳入机器人管理的群组 ID。</div>
<el-form :model="addGroupForm" :rules="addGroupRules" ref="addGroupForm">
<el-form-item label="群组ID" prop="groupId">
<el-input v-model="addGroupForm.groupId" placeholder="请输入群组ID"></el-input>
@@ -136,15 +160,12 @@
<el-button type="primary" @click="submitAddGroup">确定</el-button>
</span>
</el-dialog>
<!-- 群组消息趋势对话框 -->
<el-dialog
title="群组消息趋势"
:visible.sync="messageTrendDialogVisible"
width="70%">
<div class="chart-container">
<h3>{% raw %}{{ currentGroupName }}{% endraw %} 消息趋势</h3>
<canvas id="messageTrendChart" width="800" height="400"></canvas>
<el-dialog title="群组消息趋势" :visible.sync="messageTrendDialogVisible" width="72%">
<div class="dialog-intro">观察该群组在当前统计区间内的消息节奏与活跃度变化。</div>
<div class="chart-shell">
<div class="chart-heading">{% raw %}{{ currentGroupName }}{% endraw %} · 消息趋势</div>
<canvas id="messageTrendChart" width="800" height="360"></canvas>
</div>
</el-dialog>
</div>
@@ -164,7 +185,6 @@
searchQuery: '',
selectedGroups: [],
permissionDialogVisible: false,
// 添加群组相关数据
addGroupDialogVisible: false,
addGroupForm: {
groupId: ''
@@ -175,7 +195,6 @@
{ pattern: /^\S+$/, message: '群组ID不能包含空格', trigger: 'blur' }
]
},
// 添加消息趋势相关数据
messageTrendDialogVisible: false,
messageTrendData: {
dates: [],
@@ -187,14 +206,20 @@
filteredGroups() {
if (!this.searchQuery) return this.groups;
const query = this.searchQuery.toLowerCase();
return this.groups.filter(group =>
(group.group_id && group.group_id.toLowerCase().includes(query)) ||
return this.groups.filter(group =>
(group.group_id && group.group_id.toLowerCase().includes(query)) ||
(group.group_name && group.group_name.toLowerCase().includes(query))
);
},
enabledGroupsCount() {
return this.groups.filter(group => group.robot_status === 'enabled').length;
},
disabledGroupsCount() {
return this.groups.filter(group => group.robot_status !== 'enabled').length;
}
},
mounted() {
this.currentView = '6'; // 设置当前菜单项
this.currentView = '6';
this.loadGroups();
},
methods: {
@@ -218,11 +243,10 @@
viewGroupPermissions(group) {
this.currentGroupId = group.group_id;
this.currentGroupName = group.group_name || group.group_id;
axios.get(`/robot/api/group/${group.group_id}/permissions`)
.then(response => {
if (response.data.success) {
// 添加布尔值属性用于switch组件
const permissionsData = response.data.data || [];
this.permissions = permissionsData.map(p => ({
...p,
@@ -240,7 +264,7 @@
},
togglePermission(permission) {
const newStatus = permission.statusBool ? 'enabled' : 'disabled';
axios.post(`/robot/api/group/${this.currentGroupId}/permissions`, {
feature_id: permission.feature_id,
status: newStatus
@@ -250,13 +274,11 @@
permission.status = newStatus;
this.$message.success('更新权限成功');
} else {
// 恢复原状态
permission.statusBool = !permission.statusBool;
this.$message.error('更新权限失败');
}
})
.catch(error => {
// 恢复原状态
permission.statusBool = !permission.statusBool;
console.error('更新权限失败:', error);
this.$message.error('更新权限失败: ' + error.message);
@@ -274,7 +296,6 @@
})
.then(response => {
if (response.data.success) {
// 更新本地数据
this.permissions.forEach(p => {
p.status = status;
p.statusBool = status === 'enabled';
@@ -291,7 +312,7 @@
},
toggleRobotStatus(group) {
const newStatus = group.robot_status === 'enabled' ? 'disabled' : 'enabled';
axios.post(`/robot/api/group/${group.group_id}/status`, {
status: newStatus
})
@@ -322,7 +343,6 @@
if (response.data.success) {
this.addGroupDialogVisible = false;
this.$message.success('添加群组成功');
// 重新加载群组列表
this.loadGroups();
} else {
this.$message.error('添加群组失败');
@@ -346,7 +366,7 @@
this.$message.warning('请先选择群组');
return;
}
axios.post('/robot/api/batch_operation', {
operation: 'update_status',
group_ids: this.selectedGroups,
@@ -354,7 +374,6 @@
})
.then(response => {
if (response.data.success) {
// 更新本地数据
this.groups.forEach(g => {
if (this.selectedGroups.includes(g.group_id)) {
g.robot_status = status;
@@ -375,7 +394,7 @@
this.$message.warning('请先选择群组');
return;
}
this.$confirm(`确定要批量移除 ${this.selectedGroups.length} 个群组吗? 此操作将清除这些群组的所有设置。`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@@ -387,7 +406,6 @@
})
.then(response => {
if (response.data.success) {
// 从列表中移除这些群组
this.groups = this.groups.filter(g => !this.selectedGroups.includes(g.group_id));
this.selectedGroups = [];
this.$message.success('批量移除成功');
@@ -401,20 +419,17 @@
});
}).catch(() => {});
},
// 添加查看消息趋势的方法
viewMessageTrend(group) {
this.currentGroupId = group.group_id;
this.currentGroupName = group.group_name || group.group_id;
// 获取消息趋势数据
const days = parseInt(this.timeRange || 7);
axios.get(`/robot/api/group/${group.group_id}/message_trend?days=${days}`)
.then(response => {
if (response.data.success) {
this.messageTrendData = response.data.data || { dates: [], counts: [] };
this.messageTrendDialogVisible = true;
// 在下一个DOM更新周期渲染图表
this.$nextTick(() => {
this.renderMessageTrendChart();
});
@@ -427,21 +442,17 @@
this.$message.error('加载消息趋势失败: ' + error.message);
});
},
// 渲染消息趋势图表
renderMessageTrendChart() {
const ctx = document.getElementById('messageTrendChart').getContext('2d');
// 销毁旧图表
if (this.charts && this.charts.messageTrendChart) {
this.charts.messageTrendChart.destroy();
}
// 确保charts对象存在
if (!this.charts) {
this.charts = {};
}
// 创建新图表
this.charts.messageTrendChart = new Chart(ctx, {
type: 'line',
data: {
@@ -450,18 +461,34 @@
{
label: '消息数量',
data: this.messageTrendData.counts,
fill: false,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
tension: 0.1
fill: true,
backgroundColor: 'rgba(79, 70, 229, 0.10)',
borderColor: 'rgba(79, 70, 229, 1)',
tension: 0.28,
borderWidth: 3,
pointRadius: 2
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true
beginAtZero: true,
grid: {
color: 'rgba(148,163,184,0.12)'
}
},
x: {
grid: {
display: false
}
}
},
plugins: {
legend: {
display: false
}
}
}
@@ -470,4 +497,187 @@
}
});
</script>
{% endblock %}
{% endblock %}
{% block styles %}
<style>
.page-shell {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-hero {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
padding: 24px 26px;
border-radius: 24px;
background: linear-gradient(135deg, rgba(79,70,229,0.10), rgba(59,130,246,0.08), rgba(255,255,255,0.9));
border: 1px solid rgba(148, 163, 184, 0.16);
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
.page-eyebrow {
font-size: 12px;
text-transform: uppercase;
letter-spacing: .08em;
color: #6366f1;
font-weight: 700;
margin-bottom: 8px;
}
.page-hero-copy h1 {
font-size: 30px;
line-height: 1.1;
margin-bottom: 10px;
color: #0f172a;
}
.page-hero-copy p {
color: #64748b;
font-size: 14px;
}
.page-hero-actions {
display: flex;
align-items: center;
gap: 12px;
}
.hero-search {
width: 260px;
}
.overview-grid .el-col {
margin-bottom: 16px;
}
.overview-card {
min-height: 112px;
}
.overview-card--primary {
background: linear-gradient(180deg, rgba(79,70,229,0.10), rgba(255,255,255,0.94)) !important;
}
.overview-card--soft {
background: linear-gradient(180deg, rgba(59,130,246,0.08), rgba(255,255,255,0.94)) !important;
}
.overview-label {
font-size: 13px;
color: #64748b;
margin-bottom: 14px;
}
.overview-value {
font-size: 30px;
font-weight: 700;
color: #0f172a;
margin-bottom: 10px;
}
.overview-note {
font-size: 12px;
color: #94a3b8;
}
.workspace-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.workspace-header h3 {
font-size: 18px;
margin-bottom: 4px;
}
.workspace-header p {
font-size: 13px;
color: #64748b;
}
.workspace-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.selection-summary {
font-size: 13px;
color: #475569;
margin-right: 4px;
}
.entity-cell {
display: flex;
align-items: center;
gap: 12px;
}
.entity-badge {
width: 30px;
height: 30px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(79,70,229,0.10);
color: #4f46e5;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.entity-title {
font-size: 14px;
font-weight: 600;
color: #0f172a;
}
.entity-subtitle {
margin-top: 4px;
font-size: 12px;
color: #94a3b8;
}
.action-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.dialog-intro {
margin-bottom: 14px;
color: #64748b;
font-size: 13px;
}
.dialog-footer-split {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.chart-shell {
padding: 16px;
border-radius: 18px;
background: linear-gradient(180deg, rgba(248,250,252,0.78), rgba(255,255,255,0.96));
border: 1px solid rgba(148,163,184,0.12);
}
.chart-heading {
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
color: #334155;
}
</style>
{% endblock %}

View File

@@ -3,13 +3,29 @@
{% block title %}资源监控 - 机器人管理后台{% endblock %}
{% block content %}
<div class="iframe-page-container">
<el-card class="iframe-card">
<div slot="header">
<span>服务器资源监控</span>
<div class="page-shell system-page">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">System Workspace</div>
<h1>服务器资源监控</h1>
<p>把系统面板统一纳入控制台视图,避免工具页与主工作流割裂。</p>
</div>
<div class="iframe-container">
<iframe src="{{ src_url }}" frameborder="0"></iframe>
<div class="page-hero-actions">
<el-button type="primary" plain @click="reloadIframe"><i class="el-icon-refresh"></i> 刷新面板</el-button>
<el-button type="primary" @click="openInNewTab"><i class="el-icon-top-right"></i> 新窗口打开</el-button>
</div>
</div>
<el-card class="iframe-shell-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>监控面板</h3>
<p>直接在控制台内查看系统资源变化与运行状态。</p>
</div>
<div class="iframe-url">{{ src_url }}</div>
</div>
<div class="iframe-shell">
<iframe ref="monitorFrame" src="{{ src_url }}" frameborder="0"></iframe>
</div>
</el-card>
</div>
@@ -23,48 +39,47 @@
data() {
return {
currentView: '14',
showTimeRangeSelector: false
showTimeRangeSelector: false,
frameUrl: '{{ src_url }}'
}
},
mounted() {
this.currentView = '14';
},
methods: {
reloadIframe() {
if (this.$refs.monitorFrame) {
this.$refs.monitorFrame.src = this.frameUrl;
}
},
openInNewTab() {
window.open(this.frameUrl, '_blank');
}
}
});
</script>
<style>
.iframe-page-container {
height: 100%;
display: flex;
flex-direction: column;
.page-shell { display: flex; flex-direction: column; gap: 16px; }
.page-hero {
display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; padding: 24px 26px; border-radius: 24px;
background: linear-gradient(135deg, rgba(79,70,229,0.10), rgba(59,130,246,0.08), rgba(255,255,255,0.9));
border: 1px solid rgba(148, 163, 184, 0.16); box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
.iframe-card {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
margin: 0;
}
.el-card__body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.iframe-container {
flex: 1;
overflow: auto;
min-height: 0;
}
.iframe-container iframe {
width: 100%;
height: 100%;
border: none;
display: block;
.page-hero-actions { display: flex; align-items: center; gap: 12px; }
.page-eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #6366f1; font-weight: 700; margin-bottom: 8px; }
.page-hero-copy h1 { font-size: 30px; line-height: 1.1; margin-bottom: 10px; color: #0f172a; }
.page-hero-copy p { color: #64748b; font-size: 14px; }
.workspace-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.workspace-header h3 { font-size: 18px; margin-bottom: 4px; }
.workspace-header p { font-size: 13px; color: #64748b; }
.iframe-url {
max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px;
color: #94a3b8; padding: 8px 12px; border-radius: 999px; background: rgba(248,250,252,0.9); border: 1px solid rgba(148,163,184,0.12);
}
.iframe-shell-card { height: calc(100vh - 230px); }
.iframe-shell-card .el-card__body { height: calc(100% - 73px); }
.iframe-shell { height: 100%; border-radius: 18px; overflow: hidden; border: 1px solid rgba(148,163,184,0.12); background: rgba(248,250,252,0.82); }
.iframe-shell iframe { width: 100%; height: 100%; border: none; display: block; background: #fff; }
</style>
{% endblock %}

View File

@@ -3,75 +3,32 @@
{% block title %}用户统计 - 机器人管理后台{% endblock %}
{% block content %}
<!-- 用户统计 -->
<div>
<el-row {% raw %}:gutter="20"{% endraw %}>
<el-col {% raw %}:span="24"{% endraw %}>
<el-card shadow="hover">
<div slot="header">
<span>用户活跃度排行</span>
</div>
<el-table {% raw %}:data="userStats"{% endraw %} style="width: 100%" border>
<el-table-column label="用户信息">
<template slot-scope="scope">
{% raw %}{{ scope.row.user_name || scope.row.user_id }} ({{ scope.row.user_id }}){% endraw %}
</template>
</el-table-column>
<el-table-column prop="total_calls" label="调用次数" sortable></el-table-column>
<el-table-column prop="success_calls" label="成功次数" sortable></el-table-column>
<el-table-column prop="failed_calls" label="失败次数" sortable></el-table-column>
<el-table-column label="成功率" sortable>
<template slot-scope="scope">
{% raw %}{{ (scope.row.success_calls / scope.row.total_calls * 100).toFixed(2) }}{% endraw %}%
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" {% raw %}@click="viewUserDetail(scope.row)"{% endraw %}>查看详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<div class="page-shell stats-page">
<div class="page-hero"><div class="page-hero-copy"><div class="page-eyebrow">Users Analytics</div><h1>用户统计</h1><p>关注高频用户、成功率与调用规模,帮助识别核心活跃用户画像。</p></div></div>
<el-row :gutter="16" class="overview-grid">
<el-col :span="8"><el-card class="overview-card overview-card--primary"><div class="overview-label">用户总数</div><div class="overview-value">{% raw %}{{ userStats.length }}{% endraw %}</div><div class="overview-note">当前排行中展示的用户</div></el-card></el-col>
<el-col :span="8"><el-card class="overview-card"><div class="overview-label">总调用次数</div><div class="overview-value">{% raw %}{{ totalCalls }}{% endraw %}</div><div class="overview-note">统计周期内所有用户调用总和</div></el-card></el-col>
<el-col :span="8"><el-card class="overview-card overview-card--soft"><div class="overview-label">平均成功率</div><div class="overview-value">{% raw %}{{ averageSuccessRate }}{% endraw %}%</div><div class="overview-note">用户整体调用成功表现</div></el-card></el-col>
</el-row>
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header"><div><h3>用户活跃度排行</h3><p>查看调用次数、成功情况与重点活跃用户。</p></div></div>
<el-table :data="userStats" style="width:100%">
<el-table-column label="用户信息" min-width="300"><template slot-scope="scope"><div class="entity-cell"><div class="entity-badge">{% raw %}{{ scope.$index + 1 }}{% endraw %}</div><div class="entity-copy"><div class="entity-title">{% raw %}{{ scope.row.user_name || scope.row.user_id }}{% endraw %}</div><div class="entity-subtitle">{% raw %}{{ scope.row.user_id }}{% endraw %}</div></div></div></template></el-table-column>
<el-table-column prop="total_calls" label="调用次数" sortable width="120"></el-table-column>
<el-table-column prop="success_calls" label="成功次数" sortable width="120"></el-table-column>
<el-table-column prop="failed_calls" label="失败次数" sortable width="120"></el-table-column>
<el-table-column label="成功率" sortable width="120"><template slot-scope="scope">{% raw %}{{ scope.row.total_calls ? (scope.row.success_calls / scope.row.total_calls * 100).toFixed(2) : '0.00' }}{% endraw %}%</template></el-table-column>
<el-table-column label="操作" width="120" align="center"><template slot-scope="scope"><el-button size="mini" type="primary" plain @click="viewUserDetail(scope.row)">查看详情</el-button></template></el-table-column>
</el-table>
</el-card>
</div>
{% endblock %}
{% block scripts %}
<script>
new Vue({
el: '#app',
mixins: [baseApp],
data() {
return {
userStats: []
}
},
mounted() {
this.currentView = '3';
this.loadData();
},
methods: {
loadData() {
const days = parseInt(this.timeRange);
this.loadUserStats(days);
},
loadUserStats(days) {
axios.get(`/api/user_stats?days=${days}&limit=20`)
.then(response => {
if (response.data.success) {
this.userStats = response.data.data || [];
}
})
.catch(error => {
console.error('加载用户统计数据出错:', error);
this.$message.error('加载用户统计数据出错');
});
},
viewUserDetail(user) {
this.$message.info('用户详情功能开发中');
}
}
});
new Vue({ el:'#app', mixins:[baseApp], data(){return{userStats:[]}}, computed:{ totalCalls(){return this.userStats.reduce((s,i)=>s+(parseInt(i.total_calls)||0),0)}, averageSuccessRate(){ if(!this.userStats.length) return '0.00'; const sum=this.userStats.reduce((s,i)=>s+(i.total_calls?((i.success_calls/i.total_calls)*100):0),0); return (sum/this.userStats.length).toFixed(2)} }, mounted(){ this.currentView='3'; this.loadData(); }, methods:{ loadData(){ this.loadUserStats(parseInt(this.timeRange)); }, loadUserStats(days){ axios.get(`/api/user_stats?days=${days}&limit=20`).then(r=>{ if(r.data.success) this.userStats=r.data.data||[]; }).catch(e=>{ console.error('加载用户统计数据出错:',e); this.$message.error('加载用户统计数据出错'); }); }, viewUserDetail(){ this.$message.info('用户详情功能开发中'); } } });
</script>
{% endblock %}
<style>
.page-shell{display:flex;flex-direction:column;gap:16px}.page-hero{padding:24px 26px;border-radius:24px;background:linear-gradient(135deg, rgba(79,70,229,.10), rgba(59,130,246,.08), rgba(255,255,255,.9));border:1px solid rgba(148,163,184,.16);box-shadow:0 18px 40px rgba(15,23,42,.06)}.page-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#6366f1;font-weight:700;margin-bottom:8px}.page-hero-copy h1{font-size:30px;line-height:1.1;margin-bottom:10px;color:#0f172a}.page-hero-copy p{color:#64748b;font-size:14px}.overview-grid .el-col{margin-bottom:16px}.overview-card{min-height:112px}.overview-card--primary{background:linear-gradient(180deg, rgba(79,70,229,.10), rgba(255,255,255,.94)) !important}.overview-card--soft{background:linear-gradient(180deg, rgba(59,130,246,.08), rgba(255,255,255,.94)) !important}.overview-label{font-size:13px;color:#64748b;margin-bottom:14px}.overview-value{font-size:30px;font-weight:700;color:#0f172a;margin-bottom:10px}.overview-note{font-size:12px;color:#94a3b8}.workspace-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.workspace-header h3{font-size:18px;margin-bottom:4px}.workspace-header p{font-size:13px;color:#64748b}.entity-cell{display:flex;align-items:center;gap:12px}.entity-badge{width:30px;height:30px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:rgba(79,70,229,.10);color:#4f46e5;font-size:12px;font-weight:700;flex-shrink:0}.entity-title{font-size:14px;font-weight:600;color:#0f172a}.entity-subtitle{margin-top:4px;font-size:12px;color:#94a3b8}
</style>
{% endblock %}

View File

@@ -3,362 +3,100 @@
{% block title %}虚拟群组管理 - 机器人管理后台{% endblock %}
{% block content %}
<!-- 虚拟群组管理 -->
<div>
<el-row :gutter="20">
<el-col :span="24">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>虚拟群组管理</span>
<el-button
type="primary"
size="small"
style="float: right; margin-left: 10px;"
@click="showCreateVirtualGroupDialog">
创建虚拟群组
</el-button>
<el-input
placeholder="搜索虚拟群组..."
v-model="searchQuery"
style="width: 200px; float: right"
clearable>
</el-input>
</div>
<!-- 虚拟群组列表 -->
<el-table
:data="filteredVirtualGroups"
style="width: 100%"
border>
<el-table-column type="expand">
<template slot-scope="props">
<el-table
:data="props.row.groups"
style="width: 100%">
<el-table-column label="微信群ID" prop="id" width="280"></el-table-column>
<el-table-column label="微信群名称" prop="name"></el-table-column>
<el-table-column label="操作" width="120">
<template slot-scope="scope">
<el-button
size="mini"
type="danger"
@click="removeGroupFromVirtual(props.row.id, scope.row.id)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 10px;">
<el-button size="small" type="primary" @click="showAddGroupDialog(props.row)">添加微信群</el-button>
</div>
</template>
</el-table-column>
<el-table-column label="虚拟群组名称" prop="name"></el-table-column>
<el-table-column label="包含群数量">
<template slot-scope="scope">
{% raw %}{{ scope.row.groups.length }}{% endraw %} 个群
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
@click="editVirtualGroup(scope.row)">编辑</el-button>
<el-button
size="mini"
type="danger"
@click="deleteVirtualGroup(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<div class="page-shell virtual-page">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">Virtual Groups Workspace</div>
<h1>虚拟群组管理</h1>
<p>管理虚拟群组与其包含的微信群,让分组运营与消息路由更清楚。</p>
</div>
<div class="page-hero-actions">
<el-input placeholder="搜索虚拟群组..." v-model="searchQuery" class="hero-search" clearable>
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
<el-button type="primary" @click="showCreateVirtualGroupDialog">创建虚拟群组</el-button>
</div>
</div>
<el-row :gutter="16" class="overview-grid">
<el-col :span="8"><el-card class="overview-card overview-card--primary"><div class="overview-label">虚拟群组数</div><div class="overview-value">{% raw %}{{ virtualGroups.length }}{% endraw %}</div><div class="overview-note">当前定义的虚拟群组数量</div></el-card></el-col>
<el-col :span="8"><el-card class="overview-card"><div class="overview-label">可用微信群</div><div class="overview-value">{% raw %}{{ allGroups.length }}{% endraw %}</div><div class="overview-note">可纳入虚拟群组的微信群</div></el-card></el-col>
<el-col :span="8"><el-card class="overview-card overview-card--soft"><div class="overview-label">筛选结果</div><div class="overview-value">{% raw %}{{ filteredVirtualGroups.length }}{% endraw %}</div><div class="overview-note">当前搜索命中的虚拟群组</div></el-card></el-col>
</el-row>
<!-- 创建虚拟群组对话框 -->
<el-dialog
title="创建虚拟群组"
:visible.sync="createVirtualGroupDialogVisible"
width="30%">
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header"><div><h3>虚拟群组列表</h3><p>展开每个虚拟群组可以查看其包含的微信群并进行增删。</p></div></div>
<el-table :data="filteredVirtualGroups" style="width:100%">
<el-table-column type="expand">
<template slot-scope="props">
<div class="expand-shell">
<el-table :data="props.row.groups" style="width:100%">
<el-table-column label="微信群ID" prop="id" width="280"></el-table-column>
<el-table-column label="微信群名称" prop="name"></el-table-column>
<el-table-column label="操作" width="120"><template slot-scope="scope"><el-button size="mini" type="danger" @click="removeGroupFromVirtual(props.row.id, scope.row.id)">移除</el-button></template></el-table-column>
</el-table>
<div class="expand-actions"><el-button size="small" type="primary" @click="showAddGroupDialog(props.row)">添加微信群</el-button></div>
</div>
</template>
</el-table-column>
<el-table-column label="虚拟群组名称" min-width="260"><template slot-scope="scope"><div class="entity-cell"><div class="entity-badge">{% raw %}{{ scope.$index + 1 }}{% endraw %}</div><div class="entity-copy"><div class="entity-title">{% raw %}{{ scope.row.name }}{% endraw %}</div><div class="entity-subtitle">ID: {% raw %}{{ scope.row.id }}{% endraw %}</div></div></div></template></el-table-column>
<el-table-column label="包含群数量" width="140" align="center"><template slot-scope="scope">{% raw %}{{ scope.row.groups.length }}{% endraw %} 个群</template></el-table-column>
<el-table-column label="操作" width="220"><template slot-scope="scope"><div class="action-row"><el-button size="mini" type="primary" plain @click="editVirtualGroup(scope.row)">编辑</el-button><el-button size="mini" type="danger" @click="deleteVirtualGroup(scope.row)">删除</el-button></div></template></el-table-column>
</el-table>
</el-card>
<el-dialog title="创建虚拟群组" :visible.sync="createVirtualGroupDialogVisible" width="30%">
<div class="dialog-intro">为一组微信群创建统一的虚拟分组名称,方便集中管理。</div>
<el-form :model="virtualGroupForm" :rules="virtualGroupRules" ref="virtualGroupForm">
<el-form-item label="群组名称" prop="name">
<el-input v-model="virtualGroupForm.name" placeholder="请输入虚拟群组名称"></el-input>
</el-form-item>
<el-form-item label="群组名称" prop="name"><el-input v-model="virtualGroupForm.name" placeholder="请输入虚拟群组名称"></el-input></el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="createVirtualGroupDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitCreateVirtualGroup">确定</el-button>
</span>
<span slot="footer" class="dialog-footer"><el-button @click="createVirtualGroupDialogVisible = false">取消</el-button><el-button type="primary" @click="submitCreateVirtualGroup">确定</el-button></span>
</el-dialog>
<!-- 编辑虚拟群组对话框 -->
<el-dialog
title="编辑虚拟群组"
:visible.sync="editVirtualGroupDialogVisible"
width="30%">
<el-dialog title="编辑虚拟群组" :visible.sync="editVirtualGroupDialogVisible" width="30%">
<div class="dialog-intro">修改虚拟群组名称,不影响已包含的微信群列表。</div>
<el-form :model="editVirtualGroupForm" :rules="virtualGroupRules" ref="editVirtualGroupForm">
<el-form-item label="群组名称" prop="name">
<el-input v-model="editVirtualGroupForm.name" placeholder="请输入虚拟群组名称"></el-input>
</el-form-item>
<el-form-item label="群组名称" prop="name"><el-input v-model="editVirtualGroupForm.name" placeholder="请输入虚拟群组名称"></el-input></el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editVirtualGroupDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEditVirtualGroup">确定</el-button>
</span>
<span slot="footer" class="dialog-footer"><el-button @click="editVirtualGroupDialogVisible = false">取消</el-button><el-button type="primary" @click="submitEditVirtualGroup">确定</el-button></span>
</el-dialog>
<!-- 添加微信群对话框 -->
<el-dialog
title="添加微信群到虚拟群组"
:visible.sync="addGroupDialogVisible"
width="50%">
<el-dialog title="添加微信群到虚拟群组" :visible.sync="addGroupDialogVisible" width="50%">
<div class="dialog-intro">从可用微信群中选择一个加入当前虚拟群组。</div>
<el-form :model="addGroupForm" :rules="addGroupRules" ref="addGroupForm">
<el-form-item label="选择微信群" prop="wx_group_id">
<el-select
v-model="addGroupForm.wx_group_id"
filterable
placeholder="请选择微信群"
style="width: 100%">
<el-option
v-for="group in availableGroups"
:key="group.wxid"
:label="group.name"
:value="group.wxid">
<span style="float: left">{% raw %}{{ group.name }}{% endraw %}</span>
<span style="float: right; color: #94a3b8; font-size: 13px">{% raw %}{{ group.wxid }}{% endraw %}</span>
</el-option>
<el-select v-model="addGroupForm.wx_group_id" filterable placeholder="请选择微信群" style="width:100%">
<el-option v-for="group in availableGroups" :key="group.wxid" :label="group.name" :value="group.wxid"><span style="float:left">{% raw %}{{ group.name }}{% endraw %}</span><span style="float:right;color:#94a3b8;font-size:13px">{% raw %}{{ group.wxid }}{% endraw %}</span></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addGroupDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitAddGroup">确定</el-button>
</span>
<span slot="footer" class="dialog-footer"><el-button @click="addGroupDialogVisible = false">取消</el-button><el-button type="primary" @click="submitAddGroup">确定</el-button></span>
</el-dialog>
</div>
{% endblock %}
{% block scripts %}
<script>
new Vue({
el: '#app',
mixins: [baseApp],
data() {
return {
virtualGroups: [],
searchQuery: '',
// 创建虚拟群组相关数据
createVirtualGroupDialogVisible: false,
virtualGroupForm: {
name: ''
},
virtualGroupRules: {
name: [
{ required: true, message: '请输入虚拟群组名称', trigger: 'blur' }
]
},
// 编辑虚拟群组相关数据
editVirtualGroupDialogVisible: false,
editVirtualGroupForm: {
id: '',
name: ''
},
// 添加微信群相关数据
addGroupDialogVisible: false,
currentVirtualGroupId: '',
addGroupForm: {
wx_group_id: ''
},
addGroupRules: {
wx_group_id: [
{ required: true, message: '请选择微信群', trigger: 'change' }
]
},
availableGroups: [],
allGroups: []
}
},
computed: {
filteredVirtualGroups() {
if (!this.searchQuery) return this.virtualGroups;
const query = this.searchQuery.toLowerCase();
return this.virtualGroups.filter(group =>
group.name.toLowerCase().includes(query)
);
}
},
mounted() {
this.currentView = '12'; // 设置当前视图为虚拟群组管理
this.loadVirtualGroups();
this.loadAllGroups();
},
methods: {
loadVirtualGroups() {
axios.get('/virtual_group/api/virtual_groups')
.then(response => {
if (response.data.success) {
this.virtualGroups = response.data.data.chatGroups || [];
} else {
this.$message.error('加载虚拟群组失败');
}
})
.catch(error => {
console.error('加载虚拟群组失败:', error);
this.$message.error('加载虚拟群组失败');
});
},
loadAllGroups() {
axios.get('/contacts/api/groups')
.then(response => {
if (response.data.success) {
const groups = response.data.data.groups;
this.allGroups = Object.entries(groups).map(([wxid, name]) => ({
wxid,
name: name || wxid
}));
}
})
.catch(error => {
console.error('加载群组列表失败:', error);
this.$message.error('加载群组列表失败');
});
},
showCreateVirtualGroupDialog() {
this.virtualGroupForm = { name: '' };
this.createVirtualGroupDialogVisible = true;
this.$nextTick(() => {
this.$refs.virtualGroupForm && this.$refs.virtualGroupForm.clearValidate();
});
},
submitCreateVirtualGroup() {
this.$refs.virtualGroupForm.validate(valid => {
if (valid) {
axios.post('/virtual_group/api/virtual_groups', this.virtualGroupForm)
.then(response => {
if (response.data.success) {
this.$message.success('创建虚拟群组成功');
this.createVirtualGroupDialogVisible = false;
this.loadVirtualGroups();
} else {
this.$message.error(response.data.error || '创建虚拟群组失败');
}
})
.catch(error => {
console.error('创建虚拟群组失败:', error);
this.$message.error('创建虚拟群组失败');
});
}
});
},
editVirtualGroup(group) {
this.editVirtualGroupForm = {
id: group.id,
name: group.name
};
this.editVirtualGroupDialogVisible = true;
this.$nextTick(() => {
this.$refs.editVirtualGroupForm && this.$refs.editVirtualGroupForm.clearValidate();
});
},
submitEditVirtualGroup() {
this.$refs.editVirtualGroupForm.validate(valid => {
if (valid) {
axios.put(`/virtual_group/api/virtual_groups/${this.editVirtualGroupForm.id}`, {
name: this.editVirtualGroupForm.name
})
.then(response => {
if (response.data.success) {
this.$message.success('更新虚拟群组成功');
this.editVirtualGroupDialogVisible = false;
this.loadVirtualGroups();
} else {
this.$message.error(response.data.error || '更新虚拟群组失败');
}
})
.catch(error => {
console.error('更新虚拟群组失败:', error);
this.$message.error('更新虚拟群组失败');
});
}
});
},
deleteVirtualGroup(group) {
this.$confirm(`确定要删除虚拟群组 "${group.name}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.delete(`/virtual_group/api/virtual_groups/${group.id}`)
.then(response => {
if (response.data.success) {
this.$message.success('删除虚拟群组成功');
this.loadVirtualGroups();
} else {
this.$message.error(response.data.error || '删除虚拟群组失败');
}
})
.catch(error => {
console.error('删除虚拟群组失败:', error);
this.$message.error('删除虚拟群组失败');
});
}).catch(() => {});
},
showAddGroupDialog(virtualGroup) {
this.currentVirtualGroupId = virtualGroup.id;
this.addGroupForm = { wx_group_id: '' };
// 过滤掉已经在虚拟群组中的微信群
const existingGroupIds = virtualGroup.groups.map(g => g.id);
this.availableGroups = this.allGroups.filter(g => !existingGroupIds.includes(g.wxid));
this.addGroupDialogVisible = true;
this.$nextTick(() => {
this.$refs.addGroupForm && this.$refs.addGroupForm.clearValidate();
});
},
submitAddGroup() {
this.$refs.addGroupForm.validate(valid => {
if (valid) {
axios.post(`/virtual_group/api/virtual_groups/${this.currentVirtualGroupId}/groups`, {
wx_group_id: this.addGroupForm.wx_group_id
})
.then(response => {
if (response.data.success) {
this.$message.success('添加微信群成功');
this.addGroupDialogVisible = false;
this.loadVirtualGroups();
} else {
this.$message.error(response.data.error || '添加微信群失败');
}
})
.catch(error => {
console.error('添加微信群失败:', error);
this.$message.error('添加微信群失败');
});
}
});
},
removeGroupFromVirtual(virtualGroupId, wxGroupId) {
this.$confirm('确定要从虚拟群组中移除该微信群吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.delete(`/virtual_group/api/virtual_groups/${virtualGroupId}/groups/${wxGroupId}`)
.then(response => {
if (response.data.success) {
this.$message.success('移除微信群成功');
this.loadVirtualGroups();
} else {
this.$message.error(response.data.error || '移除微信群失败');
}
})
.catch(error => {
console.error('移除微信群失败:', error);
this.$message.error('移除微信群失败');
});
}).catch(() => {});
}
}
});
new Vue({
el:'#app', mixins:[baseApp],
data(){ return { virtualGroups:[], searchQuery:'', createVirtualGroupDialogVisible:false, virtualGroupForm:{name:''}, virtualGroupRules:{name:[{required:true,message:'请输入虚拟群组名称',trigger:'blur'}]}, editVirtualGroupDialogVisible:false, editVirtualGroupForm:{id:'',name:''}, addGroupDialogVisible:false, currentVirtualGroupId:'', addGroupForm:{wx_group_id:''}, addGroupRules:{wx_group_id:[{required:true,message:'请选择微信群',trigger:'change'}]}, availableGroups:[], allGroups:[] } },
computed:{ filteredVirtualGroups(){ if(!this.searchQuery) return this.virtualGroups; const query=this.searchQuery.toLowerCase(); return this.virtualGroups.filter(group=>group.name.toLowerCase().includes(query)); } },
mounted(){ this.currentView='12'; this.loadVirtualGroups(); this.loadAllGroups(); },
methods:{
loadVirtualGroups(){ axios.get('/virtual_group/api/virtual_groups').then(response=>{ if(response.data.success){ this.virtualGroups=response.data.data.chatGroups||[]; } else { this.$message.error('加载虚拟群组失败'); } }).catch(error=>{ console.error('加载虚拟群组失败:', error); this.$message.error('加载虚拟群组失败'); }); },
loadAllGroups(){ axios.get('/contacts/api/groups').then(response=>{ if(response.data.success){ const groups=response.data.data.groups; this.allGroups=Object.entries(groups).map(([wxid,name])=>({wxid,name:name||wxid})); } }).catch(error=>{ console.error('加载群组列表失败:', error); this.$message.error('加载群组列表失败'); }); },
showCreateVirtualGroupDialog(){ this.virtualGroupForm={name:''}; this.createVirtualGroupDialogVisible=true; this.$nextTick(()=>{ this.$refs.virtualGroupForm&&this.$refs.virtualGroupForm.clearValidate(); }); },
submitCreateVirtualGroup(){ this.$refs.virtualGroupForm.validate(valid=>{ if(valid){ axios.post('/virtual_group/api/virtual_groups', this.virtualGroupForm).then(response=>{ if(response.data.success){ this.$message.success('创建虚拟群组成功'); this.createVirtualGroupDialogVisible=false; this.loadVirtualGroups(); } else { this.$message.error(response.data.error||'创建虚拟群组失败'); } }).catch(error=>{ console.error('创建虚拟群组失败:', error); this.$message.error('创建虚拟群组失败'); }); } }); },
editVirtualGroup(group){ this.editVirtualGroupForm={ id:group.id, name:group.name }; this.editVirtualGroupDialogVisible=true; this.$nextTick(()=>{ this.$refs.editVirtualGroupForm&&this.$refs.editVirtualGroupForm.clearValidate(); }); },
submitEditVirtualGroup(){ this.$refs.editVirtualGroupForm.validate(valid=>{ if(valid){ axios.put(`/virtual_group/api/virtual_groups/${this.editVirtualGroupForm.id}`, { name:this.editVirtualGroupForm.name }).then(response=>{ if(response.data.success){ this.$message.success('更新虚拟群组成功'); this.editVirtualGroupDialogVisible=false; this.loadVirtualGroups(); } else { this.$message.error(response.data.error||'更新虚拟群组失败'); } }).catch(error=>{ console.error('更新虚拟群组失败:', error); this.$message.error('更新虚拟群组失败'); }); } }); },
deleteVirtualGroup(group){ this.$confirm(`确定要删除虚拟群组 "${group.name}" 吗?`,'提示',{ confirmButtonText:'确定', cancelButtonText:'取消', type:'warning' }).then(()=>{ axios.delete(`/virtual_group/api/virtual_groups/${group.id}`).then(response=>{ if(response.data.success){ this.$message.success('删除虚拟群组成功'); this.loadVirtualGroups(); } else { this.$message.error(response.data.error||'删除虚拟群组失败'); } }).catch(error=>{ console.error('删除虚拟群组失败:', error); this.$message.error('删除虚拟群组失败'); }); }).catch(()=>{}); },
showAddGroupDialog(virtualGroup){ this.currentVirtualGroupId=virtualGroup.id; this.addGroupForm={ wx_group_id:'' }; const existingGroupIds=virtualGroup.groups.map(g=>g.id); this.availableGroups=this.allGroups.filter(g=>!existingGroupIds.includes(g.wxid)); this.addGroupDialogVisible=true; this.$nextTick(()=>{ this.$refs.addGroupForm&&this.$refs.addGroupForm.clearValidate(); }); },
submitAddGroup(){ this.$refs.addGroupForm.validate(valid=>{ if(valid){ axios.post(`/virtual_group/api/virtual_groups/${this.currentVirtualGroupId}/groups`, { wx_group_id:this.addGroupForm.wx_group_id }).then(response=>{ if(response.data.success){ this.$message.success('添加微信群成功'); this.addGroupDialogVisible=false; this.loadVirtualGroups(); } else { this.$message.error(response.data.error||'添加微信群失败'); } }).catch(error=>{ console.error('添加微信群失败:', error); this.$message.error('添加微信群失败'); }); } }); },
removeGroupFromVirtual(virtualGroupId, wxGroupId){ this.$confirm('确定要从虚拟群组中移除该微信群吗?','提示',{ confirmButtonText:'确定', cancelButtonText:'取消', type:'warning' }).then(()=>{ axios.delete(`/virtual_group/api/virtual_groups/${virtualGroupId}/groups/${wxGroupId}`).then(response=>{ if(response.data.success){ this.$message.success('移除微信群成功'); this.loadVirtualGroups(); } else { this.$message.error(response.data.error||'移除微信群失败'); } }).catch(error=>{ console.error('移除微信群失败:', error); this.$message.error('移除微信群失败'); }); }).catch(()=>{}); }
}
});
</script>
<style>
.page-shell{display:flex;flex-direction:column;gap:16px}.page-hero{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;padding:24px 26px;border-radius:24px;background:linear-gradient(135deg, rgba(79,70,229,.10), rgba(59,130,246,.08), rgba(255,255,255,.9));border:1px solid rgba(148,163,184,.16);box-shadow:0 18px 40px rgba(15,23,42,.06)}.page-hero-actions{display:flex;align-items:center;gap:12px}.hero-search{width:260px}.page-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#6366f1;font-weight:700;margin-bottom:8px}.page-hero-copy h1{font-size:30px;line-height:1.1;margin-bottom:10px;color:#0f172a}.page-hero-copy p{color:#64748b;font-size:14px}.overview-grid .el-col{margin-bottom:16px}.overview-card{min-height:112px}.overview-card--primary{background:linear-gradient(180deg, rgba(79,70,229,.10), rgba(255,255,255,.94)) !important}.overview-card--soft{background:linear-gradient(180deg, rgba(59,130,246,.08), rgba(255,255,255,.94)) !important}.overview-label{font-size:13px;color:#64748b;margin-bottom:14px}.overview-value{font-size:30px;font-weight:700;color:#0f172a;margin-bottom:10px}.overview-note{font-size:12px;color:#94a3b8}.workspace-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.workspace-header h3{font-size:18px;margin-bottom:4px}.workspace-header p{font-size:13px;color:#64748b}.entity-cell{display:flex;align-items:center;gap:12px}.entity-badge{width:30px;height:30px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:rgba(79,70,229,.10);color:#4f46e5;font-size:12px;font-weight:700;flex-shrink:0}.entity-title{font-size:14px;font-weight:600;color:#0f172a}.entity-subtitle{margin-top:4px;font-size:12px;color:#94a3b8}.action-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.expand-shell{padding:8px 0}.expand-actions{margin-top:12px}.dialog-intro{margin-bottom:14px;color:#64748b;font-size:13px}
</style>
{% endblock %}

View File

@@ -3,38 +3,21 @@
{% block title %}微信日志查看{% endblock %}
{% block content %}
<div class="log-page-container">
<el-card class="log-card">
<div slot="header" class="clearfix">
<span>日志查看</span>
<el-radio-group v-model="logType" size="small" style="margin-left: 20px;" @change="loadLogs">
<el-radio-button label="info">信息日志</el-radio-button>
<el-radio-button label="error">错误日志</el-radio-button>
<el-radio-button label="debug">调试日志</el-radio-button>
</el-radio-group>
<el-select v-model="logLines" size="small" style="margin-left: 20px;" @change="loadLogs">
<el-option label="最近100行" :value="100"></el-option>
<el-option label="最近500行" :value="500"></el-option>
<el-option label="最近1000行" :value="1000"></el-option>
</el-select>
<el-select v-model="refreshInterval" size="small" style="margin-left: 20px; width: 100px;" @change="handleRefreshInterval">
<el-option label="手动" :value="0"></el-option>
<el-option label="1秒" :value="1"></el-option>
<el-option label="3秒" :value="3"></el-option>
<el-option label="5秒" :value="5"></el-option>
<el-option label="10秒" :value="10"></el-option>
<el-option label="60秒" :value="60"></el-option>
</el-select>
<el-button style="float: right; padding: 3px 0" type="text" @click="loadLogs">刷新</el-button>
<div class="page-shell logs-page">
<div class="page-hero"><div class="page-hero-copy"><div class="page-eyebrow">Logs Console</div><h1>微信日志查看</h1><p>把日志查看器纳入统一的运维面板,支持切换类型、行数与自动刷新。</p></div></div>
<el-card class="workspace-card log-card" shadow="hover">
<div slot="header" class="workspace-header workspace-header--wrap">
<div><h3>日志控制台</h3><p>选择日志类型、查看行数与自动刷新频率。</p></div>
<div class="toolbar-row">
<el-radio-group v-model="logType" size="small" @change="loadLogs"><el-radio-button label="info">信息日志</el-radio-button><el-radio-button label="error">错误日志</el-radio-button><el-radio-button label="debug">调试日志</el-radio-button></el-radio-group>
<el-select v-model="logLines" size="small" style="width:120px;" @change="loadLogs"><el-option label="100行" :value="100"></el-option><el-option label="500行" :value="500"></el-option><el-option label="1000行" :value="1000"></el-option></el-select>
<el-select v-model="refreshInterval" size="small" style="width:110px;" @change="handleRefreshInterval"><el-option label="手动" :value="0"></el-option><el-option label="1秒" :value="1"></el-option><el-option label="3秒" :value="3"></el-option><el-option label="5秒" :value="5"></el-option><el-option label="10秒" :value="10"></el-option><el-option label="60秒" :value="60"></el-option></el-select>
<el-button type="primary" plain @click="loadLogs">刷新</el-button>
</div>
</div>
<div v-loading="loading" class="log-content-wrapper">
<div v-if="logContent && logContent.length > 0" class="log-content">
<pre ref="logPre"></pre>
</div>
<div v-else class="empty-log">
<el-empty description="暂无日志内容"></el-empty>
</div>
<div v-if="logContent && logContent.length > 0" class="log-content"><pre ref="logPre"></pre></div>
<div v-else class="empty-log"><el-empty description="暂无日志内容"></el-empty></div>
</div>
</el-card>
</div>
@@ -42,159 +25,9 @@
{% block scripts %}
<script>
new Vue({
el: '#app',
mixins: [baseApp],
data() {
return {
loading: false,
logType: 'info',
logLines: 100,
logContent: [],
logText: '',
refreshInterval: 0,
refreshTimer: null,
currentView: '9',
showTimeRangeSelector: false,
cancelSource: null,
isAutoScroll: true
}
},
mounted() {
this.loadLogs();
this.$nextTick(() => {
const logDiv = this.$el.querySelector('.log-content');
if (logDiv) {
logDiv.addEventListener('scroll', () => {
const nearBottom = (logDiv.scrollHeight - logDiv.scrollTop - logDiv.clientHeight) < 50;
this.isAutoScroll = nearBottom;
});
}
});
},
beforeDestroy() {
this.clearRefreshTimer();
if (this.cancelSource) {
this.cancelSource.cancel('component destroyed');
this.cancelSource = null;
}
},
methods: {
loadLogs() {
this.loading = true;
if (this.cancelSource) {
this.cancelSource.cancel('new request');
}
this.cancelSource = axios.CancelToken.source();
axios.get(`/api/wx_logs`, {
params: { type: this.logType, lines: this.logLines },
cancelToken: this.cancelSource.token
})
.then(response => {
if (response.data.success) {
this.logContent = response.data.data.content || [];
this.logText = this.logContent.join('\n');
this.$nextTick(() => {
const logDiv = this.$el.querySelector('.log-content');
const pre = this.$refs.logPre;
if (pre) {
pre.textContent = this.logText;
}
if (logDiv) {
if (this.isAutoScroll) {
logDiv.scrollTop = logDiv.scrollHeight;
}
}
});
} else {
this.$message.error('加载日志失败');
}
})
.catch(error => {
if (!axios.isCancel(error)) {
console.error('加载日志出错:', error);
this.$message.error('加载日志出错');
}
})
.finally(() => {
this.loading = false;
});
},
handleRefreshInterval() {
this.clearRefreshTimer();
if (this.refreshInterval > 0) {
this.refreshTimer = setInterval(() => {
this.loadLogs();
}, this.refreshInterval * 1000);
}
},
clearRefreshTimer() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
}
}
});
new Vue({ el:'#app', mixins:[baseApp], data(){ return { loading:false, logType:'info', logLines:100, logContent:[], logText:'', refreshInterval:0, refreshTimer:null, currentView:'9', showTimeRangeSelector:false, cancelSource:null, isAutoScroll:true } }, mounted(){ this.loadLogs(); this.$nextTick(()=>{ const logDiv=this.$el.querySelector('.log-content'); if(logDiv){ logDiv.addEventListener('scroll',()=>{ const nearBottom=(logDiv.scrollHeight-logDiv.scrollTop-logDiv.clientHeight)<50; this.isAutoScroll=nearBottom; }); } }); }, beforeDestroy(){ this.clearRefreshTimer(); if(this.cancelSource){ this.cancelSource.cancel('component destroyed'); this.cancelSource=null; } }, methods:{ loadLogs(){ this.loading=true; if(this.cancelSource){ this.cancelSource.cancel('new request'); } this.cancelSource=axios.CancelToken.source(); axios.get(`/api/wx_logs`,{ params:{ type:this.logType, lines:this.logLines }, cancelToken:this.cancelSource.token }).then(response=>{ if(response.data.success){ this.logContent=response.data.data.content||[]; this.logText=this.logContent.join('\n'); this.$nextTick(()=>{ const logDiv=this.$el.querySelector('.log-content'); const pre=this.$refs.logPre; if(pre) pre.textContent=this.logText; if(logDiv&&this.isAutoScroll) logDiv.scrollTop=logDiv.scrollHeight; }); } else { this.$message.error('加载日志失败'); } }).catch(error=>{ if(!axios.isCancel(error)){ console.error('加载日志出错:',error); this.$message.error('加载日志出错'); } }).finally(()=>{ this.loading=false; }); }, handleRefreshInterval(){ this.clearRefreshTimer(); if(this.refreshInterval>0){ this.refreshTimer=setInterval(()=>this.loadLogs(), this.refreshInterval*1000); } }, clearRefreshTimer(){ if(this.refreshTimer){ clearInterval(this.refreshTimer); this.refreshTimer=null; } } } });
</script>
<style>
.log-page-container {
height: 100%;
display: flex;
flex-direction: column;
}
.log-card {
flex: 1;
display: flex;
flex-direction: column;
margin: 0;
height: 100%;
}
.el-card__body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.log-content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.log-content {
flex: 1;
overflow-y: auto !important;
overflow-x: scroll !important;
background-color: var(--tech-panel-2);
padding: 10px;
border-radius: 4px;
border: 1px solid var(--tech-border);
box-sizing: border-box;
min-height: 0;
color: var(--tech-text);
}
.log-content pre {
margin: 0;
white-space: pre;
word-break: break-all;
font-family: monospace;
line-height: 1;
color: inherit;
}
.empty-log {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.page-shell{display:flex;flex-direction:column;gap:16px}.page-hero{padding:24px 26px;border-radius:24px;background:linear-gradient(135deg, rgba(79,70,229,.10), rgba(59,130,246,.08), rgba(255,255,255,.9));border:1px solid rgba(148,163,184,.16);box-shadow:0 18px 40px rgba(15,23,42,.06)}.page-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#6366f1;font-weight:700;margin-bottom:8px}.page-hero-copy h1{font-size:30px;line-height:1.1;margin-bottom:10px;color:#0f172a}.page-hero-copy p{color:#64748b;font-size:14px}.log-card{height:calc(100vh - 230px)}.log-card .el-card__body{height:calc(100% - 73px);display:flex;flex-direction:column;overflow:hidden}.workspace-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.workspace-header--wrap{align-items:flex-start}.workspace-header h3{font-size:18px;margin-bottom:4px}.workspace-header p{font-size:13px;color:#64748b}.toolbar-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}.log-content-wrapper{flex:1;display:flex;flex-direction:column;overflow:hidden}.log-content{flex:1;overflow-y:auto !important;overflow-x:scroll !important;background:#0f172a;padding:14px;border-radius:18px;border:1px solid rgba(148,163,184,.12);box-sizing:border-box;min-height:0;color:#dbeafe}.log-content pre{margin:0;white-space:pre;word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.45;color:inherit}.empty-log{flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden}
</style>
{% endblock %}