refactor(Core): optimize admin panel and refactor
This commit is contained in:
@@ -11,165 +11,198 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof Chart === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const salesChartContext = document.getElementById('sodinoSalesChart');
|
||||
const discountChartContext = document.getElementById('sodinoDiscountChart');
|
||||
const ruleChartContext = document.getElementById('sodinoRuleChart');
|
||||
|
||||
if (salesChartContext && dashboardData.salesChart) {
|
||||
new Chart(salesChartContext.getContext('2d'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: dashboardData.salesChart.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: dashboardData.translations.afterApplying,
|
||||
data: dashboardData.salesChart.after,
|
||||
borderColor: '#0ea5e9',
|
||||
backgroundColor: 'rgba(14, 165, 233, 0.15)',
|
||||
fill: true,
|
||||
tension: 0.35,
|
||||
pointRadius: 3,
|
||||
},
|
||||
{
|
||||
label: dashboardData.translations.beforeApplying,
|
||||
data: dashboardData.salesChart.before,
|
||||
borderColor: '#ef4444',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.15)',
|
||||
fill: true,
|
||||
tension: 0.35,
|
||||
pointRadius: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: '#334155',
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
color: '#475569',
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(148, 163, 184, 0.15)',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
color: '#475569',
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(148, 163, 184, 0.15)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
function toNumbers(values) {
|
||||
return (values || []).map(function (value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (discountChartContext && dashboardData.summary) {
|
||||
new Chart(discountChartContext.getContext('2d'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: [dashboardData.translations.totalDiscount, dashboardData.translations.totalRevenue],
|
||||
datasets: [
|
||||
{
|
||||
label: dashboardData.translations.discountEffect,
|
||||
data: [dashboardData.summary.total_discount, dashboardData.summary.total_revenue],
|
||||
backgroundColor: ['#fbbf24', '#0ea5e9'],
|
||||
borderRadius: 999,
|
||||
borderSkipped: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
color: '#475569',
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
color: '#475569',
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(148, 163, 184, 0.15)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
function fitCanvas(canvas) {
|
||||
const ratio = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.max(1, Math.floor(rect.width * ratio));
|
||||
canvas.height = Math.max(1, Math.floor(rect.height * ratio));
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
||||
return { ctx, width: rect.width, height: rect.height };
|
||||
}
|
||||
|
||||
function drawGrid(ctx, area, lines) {
|
||||
ctx.strokeStyle = 'rgba(148, 163, 184, 0.22)';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
for (let i = 0; i <= lines; i++) {
|
||||
const y = area.top + (area.height / lines) * i;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(area.left, y);
|
||||
ctx.lineTo(area.left + area.width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function drawLineChart(canvas, series) {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fitted = fitCanvas(canvas);
|
||||
const ctx = fitted.ctx;
|
||||
const width = fitted.width;
|
||||
const height = fitted.height;
|
||||
const area = { left: 18, top: 18, width: width - 36, height: height - 42 };
|
||||
const allValues = series.reduce(function (values, item) {
|
||||
return values.concat(item.values);
|
||||
}, []);
|
||||
const max = Math.max(1, ...allValues);
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
drawGrid(ctx, area, 4);
|
||||
|
||||
series.forEach(function (item) {
|
||||
const values = item.values;
|
||||
if (!values.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.strokeStyle = item.color;
|
||||
ctx.fillStyle = item.fill;
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.beginPath();
|
||||
|
||||
values.forEach(function (value, index) {
|
||||
const x = area.left + (values.length === 1 ? area.width / 2 : (area.width / (values.length - 1)) * index);
|
||||
const y = area.top + area.height - ((value / max) * area.height);
|
||||
if (index === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
const lastX = area.left + area.width;
|
||||
ctx.lineTo(lastX, area.top + area.height);
|
||||
ctx.lineTo(area.left, area.top + area.height);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
if (ruleChartContext && dashboardData.rulePerformance) {
|
||||
new Chart(ruleChartContext.getContext('2d'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: dashboardData.rulePerformance.names,
|
||||
datasets: [
|
||||
{
|
||||
label: dashboardData.translations.ruleRevenue,
|
||||
data: dashboardData.rulePerformance.revenue,
|
||||
backgroundColor: '#0ea5e9',
|
||||
borderRadius: 999,
|
||||
},
|
||||
{
|
||||
label: dashboardData.translations.ruleDiscount,
|
||||
data: dashboardData.rulePerformance.discount,
|
||||
backgroundColor: '#f59e0b',
|
||||
borderRadius: 999,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
color: '#475569',
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
color: '#475569',
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(148, 163, 184, 0.15)',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
color: '#475569',
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
function drawVerticalBars(canvas, labels, values, colors) {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fitted = fitCanvas(canvas);
|
||||
const ctx = fitted.ctx;
|
||||
const width = fitted.width;
|
||||
const height = fitted.height;
|
||||
const area = { left: 24, top: 18, width: width - 48, height: height - 52 };
|
||||
const max = Math.max(1, ...values);
|
||||
const gap = 18;
|
||||
const barWidth = Math.max(18, (area.width - gap * (values.length - 1)) / Math.max(1, values.length));
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
drawGrid(ctx, area, 4);
|
||||
ctx.font = '12px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#475569';
|
||||
|
||||
values.forEach(function (value, index) {
|
||||
const x = area.left + index * (barWidth + gap);
|
||||
const barHeight = (value / max) * area.height;
|
||||
const y = area.top + area.height - barHeight;
|
||||
|
||||
ctx.fillStyle = colors[index % colors.length];
|
||||
roundRect(ctx, x, y, barWidth, barHeight, 8);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#475569';
|
||||
ctx.fillText(labels[index] || '', x + barWidth / 2, height - 16);
|
||||
});
|
||||
}
|
||||
|
||||
function drawHorizontalBars(canvas, labels, revenue, discount) {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fitted = fitCanvas(canvas);
|
||||
const ctx = fitted.ctx;
|
||||
const width = fitted.width;
|
||||
const height = fitted.height;
|
||||
const area = { left: 18, top: 18, width: width - 36, height: height - 36 };
|
||||
const rows = Math.max(1, labels.length);
|
||||
const max = Math.max(1, ...revenue, ...discount);
|
||||
const rowHeight = area.height / rows;
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.font = '12px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
||||
ctx.textAlign = 'right';
|
||||
|
||||
labels.forEach(function (label, index) {
|
||||
const y = area.top + rowHeight * index + 10;
|
||||
const labelWidth = Math.min(110, area.width * 0.35);
|
||||
const chartLeft = area.left;
|
||||
const chartWidth = area.width - labelWidth - 12;
|
||||
const revenueWidth = (Number(revenue[index] || 0) / max) * chartWidth;
|
||||
const discountWidth = (Number(discount[index] || 0) / max) * chartWidth;
|
||||
|
||||
ctx.fillStyle = '#475569';
|
||||
ctx.fillText(String(label || '').slice(0, 18), width - 18, y + 14);
|
||||
|
||||
ctx.fillStyle = '#0ea5e9';
|
||||
roundRect(ctx, chartLeft, y, revenueWidth, 8, 6);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#f59e0b';
|
||||
roundRect(ctx, chartLeft, y + 13, discountWidth, 8, 6);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
function roundRect(ctx, x, y, width, height, radius) {
|
||||
const r = Math.min(radius, Math.abs(width) / 2, Math.abs(height) / 2);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.lineTo(x + width - r, y);
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
||||
ctx.lineTo(x + width, y + height - r);
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
||||
ctx.lineTo(x + r, y + height);
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
||||
ctx.lineTo(x, y + r);
|
||||
ctx.quadraticCurveTo(x, y, x + r, y);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function render() {
|
||||
const sales = dashboardData.salesChart || {};
|
||||
drawLineChart(document.getElementById('sodinoSalesChart'), [
|
||||
{ values: toNumbers(sales.after), color: '#0ea5e9', fill: 'rgba(14, 165, 233, 0.12)' },
|
||||
{ values: toNumbers(sales.before), color: '#ef4444', fill: 'rgba(239, 68, 68, 0.10)' },
|
||||
]);
|
||||
|
||||
const summary = dashboardData.summary || {};
|
||||
drawVerticalBars(
|
||||
document.getElementById('sodinoDiscountChart'),
|
||||
[dashboardData.translations.totalDiscount, dashboardData.translations.totalRevenue],
|
||||
toNumbers([summary.total_discount, summary.total_revenue]),
|
||||
['#f59e0b', '#0ea5e9']
|
||||
);
|
||||
|
||||
const rules = dashboardData.rulePerformance || {};
|
||||
drawHorizontalBars(
|
||||
document.getElementById('sodinoRuleChart'),
|
||||
rules.names || [],
|
||||
toNumbers(rules.revenue),
|
||||
toNumbers(rules.discount)
|
||||
);
|
||||
}
|
||||
|
||||
render();
|
||||
window.addEventListener('resize', render);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user