stock-ai-agent/frontend/admin.html
2026-03-29 11:39:40 +08:00

650 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理 - XClaw</title>
<!-- Global Styles -->
<link rel="stylesheet" href="/static/css/style.css">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Page-Specific Styles -->
<style>
/* Admin Layout */
.admin-page {
display: flex;
min-height: 100vh;
}
/* Sidebar */
.admin-sidebar {
width: 240px;
background: var(--bg-primary);
border-right: 1px solid var(--border);
padding: 24px 0;
position: fixed;
height: 100vh;
}
.admin-logo {
padding: 0 24px 24px;
border-bottom: 1px solid var(--border);
margin-bottom: 16px;
}
.admin-logo h2 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.admin-nav {
list-style: none;
padding: 0;
}
.admin-nav-item {
padding: 12px 24px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.admin-nav-item:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.admin-nav-item.active {
background: var(--primary-light);
color: var(--primary);
border-left-color: var(--primary);
font-weight: 500;
}
/* Main Content */
.admin-main {
margin-left: 240px;
flex: 1;
padding: 32px;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.admin-title {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
}
.logout-btn {
padding: 10px 20px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.logout-btn:hover {
background: var(--error-light);
border-color: var(--error);
color: var(--error);
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.stat-card {
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 24px;
}
.stat-label {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: var(--primary);
}
/* Search Bar */
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.search-input {
flex: 1;
padding: 12px 16px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-size: 14px;
color: var(--text-primary);
transition: all 0.2s;
}
.search-input:focus {
outline: none;
border-color: var(--primary);
}
.search-input::placeholder {
color: var(--text-tertiary);
}
.search-btn {
padding: 12px 24px;
background: var(--primary);
border: none;
border-radius: var(--radius-md);
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.search-btn:hover {
background: var(--primary-dark);
}
/* Table */
.users-table {
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background: var(--bg-tertiary);
padding: 14px 16px;
text-align: left;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
border-bottom: 1px solid var(--border);
text-transform: uppercase;
letter-spacing: 0.5px;
}
td {
padding: 14px 16px;
font-size: 14px;
color: var(--text-primary);
border-bottom: 1px solid var(--border);
}
tr:last-child td {
border-bottom: none;
}
tr:hover {
background: var(--bg-secondary);
}
/* Status Badge */
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.status-active {
background: var(--success-light);
color: var(--success);
}
.status-inactive {
background: var(--error-light);
color: var(--error);
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
margin-top: 24px;
}
.pagination button {
padding: 8px 16px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.pagination button:hover:not(:disabled) {
background: var(--primary-light);
border-color: var(--primary);
color: var(--primary);
}
.pagination button:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.page-info {
color: var(--text-secondary);
font-size: 14px;
}
/* Login Overlay */
.login-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-secondary);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.login-box {
width: 100%;
max-width: 400px;
background: var(--bg-primary);
border-radius: var(--radius-lg);
padding: 48px;
box-shadow: var(--shadow-lg);
}
.login-box h2 {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 32px;
text-align: center;
}
.login-box input {
width: 100%;
padding: 12px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-size: 14px;
color: var(--text-primary);
margin-bottom: 16px;
}
.login-box input:focus {
outline: none;
background: var(--bg-primary);
border-color: var(--primary);
}
.login-box input::placeholder {
color: var(--text-tertiary);
}
.login-box button {
width: 100%;
padding: 14px;
background: var(--primary);
border: none;
border-radius: var(--radius-md);
color: white;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.login-box button:hover {
background: var(--primary-dark);
}
.error-message {
padding: 12px 16px;
background: var(--error-light);
border: 1px solid var(--error);
border-radius: var(--radius-md);
color: var(--error);
font-size: 13px;
text-align: center;
margin-bottom: 16px;
}
/* Responsive */
@media (max-width: 768px) {
.admin-sidebar {
width: 200px;
}
.admin-main {
margin-left: 200px;
padding: 20px;
}
.admin-title {
font-size: 24px;
}
}
</style>
</head>
<body>
<div id="app">
<!-- Login Overlay -->
<div v-if="!authenticated" class="login-overlay">
<div class="login-box">
<h2>后台管理</h2>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<input
type="password"
v-model="password"
placeholder="请输入管理密码"
@keyup.enter="login"
>
<button @click="login">登录</button>
</div>
</div>
<!-- Admin Page -->
<div v-if="authenticated" class="admin-page">
<!-- Sidebar -->
<div class="admin-sidebar">
<div class="admin-logo">
<h2>XClaw 管理后台</h2>
</div>
<ul class="admin-nav">
<li class="admin-nav-item"
:class="{active: currentView === 'dashboard'}"
@click="currentView = 'dashboard'">
数据统计
</li>
<li class="admin-nav-item"
:class="{active: currentView === 'users'}"
@click="currentView = 'users'">
用户管理
</li>
</ul>
</div>
<!-- Main Content -->
<div class="admin-main">
<div class="admin-header">
<h1 class="admin-title">{{ currentViewTitle }}</h1>
<button class="logout-btn" @click="logout">退出</button>
</div>
<!-- Dashboard View -->
<div v-if="currentView === 'dashboard'">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">用户总数</div>
<div class="stat-value">{{ stats.total_users }}</div>
</div>
<div class="stat-card">
<div class="stat-label">活跃用户7天</div>
<div class="stat-value">{{ stats.active_users }}</div>
</div>
<div class="stat-card">
<div class="stat-label">对话总数</div>
<div class="stat-value">{{ stats.total_conversations }}</div>
</div>
<div class="stat-card">
<div class="stat-label">消息总数</div>
<div class="stat-value">{{ stats.total_messages }}</div>
</div>
</div>
</div>
<!-- Users View -->
<div v-if="currentView === 'users'">
<div class="search-bar">
<input
type="text"
class="search-input"
v-model="searchKeyword"
placeholder="搜索手机号..."
@keyup.enter="searchUsers"
>
<button class="search-btn" @click="searchUsers">搜索</button>
</div>
<div class="users-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>手机号</th>
<th>注册时间</th>
<th>最后登录</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.phone }}</td>
<td>{{ formatDate(user.created_at) }}</td>
<td>{{ formatDate(user.last_login_at) }}</td>
<td>
<span :class="['status-badge', user.is_active ? 'status-active' : 'status-inactive']">
{{ user.is_active ? '正常' : '禁用' }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<button @click="prevPage" :disabled="currentPage <= 1">上一页</button>
<span class="page-info">第 {{ currentPage }} / {{ totalPages }} 页</span>
<button @click="nextPage" :disabled="currentPage >= totalPages">下一页</button>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
authenticated: false,
password: '',
errorMessage: '',
adminPassword: '',
currentView: 'users',
stats: {
total_users: 0,
active_users: 0,
total_conversations: 0,
total_messages: 0
},
users: [],
searchKeyword: '',
currentPage: 1,
pageSize: 20,
totalPages: 1,
total: 0
};
},
computed: {
currentViewTitle() {
const titles = {
'dashboard': '数据统计',
'users': '用户管理'
};
return titles[this.currentView] || '后台管理';
}
},
mounted() {
const savedPassword = sessionStorage.getItem('admin_password');
if (savedPassword) {
this.adminPassword = savedPassword;
this.authenticated = true;
this.loadData();
}
},
methods: {
async login() {
if (!this.password) {
this.errorMessage = '请输入密码';
return;
}
try {
const response = await fetch(`/api/admin/verify?password=${this.password}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
this.adminPassword = this.password;
sessionStorage.setItem('admin_password', this.password);
this.authenticated = true;
this.errorMessage = '';
this.loadData();
} else {
this.errorMessage = '密码错误';
}
} catch (error) {
console.error('登录失败:', error);
this.errorMessage = '登录失败,请稍后重试';
}
},
logout() {
sessionStorage.removeItem('admin_password');
this.authenticated = false;
this.adminPassword = '';
this.password = '';
},
async loadData() {
await Promise.all([
this.loadStats(),
this.loadUsers()
]);
},
async loadStats() {
try {
const response = await fetch(`/api/admin/stats?password=${this.adminPassword}`);
const data = await response.json();
if (data.success) {
this.stats = data.data;
}
} catch (error) {
console.error('加载统计数据失败:', error);
}
},
async loadUsers() {
try {
let url = `/api/admin/users?password=${this.adminPassword}&page=${this.currentPage}&page_size=${this.pageSize}`;
if (this.searchKeyword) {
url += `&search=${encodeURIComponent(this.searchKeyword)}`;
}
const response = await fetch(url);
const data = await response.json();
if (data.success) {
this.users = data.data.users;
this.total = data.data.total;
this.totalPages = data.data.total_pages;
}
} catch (error) {
console.error('加载用户列表失败:', error);
}
},
searchUsers() {
this.currentPage = 1;
this.loadUsers();
},
prevPage() {
if (this.currentPage > 1) {
this.currentPage--;
this.loadUsers();
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.loadUsers();
}
},
formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString + 'Z');
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
}
}).mount('#app');
</script>
</body>
</html>