654 lines
21 KiB
HTML
654 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>订单指标日汇总 - 系统仪表盘</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
|
<style>
|
|
:root {
|
|
--primary: #2563eb;
|
|
--primary-dark: #1d4ed8;
|
|
--success: #10b981;
|
|
--warning: #f59e0b;
|
|
--danger: #ef4444;
|
|
--bg-primary: #f8fafc;
|
|
--text-primary: #1e293b;
|
|
--text-secondary: #475569;
|
|
--border: #e2e8f0;
|
|
}
|
|
|
|
* {
|
|
font-family: 'Inter', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
}
|
|
|
|
body {
|
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
min-height: 100vh;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.container-main {
|
|
max-width: 1400px;
|
|
padding: 32px 24px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.page-header {
|
|
text-align: center;
|
|
margin-bottom: 32px;
|
|
padding: 28px 0;
|
|
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
color: white;
|
|
}
|
|
|
|
.page-header h1 {
|
|
font-size: 1.8rem;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
}
|
|
|
|
.page-header p {
|
|
color: rgba(255,255,255,0.85);
|
|
margin: 8px 0 0 0;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.query-section {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
margin-bottom: 32px;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.query-controls {
|
|
display: flex;
|
|
gap: 16px;
|
|
align-items: flex-end;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.form-group-inline {
|
|
flex: 0 1 auto;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.form-label {
|
|
font-weight: 500;
|
|
margin-bottom: 8px;
|
|
color: var(--text-primary);
|
|
display: block;
|
|
}
|
|
|
|
.form-control, .form-select {
|
|
padding: 10px 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
font-size: 0.95rem;
|
|
transition: all 150ms ease;
|
|
width: 100%;
|
|
}
|
|
|
|
.form-control:focus, .form-select:focus {
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
outline: none;
|
|
}
|
|
|
|
.btn-query {
|
|
padding: 10px 28px;
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
|
|
.btn-query:hover {
|
|
background: var(--primary-dark);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.btn-refresh {
|
|
padding: 10px 16px;
|
|
background: #e2e8f0;
|
|
color: var(--text-primary);
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
|
|
.btn-refresh:hover {
|
|
background: #cbd5e1;
|
|
}
|
|
|
|
.loading-spinner {
|
|
text-align: center;
|
|
padding: 40px;
|
|
}
|
|
|
|
.spinner {
|
|
border: 4px solid var(--bg-primary);
|
|
border-top: 4px solid var(--primary);
|
|
border-radius: 50%;
|
|
width: 50px;
|
|
height: 50px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 16px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.alert-custom {
|
|
padding: 16px;
|
|
border-radius: 6px;
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
}
|
|
|
|
.alert-error {
|
|
background: #fef2f2;
|
|
color: #7f1d1d;
|
|
border-left: 4px solid var(--danger);
|
|
}
|
|
|
|
.header-info {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 24px;
|
|
padding: 16px;
|
|
background: var(--bg-primary);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.header-date {
|
|
font-size: 1.2rem;
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.header-time {
|
|
font-size: 0.9rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.metrics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.metric-card {
|
|
background: white;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
transition: all 150ms ease;
|
|
}
|
|
|
|
.metric-card:hover {
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.metric-card.large {
|
|
grid-column: span 2;
|
|
min-width: unset;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.metric-card.large {
|
|
grid-column: span 1;
|
|
}
|
|
}
|
|
|
|
.metric-label {
|
|
font-size: 0.85rem;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 2.2rem;
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.metric-card.large .metric-value {
|
|
font-size: 2.8rem;
|
|
}
|
|
|
|
.metric-unit {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
font-weight: 400;
|
|
}
|
|
|
|
.metric-card.success {
|
|
border-color: var(--success);
|
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(16, 185, 129, 0.02) 100%);
|
|
}
|
|
|
|
.metric-card.warning {
|
|
border-color: var(--warning);
|
|
background: linear-gradient(135deg, rgba(245, 158, 11, 0.05) 0%, rgba(245, 158, 11, 0.02) 100%);
|
|
}
|
|
|
|
.metric-card.danger {
|
|
border-color: var(--danger);
|
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.05) 0%, rgba(239, 68, 68, 0.02) 100%);
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
margin: 28px 0 16px 0;
|
|
color: var(--text-primary);
|
|
border-bottom: 2px solid var(--primary);
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
.table-container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.table-custom {
|
|
margin: 0;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.table-custom thead th {
|
|
background: var(--bg-primary);
|
|
border-color: var(--border);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
padding: 16px;
|
|
}
|
|
|
|
.table-custom tbody td {
|
|
padding: 16px;
|
|
border-color: var(--border);
|
|
}
|
|
|
|
.table-custom tbody tr:hover {
|
|
background: var(--bg-primary);
|
|
}
|
|
|
|
.details-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 12px;
|
|
}
|
|
|
|
.detail-item {
|
|
background: white;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 12px;
|
|
}
|
|
|
|
.detail-label {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.detail-value {
|
|
font-size: 1.3rem;
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.no-data {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.no-data-icon {
|
|
font-size: 3rem;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.query-controls {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.form-group-inline {
|
|
width: 100%;
|
|
}
|
|
|
|
.btn-query {
|
|
width: 100%;
|
|
}
|
|
|
|
.header-info {
|
|
flex-direction: column;
|
|
text-align: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.metrics-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.metric-card.large {
|
|
grid-column: span 1;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container-main">
|
|
<div class="page-header">
|
|
<h1>📊 订单处理指标日汇总</h1>
|
|
<p>一键查询完整的日统计指标</p>
|
|
</div>
|
|
|
|
<!-- 查询控制区 -->
|
|
<div class="query-section">
|
|
<div class="query-controls">
|
|
<div class="form-group-inline">
|
|
<label class="form-label">环境选择</label>
|
|
<select id="environment" class="form-select">
|
|
<option value="local">本地环境</option>
|
|
<option value="test" selected>测试环境</option>
|
|
<option value="production">生产环境</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group-inline">
|
|
<label class="form-label">查询日期</label>
|
|
<input type="date" id="dashboardDate" class="form-control">
|
|
</div>
|
|
|
|
<div class="form-group-inline">
|
|
<button class="btn-query" onclick="queryDailyDashboard()">📊 查询汇总</button>
|
|
</div>
|
|
|
|
<div class="form-group-inline">
|
|
<button class="btn-refresh" onclick="refreshCurrentData()" style="display:none;" id="refreshBtn">🔄 刷新</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 结果展示区 -->
|
|
<div id="resultContainer"></div>
|
|
</div>
|
|
|
|
<script>
|
|
// 配置环境
|
|
const environments = {
|
|
local: 'http://localhost:5002',
|
|
test: 'http://172.232.21.79:5002',
|
|
production: 'https://lr.tooexp.com'
|
|
};
|
|
|
|
function getBaseUrl() {
|
|
const env = document.getElementById('environment').value;
|
|
return environments[env] || environments.test;
|
|
}
|
|
|
|
let currentDate = null;
|
|
|
|
|
|
// 初始化日期
|
|
function initDate() {
|
|
const today = new Date();
|
|
const year = today.getFullYear();
|
|
const month = String(today.getMonth() + 1).padStart(2, '0');
|
|
const day = String(today.getDate()).padStart(2, '0');
|
|
document.getElementById('dashboardDate').value = `${year}-${month}-${day}`;
|
|
}
|
|
|
|
// 显示加载状态
|
|
function showLoading() {
|
|
console.log('显示加载状态');
|
|
document.getElementById('resultContainer').innerHTML = `
|
|
<div class="loading-spinner">
|
|
<div class="spinner"></div>
|
|
<p>正在加载数据...</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 显示错误
|
|
function showError(message) {
|
|
console.error('显示错误信息:', message);
|
|
document.getElementById('resultContainer').innerHTML = `
|
|
<div class="alert-custom alert-error">
|
|
<span>❌</span>
|
|
<div><strong>错误</strong><br/>${message}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// JSONP 请求
|
|
function jsonpRequest(url, callbackName) {
|
|
$.ajax({
|
|
url: url,
|
|
type: 'GET',
|
|
dataType: 'jsonp',
|
|
jsonp: 'callback',
|
|
jsonpCallback: callbackName,
|
|
cache: false,
|
|
timeout: 15000,
|
|
success: function(response) {
|
|
console.log('AJAX success - 响应接收:', response);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX 失败:', status, error);
|
|
delete window[callbackName];
|
|
showError('请求失败: ' + (error || status));
|
|
}
|
|
});
|
|
}
|
|
|
|
// 查询日汇总
|
|
function queryDailyDashboard() {
|
|
const date = document.getElementById('dashboardDate').value;
|
|
if (!date) {
|
|
showError('请选择日期');
|
|
return;
|
|
}
|
|
|
|
currentDate = date;
|
|
showLoading();
|
|
|
|
const baseUrl = getBaseUrl();
|
|
const url = baseUrl + '/api/metrics/daily-dashboard?date=' + date;
|
|
|
|
console.log('查询仪表盘数据', { date, env: document.getElementById('environment').value, url: url });
|
|
|
|
// 使用JSONP方式避免CORS问题
|
|
const callbackName = 'dashboardCallback_' + Date.now();
|
|
window[callbackName] = function(response) {
|
|
console.log('收到响应:', response);
|
|
handleDashboardResponse(response);
|
|
delete window[callbackName];
|
|
};
|
|
|
|
$.ajax({
|
|
url: url,
|
|
type: 'GET',
|
|
dataType: 'jsonp',
|
|
jsonp: 'callback',
|
|
jsonpCallback: callbackName,
|
|
cache: false,
|
|
timeout: 60000,
|
|
success: function(response) {
|
|
// JSONP成功处理已在回调中进行
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('请求失败:', status, error);
|
|
showError('请求失败: ' + (error || status));
|
|
delete window[callbackName];
|
|
}
|
|
});
|
|
}
|
|
|
|
// 处理响应
|
|
function handleDashboardResponse(response) {
|
|
if (response && response.success) {
|
|
displayDashboard(response.data);
|
|
} else {
|
|
showError(response?.error || '查询失败');
|
|
}
|
|
}
|
|
|
|
// 生成颜色类
|
|
function getMetricClass(rate) {
|
|
if (typeof rate === 'string') {
|
|
const percent = parseFloat(rate);
|
|
if (percent >= 95) return 'success';
|
|
if (percent >= 85) return 'warning';
|
|
return 'danger';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
// 显示仪表盘
|
|
function displayDashboard(data) {
|
|
const date = data.date;
|
|
const summary = data.summary || {};
|
|
const breakdown = data.breakdown || {};
|
|
const details = data.details || {};
|
|
|
|
let html = `
|
|
<div class="header-info">
|
|
<div>
|
|
<span class="header-date">📅 ${date}</span>
|
|
</div>
|
|
<div class="header-time">
|
|
更新于: ${new Date().toLocaleString('zh-CN')}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 核心指标 -->
|
|
<h2 class="section-title">🎯 核心指标</h2>
|
|
<div class="metrics-grid">
|
|
<div class="metric-card large">
|
|
<div class="metric-label">当天新增换单数</div>
|
|
<div class="metric-value">${summary.dailyNewReplaceCount || 0}</div>
|
|
</div>
|
|
<div class="metric-card large">
|
|
<div class="metric-label">当天应该换单数</div>
|
|
<div class="metric-value">${summary.dailyShouldReplaceCount || 0}</div>
|
|
</div>
|
|
<div class="metric-card large">
|
|
<div class="metric-label">当日换单完成数</div>
|
|
<div class="metric-value">${summary.dailySuccessCount || 0}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 完成率 -->
|
|
<h2 class="section-title">📈 完成率指标</h2>
|
|
<div class="metrics-grid">
|
|
<div class="metric-card ${getMetricClass(summary.dailyCompletionRate)}">
|
|
<div class="metric-label">当日完成率</div>
|
|
<div class="metric-value">${summary.dailyCompletionRate || '0%'}</div>
|
|
</div>
|
|
<div class="metric-card ${getMetricClass(summary.rate24Hour)}">
|
|
<div class="metric-label">24小时完成率</div>
|
|
<div class="metric-value">${summary.rate24Hour || '0%'}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 分时段统计 -->
|
|
<h2 class="section-title">⏰ 分时段统计</h2>
|
|
<div class="table-container">
|
|
<table class="table table-custom">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 20%;">时段</th>
|
|
<th style="width: 20%;">到仓数</th>
|
|
<th style="width: 20%;">完成数</th>
|
|
<th style="width: 40%;">完成率</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>🌅 16点前</td>
|
|
<td>${breakdown.beforeNoon?.arrived || 0}</td>
|
|
<td>${breakdown.beforeNoon?.passed || 0}</td>
|
|
<td><span style="background: ${getMetricClass(breakdown.beforeNoon?.rate) === 'success' ? '#d1fae5' : getMetricClass(breakdown.beforeNoon?.rate) === 'warning' ? '#fef3c7' : '#fee2e2'}; padding: 4px 8px; border-radius: 4px;">${breakdown.beforeNoon?.rate || '0%'}</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>🌆 16点后</td>
|
|
<td>${breakdown.afternoon?.arrived || 0}</td>
|
|
<td>${breakdown.afternoon?.passed || 0}</td>
|
|
<td><span style="background: ${getMetricClass(breakdown.afternoon?.rate) === 'success' ? '#d1fae5' : getMetricClass(breakdown.afternoon?.rate) === 'warning' ? '#fef3c7' : '#fee2e2'}; padding: 4px 8px; border-radius: 4px;">${breakdown.afternoon?.rate || '0%'}</span></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 详细指标 -->
|
|
<h2 class="section-title">📊 详细指标</h2>
|
|
<div class="details-grid">
|
|
<div class="detail-item">
|
|
<div class="detail-label">当日STOP数</div>
|
|
<div class="detail-value">${details.dailyStop || 0}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="detail-label">当日标签推送</div>
|
|
<div class="detail-value">${details.dailyLabelPush || 0}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="detail-label">当日扫描数</div>
|
|
<div class="detail-value">${details.dailyScanCount || 0}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="detail-label">当日失败数</div>
|
|
<div class="detail-value">${details.dailyFailure || 0}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="detail-label">累计未完成</div>
|
|
<div class="detail-value">${details.cumulativeTotal || 0}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('resultContainer').innerHTML = html;
|
|
document.getElementById('refreshBtn').style.display = 'inline-block';
|
|
}
|
|
|
|
// 刷新当前数据
|
|
function refreshCurrentData() {
|
|
if (currentDate) {
|
|
queryDailyDashboard();
|
|
}
|
|
}
|
|
|
|
// 页面加载
|
|
$(document).ready(function() {
|
|
initDate();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|