489 lines
13 KiB
Vue
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> |