feat(frontend): 添加交易看板,统一时区为CST
refactor(db): 移除所有SQLite相关代码,迁移至MySQL
This commit is contained in:
1 parent
5e0d6b18ef
commit
55453d3739
12 files changed
+2116
-501
No files matched your search
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* BTC 预测交易看板 - Vue 3 应用
|
||||
*/
|
||||
const { createApp } = Vue;
|
||||
|
||||
const app = createApp({
|
||||
data() {
|
||||
return {
|
||||
// 状态
|
||||
loading: false,
|
||||
activeTab: 'trades',
|
||||
serverTime: '',
|
||||
|
||||
// 当前交易
|
||||
currentTrade: null,
|
||||
|
||||
// 交易历史
|
||||
tradeHistory: {
|
||||
data: [],
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
total_pages: 0
|
||||
},
|
||||
tradeFilter: {
|
||||
month: '',
|
||||
day: ''
|
||||
},
|
||||
|
||||
// 准确率
|
||||
accuracyFilter: {
|
||||
month: this.getCurrentMonth()
|
||||
},
|
||||
monthlyStats: {},
|
||||
dailyStats: {
|
||||
data: [],
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
total_pages: 0
|
||||
},
|
||||
|
||||
// 弹窗数据
|
||||
hourlyModal: {
|
||||
date: '',
|
||||
data: []
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// 初始化月份筛选
|
||||
this.accuracyFilter.month = this.getCurrentMonth();
|
||||
|
||||
// 加载数据
|
||||
this.loadCurrentTrade();
|
||||
this.loadTradeHistory(1);
|
||||
|
||||
// 每分钟更新时间
|
||||
this.updateServerTime();
|
||||
setInterval(() => this.updateServerTime(), 60000);
|
||||
},
|
||||
|
||||
methods: {
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
getCurrentMonth() {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
updateServerTime() {
|
||||
const now = new Date();
|
||||
this.serverTime = now.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
formatTime(timestamp, timestamp_cst) {
|
||||
// 优先使用后端返回的 CST 时间
|
||||
if (timestamp_cst) {
|
||||
return timestamp_cst;
|
||||
}
|
||||
if (!timestamp) return '-';
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
formatPercent(value) {
|
||||
if (value === null || value === undefined) return '-';
|
||||
return `${(value * 100).toFixed(1)}%`;
|
||||
},
|
||||
|
||||
getPredictionClass(prediction) {
|
||||
if (prediction === 'UP') return 'text-success fw-bold';
|
||||
if (prediction === 'DOWN') return 'text-danger fw-bold';
|
||||
return '';
|
||||
},
|
||||
|
||||
getStatusClass(status) {
|
||||
switch (status) {
|
||||
case 'success': return 'text-success';
|
||||
case 'test': return 'text-info';
|
||||
case 'failure': return 'text-danger';
|
||||
default: return 'text-muted';
|
||||
}
|
||||
},
|
||||
|
||||
getStatusText(status) {
|
||||
switch (status) {
|
||||
case 'success': return '成功';
|
||||
case 'test': return '测试';
|
||||
case 'failure': return '失败';
|
||||
case 'in_progress': return '进行中';
|
||||
default: return status || '-';
|
||||
}
|
||||
},
|
||||
|
||||
getAccuracyClass(accuracy) {
|
||||
if (accuracy >= 60) return 'text-success fw-bold';
|
||||
if (accuracy >= 50) return 'text-warning';
|
||||
return 'text-danger';
|
||||
},
|
||||
|
||||
// ==================== API 调用 ====================
|
||||
|
||||
async fetchAPI(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
console.error('API Error:', data);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Fetch Error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
async loadCurrentTrade() {
|
||||
const data = await this.fetchAPI('/api/trades/current');
|
||||
if (data.success) {
|
||||
this.currentTrade = data.data;
|
||||
}
|
||||
},
|
||||
|
||||
async loadTradeHistory(page) {
|
||||
if (page < 1) return;
|
||||
|
||||
this.loading = true;
|
||||
|
||||
let url = `/api/trades/history?page=${page}&limit=${this.tradeHistory.limit}`;
|
||||
if (this.tradeFilter.month) {
|
||||
url += `&month=${this.tradeFilter.month}`;
|
||||
}
|
||||
if (this.tradeFilter.day) {
|
||||
url += `&day=${this.tradeFilter.day}`;
|
||||
}
|
||||
|
||||
const data = await this.fetchAPI(url);
|
||||
if (data.success) {
|
||||
this.tradeHistory = {
|
||||
data: data.data,
|
||||
page: data.page,
|
||||
limit: data.limit,
|
||||
total: data.total,
|
||||
total_pages: data.total_pages
|
||||
};
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
clearTradeFilter() {
|
||||
this.tradeFilter.month = '';
|
||||
this.tradeFilter.day = '';
|
||||
this.loadTradeHistory(1);
|
||||
},
|
||||
|
||||
async loadAccuracyData() {
|
||||
this.loading = true;
|
||||
|
||||
// 加载月度统计
|
||||
const monthlyData = await this.fetchAPI(
|
||||
`/api/accuracy/monthly?month=${this.accuracyFilter.month}`
|
||||
);
|
||||
if (monthlyData.success) {
|
||||
this.monthlyStats = monthlyData.data || {};
|
||||
}
|
||||
|
||||
// 加载每日统计
|
||||
await this.loadDailyStats(1);
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
async loadDailyStats(page) {
|
||||
if (page < 1) return;
|
||||
|
||||
const data = await this.fetchAPI(
|
||||
`/api/accuracy/daily?month=${this.accuracyFilter.month}&page=${page}&limit=10`
|
||||
);
|
||||
|
||||
if (data.success) {
|
||||
this.dailyStats = {
|
||||
data: data.data,
|
||||
page: data.page,
|
||||
limit: data.limit,
|
||||
total: data.total,
|
||||
total_pages: data.total_pages
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async showHourlyDetails(date) {
|
||||
this.loading = true;
|
||||
this.hourlyModal.date = date;
|
||||
|
||||
const data = await this.fetchAPI(`/api/accuracy/hourly/${date}`);
|
||||
|
||||
if (data.success) {
|
||||
this.hourlyModal.data = data.data || [];
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
|
||||
// 显示弹窗
|
||||
const modal = new bootstrap.Modal(document.getElementById('hourlyModal'));
|
||||
modal.show();
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
activeTab(newTab) {
|
||||
if (newTab === 'accuracy' && !this.dailyStats.data.length) {
|
||||
this.loadAccuracyData();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.mount('#app');
|
||||
@@ -0,0 +1,414 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BTC 预测交易看板</title>
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<!-- Custom CSS -->
|
||||
<link href="/static/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="#">
|
||||
<i class="bi bi-currency-bitcoin"></i> BTC 预测交易看板
|
||||
</a>
|
||||
<span class="navbar-text text-light">
|
||||
<i class="bi bi-clock"></i> {{ serverTime }}
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主容器 -->
|
||||
<div class="container mt-4">
|
||||
<!-- Tab 导航 -->
|
||||
<ul class="nav nav-tabs mb-4" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" :class="{ active: activeTab === 'trades' }"
|
||||
@click="activeTab = 'trades'">
|
||||
<i class="bi bi-graph-up"></i> 交易详情
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" :class="{ active: activeTab === 'accuracy' }"
|
||||
@click="activeTab = 'accuracy'">
|
||||
<i class="bi bi-bullseye"></i> 模型准确率
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 交易详情 Tab -->
|
||||
<div v-show="activeTab === 'trades'">
|
||||
<!-- 当前交易卡片 -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<i class="bi bi-lightning-charge"></i> 当前小时交易
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-if="currentTrade" class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">市场</div>
|
||||
<div class="stat-value small">{{ currentTrade.market_title || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">预测方向</div>
|
||||
<div class="stat-value" :class="getPredictionClass(currentTrade.prediction)">
|
||||
{{ currentTrade.prediction || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">概率</div>
|
||||
<div class="stat-value">{{ formatPercent(currentTrade.probability) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">价格</div>
|
||||
<div class="stat-value">${{ currentTrade.price?.toFixed(3) || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">状态</div>
|
||||
<div class="stat-value" :class="getStatusClass(currentTrade.status)">
|
||||
{{ getStatusText(currentTrade.status) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center text-muted py-4">
|
||||
<i class="bi bi-inbox"></i> 暂无交易记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史交易 -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-clock-history"></i> 历史交易记录</span>
|
||||
<div class="d-flex gap-2">
|
||||
<input type="month" class="form-control form-control-sm"
|
||||
v-model="tradeFilter.month" @change="loadTradeHistory(1)"
|
||||
style="width: 150px;">
|
||||
<input type="date" class="form-control form-control-sm"
|
||||
v-model="tradeFilter.day" @change="loadTradeHistory(1)"
|
||||
style="width: 150px;">
|
||||
<button class="btn btn-sm btn-outline-secondary" @click="clearTradeFilter">
|
||||
<i class="bi bi-x-circle"></i> 清除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>时间 (北京)</th>
|
||||
<th>市场</th>
|
||||
<th>预测</th>
|
||||
<th>概率</th>
|
||||
<th>价格</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="trade in tradeHistory.data" :key="trade.id">
|
||||
<td>{{ formatTime(trade.timestamp, trade.timestamp_cst) }}</td>
|
||||
<td class="text-truncate" style="max-width: 200px;">
|
||||
{{ trade.market_title }}
|
||||
</td>
|
||||
<td :class="getPredictionClass(trade.prediction)">
|
||||
<i :class="trade.prediction === 'UP' ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
||||
{{ trade.prediction }}
|
||||
</td>
|
||||
<td>{{ formatPercent(trade.probability) }}</td>
|
||||
<td>${{ trade.price?.toFixed(3) }}</td>
|
||||
<td :class="getStatusClass(trade.status)">
|
||||
{{ getStatusText(trade.status) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!tradeHistory.data?.length">
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
暂无数据
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted">共 {{ tradeHistory.total }} 条</span>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item" :class="{ disabled: tradeHistory.page <= 1 }">
|
||||
<a class="page-link" href="#" @click.prevent="loadTradeHistory(tradeHistory.page - 1)">
|
||||
上一页
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">{{ tradeHistory.page }} / {{ tradeHistory.total_pages }}</span>
|
||||
</li>
|
||||
<li class="page-item" :class="{ disabled: tradeHistory.page >= tradeHistory.total_pages }">
|
||||
<a class="page-link" href="#" @click.prevent="loadTradeHistory(tradeHistory.page + 1)">
|
||||
下一页
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型准确率 Tab -->
|
||||
<div v-show="activeTab === 'accuracy'">
|
||||
<!-- 月份筛选 -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<label class="form-label mb-0">选择月份:</label>
|
||||
<input type="month" class="form-control" v-model="accuracyFilter.month"
|
||||
@change="loadAccuracyData" style="width: 180px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当月汇总卡片 -->
|
||||
<div class="row mb-4">
|
||||
<!-- DLinear 卡片 -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100 border-primary">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<i class="bi bi-graph-up-arrow"></i> DLinear 模型
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center" v-if="monthlyStats.DLinear">
|
||||
<div class="col-3">
|
||||
<div class="stat-number">{{ monthlyStats.DLinear.total }}</div>
|
||||
<div class="stat-label">预测次数</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="stat-number text-success">{{ monthlyStats.DLinear.correct }}</div>
|
||||
<div class="stat-label">正确</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="stat-number text-danger">{{ monthlyStats.DLinear.incorrect }}</div>
|
||||
<div class="stat-label">错误</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="stat-number text-primary">{{ monthlyStats.DLinear.accuracy }}%</div>
|
||||
<div class="stat-label">准确率</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center text-muted py-3">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TTM-R2 卡片 -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100 border-success">
|
||||
<div class="card-header bg-success text-white">
|
||||
<i class="bi bi-cpu"></i> TTM-R2 模型
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center" v-if="monthlyStats['TTM-R2']">
|
||||
<div class="col-3">
|
||||
<div class="stat-number">{{ monthlyStats['TTM-R2'].total }}</div>
|
||||
<div class="stat-label">预测次数</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="stat-number text-success">{{ monthlyStats['TTM-R2'].correct }}</div>
|
||||
<div class="stat-label">正确</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="stat-number text-danger">{{ monthlyStats['TTM-R2'].incorrect }}</div>
|
||||
<div class="stat-label">错误</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="stat-number text-primary">{{ monthlyStats['TTM-R2'].accuracy }}%</div>
|
||||
<div class="stat-label">准确率</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center text-muted py-3">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 每日准确率表格 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-calendar3"></i> 每日准确率
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>日期</th>
|
||||
<th>DLinear 预测</th>
|
||||
<th>DLinear 准确率</th>
|
||||
<th>TTM-R2 预测</th>
|
||||
<th>TTM-R2 准确率</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="day in dailyStats.data" :key="day.date">
|
||||
<td>{{ day.date }}</td>
|
||||
<td>
|
||||
<span v-if="day.DLinear">
|
||||
{{ day.DLinear.correct }}/{{ day.DLinear.total }}
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="day.DLinear" :class="getAccuracyClass(day.DLinear.accuracy)">
|
||||
{{ day.DLinear.accuracy }}%
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="day['TTM-R2']">
|
||||
{{ day['TTM-R2'].correct }}/{{ day['TTM-R2'].total }}
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="day['TTM-R2']" :class="getAccuracyClass(day['TTM-R2'].accuracy)">
|
||||
{{ day['TTM-R2'].accuracy }}%
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
@click="showHourlyDetails(day.date)">
|
||||
<i class="bi bi-eye"></i> 查看
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!dailyStats.data?.length">
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
暂无数据
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted">共 {{ dailyStats.total }} 天</span>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item" :class="{ disabled: dailyStats.page <= 1 }">
|
||||
<a class="page-link" href="#" @click.prevent="loadDailyStats(dailyStats.page - 1)">
|
||||
上一页
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">{{ dailyStats.page }} / {{ dailyStats.total_pages }}</span>
|
||||
</li>
|
||||
<li class="page-item" :class="{ disabled: dailyStats.page >= dailyStats.total_pages }">
|
||||
<a class="page-link" href="#" @click.prevent="loadDailyStats(dailyStats.page + 1)">
|
||||
下一页
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 每小时详情弹窗 -->
|
||||
<div class="modal fade" id="hourlyModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-clock"></i> {{ hourlyModal.date }} 每小时预测详情
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>时间 (北京)</th>
|
||||
<th>实际涨跌</th>
|
||||
<th>DLinear</th>
|
||||
<th>TTM-R2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="hour in hourlyModal.data" :key="hour.hour">
|
||||
<td>{{ hour.timestamp_cst || (hour.hour + ':00') }}</td>
|
||||
<td :class="hour.actual_result === 1 ? 'text-success' : 'text-danger'">
|
||||
<i :class="hour.actual_result === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
||||
{{ hour.actual_result === 1 ? 'UP' : 'DOWN' }}
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="hour.models?.DLinear"
|
||||
:class="hour.models.DLinear.is_correct ? 'text-success' : 'text-danger'">
|
||||
<i :class="hour.models.DLinear.prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
||||
{{ hour.models.DLinear.prediction === 1 ? 'UP' : 'DOWN' }}
|
||||
<i :class="hour.models.DLinear.is_correct ? 'bi-check-circle' : 'bi-x-circle'"></i>
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="hour.models?.['TTM-R2']"
|
||||
:class="hour.models['TTM-R2'].is_correct ? 'text-success' : 'text-danger'">
|
||||
<i :class="hour.models['TTM-R2'].prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
||||
{{ hour.models['TTM-R2'].prediction === 1 ? 'UP' : 'DOWN' }}
|
||||
<i :class="hour.models['TTM-R2'].is_correct ? 'bi-check-circle' : 'bi-x-circle'"></i>
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!hourlyModal.data?.length">
|
||||
<td colspan="4" class="text-center text-muted py-4">
|
||||
暂无数据
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading 遮罩 -->
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- Vue 3 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.15/dist/vue.global.prod.js"></script>
|
||||
<!-- App JS -->
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* BTC 预测交易看板 - 自定义样式
|
||||
*/
|
||||
|
||||
/* 全局样式 */
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 导航栏 */
|
||||
.navbar-brand {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.navbar-brand i {
|
||||
color: #f7931a;
|
||||
}
|
||||
|
||||
/* Tab 样式 */
|
||||
.nav-tabs .nav-link {
|
||||
color: #6c757d;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #0d6efd;
|
||||
border-bottom: 3px solid #0d6efd;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover:not(.active) {
|
||||
border-bottom: 3px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
border-radius: 0.5rem 0.5rem 0 0 !important;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stat-card {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #6c757d;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(13, 110, 253, 0.05);
|
||||
}
|
||||
|
||||
/* 涨跌颜色 */
|
||||
.text-success {
|
||||
color: #198754 !important;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.page-item.disabled .page-link {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Loading 遮罩 */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* 弹窗 */
|
||||
.modal-content {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.stat-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-header .d-flex {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 准确率颜色 */
|
||||
.accuracy-high {
|
||||
color: #198754;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.accuracy-medium {
|
||||
color: #fd7e14;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.accuracy-low {
|
||||
color: #dc3545;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 边框颜色增强 */
|
||||
.border-primary {
|
||||
border-color: #0d6efd !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
.border-success {
|
||||
border-color: #198754 !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-outline-primary:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 截断文本 */
|
||||
.text-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 间距调整 */
|
||||
.gap-2 {
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
Reference in new issue
Block a user