mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 14:46:04 +02:00
improve: instance activity chart (#1144)
* improve: instance activity chart * wip: ? * Wrong commit * add prev/next day btn, bar width setting, i18n * add show solo, no friend instance btn, add friend icon, i18n * add favorite friend icon, tips, improve the chart display effect, and i18n
This commit is contained in:
@@ -1,7 +1,80 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="options-container flex-between" style="margin-top: 0">
|
<div class="options-container instance-activity" style="margin-top: 0">
|
||||||
<span style="margin-top: 10px">Instance Activity</span>
|
<div>
|
||||||
|
<span>Instance Activity</span>
|
||||||
|
<el-popover placement="bottom-start" trigger="hover" width="300">
|
||||||
|
<div class="tips-popover">
|
||||||
|
<div>{{ $t('view.charts.instance_activity.tips.header') }}</div>
|
||||||
|
<div>{{ $t('view.charts.instance_activity.tips.online_time') }}</div>
|
||||||
|
<div>{{ $t('view.charts.instance_activity.tips.click_Y_axis') }}</div>
|
||||||
|
<div>{{ $t('view.charts.instance_activity.tips.click_instance_name') }}</div>
|
||||||
|
<div>
|
||||||
|
<i class="el-icon-warning-outline"></i
|
||||||
|
><i>{{ $t('view.charts.instance_activity.tips.accuracy_notice') }}</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<i class="el-icon-info" slot="reference" style="margin-left: 5px; font-size: 12px"></i>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<el-tooltip :content="$t('view.charts.instance_activity.refresh')" placement="top"
|
||||||
|
><el-button icon="el-icon-refresh" circle style="margin-right: 9px" @click="reloadData"></el-button
|
||||||
|
></el-tooltip>
|
||||||
|
<el-tooltip :content="$t('view.charts.instance_activity.settings.header')" placement="top">
|
||||||
|
<el-popover placement="bottom" trigger="click" style="margin-right: 9px">
|
||||||
|
<div class="settings">
|
||||||
|
<div>
|
||||||
|
<span>{{ $t('view.charts.instance_activity.settings.bar_width') }}</span>
|
||||||
|
<div>
|
||||||
|
<el-slider
|
||||||
|
v-model.lazy="barWidth"
|
||||||
|
content="bar width"
|
||||||
|
:max="50"
|
||||||
|
:min="1"
|
||||||
|
@change="changeBarWidth"
|
||||||
|
></el-slider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{{ $t('view.charts.instance_activity.settings.show_detail') }}</span>
|
||||||
|
<el-switch v-model="isDetailVisible" @change="changeIsDetailInstanceVisible">
|
||||||
|
</el-switch>
|
||||||
|
</div>
|
||||||
|
<div v-if="isDetailVisible">
|
||||||
|
<span>{{ $t('view.charts.instance_activity.settings.show_solo_instance') }}</span>
|
||||||
|
<el-switch v-model="isSoloInstanceVisible" @change="changeIsSoloInstanceVisible">
|
||||||
|
</el-switch>
|
||||||
|
</div>
|
||||||
|
<div v-if="isDetailVisible">
|
||||||
|
<span>{{ $t('view.charts.instance_activity.settings.show_no_friend_instance') }}</span>
|
||||||
|
<el-switch
|
||||||
|
v-model="isNoFriendInstanceVisible"
|
||||||
|
@change="changeIsNoFriendInstanceVisible"
|
||||||
|
>
|
||||||
|
</el-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-button slot="reference" icon="el-icon-setting" circle></el-button>
|
||||||
|
</el-popover>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-button-group style="margin-right: 10px">
|
||||||
|
<el-tooltip :content="$t('view.charts.instance_activity.previous_day')" placement="top">
|
||||||
|
<el-button
|
||||||
|
icon="el-icon-arrow-left"
|
||||||
|
:disabled="isPrevDayBtnDisabled"
|
||||||
|
@click="changeSelectedDateFromBtn(false)"
|
||||||
|
></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip :content="$t('view.charts.instance_activity.next_day')" placement="top">
|
||||||
|
<el-button :disabled="isNextDayBtnDisabled" @click="changeSelectedDateFromBtn(true)"
|
||||||
|
><i class="el-icon-arrow-right el-icon--right"></i
|
||||||
|
></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</el-button-group>
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="selectedDate"
|
v-model="selectedDate"
|
||||||
type="date"
|
type="date"
|
||||||
@@ -10,11 +83,12 @@
|
|||||||
:picker-options="{
|
:picker-options="{
|
||||||
disabledDate: (time) => getDatePickerDisabledDate(time)
|
disabledDate: (time) => getDatePickerDisabledDate(time)
|
||||||
}"
|
}"
|
||||||
@change="handleSelectDate"
|
@change="reloadData"
|
||||||
></el-date-picker>
|
></el-date-picker>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div style="position: relative">
|
<div style="position: relative">
|
||||||
<el-statistic title="Total Online Time">
|
<el-statistic :title="$t('view.charts.instance_activity.online_time')">
|
||||||
<template #formatter>
|
<template #formatter>
|
||||||
<span :style="isDarkMode ? 'color:rgb(120,120,120)' : ''">{{ totalOnlineTime }}</span>
|
<span :style="isDarkMode ? 'color:rgb(120,120,120)' : ''">{{ totalOnlineTime }}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -27,16 +101,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition name="el-fade-in-linear">
|
<transition name="el-fade-in-linear">
|
||||||
<div v-show="!isLoading && activityData.length !== 0" class="divider"><el-divider>·</el-divider></div>
|
<div v-show="isDetailVisible && !isLoading" class="divider">
|
||||||
|
<el-divider>·</el-divider>
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
<instance-activity-detail
|
<instance-activity-detail
|
||||||
v-for="arr in activityDetailData"
|
v-for="arr in filteredActivityDetailData"
|
||||||
:key="arr[0].location"
|
:key="arr[0].location + arr[0].created_at"
|
||||||
ref="activityDetailChartRef"
|
ref="activityDetailChartRef"
|
||||||
:activity-data="activityData"
|
|
||||||
:activity-detail-data="arr"
|
:activity-detail-data="arr"
|
||||||
:is-dark-mode="isDarkMode"
|
:is-dark-mode="isDarkMode"
|
||||||
style="width: 100%"
|
:dt-hour12="dtHour12"
|
||||||
|
:bar-width="barWidth"
|
||||||
@open-previous-instance-info-dialog="$emit('open-previous-instance-info-dialog', $event)"
|
@open-previous-instance-info-dialog="$emit('open-previous-instance-info-dialog', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,6 +122,7 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import database from '../../repository/database';
|
import database from '../../repository/database';
|
||||||
import utils from '../../classes/utils';
|
import utils from '../../classes/utils';
|
||||||
|
import configRepository from '../../repository/config';
|
||||||
import InstanceActivityDetail from './InstanceActivityDetail.vue';
|
import InstanceActivityDetail from './InstanceActivityDetail.vue';
|
||||||
|
|
||||||
let echarts = null;
|
let echarts = null;
|
||||||
@@ -57,22 +134,31 @@
|
|||||||
},
|
},
|
||||||
inject: ['API'],
|
inject: ['API'],
|
||||||
props: {
|
props: {
|
||||||
getWorldName: Function,
|
getWorldName: { type: Function, default: () => [] },
|
||||||
isDarkMode: Boolean
|
isDarkMode: Boolean,
|
||||||
|
dtHour12: Boolean,
|
||||||
|
friendsMap: Map,
|
||||||
|
localFavoriteFriends: Set
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
// echarts and observer
|
||||||
echartsInstance: null,
|
echartsInstance: null,
|
||||||
resizeObserver: null,
|
resizeObserver: null,
|
||||||
intersectionObservers: [],
|
intersectionObservers: [],
|
||||||
selectedDate: dayjs().add(-1, 'day'),
|
selectedDate: dayjs().add(-1, 'day'),
|
||||||
|
// data
|
||||||
activityData: [],
|
activityData: [],
|
||||||
activityDetailData: [],
|
activityDetailData: [],
|
||||||
// previousDarkMode: this.isDarkMode,
|
|
||||||
allDateOfActivity: null,
|
allDateOfActivity: null,
|
||||||
firstDateOfActivity: null,
|
|
||||||
worldNameArray: [],
|
worldNameArray: [],
|
||||||
isLoading: true
|
// options
|
||||||
|
isLoading: true,
|
||||||
|
// settings
|
||||||
|
barWidth: 30,
|
||||||
|
isDetailVisible: true,
|
||||||
|
isSoloInstanceVisible: true,
|
||||||
|
isNoFriendInstanceVisible: true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -81,21 +167,65 @@
|
|||||||
this.activityData.reduce((acc, item) => acc + item.time, 0),
|
this.activityData.reduce((acc, item) => acc + item.time, 0),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
isNextDayBtnDisabled() {
|
||||||
|
return dayjs(this.selectedDate).isSame(dayjs(), 'day');
|
||||||
|
},
|
||||||
|
isPrevDayBtnDisabled() {
|
||||||
|
return dayjs(this.selectedDate).isSame(
|
||||||
|
this.allDateOfActivityArray[this.allDateOfActivityArray.length - 1],
|
||||||
|
'day'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
allDateOfActivityArray() {
|
||||||
|
return this.allDateOfActivity
|
||||||
|
? Array.from(this.allDateOfActivity)
|
||||||
|
.map((item) => dayjs(item))
|
||||||
|
.sort((a, b) => b.valueOf() - a.valueOf())
|
||||||
|
: [];
|
||||||
|
},
|
||||||
|
filteredActivityDetailData() {
|
||||||
|
if (!this.isDetailVisible) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let result = [...this.activityDetailData];
|
||||||
|
if (!this.isSoloInstanceVisible) {
|
||||||
|
result = result.filter((arr) => arr.length > 1);
|
||||||
|
}
|
||||||
|
if (!this.isNoFriendInstanceVisible) {
|
||||||
|
result = result.filter((arr) => {
|
||||||
|
// solo instance
|
||||||
|
if (arr.length === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return arr.some((item) => item.isFriend);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isDarkMode() {
|
||||||
|
if (this.echartsInstance) {
|
||||||
|
this.echartsInstance.dispose();
|
||||||
|
this.echartsInstance = null;
|
||||||
|
this.initEcharts();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dtHour12() {
|
||||||
|
if (this.echartsInstance) {
|
||||||
|
this.initEcharts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
// first time also call activated
|
// first time also call activated
|
||||||
if (!this.echartsInstance) {
|
if (this.echartsInstance) {
|
||||||
return;
|
this.reloadData();
|
||||||
}
|
}
|
||||||
// if (this.isDarkMode === this.previousDarkMode) {
|
|
||||||
// when tab activated, play animation
|
|
||||||
this.echartsInstance.clear();
|
|
||||||
this.initEcharts();
|
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
deactivated() {
|
deactivated() {
|
||||||
// prevent switch tab play resize animation
|
// prevent resize animation when switch tab
|
||||||
this.resizeObserver.disconnect();
|
this.resizeObserver.disconnect();
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -109,6 +239,18 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
configRepository.getInt('VRCX_InstanceActivityBarWidth', 30).then((value) => {
|
||||||
|
this.barWidth = value;
|
||||||
|
});
|
||||||
|
configRepository.getBool('VRCX_InstanceActivityDetailVisible', true).then((value) => {
|
||||||
|
this.isDetailVisible = value;
|
||||||
|
});
|
||||||
|
configRepository.getBool('VRCX_InstanceActivitySoloInstanceVisible', true).then((value) => {
|
||||||
|
this.isSoloInstanceVisible = value;
|
||||||
|
});
|
||||||
|
configRepository.getBool('VRCX_InstanceActivityNoFriendInstaceVisible', true).then((value) => {
|
||||||
|
this.isNoFriendInstanceVisible = value;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
@@ -128,7 +270,6 @@
|
|||||||
if (this.activityData.length && echarts) {
|
if (this.activityData.length && echarts) {
|
||||||
// activity data is ready, but world name data isn't ready
|
// activity data is ready, but world name data isn't ready
|
||||||
// so init echarts with empty data, reduce the render time of init screen
|
// so init echarts with empty data, reduce the render time of init screen
|
||||||
// TODO: move to created lifecycle, init screen faster
|
|
||||||
this.initEcharts(true);
|
this.initEcharts(true);
|
||||||
this.getAllDateOfActivity();
|
this.getAllDateOfActivity();
|
||||||
this.getWorldNameData();
|
this.getWorldNameData();
|
||||||
@@ -140,14 +281,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// reload data
|
async reloadData() {
|
||||||
async handleSelectDate() {
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
await this.getActivityData();
|
await this.getActivityData();
|
||||||
this.getWorldNameData();
|
this.getWorldNameData();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// echarts - start
|
||||||
initEcharts(isFirstTime = false) {
|
initEcharts(isFirstTime = false) {
|
||||||
const chartsHeight = this.activityData.length * 40 + 200;
|
const chartsHeight = this.activityData.length * (this.barWidth + 10) + 200;
|
||||||
const chartDom = this.$refs.activityChartRef;
|
const chartDom = this.$refs.activityChartRef;
|
||||||
if (!this.echartsInstance) {
|
if (!this.echartsInstance) {
|
||||||
this.echartsInstance = echarts.init(chartDom, `${this.isDarkMode ? 'dark' : null}`, {
|
this.echartsInstance = echarts.init(chartDom, `${this.isDarkMode ? 'dark' : null}`, {
|
||||||
@@ -163,101 +305,27 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
const handleClickYAxisLabel = (params) => {
|
||||||
this.echartsInstance.setOption(this.getNewOption(isFirstTime), { lazyUpdate: true });
|
const detailDataIdx = this.filteredActivityDetailData.findIndex((arr) => {
|
||||||
this.echartsInstance.on('click', 'yAxis', this.handleClickYAxisLabel);
|
const sameLocation = arr[0]?.location === this.activityData[params?.dataIndex]?.location;
|
||||||
this.isLoading = false;
|
const sameJoinTime = arr
|
||||||
|
.find((item) => item.user_id === this.API.currentUser.id)
|
||||||
|
.joinTime.isSame(this.activityData[params?.dataIndex].joinTime);
|
||||||
|
return sameLocation && sameJoinTime;
|
||||||
});
|
});
|
||||||
},
|
if (detailDataIdx === -1) {
|
||||||
handleClickYAxisLabel(params) {
|
console.error('handleClickYAxisLabel failed', params);
|
||||||
const detailDataIdx = this.activityDetailData.findIndex(
|
} else {
|
||||||
(arr) => arr[0]?.location === this.activityData[params?.dataIndex]?.location
|
|
||||||
);
|
|
||||||
if (detailDataIdx !== -1) {
|
|
||||||
this.$refs.activityDetailChartRef[detailDataIdx].$el.scrollIntoView({
|
this.$refs.activityDetailChartRef[detailDataIdx].$el.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'start'
|
block: 'start'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
getDatePickerDisabledDate(time) {
|
|
||||||
if (time > Date.now() || time < this.firstDateOfActivity) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !this.allDateOfActivity.has(dayjs(time).format('YYYY-MM-DD'));
|
|
||||||
},
|
|
||||||
async getAllDateOfActivity() {
|
|
||||||
const utcDateStrings = await database.getDateOfInstanceActivity();
|
|
||||||
|
|
||||||
const uniqueDates = new Set();
|
this.echartsInstance.setOption(this.getNewOption(isFirstTime), { lazyUpdate: true });
|
||||||
this.firstDateOfActivity = dayjs.utc(utcDateStrings[0]).startOf('day');
|
this.echartsInstance.on('click', 'yAxis', handleClickYAxisLabel);
|
||||||
|
this.isLoading = false;
|
||||||
for (const utcString of utcDateStrings) {
|
|
||||||
const formattedDate = dayjs.utc(utcString).tz().format('YYYY-MM-DD');
|
|
||||||
uniqueDates.add(formattedDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.allDateOfActivity = uniqueDates;
|
|
||||||
},
|
|
||||||
async getActivityData() {
|
|
||||||
const localStartDate = dayjs.tz(this.selectedDate).startOf('day').toISOString();
|
|
||||||
const localEndDate = dayjs.tz(this.selectedDate).endOf('day').toISOString();
|
|
||||||
const dbData = await database.getInstanceActivity(localStartDate, localEndDate);
|
|
||||||
|
|
||||||
const transformData = (item) => ({
|
|
||||||
...item,
|
|
||||||
joinTime: dayjs(item.created_at).subtract(item.time, 'millisecond'),
|
|
||||||
leaveTime: dayjs(item.created_at),
|
|
||||||
time: item.time < 0 ? 0 : item.time
|
|
||||||
});
|
|
||||||
|
|
||||||
this.activityData = dbData.currentUserData.map(transformData);
|
|
||||||
|
|
||||||
// FIXME: some detail data missing current user activity
|
|
||||||
this.activityDetailData = Array.from(dbData.detailData.values()).map((arr) =>
|
|
||||||
arr.map(transformData).sort((a, b) => {
|
|
||||||
const timeDiff = Math.abs(a.joinTime.diff(b.joinTime, 'second'));
|
|
||||||
// recording delay, under 2s is considered the same time entry, beautify the chart
|
|
||||||
if (timeDiff < 2) {
|
|
||||||
return a.leaveTime - b.leaveTime;
|
|
||||||
}
|
|
||||||
return a.joinTime - b.joinTime;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.handleIntersectionObserver();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleIntersectionObserver() {
|
|
||||||
this.$refs.activityDetailChartRef.forEach((child, index) => {
|
|
||||||
const observer = new IntersectionObserver(this.handleIntersection.bind(this, index));
|
|
||||||
observer.observe(child.$el);
|
|
||||||
this.intersectionObservers[index] = observer;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleIntersection(index, entries) {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
this.$refs.activityDetailChartRef[index].initEcharts();
|
|
||||||
this.intersectionObservers[index].unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async getWorldNameData() {
|
|
||||||
this.worldNameArray = await Promise.all(
|
|
||||||
this.activityData.map(async (item) => {
|
|
||||||
try {
|
|
||||||
return await this.getWorldName(item.location);
|
|
||||||
} catch {
|
|
||||||
// TODO: no notification
|
|
||||||
console.error('getWorldName failed location', item.location);
|
|
||||||
return 'Unknown world';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (this.worldNameArray && this.echartsInstance) {
|
|
||||||
this.initEcharts();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
getNewOption(isFirstTime) {
|
getNewOption(isFirstTime) {
|
||||||
const getTooltip = (params) => {
|
const getTooltip = (params) => {
|
||||||
@@ -270,8 +338,10 @@
|
|||||||
|
|
||||||
const instanceData = activityData[param.dataIndex];
|
const instanceData = activityData[param.dataIndex];
|
||||||
|
|
||||||
const formattedLeftDateTime = dayjs(instanceData.leaveTime).format('HH:mm:ss');
|
const format = this.dtHour12 ? 'hh:mm:ss A' : 'HH:mm:ss';
|
||||||
const formattedJoinDateTime = dayjs(instanceData.joinTime).format('HH:mm:ss');
|
|
||||||
|
const formattedLeftDateTime = dayjs(instanceData.leaveTime).format(format);
|
||||||
|
const formattedJoinDateTime = dayjs(instanceData.joinTime).format(format);
|
||||||
|
|
||||||
const timeString = utils.timeToText(param.data, true);
|
const timeString = utils.timeToText(param.data, true);
|
||||||
const color = param.color;
|
const color = param.color;
|
||||||
@@ -290,6 +360,8 @@
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const format = this.dtHour12 ? 'hh:mm A' : 'HH:mm';
|
||||||
|
|
||||||
const echartsOption = {
|
const echartsOption = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
@@ -322,8 +394,7 @@
|
|||||||
interval: 3 * 60 * 60 * 1000,
|
interval: 3 * 60 * 60 * 1000,
|
||||||
axisLine: { show: true },
|
axisLine: { show: true },
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
formatter: (value) =>
|
formatter: (value) => dayjs(value).utc().format(format)
|
||||||
value === 24 * 60 * 60 * 1000 ? '24:00' : dayjs(value).utc().format('HH:mm')
|
|
||||||
},
|
},
|
||||||
splitLine: { lineStyle: { type: 'dashed' } }
|
splitLine: { lineStyle: { type: 'dashed' } }
|
||||||
},
|
},
|
||||||
@@ -359,7 +430,10 @@
|
|||||||
type: 'bar',
|
type: 'bar',
|
||||||
stack: 'Total',
|
stack: 'Total',
|
||||||
colorBy: 'data',
|
colorBy: 'data',
|
||||||
barWidth: 30,
|
barWidth: this.barWidth,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'self'
|
||||||
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
shadowBlur: 2,
|
shadowBlur: 2,
|
||||||
@@ -380,24 +454,311 @@
|
|||||||
return item.time;
|
return item.time;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
backgroundColor: 'transparent'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.isDarkMode) {
|
|
||||||
echartsOption.backgroundColor = 'rgba(0, 0, 0, 0)';
|
|
||||||
}
|
|
||||||
return echartsOption;
|
return echartsOption;
|
||||||
|
},
|
||||||
|
// echarts - end
|
||||||
|
|
||||||
|
// settings - start
|
||||||
|
changeBarWidth(value) {
|
||||||
|
this.barWidth = value;
|
||||||
|
this.initEcharts();
|
||||||
|
configRepository.setInt('VRCX_InstanceActivityBarWidth', value).finally(() => {
|
||||||
|
this.handleChangeSettings();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
changeIsDetailInstanceVisible(value) {
|
||||||
|
this.isDetailVisible = value;
|
||||||
|
configRepository.setBool('VRCX_InstanceActivityDetailVisible', value).finally(() => {
|
||||||
|
this.handleChangeSettings();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
changeIsSoloInstanceVisible(value) {
|
||||||
|
this.isSoloInstanceVisible = value;
|
||||||
|
configRepository.setBool('VRCX_InstanceActivitySoloInstanceVisible', value).finally(() => {
|
||||||
|
this.handleChangeSettings();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
changeIsNoFriendInstanceVisible(value) {
|
||||||
|
this.isNoFriendInstanceVisible = value;
|
||||||
|
configRepository.setBool('VRCX_InstanceActivityNoFriendInstaceVisible', value).finally(() => {
|
||||||
|
this.handleChangeSettings();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleChangeSettings() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.activityDetailChartRef) {
|
||||||
|
this.$refs.activityDetailChartRef.forEach((child) => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (child.echartsInstance) {
|
||||||
|
child.initEcharts();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//rerender detail chart
|
||||||
|
},
|
||||||
|
// settings - end
|
||||||
|
|
||||||
|
// options - start
|
||||||
|
changeSelectedDateFromBtn(isNext = false) {
|
||||||
|
const idx = this.allDateOfActivityArray.findIndex((date) => date.isSame(this.selectedDate, 'day'));
|
||||||
|
if (idx !== -1) {
|
||||||
|
if (isNext) {
|
||||||
|
if (idx - 1 < this.allDateOfActivityArray.length) {
|
||||||
|
console.log(this.selectedDate, this.allDateOfActivityArray[idx + 1]);
|
||||||
|
this.selectedDate = this.allDateOfActivityArray[idx - 1];
|
||||||
|
this.reloadData();
|
||||||
|
}
|
||||||
|
} else if (idx + 1 >= 0) {
|
||||||
|
this.selectedDate = this.allDateOfActivityArray[idx + 1];
|
||||||
|
this.reloadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDatePickerDisabledDate(time) {
|
||||||
|
if (
|
||||||
|
time > Date.now() ||
|
||||||
|
this.allDateOfActivityArray[this.allDateOfActivityArray.length - 1]
|
||||||
|
.add('-1', 'day')
|
||||||
|
.isAfter(time, 'day') ||
|
||||||
|
!this.allDateOfActivity
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !this.allDateOfActivity.has(dayjs(time).format('YYYY-MM-DD'));
|
||||||
|
},
|
||||||
|
// options - end
|
||||||
|
|
||||||
|
// data - start
|
||||||
|
async getWorldNameData() {
|
||||||
|
this.worldNameArray = await Promise.all(
|
||||||
|
this.activityData.map(async (item) => {
|
||||||
|
try {
|
||||||
|
return await this.getWorldName(item.location);
|
||||||
|
} catch {
|
||||||
|
// TODO: no notification
|
||||||
|
console.error('getWorldName failed location', item.location);
|
||||||
|
return 'Unknown world';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (this.worldNameArray && this.echartsInstance) {
|
||||||
|
this.initEcharts();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getAllDateOfActivity() {
|
||||||
|
const utcDateStrings = await database.getDateOfInstanceActivity();
|
||||||
|
const uniqueDates = new Set();
|
||||||
|
|
||||||
|
for (const utcString of utcDateStrings) {
|
||||||
|
const formattedDate = dayjs.utc(utcString).tz().format('YYYY-MM-DD');
|
||||||
|
uniqueDates.add(formattedDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.allDateOfActivity = uniqueDates;
|
||||||
|
},
|
||||||
|
async getActivityData() {
|
||||||
|
const localStartDate = dayjs.tz(this.selectedDate).startOf('day').toISOString();
|
||||||
|
const localEndDate = dayjs.tz(this.selectedDate).endOf('day').toISOString();
|
||||||
|
const dbData = await database.getInstanceActivity(localStartDate, localEndDate);
|
||||||
|
|
||||||
|
const transformData = (item) => ({
|
||||||
|
...item,
|
||||||
|
joinTime: dayjs(item.created_at).subtract(item.time, 'millisecond'),
|
||||||
|
leaveTime: dayjs(item.created_at),
|
||||||
|
time: item.time < 0 ? 0 : item.time,
|
||||||
|
isFriend: item.user_id === this.API.currentUser.id ? null : this.friendsMap.has(item.user_id),
|
||||||
|
isFavorite:
|
||||||
|
item.user_id === this.API.currentUser.id ? null : this.localFavoriteFriends.has(item.user_id)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activityData = dbData.currentUserData.map(transformData);
|
||||||
|
|
||||||
|
const transformAndSort = (arr) => {
|
||||||
|
return arr.map(transformData).sort((a, b) => {
|
||||||
|
const timeDiff = Math.abs(a.joinTime.diff(b.joinTime, 'second'));
|
||||||
|
// recording delay, under 3s is considered the same time entry, beautify the chart
|
||||||
|
return timeDiff < 3 ? a.leaveTime - b.leaveTime : a.joinTime - b.joinTime;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterByLocation = (innerArray, locationSet) => {
|
||||||
|
return innerArray.every((innerObject) => locationSet.has(innerObject.location));
|
||||||
|
};
|
||||||
|
const locationSet = new Set(this.activityData.map((item) => item.location));
|
||||||
|
|
||||||
|
const preSplitActivityDetailData = Array.from(dbData.detailData.values())
|
||||||
|
.map(transformAndSort)
|
||||||
|
.filter((innerArray) => filterByLocation(innerArray, locationSet));
|
||||||
|
|
||||||
|
this.activityDetailData = this.handleSplitActivityDetailData(
|
||||||
|
preSplitActivityDetailData,
|
||||||
|
this.API.currentUser.id
|
||||||
|
);
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.handleIntersectionObserver();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSplitActivityDetailData(activityDetailData, currentUserId) {
|
||||||
|
function countTargetIdOccurrences(innerArray, targetId) {
|
||||||
|
let count = 0;
|
||||||
|
for (const obj of innerArray) {
|
||||||
|
if (obj.user_id === targetId) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function areIntervalsOverlapping(objA, objB) {
|
||||||
|
const isObj1EndTimeBeforeObj2StartTime = objA.leaveTime.isBefore(objB.joinTime, 'second');
|
||||||
|
const isObj2EndTimeBeforeObj1StartTime = objB.leaveTime.isBefore(objA.joinTime, 'second');
|
||||||
|
return !(isObj1EndTimeBeforeObj2StartTime || isObj2EndTimeBeforeObj1StartTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOverlapGraph(innerArray) {
|
||||||
|
const numObjects = innerArray.length;
|
||||||
|
const adjacencyList = Array.from({ length: numObjects }, () => []);
|
||||||
|
|
||||||
|
for (let i = 0; i < numObjects; i++) {
|
||||||
|
for (let j = i + 1; j < numObjects; j++) {
|
||||||
|
if (areIntervalsOverlapping(innerArray[i], innerArray[j])) {
|
||||||
|
adjacencyList[i].push(j);
|
||||||
|
adjacencyList[j].push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return adjacencyList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function depthFirstSearch(nodeIndex, visited, graph, component) {
|
||||||
|
visited[nodeIndex] = true;
|
||||||
|
component.push(nodeIndex);
|
||||||
|
for (const neighborIndex of graph[nodeIndex]) {
|
||||||
|
if (!visited[neighborIndex]) {
|
||||||
|
depthFirstSearch(neighborIndex, visited, graph, component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findConnectedComponents(graph, numNodes) {
|
||||||
|
const visited = new Array(numNodes).fill(false);
|
||||||
|
const components = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < numNodes; i++) {
|
||||||
|
if (!visited[i]) {
|
||||||
|
const component = [];
|
||||||
|
depthFirstSearch(i, visited, graph, component);
|
||||||
|
components.push(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processOuterArrayWithTargetId(outerArray, targetId) {
|
||||||
|
let i = 0;
|
||||||
|
while (i < outerArray.length) {
|
||||||
|
let currentInnerArray = outerArray[i];
|
||||||
|
let targetIdCount = countTargetIdOccurrences(currentInnerArray, targetId);
|
||||||
|
if (targetIdCount > 1) {
|
||||||
|
let graph = buildOverlapGraph(currentInnerArray);
|
||||||
|
let connectedComponents = findConnectedComponents(graph, currentInnerArray.length);
|
||||||
|
let newInnerArrays = connectedComponents.map((componentIndices) => {
|
||||||
|
return componentIndices.map((index) => currentInnerArray[index]);
|
||||||
|
});
|
||||||
|
outerArray.splice(i, 1, ...newInnerArrays);
|
||||||
|
i += newInnerArrays.length;
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outerArray.sort((a, b) => a[0].joinTime - b[0].joinTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return processOuterArrayWithTargetId(activityDetailData, currentUserId);
|
||||||
|
},
|
||||||
|
// data - end
|
||||||
|
|
||||||
|
// intersection observer - start
|
||||||
|
handleIntersectionObserver() {
|
||||||
|
this.$refs.activityDetailChartRef.forEach((child, index) => {
|
||||||
|
const observer = new IntersectionObserver(this.handleIntersection.bind(this, index));
|
||||||
|
observer.observe(child.$el);
|
||||||
|
this.intersectionObservers[index] = observer;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleIntersection(index, entries) {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.$refs.activityDetailChartRef[index].initEcharts();
|
||||||
|
this.intersectionObservers[index].unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// intersection observer - end
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
.flex-between {
|
%flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
}
|
||||||
|
%flex-between {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
.instance-activity {
|
||||||
|
@extend %flex;
|
||||||
|
@extend %flex-between;
|
||||||
|
& > div:first-child {
|
||||||
|
@extend %flex-between;
|
||||||
|
}
|
||||||
|
& > div {
|
||||||
|
@extend %flex;
|
||||||
|
> span {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tips-popover {
|
||||||
|
:first-child {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
:not(:first-child) {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.settings {
|
||||||
|
& > div {
|
||||||
|
@extend %flex;
|
||||||
|
@extend %flex-between;
|
||||||
|
padding: 0 2px;
|
||||||
|
height: 30px;
|
||||||
|
> span {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& > div:first-child {
|
||||||
|
> div {
|
||||||
|
width: 160px;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.nodata {
|
.nodata {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -410,6 +771,7 @@
|
|||||||
transition: top 0.3s ease;
|
transition: top 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// override el-ui
|
||||||
.el-date-editor.el-input,
|
.el-date-editor.el-input,
|
||||||
.el-date-editor.el-input__inner {
|
.el-date-editor.el-input__inner {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div style="width: 100%">
|
||||||
<div style="height: 25px; margin-top: 60px">
|
<div style="height: 25px; margin-top: 60px">
|
||||||
<transition name="el-fade-in-linear">
|
<transition name="el-fade-in-linear">
|
||||||
<location
|
<location
|
||||||
@@ -30,13 +30,18 @@
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
activityData: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
isDarkMode: {
|
isDarkMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
dtHour12: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
barWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
default: 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -48,16 +53,30 @@
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
startTimeStamp() {
|
startTimeStamp() {
|
||||||
return this.activityData
|
return this.activityDetailData
|
||||||
.find((item) => item.location === this.activityDetailData[0].location)
|
.find((item) => item.user_id === this.API.currentUser.id)
|
||||||
?.joinTime.valueOf();
|
?.joinTime.valueOf();
|
||||||
},
|
},
|
||||||
endTimeStamp() {
|
endTimeStamp() {
|
||||||
return this.activityData
|
return this.activityDetailData
|
||||||
.findLast((item) => item.location === this.activityDetailData[0].location)
|
.find((item) => item.user_id === this.API.currentUser.id)
|
||||||
?.leaveTime.valueOf();
|
?.leaveTime.valueOf();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
isDarkMode() {
|
||||||
|
if (this.echartsInstance) {
|
||||||
|
this.echartsInstance.dispose();
|
||||||
|
this.echartsInstance = null;
|
||||||
|
this.initEcharts();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dtHour12() {
|
||||||
|
if (this.echartsInstance) {
|
||||||
|
this.initEcharts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
created() {
|
created() {
|
||||||
this.resizeObserver = new ResizeObserver((entries) => {
|
this.resizeObserver = new ResizeObserver((entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -70,38 +89,46 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initEcharts(true);
|
||||||
|
},
|
||||||
deactivated() {
|
deactivated() {
|
||||||
// prevent switch tab play resize animation
|
// prevent switch tab play resize animation
|
||||||
this.resizeObserver.disconnect();
|
this.resizeObserver.disconnect();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async initEcharts() {
|
async initEcharts(isFirstLoad = false) {
|
||||||
// TODO: unnecessary import, import from individual js file
|
// TODO: unnecessary import, import from individual js file
|
||||||
await import('echarts').then((echartsModule) => {
|
await import('echarts').then((echartsModule) => {
|
||||||
echarts = echartsModule;
|
echarts = echartsModule;
|
||||||
});
|
});
|
||||||
|
const chartsHeight = this.activityDetailData.length * (this.barWidth + 10) + 200;
|
||||||
const chartDom = this.$refs.activityDetailChart;
|
const chartDom = this.$refs.activityDetailChart;
|
||||||
if (!this.echartsInstance) {
|
if (!this.echartsInstance) {
|
||||||
this.echartsInstance = echarts.init(chartDom, `${this.isDarkMode ? 'dark' : null}`, {
|
this.echartsInstance = echarts.init(chartDom, `${this.isDarkMode ? 'dark' : null}`, {
|
||||||
height: this.activityDetailData.length * 40 + 200,
|
height: chartsHeight,
|
||||||
useDirtyRect: this.activityDetailData.length > 30
|
useDirtyRect: this.activityDetailData.length > 80
|
||||||
});
|
});
|
||||||
this.resizeObserver.observe(chartDom);
|
this.resizeObserver.observe(chartDom);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.echartsInstance.resize({
|
this.echartsInstance.resize({
|
||||||
height: this.activityDetailData.length * 40 + 200,
|
height: chartsHeight,
|
||||||
animation: {
|
animation: {
|
||||||
duration: 300
|
duration: 300
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.echartsInstance.setOption(this.getNewOption(), { lazyUpdate: true });
|
this.echartsInstance.setOption(isFirstLoad ? {} : this.getNewOption(), { lazyUpdate: true });
|
||||||
this.echartsInstance.on('click', 'yAxis', this.handleClickYAxisLabel);
|
this.echartsInstance.on('click', 'yAxis', this.handleClickYAxisLabel);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
this.echartsInstance.dispatchAction({
|
||||||
|
type: 'highlight',
|
||||||
|
seriesIndex: 3 // 对于 seriesLayoutBy: 'row',seriesIndex 对应行索引
|
||||||
|
});
|
||||||
}, 200);
|
}, 200);
|
||||||
},
|
},
|
||||||
handleClickYAxisLabel(params) {
|
handleClickYAxisLabel(params) {
|
||||||
@@ -111,6 +138,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getNewOption() {
|
getNewOption() {
|
||||||
|
const friendOrFavIcon = (display_name) => {
|
||||||
|
const foundItem = this.activityDetailData.find((item) => item.display_name === display_name);
|
||||||
|
|
||||||
|
if (!foundItem) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundItem.isFavorite) {
|
||||||
|
return '⭐';
|
||||||
|
}
|
||||||
|
if (foundItem.isFriend) {
|
||||||
|
return '💚';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
const getTooltip = (params) => {
|
const getTooltip = (params) => {
|
||||||
const activityDetailData = this.activityDetailData;
|
const activityDetailData = this.activityDetailData;
|
||||||
const param = params[1];
|
const param = params[1];
|
||||||
@@ -121,8 +164,9 @@
|
|||||||
|
|
||||||
const instanceData = activityDetailData[param.dataIndex];
|
const instanceData = activityDetailData[param.dataIndex];
|
||||||
|
|
||||||
const formattedLeftDateTime = dayjs(instanceData.leaveTime).format('HH:mm:ss');
|
const format = this.dtHour12 ? 'hh:mm:ss A' : 'HH:mm:ss';
|
||||||
const formattedJoinDateTime = dayjs(instanceData.joinTime).format('HH:mm:ss');
|
const formattedLeftDateTime = dayjs(instanceData.leaveTime).format(format);
|
||||||
|
const formattedJoinDateTime = dayjs(instanceData.joinTime).format(format);
|
||||||
|
|
||||||
const timeString = utils.timeToText(instanceData.time, true);
|
const timeString = utils.timeToText(instanceData.time, true);
|
||||||
const color = param.color;
|
const color = param.color;
|
||||||
@@ -131,7 +175,7 @@
|
|||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<div style="width: 10px; height: 55px; background-color: ${color}; margin-right: 5px;"></div>
|
<div style="width: 10px; height: 55px; background-color: ${color}; margin-right: 5px;"></div>
|
||||||
<div>
|
<div>
|
||||||
<div>${instanceData.display_name}</div>
|
<div>${instanceData.display_name} ${friendOrFavIcon(instanceData.display_name)}</div>
|
||||||
<div>${formattedJoinDateTime} - ${formattedLeftDateTime}</div>
|
<div>${formattedJoinDateTime} - ${formattedLeftDateTime}</div>
|
||||||
<div>${timeString}</div>
|
<div>${timeString}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,6 +183,8 @@
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const format = this.dtHour12 ? 'hh:mm A' : 'HH:mm';
|
||||||
|
|
||||||
const echartsOption = {
|
const echartsOption = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
@@ -148,7 +194,7 @@
|
|||||||
formatter: getTooltip
|
formatter: getTooltip
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
top: 60,
|
top: 50,
|
||||||
left: 160,
|
left: 160,
|
||||||
right: 90
|
right: 90
|
||||||
},
|
},
|
||||||
@@ -156,7 +202,11 @@
|
|||||||
type: 'category',
|
type: 'category',
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
interval: 0,
|
interval: 0,
|
||||||
formatter: (value) => (value.length > 20 ? `${value.slice(0, 20)}...` : value)
|
formatter: (value) => {
|
||||||
|
const MAX_LENGTH = 20;
|
||||||
|
const len = value.length;
|
||||||
|
return `${friendOrFavIcon(value)} ${len > MAX_LENGTH ? `${value.substring(0, MAX_LENGTH)}...` : value}`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
inverse: true,
|
inverse: true,
|
||||||
data: this.activityDetailData.map((item) => item.display_name),
|
data: this.activityDetailData.map((item) => item.display_name),
|
||||||
@@ -168,7 +218,7 @@
|
|||||||
max: this.endTimeStamp - this.startTimeStamp,
|
max: this.endTimeStamp - this.startTimeStamp,
|
||||||
axisLine: { show: true },
|
axisLine: { show: true },
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
formatter: (value) => dayjs(value + this.startTimeStamp).format('HH:mm')
|
formatter: (value) => dayjs(value + this.startTimeStamp).format(format)
|
||||||
},
|
},
|
||||||
splitLine: { lineStyle: { type: 'dashed' } }
|
splitLine: { lineStyle: { type: 'dashed' } }
|
||||||
},
|
},
|
||||||
@@ -194,7 +244,10 @@
|
|||||||
type: 'bar',
|
type: 'bar',
|
||||||
stack: 'Total',
|
stack: 'Total',
|
||||||
colorBy: 'data',
|
colorBy: 'data',
|
||||||
barWidth: 30,
|
barWidth: this.barWidth,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'self'
|
||||||
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
shadowBlur: 2,
|
shadowBlur: 2,
|
||||||
@@ -203,12 +256,10 @@
|
|||||||
},
|
},
|
||||||
data: this.activityDetailData.map((item) => item.time)
|
data: this.activityDetailData.map((item) => item.time)
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0)'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.isDarkMode) {
|
|
||||||
echartsOption.backgroundColor = 'rgba(0, 0, 0, 0)';
|
|
||||||
}
|
|
||||||
return echartsOption;
|
return echartsOption;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -106,7 +106,11 @@ html
|
|||||||
v-if='$refs.menu?.activeIndex === "charts"'
|
v-if='$refs.menu?.activeIndex === "charts"'
|
||||||
:get-world-name='getWorldName'
|
:get-world-name='getWorldName'
|
||||||
:is-dark-mode='isDarkMode'
|
:is-dark-mode='isDarkMode'
|
||||||
@open-previous-instance-info-dialog='showPreviousInstanceInfoDialog')
|
:dt-hour12='dtHour12'
|
||||||
|
@open-previous-instance-info-dialog='showPreviousInstanceInfoDialog'
|
||||||
|
:friends-map='friends'
|
||||||
|
:local-favorite-friends='localFavoriteFriends'
|
||||||
|
)
|
||||||
|
|
||||||
//- settings
|
//- settings
|
||||||
include ./mixins/tabs/settings.pug
|
include ./mixins/tabs/settings.pug
|
||||||
|
|||||||
@@ -218,6 +218,30 @@
|
|||||||
"clear_tooltip": "Clear Results",
|
"clear_tooltip": "Clear Results",
|
||||||
"cancel_tooltip": "Cancel"
|
"cancel_tooltip": "Cancel"
|
||||||
},
|
},
|
||||||
|
"charts": {
|
||||||
|
"header": "Charts",
|
||||||
|
"instance_activity": {
|
||||||
|
"header": "Instance Activity",
|
||||||
|
"online_time": "Online Time",
|
||||||
|
"previous_day": "Previous Day",
|
||||||
|
"next_day": "Next Day",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"tips": {
|
||||||
|
"header": "Tips",
|
||||||
|
"online_time": "Online time refers to the game time recorded by VRCX for the current day.",
|
||||||
|
"click_Y_axis": "Click Y-axis label to open user/world dialog.",
|
||||||
|
"click_instance_name": "Click instance name for previous instance info.",
|
||||||
|
"accuracy_notice": "Info from local database may not be accurate"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"header": "Settings",
|
||||||
|
"bar_width": "Bar Width",
|
||||||
|
"show_solo_instance": "Show Solo Instance",
|
||||||
|
"show_no_friend_instance": "Show No Friend Instance",
|
||||||
|
"show_detail": "Show Detail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"profile": {
|
"profile": {
|
||||||
"header": "Profile",
|
"header": "Profile",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"moderation": "モデレーション",
|
"moderation": "モデレーション",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"friend_list": "フレンドリスト",
|
"friend_list": "フレンドリスト",
|
||||||
|
"charts": "チャート",
|
||||||
"profile": "プロフィール",
|
"profile": "プロフィール",
|
||||||
"settings": "設定"
|
"settings": "設定"
|
||||||
},
|
},
|
||||||
@@ -217,6 +218,30 @@
|
|||||||
"clear_tooltip": "結果を消去",
|
"clear_tooltip": "結果を消去",
|
||||||
"cancel_tooltip": "キャンセル"
|
"cancel_tooltip": "キャンセル"
|
||||||
},
|
},
|
||||||
|
"charts": {
|
||||||
|
"header": "チャート",
|
||||||
|
"instance_activity": {
|
||||||
|
"header": "インスタンスアクティビティ",
|
||||||
|
"online_time": "オンライン時間",
|
||||||
|
"previous_day": "前日",
|
||||||
|
"next_day": "翌日",
|
||||||
|
"refresh": "更新",
|
||||||
|
"tips": {
|
||||||
|
"header": "ヒント",
|
||||||
|
"online_time": "オンライン時間とは、VRCXが記録した当日のゲームプレイ時間です。",
|
||||||
|
"click_Y_axis": "Y軸ラベルをクリックでプレイヤー/ワールドダイアログを表示します。",
|
||||||
|
"click_instance_name": "インスタンス名をクリックで以前のインスタンス情報ダイアログを表示します。",
|
||||||
|
"accuracy_notice": "ローカルデータベースからの情報であり、正確ではない可能性があります。"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"header": "設定",
|
||||||
|
"bar_width": "棒グラフ幅",
|
||||||
|
"show_solo_instance": "一人インスタンス",
|
||||||
|
"show_no_friend_instance": "フレンドなしインスタンス",
|
||||||
|
"show_detail": "インスタンス詳細"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"profile": {
|
"profile": {
|
||||||
"header": "プロフィール",
|
"header": "プロフィール",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"moderation": "用户管理(屏蔽 / 静音)",
|
"moderation": "用户管理(屏蔽 / 静音)",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"friend_list": "好友列表",
|
"friend_list": "好友列表",
|
||||||
|
"charts": "图表",
|
||||||
"profile": "个人信息",
|
"profile": "个人信息",
|
||||||
"settings": "设置"
|
"settings": "设置"
|
||||||
},
|
},
|
||||||
@@ -217,6 +218,30 @@
|
|||||||
"clear_tooltip": "清除结果",
|
"clear_tooltip": "清除结果",
|
||||||
"cancel_tooltip": "取消"
|
"cancel_tooltip": "取消"
|
||||||
},
|
},
|
||||||
|
"charts": {
|
||||||
|
"header": "图表",
|
||||||
|
"instance_activity": {
|
||||||
|
"header": "Instance Activity",
|
||||||
|
"online_time": "在线时长",
|
||||||
|
"previous_day": "前一天",
|
||||||
|
"next_day": "后一天",
|
||||||
|
"refresh": "刷新",
|
||||||
|
"tips": {
|
||||||
|
"header": "提示",
|
||||||
|
"online_time": "在线时间是指VRCX记录地当天的游戏时间。",
|
||||||
|
"click_Y_axis": "点击Y轴标签打开“玩家“、“世界“对话框。",
|
||||||
|
"click_instance_name": "点击房间名称查看上个“曾加入的房间信息“对话框。",
|
||||||
|
"accuracy_notice": "本地数据库的数据可能不准确。"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"header": "设置",
|
||||||
|
"bar_width": "柱条宽度",
|
||||||
|
"show_solo_instance": "显示独自一人的房间",
|
||||||
|
"show_no_friend_instance": "显示没有好友的房间",
|
||||||
|
"show_detail": "显示详细房间"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"profile": {
|
"profile": {
|
||||||
"header": "个人信息",
|
"header": "个人信息",
|
||||||
|
|||||||
@@ -241,12 +241,12 @@ mixin settingsTab
|
|||||||
el-dropdown-item(
|
el-dropdown-item(
|
||||||
v-text='$t("view.settings.appearance.appearance.theme_mode_dark")'
|
v-text='$t("view.settings.appearance.appearance.theme_mode_dark")'
|
||||||
@click.native='saveThemeMode("dark")')
|
@click.native='saveThemeMode("dark")')
|
||||||
el-dropdown-item(
|
|
||||||
v-text='$t("view.settings.appearance.appearance.theme_mode_darkvanillaold")'
|
|
||||||
@click.native='saveThemeMode("darkvanillaold")')
|
|
||||||
el-dropdown-item(
|
el-dropdown-item(
|
||||||
v-text='$t("view.settings.appearance.appearance.theme_mode_darkvanilla")'
|
v-text='$t("view.settings.appearance.appearance.theme_mode_darkvanilla")'
|
||||||
@click.native='saveThemeMode("darkvanilla")')
|
@click.native='saveThemeMode("darkvanilla")')
|
||||||
|
el-dropdown-item(
|
||||||
|
v-text='$t("view.settings.appearance.appearance.theme_mode_darkvanillaold")'
|
||||||
|
@click.native='saveThemeMode("darkvanillaold")')
|
||||||
el-dropdown-item(
|
el-dropdown-item(
|
||||||
v-text='$t("view.settings.appearance.appearance.theme_mode_pink")'
|
v-text='$t("view.settings.appearance.appearance.theme_mode_pink")'
|
||||||
@click.native='saveThemeMode("pink")')
|
@click.native='saveThemeMode("pink")')
|
||||||
|
|||||||
@@ -439,3 +439,40 @@ button {
|
|||||||
background: #222;
|
background: #222;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
.el-backtop {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date picker
|
||||||
|
.el-date-picker {
|
||||||
|
background-color: #222;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.el-date-table td.disabled div {
|
||||||
|
background-color: #333;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.el-date-table td.next-month,
|
||||||
|
.el-date-table td.prev-month {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.el-picker-panel__icon-btn {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.el-date-table th {
|
||||||
|
border-bottom: 1px solid #555;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.el-date-picker__header-label {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.el-picker-panel {
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
.el-year-table td.disabled .cell {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.el-month-table td.disabled .cell {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
// Date picker end
|
||||||
|
|||||||
@@ -704,3 +704,38 @@ i[class='el-icon-star-off']:not(.el-menu-item div.el-tooltip i) {
|
|||||||
color: #efefef;
|
color: #efefef;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
.el-backtop {
|
||||||
|
background-color: var(--dv_bg-top);
|
||||||
|
color: var(--dv_muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date picker
|
||||||
|
.el-date-picker {
|
||||||
|
background-color: var(--dv_bg-top);
|
||||||
|
}
|
||||||
|
.el-date-table td.current:not(.disabled) span {
|
||||||
|
background-color: var(--dv_muted);
|
||||||
|
}
|
||||||
|
.el-date-table td.today span {
|
||||||
|
color: var(--dv_muted);
|
||||||
|
}
|
||||||
|
.el-date-table td.available:hover span {
|
||||||
|
background-color: var(--dv_muted);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.el-date-table td.available:hover {
|
||||||
|
color: #fff
|
||||||
|
}
|
||||||
|
.el-year-table td .cell:hover, .el-year-table td.current:not(.disabled) .cell {
|
||||||
|
color: var(--dv_muted);
|
||||||
|
}
|
||||||
|
.el-month-table td.current:not(.disabled) .cell {
|
||||||
|
color: var(--dv_muted);
|
||||||
|
}
|
||||||
|
.el-date-picker__header-label.active, .el-date-picker__header-label:hover {
|
||||||
|
color: var(--dv_muted);
|
||||||
|
}
|
||||||
|
.el-picker-panel__icon-btn:hover {
|
||||||
|
color: var(--dv_muted);
|
||||||
|
}
|
||||||
|
// Date picker end
|
||||||
|
|||||||
@@ -325,3 +325,33 @@ path[stroke='#e5e9f2'] {
|
|||||||
background: var(--farback);
|
background: var(--farback);
|
||||||
color: #efefef;
|
color: #efefef;
|
||||||
}
|
}
|
||||||
|
.el-backtop {
|
||||||
|
color: var(--theme-text-muted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date picker
|
||||||
|
.el-date-table td.current:not(.disabled) span {
|
||||||
|
background-color: var(--theme-text-muted);
|
||||||
|
}
|
||||||
|
.el-date-table td.today span {
|
||||||
|
color: var(--theme-text-muted);
|
||||||
|
}
|
||||||
|
.el-date-table td.available:hover span {
|
||||||
|
background-color: var(--theme-text-muted);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.el-date-table td.available:hover {
|
||||||
|
color: #fff
|
||||||
|
}
|
||||||
|
.el-year-table td .cell:hover, .el-year-table td.current:not(.disabled) .cell {
|
||||||
|
color: var(--theme-text-muted);
|
||||||
|
}
|
||||||
|
.el-month-table td.current:not(.disabled) .cell {
|
||||||
|
color: var(--theme-text-muted);
|
||||||
|
}
|
||||||
|
.el-date-picker__header-label.active, .el-date-picker__header-label:hover {
|
||||||
|
color: var(--theme-text-muted);
|
||||||
|
}
|
||||||
|
.el-picker-panel__icon-btn:hover {
|
||||||
|
color: var(--theme-text-muted);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2019,3 +2019,41 @@ i.x-user-status {
|
|||||||
background: rgba(var(--md-sys-color-background));
|
background: rgba(var(--md-sys-color-background));
|
||||||
color: #efefef;
|
color: #efefef;
|
||||||
}
|
}
|
||||||
|
.el-backtop {
|
||||||
|
background: var(--md-sys-color-surface-1);
|
||||||
|
color: rgb(var(--md-sys-color-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date picker
|
||||||
|
.el-date-table td.available:hover span {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.el-date-table td.available:hover {
|
||||||
|
color: #fff
|
||||||
|
}
|
||||||
|
.el-date-table td.disabled div {
|
||||||
|
background-color: rgb(48, 46, 53)
|
||||||
|
}
|
||||||
|
.el-date-table td.current:not(.disabled) span
|
||||||
|
{
|
||||||
|
background: var(--md-sys-color-surface-2);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.el-date-table td.today span {
|
||||||
|
color: rgb(var(--md-sys-color-primary-container));
|
||||||
|
}
|
||||||
|
.el-year-table td .cell:hover, .el-year-table td.current:not(.disabled) .cell {
|
||||||
|
color: rgb(48, 46, 53);
|
||||||
|
}
|
||||||
|
.el-month-table td.current:not(.disabled) .cell {
|
||||||
|
color: rgb(48, 46, 53);
|
||||||
|
}
|
||||||
|
.el-date-picker__header-label.active, .el-date-picker__header-label:hover {
|
||||||
|
color: rgb(48, 46, 53);
|
||||||
|
}
|
||||||
|
.el-picker-panel__icon-btn:hover {
|
||||||
|
color: rgb(48, 46, 53);
|
||||||
|
}
|
||||||
|
.el-month-table td .cell:hover {
|
||||||
|
color: rgb(48, 46, 53);
|
||||||
|
}
|
||||||
|
|||||||
@@ -373,3 +373,43 @@ input[type='checkbox']:checked + .el-switch__core {
|
|||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: #efefef;
|
color: #efefef;
|
||||||
}
|
}
|
||||||
|
.el-backtop {
|
||||||
|
background: var(--lighter-bg);
|
||||||
|
color: var(--theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date picker
|
||||||
|
.el-date-picker {
|
||||||
|
background-color: var(--lighter-bg);
|
||||||
|
}
|
||||||
|
.el-date-table td.current:not(.disabled) span {
|
||||||
|
background-color: var(--lighter-bg);
|
||||||
|
}
|
||||||
|
.el-date-table td.today span {
|
||||||
|
color: var(--theme);
|
||||||
|
}
|
||||||
|
.el-date-table td.available:hover span {
|
||||||
|
background-color: var(--lighter-bg);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.el-date-table td.available:hover {
|
||||||
|
color: #fff
|
||||||
|
}
|
||||||
|
.el-year-table td .cell:hover, .el-year-table td.current:not(.disabled) .cell {
|
||||||
|
color: var(--theme);
|
||||||
|
}
|
||||||
|
.el-month-table td.current:not(.disabled) .cell {
|
||||||
|
color: var(--theme);
|
||||||
|
}
|
||||||
|
.el-date-picker__header-label.active, .el-date-picker__header-label:hover {
|
||||||
|
color: var(--theme);
|
||||||
|
}
|
||||||
|
.el-picker-panel__icon-btn:hover {
|
||||||
|
color: var(--theme);
|
||||||
|
}
|
||||||
|
.el-month-table td .cell:hover {
|
||||||
|
color: var(--theme);
|
||||||
|
}
|
||||||
|
.el-date-table td.disabled div {
|
||||||
|
background-color: #3a2b2b
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="x-container" id="chart">
|
<div id="chart" class="x-container">
|
||||||
<div class="options-container" style="margin-top: 0">
|
<div class="options-container" style="margin-top: 0">
|
||||||
<span class="header">Charts</span>
|
<span class="header">{{ $t('view.charts.header') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<instance-activity
|
<instance-activity
|
||||||
:get-world-name="getWorldName"
|
:get-world-name="getWorldName"
|
||||||
:is-dark-mode="isDarkMode"
|
:is-dark-mode="isDarkMode"
|
||||||
|
:dt-hour12="dtHour12"
|
||||||
|
:friends-map="friendsMap"
|
||||||
|
:localFavoriteFriends="localFavoriteFriends"
|
||||||
@open-previous-instance-info-dialog="$emit('open-previous-instance-info-dialog', $event)"
|
@open-previous-instance-info-dialog="$emit('open-previous-instance-info-dialog', $event)"
|
||||||
id="instance-activity"
|
|
||||||
></instance-activity>
|
></instance-activity>
|
||||||
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop>
|
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,13 +19,15 @@
|
|||||||
import InstanceActivity from '../../components/charts/InstanceActivity.vue';
|
import InstanceActivity from '../../components/charts/InstanceActivity.vue';
|
||||||
export default {
|
export default {
|
||||||
name: 'ChartsTab',
|
name: 'ChartsTab',
|
||||||
inject: ['API'],
|
|
||||||
props: {
|
|
||||||
getWorldName: Function,
|
|
||||||
isDarkMode: Boolean
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
InstanceActivity
|
InstanceActivity
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
getWorldName: Function,
|
||||||
|
isDarkMode: Boolean,
|
||||||
|
dtHour12: Boolean,
|
||||||
|
friendsMap: Map,
|
||||||
|
localFavoriteFriends: Set
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user