stock-ai-agent/frontend/admin.html
2026-02-04 21:41:59 +08:00

619 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>后台管理 - Tradus</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.admin-page {
min-height: 100vh;
background: var(--bg-primary);
display: flex;
}
.admin-sidebar {
width: 240px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
padding: 20px 0;
position: fixed;
height: 100vh;
overflow-y: auto;
}
.admin-logo {
padding: 0 20px 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 20px;
}
.admin-logo h2 {
font-size: 18px;
font-weight: 300;
color: var(--text-primary);
}
.admin-nav {
list-style: none;
padding: 0;
margin: 0;
}
.admin-nav-item {
padding: 12px 20px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.admin-nav-item:hover {
background: var(--bg-primary);
color: var(--text-primary);
}
.admin-nav-item.active {
background: var(--bg-primary);
color: var(--accent);
border-left-color: var(--accent);
}
.admin-main {
margin-left: 240px;
flex: 1;
padding: 20px;
min-width: 800px; /* 设置最小宽度 */
}
.admin-container {
max-width: 1400px;
margin: 0 auto;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}
.admin-title {
font-size: 24px;
font-weight: 300;
color: var(--text-primary);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 30px;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 4px;
padding: 16px;
}
.stat-label {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 6px;
}
.stat-value {
font-size: 24px;
font-weight: 300;
color: var(--accent);
}
.section-title {
font-size: 18px;
font-weight: 300;
color: var(--text-primary);
margin-bottom: 20px;
}
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.search-input {
flex: 1;
padding: 10px 14px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 2px;
color: var(--text-primary);
font-size: 14px;
}
.search-input:focus {
outline: none;
border-color: var(--accent);
}
.search-btn {
padding: 10px 20px;
background: var(--accent);
border: none;
border-radius: 2px;
color: var(--bg-primary);
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.search-btn:hover {
box-shadow: 0 0 16px var(--accent-dim);
}
.logout-btn {
padding: 8px 16px;
background: transparent;
border: 1px solid var(--border-bright);
border-radius: 2px;
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap; /* 防止文字换行 */
}
.logout-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.users-table {
width: 100%;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
}
.users-table table {
width: 100%;
border-collapse: collapse;
}
.users-table th {
background: var(--bg-primary);
padding: 12px 16px;
text-align: left;
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
border-bottom: 1px solid var(--border);
}
.users-table td {
padding: 12px 16px;
font-size: 14px;
color: var(--text-primary);
border-bottom: 1px solid var(--border);
}
.users-table tr:last-child td {
border-bottom: none;
}
.users-table tr:hover {
background: var(--bg-primary);
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 2px;
font-size: 12px;
}
.status-active {
background: rgba(0, 255, 128, 0.1);
color: #00ff80;
border: 1px solid rgba(0, 255, 128, 0.3);
}
.status-inactive {
background: rgba(255, 0, 64, 0.1);
color: #ff0040;
border: 1px solid rgba(255, 0, 64, 0.3);
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
margin-top: 20px;
}
.pagination button {
padding: 8px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-bright);
border-radius: 2px;
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.pagination button:hover:not(:disabled) {
border-color: var(--accent);
color: var(--accent);
}
.pagination button:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.pagination .page-info {
color: var(--text-secondary);
font-size: 14px;
}
.login-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.login-box {
width: 100%;
max-width: 400px;
padding: 40px;
background: var(--bg-secondary);
border: 1px solid var(--border-bright);
border-radius: 4px;
}
.login-box h2 {
font-size: 24px;
font-weight: 300;
color: var(--text-primary);
margin-bottom: 24px;
text-align: center;
}
.login-box input {
width: 100%;
padding: 12px 16px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 2px;
color: var(--text-primary);
font-size: 14px;
margin-bottom: 16px;
}
.login-box input:focus {
outline: none;
border-color: var(--accent);
}
.login-box button {
width: 100%;
padding: 14px;
background: var(--accent);
border: none;
border-radius: 2px;
color: var(--bg-primary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.login-box button:hover {
box-shadow: 0 0 16px var(--accent-dim);
}
.error-message {
padding: 12px;
background: rgba(255, 0, 64, 0.1);
border: 1px solid rgba(255, 0, 64, 0.3);
border-radius: 2px;
color: #ff0040;
font-size: 13px;
text-align: center;
margin-bottom: 16px;
}
</style>
</head>
<body>
<div id="app">
<!-- 登录遮罩 -->
<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>
<!-- 管理页面 -->
<div v-if="authenticated" class="admin-page">
<!-- 侧边栏 -->
<div class="admin-sidebar">
<div class="admin-logo">
<h2>Tradus 管理后台</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>
<!-- 主内容区 -->
<div class="admin-main">
<div class="admin-container">
<div class="admin-header">
<h1 class="admin-title">{{ currentViewTitle }}</h1>
<button class="logout-btn" @click="logout">退出</button>
</div>
<!-- 数据统计视图 -->
<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>
<!-- 用户管理视图 -->
<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>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
authenticated: false,
password: '',
errorMessage: '',
adminPassword: '',
currentView: 'users', // 当前视图dashboard 或 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 '-';
// 直接解析 ISO 格式的时间字符串,不做时区转换
// 假设服务器返回的是 UTC 时间,我们将其视为本地时间显示
const date = new Date(dateString + 'Z'); // 添加 Z 表示 UTC
// 获取各个时间部分
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>