mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 22:33:50 +02:00
feat: Add online overlap visualization in user activity tab
This commit is contained in:
@@ -53,6 +53,80 @@
|
||||
style="width: 100%; height: 240px"
|
||||
@contextmenu.prevent="onChartRightClick">
|
||||
</div>
|
||||
|
||||
<!-- Online Overlap Section -->
|
||||
<div v-if="hasAnyData" class="mt-4 border-t border-border pt-3">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium">{{ t('dialog.user.activity.overlap.header') }}</span>
|
||||
<Spinner v-if="isOverlapLoading" class="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div v-if="hasOverlapData" class="flex items-center gap-1.5 flex-shrink-0">
|
||||
<Switch
|
||||
:model-value="excludeHoursEnabled"
|
||||
class="scale-75"
|
||||
@update:model-value="onExcludeToggle" />
|
||||
<span class="text-sm text-muted-foreground whitespace-nowrap">{{ t('dialog.user.activity.overlap.exclude_hours') }}</span>
|
||||
<Select v-model="excludeStartHour" @update:model-value="onExcludeRangeChange">
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
class="w-[78px] h-6 text-xs px-2"
|
||||
@click.stop>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="h in 24" :key="h - 1" :value="String(h - 1)">
|
||||
{{ String(h - 1).padStart(2, '0') }}:00
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span class="text-xs text-muted-foreground">–</span>
|
||||
<Select v-model="excludeEndHour" @update:model-value="onExcludeRangeChange">
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
class="w-[78px] h-6 text-xs px-2"
|
||||
@click.stop>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="h in 24" :key="h - 1" :value="String(h - 1)">
|
||||
{{ String(h - 1).padStart(2, '0') }}:00
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isOverlapLoading && hasOverlapData" class="flex flex-col gap-1 mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium" :class="overlapPercent > 0 ? 'text-accent-foreground' : 'text-muted-foreground'">
|
||||
{{ overlapPercent }}%
|
||||
</span>
|
||||
<div class="flex-1 h-2 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500"
|
||||
:style="{
|
||||
width: `${overlapPercent}%`,
|
||||
backgroundColor: isDarkMode ? 'hsl(260, 60%, 55%)' : 'hsl(260, 55%, 50%)'
|
||||
}" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bestOverlapTime" class="text-sm">
|
||||
<span class="text-muted-foreground">{{ t('dialog.user.activity.overlap.peak_overlap') }}</span>
|
||||
<span class="font-medium ml-1">{{ bestOverlapTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hasOverlapData"
|
||||
ref="overlapChartRef"
|
||||
style="width: 100%; height: 240px"
|
||||
@contextmenu.prevent="onOverlapChartRightClick" />
|
||||
|
||||
<div v-if="!isOverlapLoading && !hasOverlapData && hasAnyData" class="text-sm text-muted-foreground py-2">
|
||||
{{ t('dialog.user.activity.overlap.no_data') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -61,7 +135,8 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DataTableEmpty } from '@/components/ui/data-table';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { RefreshCw, Tractor } from 'lucide-vue-next';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { RefreshCw, Tractor, Sprout } from 'lucide-vue-next';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
@@ -71,7 +146,16 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { database } from '../../../services/database';
|
||||
import configRepository from '../../../services/config';
|
||||
import { useAppearanceSettingsStore, useUserStore } from '../../../stores';
|
||||
import {
|
||||
buildSessionsFromEvents,
|
||||
buildSessionsFromGamelog,
|
||||
calculateOverlapGrid,
|
||||
filterSessionsByPeriod,
|
||||
findBestOverlapTime,
|
||||
aggregateSessionsToGrid
|
||||
} from '../../../shared/utils/overlapCalculator';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { userDialog } = storeToRefs(useUserStore());
|
||||
@@ -86,10 +170,25 @@
|
||||
const selectedPeriod = ref('all');
|
||||
const filteredEventCount = ref(0);
|
||||
|
||||
const overlapChartRef = ref(null);
|
||||
const isOverlapLoading = ref(false);
|
||||
const hasOverlapData = ref(false);
|
||||
const overlapPercent = ref(0);
|
||||
const bestOverlapTime = ref('');
|
||||
|
||||
const excludeHoursEnabled = ref(false);
|
||||
const excludeStartHour = ref('1');
|
||||
const excludeEndHour = ref('6');
|
||||
|
||||
let echartsInstance = null;
|
||||
let resizeObserver = null;
|
||||
let lastLoadedUserId = '';
|
||||
|
||||
let overlapEchartsInstance = null;
|
||||
let overlapResizeObserver = null;
|
||||
let cachedTargetSessions = [];
|
||||
let cachedCurrentSessions = [];
|
||||
|
||||
const dayLabels = computed(() => [
|
||||
t('dialog.user.activity.days.sun'),
|
||||
t('dialog.user.activity.days.mon'),
|
||||
@@ -128,16 +227,37 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
if (overlapEchartsInstance) {
|
||||
overlapEchartsInstance.dispose();
|
||||
overlapEchartsInstance = null;
|
||||
if (hasOverlapData.value && overlapChartRef.value) {
|
||||
nextTick(() => {
|
||||
overlapEchartsInstance = echarts.init(
|
||||
overlapChartRef.value,
|
||||
isDarkMode.value ? 'dark' : null,
|
||||
{ height: 240 }
|
||||
);
|
||||
updateOverlapChart();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => isDarkMode.value, rebuildChart);
|
||||
watch(locale, rebuildChart);
|
||||
watch(selectedPeriod, () => {
|
||||
if (cachedTimestamps.length > 0 && echartsInstance) {
|
||||
if (cachedTargetSessions.length > 0 && echartsInstance) {
|
||||
initChart();
|
||||
}
|
||||
updateOverlapChart();
|
||||
});
|
||||
|
||||
(async () => {
|
||||
excludeHoursEnabled.value = await configRepository.getBool('VRCX_overlapExcludeEnabled', false);
|
||||
excludeStartHour.value = await configRepository.getString('VRCX_overlapExcludeStart', '1');
|
||||
excludeEndHour.value = await configRepository.getString('VRCX_overlapExcludeEnd', '6');
|
||||
})();
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
disposeChart();
|
||||
});
|
||||
@@ -151,42 +271,30 @@
|
||||
echartsInstance.dispose();
|
||||
echartsInstance = null;
|
||||
}
|
||||
if (overlapResizeObserver) {
|
||||
overlapResizeObserver.disconnect();
|
||||
overlapResizeObserver = null;
|
||||
}
|
||||
if (overlapEchartsInstance) {
|
||||
overlapEchartsInstance.dispose();
|
||||
overlapEchartsInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getFilteredTimestamps() {
|
||||
if (selectedPeriod.value === 'all') return cachedTimestamps;
|
||||
function getFilteredSessions() {
|
||||
if (selectedPeriod.value === 'all') return cachedTargetSessions;
|
||||
const days = parseInt(selectedPeriod.value, 10);
|
||||
const cutoff = dayjs().subtract(days, 'day');
|
||||
return cachedTimestamps.filter((ts) => dayjs(ts).isAfter(cutoff));
|
||||
const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1000;
|
||||
return filterSessionsByPeriod(cachedTargetSessions, cutoffMs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string[]} timestamps
|
||||
* @returns {{ data: number[][], maxVal: number, peakText: string }}
|
||||
* Compute peak day/time from a 7×24 grid
|
||||
* @param grid
|
||||
* @returns {{peakDay: string, peakTime: string}}
|
||||
*/
|
||||
function aggregateHeatmapData(timestamps) {
|
||||
const grid = Array.from({ length: 7 }, () => new Array(24).fill(0));
|
||||
|
||||
for (const ts of timestamps) {
|
||||
const d = dayjs(ts);
|
||||
const dayOfWeek = d.day(); // 0=Sun, 1=Mon, ..., 6=Sat
|
||||
const hour = d.hour();
|
||||
grid[dayOfWeek][hour]++;
|
||||
}
|
||||
|
||||
const data = [];
|
||||
let maxVal = 0;
|
||||
for (let day = 0; day < 7; day++) {
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
const count = grid[day][hour];
|
||||
const displayDay = day === 0 ? 6 : day - 1;
|
||||
data.push([hour, displayDay, count]);
|
||||
if (count > maxVal) maxVal = count;
|
||||
}
|
||||
}
|
||||
|
||||
// Peak day: sum each day across all hours
|
||||
function computePeaks(grid) {
|
||||
// Peak day
|
||||
let peakDayResult = '';
|
||||
const daySums = new Array(7).fill(0);
|
||||
for (let day = 0; day < 7; day++) {
|
||||
@@ -206,7 +314,7 @@
|
||||
peakDayResult = dayLabels.value[maxDayIdx];
|
||||
}
|
||||
|
||||
// Peak time: sum each hour across all days, find contiguous peak range
|
||||
// Peak time range
|
||||
let peakTimeResult = '';
|
||||
const hourSums = new Array(24).fill(0);
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
@@ -235,29 +343,42 @@
|
||||
if (startHour === endHour) {
|
||||
peakTimeResult = `${String(startHour).padStart(2, '0')}:00`;
|
||||
} else {
|
||||
peakTimeResult = `${String(startHour).padStart(2, '0')}:00–${String(endHour + 1).padStart(2, '0')}:00`;
|
||||
peakTimeResult = `${String(startHour).padStart(2, '0')}:00\u2013${String(endHour + 1).padStart(2, '0')}:00`;
|
||||
}
|
||||
}
|
||||
|
||||
return { data, maxVal, peakDayResult, peakTimeResult };
|
||||
return { peakDayResult, peakTimeResult };
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
if (!chartRef.value || !echartsInstance) return;
|
||||
|
||||
const filtered = getFilteredTimestamps();
|
||||
filteredEventCount.value = filtered.length;
|
||||
totalOnlineEvents.value = filtered.length;
|
||||
const filteredSessions = getFilteredSessions();
|
||||
// Use timestamps for event count display
|
||||
const filteredTs = getFilteredTimestamps();
|
||||
filteredEventCount.value = filteredTs.length;
|
||||
totalOnlineEvents.value = filteredTs.length;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
if (filteredSessions.length === 0) {
|
||||
peakDayText.value = '';
|
||||
peakTimeText.value = '';
|
||||
return;
|
||||
}
|
||||
const { data, maxVal, peakDayResult, peakTimeResult } = aggregateHeatmapData(filtered);
|
||||
|
||||
const { grid, maxVal } = aggregateSessionsToGrid(filteredSessions);
|
||||
const { peakDayResult, peakTimeResult } = computePeaks(grid);
|
||||
peakDayText.value = peakDayResult;
|
||||
peakTimeText.value = peakTimeResult;
|
||||
|
||||
const data = [];
|
||||
for (let day = 0; day < 7; day++) {
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
const count = grid[day][hour];
|
||||
const displayDay = day === 0 ? 6 : day - 1;
|
||||
data.push([hour, displayDay, count]);
|
||||
}
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
position: 'top',
|
||||
@@ -352,10 +473,15 @@
|
||||
const requestId = ++activeRequestId;
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const timestamps = await database.getOnlineFrequencyData(userId);
|
||||
const [timestamps, events] = await Promise.all([
|
||||
database.getOnlineFrequencyData(userId),
|
||||
database.getOnlineOfflineSessions(userId)
|
||||
]);
|
||||
if (requestId !== activeRequestId) return;
|
||||
if (userDialog.value.id !== userId) return;
|
||||
|
||||
cachedTimestamps = timestamps;
|
||||
cachedTargetSessions = buildSessionsFromEvents(events);
|
||||
hasAnyData.value = timestamps.length > 0;
|
||||
totalOnlineEvents.value = timestamps.length;
|
||||
lastLoadedUserId = userId;
|
||||
@@ -363,8 +489,8 @@
|
||||
await nextTick();
|
||||
|
||||
if (timestamps.length > 0) {
|
||||
const filtered = getFilteredTimestamps();
|
||||
filteredEventCount.value = filtered.length;
|
||||
const filteredTs = getFilteredTimestamps();
|
||||
filteredEventCount.value = filteredTs.length;
|
||||
|
||||
await nextTick();
|
||||
|
||||
@@ -399,6 +525,18 @@
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load overlap data after main data (target sessions already cached)
|
||||
if (hasAnyData.value) {
|
||||
loadOverlapData(userId);
|
||||
}
|
||||
}
|
||||
|
||||
function getFilteredTimestamps() {
|
||||
if (selectedPeriod.value === 'all') return cachedTimestamps;
|
||||
const days = parseInt(selectedPeriod.value, 10);
|
||||
const cutoff = dayjs().subtract(days, 'day');
|
||||
return cachedTimestamps.filter((ts) => dayjs(ts).isAfter(cutoff));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -411,8 +549,250 @@
|
||||
loadData();
|
||||
}
|
||||
|
||||
let easterEggTimer = null;
|
||||
|
||||
function onChartRightClick() {
|
||||
toast(t('dialog.user.activity.easter_egg'), { position: 'bottom-center', icon: h(Tractor) });
|
||||
clearTimeout(easterEggTimer);
|
||||
easterEggTimer = setTimeout(() => {
|
||||
easterEggTimer = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function onOverlapChartRightClick() {
|
||||
if (easterEggTimer) {
|
||||
toast('You can\'t farm this.', { position: 'bottom-center', icon: h(Sprout) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function loadOverlapData(userId) {
|
||||
if (!userId) return;
|
||||
|
||||
isOverlapLoading.value = true;
|
||||
hasOverlapData.value = false;
|
||||
try {
|
||||
// Target sessions already cached from loadData, only fetch current user
|
||||
const currentUserRows = await database.getCurrentUserOnlineSessions();
|
||||
|
||||
if (userDialog.value.id !== userId) return;
|
||||
|
||||
const currentSessions = buildSessionsFromGamelog(currentUserRows);
|
||||
|
||||
if (cachedTargetSessions.length === 0 || currentSessions.length === 0) {
|
||||
hasOverlapData.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
cachedCurrentSessions = currentSessions;
|
||||
hasOverlapData.value = true;
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (!overlapEchartsInstance && overlapChartRef.value) {
|
||||
overlapEchartsInstance = echarts.init(
|
||||
overlapChartRef.value,
|
||||
isDarkMode.value ? 'dark' : null,
|
||||
{ height: 240 }
|
||||
);
|
||||
overlapResizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (overlapEchartsInstance) {
|
||||
overlapEchartsInstance.resize({
|
||||
width: entry.contentRect.width
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
overlapResizeObserver.observe(overlapChartRef.value);
|
||||
}
|
||||
|
||||
updateOverlapChart();
|
||||
} catch (error) {
|
||||
console.error('Error loading overlap data:', error);
|
||||
hasOverlapData.value = false;
|
||||
} finally {
|
||||
isOverlapLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateOverlapChart() {
|
||||
if (cachedTargetSessions.length === 0 || cachedCurrentSessions.length === 0) return;
|
||||
|
||||
let targetSessions = cachedTargetSessions;
|
||||
let currentSessions = cachedCurrentSessions;
|
||||
|
||||
if (selectedPeriod.value !== 'all') {
|
||||
const days = parseInt(selectedPeriod.value, 10);
|
||||
const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1000;
|
||||
targetSessions = filterSessionsByPeriod(targetSessions, cutoffMs);
|
||||
currentSessions = filterSessionsByPeriod(currentSessions, cutoffMs);
|
||||
}
|
||||
|
||||
if (targetSessions.length === 0 || currentSessions.length === 0) {
|
||||
overlapPercent.value = 0;
|
||||
bestOverlapTime.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const result = calculateOverlapGrid(currentSessions, targetSessions);
|
||||
|
||||
// Apply hour exclusion to the grid
|
||||
if (excludeHoursEnabled.value) {
|
||||
const start = parseInt(excludeStartHour.value, 10);
|
||||
const end = parseInt(excludeEndHour.value, 10);
|
||||
applyHourExclusion(result.grid, start, end);
|
||||
// Recalculate maxVal after exclusion
|
||||
result.maxVal = 0;
|
||||
for (let d = 0; d < 7; d++) {
|
||||
for (let h = 0; h < 24; h++) {
|
||||
if (result.grid[d][h] > result.maxVal) result.maxVal = result.grid[d][h];
|
||||
}
|
||||
}
|
||||
// Recalculate overlap percent excluding those hours
|
||||
const totalGrid = result.grid.flat().reduce((a, b) => a + b, 0);
|
||||
if (totalGrid === 0) {
|
||||
overlapPercent.value = 0;
|
||||
bestOverlapTime.value = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
overlapPercent.value = result.overlapPercent;
|
||||
bestOverlapTime.value = findBestOverlapTime(result.grid, dayLabels.value);
|
||||
|
||||
if (result.overlapPercent > 0 || result.maxVal > 0) {
|
||||
initOverlapChart(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zero out hours in the grid that fall within the exclusion range.
|
||||
* Supports wrapping (e.g. 23 to 5 means 23,0,1,2,3,4).
|
||||
* @param grid
|
||||
* @param startHour
|
||||
* @param endHour
|
||||
* @returns {void}
|
||||
*/
|
||||
function applyHourExclusion(grid, startHour, endHour) {
|
||||
for (let d = 0; d < 7; d++) {
|
||||
if (startHour <= endHour) {
|
||||
for (let h = startHour; h < endHour; h++) {
|
||||
grid[d][h] = 0;
|
||||
}
|
||||
} else {
|
||||
for (let h = startHour; h < 24; h++) {
|
||||
grid[d][h] = 0;
|
||||
}
|
||||
for (let h = 0; h < endHour; h++) {
|
||||
grid[d][h] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onExcludeToggle(value) {
|
||||
excludeHoursEnabled.value = value;
|
||||
configRepository.setBool('VRCX_overlapExcludeEnabled', value);
|
||||
updateOverlapChart();
|
||||
}
|
||||
|
||||
function onExcludeRangeChange() {
|
||||
configRepository.setString('VRCX_overlapExcludeStart', excludeStartHour.value);
|
||||
configRepository.setString('VRCX_overlapExcludeEnd', excludeEndHour.value);
|
||||
updateOverlapChart();
|
||||
}
|
||||
|
||||
function initOverlapChart(result) {
|
||||
if (!overlapEchartsInstance) return;
|
||||
|
||||
const data = [];
|
||||
for (let day = 0; day < 7; day++) {
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
const count = result.grid[day][hour];
|
||||
const displayDay = day === 0 ? 6 : day - 1;
|
||||
data.push([hour, displayDay, count]);
|
||||
}
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
position: 'top',
|
||||
formatter: (params) => {
|
||||
const [hour, dayIdx, count] = params.data;
|
||||
const dayName = displayDayLabels.value[dayIdx];
|
||||
return `${dayName} ${hourLabels[hour]}<br/><b>${count}</b> ${t('dialog.user.activity.overlap.times_overlap')}`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 6,
|
||||
left: 42,
|
||||
right: 16,
|
||||
bottom: 32
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: hourLabels,
|
||||
splitArea: { show: false },
|
||||
axisLabel: {
|
||||
interval: 2,
|
||||
fontSize: 10
|
||||
},
|
||||
axisTick: { show: false }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: displayDayLabels.value,
|
||||
splitArea: { show: false },
|
||||
axisLabel: {
|
||||
fontSize: 11
|
||||
},
|
||||
axisTick: { show: false }
|
||||
},
|
||||
visualMap: {
|
||||
min: 0,
|
||||
max: Math.max(result.maxVal, 1),
|
||||
calculable: false,
|
||||
show: false,
|
||||
inRange: {
|
||||
color: isDarkMode.value
|
||||
? [
|
||||
'hsl(220, 15%, 12%)',
|
||||
'hsl(260, 30%, 25%)',
|
||||
'hsl(260, 50%, 42%)',
|
||||
'hsl(260, 65%, 55%)',
|
||||
'hsl(260, 70%, 65%)'
|
||||
]
|
||||
: [
|
||||
'hsl(210, 30%, 95%)',
|
||||
'hsl(260, 35%, 82%)',
|
||||
'hsl(260, 48%, 62%)',
|
||||
'hsl(260, 55%, 48%)',
|
||||
'hsl(260, 60%, 38%)'
|
||||
]
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'heatmap',
|
||||
data,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 6,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
borderWidth: 1.5,
|
||||
borderColor: isDarkMode.value ? 'hsl(220, 15%, 8%)' : 'hsl(0, 0%, 100%)',
|
||||
borderRadius: 2
|
||||
}
|
||||
}
|
||||
],
|
||||
backgroundColor: 'transparent'
|
||||
};
|
||||
|
||||
overlapEchartsInstance.setOption(option, { notMerge: true });
|
||||
}
|
||||
|
||||
defineExpose({ loadOnlineFrequency });
|
||||
|
||||
Reference in New Issue
Block a user