feat: 添加 cloudflare_pages、mysql_d1_backup_scheduler,更新 start_system、ensemble_models 及配置文件
This commit is contained in:
1 parent
6184c34a3a
commit
62110554d1
20 files changed
+1438
-19
No files matched your search
@@ -0,0 +1,200 @@
|
||||
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: '',
|
||||
},
|
||||
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) {
|
||||
if (!timestamp) return '-';
|
||||
return String(timestamp).slice(0, 19);
|
||||
},
|
||||
|
||||
formatPercent(value) {
|
||||
if (value === null || value === undefined) return '-';
|
||||
return `${(Number(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';
|
||||
case 'in_progress': return 'text-warning';
|
||||
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';
|
||||
},
|
||||
|
||||
async fetchAPI(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
},
|
||||
|
||||
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,251 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BTC 预测交易看板 (Pages + D1)</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="/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 预测交易看板 (Pages)
|
||||
</a>
|
||||
<span class="navbar-text text-light">
|
||||
<i class="bi bi-clock"></i> {{ serverTime }}
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<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>
|
||||
|
||||
<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 small">{{ currentTrade.strategy || 'Ensemble' }}</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>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="trade in tradeHistory.data" :key="trade.id">
|
||||
<td>{{ formatTime(trade.timestamp) }}</td>
|
||||
<td class="text-truncate" style="max-width: 200px;">{{ trade.market_title }}</td>
|
||||
<td><span class="badge bg-secondary">{{ trade.strategy || 'Ensemble' }}</span></td>
|
||||
<td :class="getPredictionClass(trade.prediction)">{{ trade.prediction }}</td>
|
||||
<td>{{ formatPercent(trade.probability) }}</td>
|
||||
<td>{{ trade.price ?? '-' }}</td>
|
||||
<td :class="getStatusClass(trade.status)">{{ getStatusText(trade.status) }}</td>
|
||||
</tr>
|
||||
<tr v-if="!tradeHistory.data?.length"><td colspan="7" 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>
|
||||
|
||||
<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">
|
||||
<div class="col-md-3"><div class="card h-100 border-primary shadow-sm"><div class="card-header bg-primary text-white text-center">集成策略</div><div class="card-body text-center" v-if="monthlyStats.Ensemble"><h2 class="display-6 text-primary fw-bold">{{ monthlyStats.Ensemble.accuracy }}%</h2><div class="text-muted mb-2">准确率</div><div class="small text-muted">{{ monthlyStats.Ensemble.correct }}/{{ monthlyStats.Ensemble.total }}</div></div><div v-else class="text-center text-muted py-3">暂无数据</div></div></div>
|
||||
<div class="col-md-3"><div class="card h-100 border-secondary"><div class="card-header bg-secondary text-white text-center">DLinear</div><div class="card-body text-center" v-if="monthlyStats.DLinear"><h3 class="text-dark">{{ monthlyStats.DLinear.accuracy }}%</h3><div class="small text-muted">{{ monthlyStats.DLinear.correct }}/{{ monthlyStats.DLinear.total }}</div></div><div v-else class="text-center text-muted py-3">暂无数据</div></div></div>
|
||||
<div class="col-md-3"><div class="card h-100 border-secondary"><div class="card-header bg-secondary text-white text-center">N-BEATS</div><div class="card-body text-center" v-if="monthlyStats['N-BEATS']"><h3 class="text-dark">{{ monthlyStats['N-BEATS'].accuracy }}%</h3><div class="small text-muted">{{ monthlyStats['N-BEATS'].correct }}/{{ monthlyStats['N-BEATS'].total }}</div></div><div v-else class="text-center text-muted py-3">暂无数据</div></div></div>
|
||||
<div class="col-md-3"><div class="card h-100 border-secondary"><div class="card-header bg-secondary text-white text-center">TTM-R2</div><div class="card-body text-center" v-if="monthlyStats['TTM-R2']"><h3 class="text-dark">{{ monthlyStats['TTM-R2'].accuracy }}%</h3><div class="small text-muted">{{ monthlyStats['TTM-R2'].correct }}/{{ monthlyStats['TTM-R2'].total }}</div></div><div v-else class="text-center text-muted py-3">暂无数据</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 text-center">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="text-start ps-4">日期</th>
|
||||
<th>集成策略</th>
|
||||
<th>DLinear</th>
|
||||
<th>N-BEATS</th>
|
||||
<th>TTM-R2</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="day in dailyStats.data" :key="day.date">
|
||||
<td class="text-start ps-4">{{ day.date }}</td>
|
||||
<td><span v-if="day.Ensemble" :class="getAccuracyClass(day.Ensemble.accuracy)">{{ day.Ensemble.accuracy }}%</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['N-BEATS']" :class="getAccuracyClass(day['N-BEATS'].accuracy)">{{ day['N-BEATS'].accuracy }}%</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-xl 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 text-center align-middle">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>开盘</th>
|
||||
<th>收盘</th>
|
||||
<th>实际</th>
|
||||
<th class="table-primary">集成策略</th>
|
||||
<th>DLinear</th>
|
||||
<th>N-BEATS</th>
|
||||
<th>TTM-R2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="hour in hourlyModal.data" :key="hour.hour">
|
||||
<td>{{ hour.timestamp_cst ? hour.timestamp_cst.split(' ')[1] : (hour.hour + ':00') }}</td>
|
||||
<td>{{ hour.open_price ?? '-' }}</td>
|
||||
<td>{{ hour.close_price ?? '-' }}</td>
|
||||
<td :class="hour.actual_result === 1 ? 'text-success fw-bold' : (hour.actual_result === 0 ? 'text-danger fw-bold' : 'text-muted')">{{ hour.actual_result === 1 ? 'UP' : (hour.actual_result === 0 ? 'DOWN' : '-') }}</td>
|
||||
<td class="table-primary bg-opacity-10"><span v-if="hour.models?.Ensemble" :class="hour.models.Ensemble.is_correct ? 'text-success' : 'text-danger'">{{ hour.models.Ensemble.prediction === 1 ? 'UP' : 'DOWN' }}</span></td>
|
||||
<td><span v-if="hour.models?.DLinear" :class="hour.models.DLinear.is_correct ? 'text-success' : 'text-danger'">{{ hour.models.DLinear.prediction === 1 ? 'UP' : 'DOWN' }}</span></td>
|
||||
<td><span v-if="hour.models?.['N-BEATS']" :class="hour.models['N-BEATS'].is_correct ? 'text-success' : 'text-danger'">{{ hour.models['N-BEATS'].prediction === 1 ? 'UP' : 'DOWN' }}</span></td>
|
||||
<td><span v-if="hour.models?.['TTM-R2']" :class="hour.models['TTM-R2'].is_correct ? 'text-success' : 'text-danger'">{{ hour.models['TTM-R2'].prediction === 1 ? 'UP' : 'DOWN' }}</span></td>
|
||||
</tr>
|
||||
<tr v-if="!hourlyModal.data?.length"><td colspan="8" 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>
|
||||
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">加载中...</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.15/dist/vue.global.prod.js"></script>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,118 @@
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user