hesabixCore/webUI/src/components/widgets/AIChart.vue
2025-07-22 08:55:13 +00:00

489 lines
13 KiB
Vue

<template>
<v-card class="ai-chart-widget" elevation="0" variant="outlined">
<v-card-title class="d-flex align-center justify-space-between pa-4">
<span class="text-h6">{{ chartTitle }}</span>
<div class="d-flex align-center gap-2">
<v-btn
icon="mdi-download"
variant="text"
size="small"
@click="downloadChart"
:title="$t('chart.download')"
></v-btn>
<v-btn
icon="mdi-refresh"
variant="text"
size="small"
@click="refreshChart"
:title="$t('chart.refresh')"
></v-btn>
</div>
</v-card-title>
<v-card-text class="pa-4">
<div class="chart-container">
<apexchart
v-if="chartSeries && chartSeries.length > 0 && chartOptions && chartOptions.xaxis && Array.isArray(chartOptions.xaxis.categories)"
ref="chart"
:type="chartType"
:height="chartHeight"
:options="chartOptions"
:series="chartSeries"
/>
<div v-else class="text-center pa-4" style="color: #888;">داده‌ای برای نمایش نمودار وجود ندارد.</div>
</div>
<!-- اطلاعات نمودار -->
<div class="chart-info mt-4">
<v-expansion-panels variant="accordion">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon start>mdi-information</v-icon>
{{ $t('chart.details') }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<div class="chart-details">
<div class="detail-item">
<strong>{{ $t('chart.type') }}:</strong> {{ getChartTypeName(chartType) }}
</div>
<div class="detail-item">
<strong>{{ $t('chart.id') }}:</strong> {{ chartId }}
</div>
<div class="detail-item">
<strong>{{ $t('chart.dataPoints') }}:</strong> {{ dataPointsCount }}
</div>
<div class="detail-item">
<strong>{{ $t('chart.created') }}:</strong> {{ formatDate(createdAt) }}
</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</div>
</v-card-text>
</v-card>
</template>
<script>
import VueApexCharts from 'vue3-apexcharts';
export default {
name: 'AIChart',
components: {
apexchart: VueApexCharts,
},
props: {
chartData: {
type: Object,
required: true
},
height: {
type: [String, Number],
default: 400
}
},
data() {
return {
chartId: '',
createdAt: new Date(),
chartType: 'bar',
chartTitle: 'نمودار',
chartOptions: {},
chartSeries: []
};
},
computed: {
chartHeight() {
return this.height;
},
dataPointsCount() {
if (this.chartData && this.chartData.data) {
const data = this.chartData.data;
if (data.categories) {
return data.categories.length;
}
if (data.labels) {
return data.labels.length;
}
if (data.series && data.series[0] && data.series[0].data) {
return data.series[0].data.length;
}
}
return 0;
}
},
watch: {
chartData: {
handler(newData) {
console.debug('AIChart.vue watch chartData', newData);
this.initializeChart(newData);
},
immediate: true,
deep: true
}
},
mounted() {
console.debug('AIChart.vue mounted', this.chartData);
this.initializeChart(this.chartData);
},
methods: {
initializeChart(data) {
console.debug('AIChart.vue initializeChart data:', data);
if (!data) return;
this.chartType = data.chartType || 'bar'; // اصلاح مقداردهی نوع نمودار
this.chartTitle = data.title || 'نمودار';
this.chartId = data.chart_id || this.generateChartId();
this.createdAt = new Date();
// تنظیمات پایه نمودار
this.chartOptions = {
chart: {
id: this.chartId,
type: this.chartType,
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
toolbar: {
show: true,
tools: {
download: true,
selection: true,
zoom: true,
zoomin: true,
zoomout: true,
pan: true,
reset: true
}
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
animateGradually: {
enabled: true,
delay: 150
},
dynamicAnimation: {
enabled: true,
speed: 350
}
}
},
title: {
text: this.chartTitle,
align: 'center',
style: {
fontSize: '16px',
fontWeight: 'bold',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
}
},
colors: ['#2196F3', '#4CAF50', '#FFC107', '#F44336', '#9C27B0', '#00BCD4', '#FF9800', '#795548', '#607D8B', '#E91E63'],
legend: {
position: 'bottom',
fontSize: '14px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
markers: {
width: 12,
height: 12,
radius: 6
}
},
tooltip: {
theme: 'light',
style: {
fontSize: '12px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
},
y: {
formatter: function(value) {
return typeof value === 'number' ? value.toLocaleString('fa-IR') : value;
}
}
},
responsive: [
{
breakpoint: 480,
options: {
chart: { width: '100%' },
legend: { position: 'bottom' }
}
}
]
};
// تنظیمات خاص بر اساس نوع نمودار
this.setupChartSpecificOptions(data);
// تنظیم سری‌های داده
this.setupChartSeries(data);
},
setupChartSpecificOptions(data) {
switch (this.chartType) {
case 'bar':
case 'line':
case 'area':
if (data.categories) {
this.chartOptions.xaxis = {
categories: data.categories,
labels: {
style: {
fontSize: '12px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
}
}
};
}
this.chartOptions.yaxis = {
labels: {
style: {
fontSize: '12px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
},
formatter: function(value) {
return typeof value === 'number' ? value.toLocaleString('fa-IR') : value;
}
}
};
break;
case 'pie':
case 'doughnut':
if (data.labels) {
this.chartOptions.labels = data.labels;
}
this.chartOptions.plotOptions = {
pie: {
donut: {
labels: {
show: true,
name: {
show: true,
fontSize: '14px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
},
value: {
show: true,
fontSize: '16px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
formatter: function(value) {
return typeof value === 'number' ? value.toLocaleString('fa-IR') : value;
}
}
}
}
}
};
break;
case 'radar':
if (data.categories) {
this.chartOptions.xaxis = {
categories: data.categories
};
}
break;
case 'scatter':
case 'bubble':
this.chartOptions.xaxis = {
type: 'numeric',
labels: {
style: {
fontSize: '12px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
}
}
};
this.chartOptions.yaxis = {
type: 'numeric',
labels: {
style: {
fontSize: '12px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
}
}
};
break;
}
},
setupChartSeries(data) {
if (!data) {
this.chartSeries = [];
// مقداردهی پیش‌فرض به xaxis برای جلوگیری از خطا
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = { categories: [] };
return;
}
if (this.chartType === 'pie' || this.chartType === 'doughnut') {
if (data.values && Array.isArray(data.values)) {
this.chartSeries = data.values;
} else if (
data.series &&
Array.isArray(data.series) &&
data.series.length > 0 &&
Array.isArray(data.series[0].data)
) {
this.chartSeries = data.series[0].data;
} else {
this.chartSeries = [];
}
} else {
if (data.series && Array.isArray(data.series) && data.series.length > 0) {
this.chartSeries = data.series;
// مقداردهی categories اگر وجود دارد
if (data.labels && Array.isArray(data.labels)) {
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = this.chartOptions.xaxis || {};
this.chartOptions.xaxis.categories = data.labels;
}
} else if (data.labels && data.values && Array.isArray(data.labels) && Array.isArray(data.values)) {
this.chartSeries = [
{
name: this.chartTitle || 'داده‌ها',
data: data.values
}
];
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = this.chartOptions.xaxis || {};
this.chartOptions.xaxis.categories = data.labels;
} else {
this.chartSeries = [
{
name: 'داده‌ها',
data: []
}
];
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = { categories: [] };
}
}
},
getChartTypeName(type) {
const typeNames = {
'bar': 'ستونی',
'line': 'خطی',
'pie': 'دایره‌ای',
'doughnut': 'دونات',
'area': 'ناحیه‌ای',
'radar': 'راداری',
'scatter': 'پراکندگی',
'bubble': 'حبابی'
};
return typeNames[type] || type;
},
generateChartId() {
return 'ai_chart_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
},
formatDate(date) {
return new Intl.DateTimeFormat('fa-IR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
},
downloadChart() {
if (this.$refs.chart && this.$refs.chart.dataURI) {
this.$refs.chart.dataURI().then(({ imgURI }) => {
const link = document.createElement('a');
link.href = imgURI;
link.download = 'chart.png';
link.click();
});
}
},
refreshChart() {
if (this.$refs.chart && this.$refs.chart.updateSeries) {
// داده فعلی را دوباره ست می‌کنیم تا رفرش شود
this.$refs.chart.updateSeries(this.chartSeries);
}
}
}
};
</script>
<style scoped>
.ai-chart-widget {
border-radius: 12px;
overflow: hidden;
}
.chart-container {
position: relative;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
}
.chart-container.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw !important;
height: 100vh !important;
z-index: 9999;
background: white;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-container.fullscreen .apexcharts-canvas {
width: 100% !important;
height: 100% !important;
min-width: 0 !important;
min-height: 0 !important;
}
.chart-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.detail-item {
padding: 8px 12px;
background: #f5f5f5;
border-radius: 6px;
font-size: 14px;
}
.detail-item strong {
color: #1976d2;
}
.gap-2 {
gap: 8px;
}
/* Responsive Design */
@media (max-width: 768px) {
.chart-details {
grid-template-columns: 1fr;
}
.chart-container.fullscreen {
padding: 10px;
}
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.detail-item {
background: #424242;
color: #ffffff;
}
.detail-item strong {
color: #64b5f6;
}
}
</style>