mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-15 12:53:51 +02:00
* refactor: dialogs * fix: storeAvatarImage * FriendLog.vue * FriendLog.vue * FriendLog.vue * GameLog.vue * fix: next day button jumping to the wrong date * sync master * fix: launchGame * Notification.vue * Feed.vue * Search.vue * Profile.vue * PlayerList.vue * Login.vue * utils * update dialog * del gameLog.pug * fix * fix: group role cannot be displayed currently * fix: "Hide Friends in Same Instance" hides players in unrelated private instances (#1210) * fix * fix: "Hide Friends in Same Instance" does not work when "Split Favorite Friends" is enabled * fix Notification.vue message * fix: deleteFavoriteNoConfirm * fix: feed status style * fix: infinite loading when deleting note * fix: private players will not be hidden when 'Hide Friends in Same Instance', and 'Hide Friends in Same Instance' will not work when 'Split Favorite Friends'
828 lines
34 KiB
Vue
828 lines
34 KiB
Vue
<template>
|
|
<div>
|
|
<div class="options-container instance-activity" style="margin-top: 0">
|
|
<div>
|
|
<span>{{ $t('view.charts.instance_activity.header') }}</span>
|
|
<el-popover placement="bottom-start" trigger="hover" width="300">
|
|
<div class="tips-popover">
|
|
<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
|
|
slot="reference"
|
|
class="el-icon-info"
|
|
style="margin-left: 5px; font-size: 12px; opacity: 0.7"></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"
|
|
: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
|
|
v-model="selectedDate"
|
|
type="date"
|
|
:clearable="false"
|
|
align="right"
|
|
:picker-options="{
|
|
disabledDate: (time) => getDatePickerDisabledDate(time)
|
|
}"
|
|
@change="reloadData"></el-date-picker>
|
|
</div>
|
|
</div>
|
|
<div style="position: relative">
|
|
<el-statistic :title="$t('view.charts.instance_activity.online_time')">
|
|
<template #formatter>
|
|
<span :style="isDarkMode ? 'color:rgb(120,120,120)' : ''">{{ totalOnlineTime }}</span>
|
|
</template>
|
|
</el-statistic>
|
|
</div>
|
|
|
|
<div ref="activityChartRef" style="width: 100%"></div>
|
|
<div v-if="!isLoading && activityData.length === 0" class="nodata">
|
|
<span>No data here, try another day</span>
|
|
</div>
|
|
|
|
<transition name="el-fade-in-linear">
|
|
<div v-show="isDetailVisible && !isLoading && !activityData.length === 0" class="divider">
|
|
<el-divider>·</el-divider>
|
|
</div>
|
|
</transition>
|
|
<InstanceActivityDetail
|
|
v-for="arr in filteredActivityDetailData"
|
|
:key="arr[0].location + arr[0].created_at"
|
|
ref="activityDetailChartRef"
|
|
:activity-detail-data="arr"
|
|
:is-dark-mode="isDarkMode"
|
|
:dt-hour12="dtHour12"
|
|
:bar-width="barWidth"
|
|
@open-previous-instance-info-dialog="$emit('open-previous-instance-info-dialog', $event)" />
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import dayjs from 'dayjs';
|
|
import { parseLocation } from '../../../composables/instance/utils';
|
|
import database from '../../../service/database';
|
|
import utils from '../../../classes/utils';
|
|
import configRepository from '../../../service/config';
|
|
import InstanceActivityDetail from './InstanceActivityDetail.vue';
|
|
|
|
export default {
|
|
name: 'InstanceActivity',
|
|
components: {
|
|
InstanceActivityDetail
|
|
},
|
|
inject: ['API'],
|
|
props: {
|
|
getWorldName: { type: Function, default: () => [] },
|
|
isDarkMode: Boolean,
|
|
dtHour12: Boolean,
|
|
friendsMap: Map,
|
|
localFavoriteFriends: Set
|
|
},
|
|
data() {
|
|
return {
|
|
// echarts and observer
|
|
echarts: null,
|
|
echartsInstance: null,
|
|
resizeObserver: null,
|
|
intersectionObservers: [],
|
|
selectedDate: dayjs(),
|
|
// data
|
|
activityData: [],
|
|
activityDetailData: [],
|
|
allDateOfActivity: new Set(),
|
|
worldNameArray: [],
|
|
// options
|
|
isLoading: true,
|
|
// settings
|
|
barWidth: 25,
|
|
isDetailVisible: true,
|
|
isSoloInstanceVisible: true,
|
|
isNoFriendInstanceVisible: true
|
|
};
|
|
},
|
|
computed: {
|
|
totalOnlineTime() {
|
|
return utils.timeToText(
|
|
this.activityData.reduce((acc, item) => acc + item.time, 0),
|
|
true
|
|
);
|
|
},
|
|
isNextDayBtnDisabled() {
|
|
return dayjs(this.selectedDate).isSameOrAfter(this.allDateOfActivityArray[0], '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() {
|
|
// first time also call activated
|
|
if (this.echartsInstance) {
|
|
this.reloadData();
|
|
}
|
|
},
|
|
deactivated() {
|
|
// prevent resize animation when switch tab
|
|
this.resizeObserver.disconnect();
|
|
},
|
|
created() {
|
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
for (const entry of entries) {
|
|
this.echartsInstance.resize({
|
|
width: entry.contentRect.width,
|
|
animation: {
|
|
duration: 300
|
|
}
|
|
});
|
|
}
|
|
});
|
|
configRepository.getInt('VRCX_InstanceActivityBarWidth', 25).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_InstanceActivityNoFriendInstanceVisible', true).then((value) => {
|
|
this.isNoFriendInstanceVisible = value;
|
|
});
|
|
},
|
|
async mounted() {
|
|
try {
|
|
this.getAllDateOfActivity();
|
|
const [echartsModule] = await Promise.all([
|
|
// lazy load echarts
|
|
utils.loadEcharts().catch((error) => {
|
|
console.error('lazy load echarts failed', error);
|
|
return null;
|
|
}),
|
|
this.getActivityData()
|
|
]);
|
|
if (echartsModule) {
|
|
this.echarts = echartsModule;
|
|
}
|
|
if (echartsModule) {
|
|
// activity data is ready, but world name data isn't ready
|
|
// so init echarts with empty data, reduce the render time of init screen
|
|
this.initEcharts(true);
|
|
this.getWorldNameData();
|
|
} else {
|
|
this.isLoading = false;
|
|
}
|
|
} catch (error) {
|
|
console.error('error in mounted', error);
|
|
}
|
|
},
|
|
methods: {
|
|
async reloadData() {
|
|
this.isLoading = true;
|
|
await this.getActivityData();
|
|
this.getWorldNameData();
|
|
// possibility past 24:00
|
|
this.getAllDateOfActivity();
|
|
},
|
|
|
|
// echarts - start
|
|
initEcharts() {
|
|
const chartsHeight = this.activityData.length * (this.barWidth + 10) + 200;
|
|
const chartDom = this.$refs.activityChartRef;
|
|
|
|
const afterInit = () => {
|
|
this.echartsInstance.resize({
|
|
height: chartsHeight,
|
|
animation: {
|
|
duration: 300
|
|
}
|
|
});
|
|
|
|
const handleClickYAxisLabel = (params) => {
|
|
const detailDataIdx = this.filteredActivityDetailData.findIndex((arr) => {
|
|
const sameLocation = arr[0]?.location === this.activityData[params?.dataIndex]?.location;
|
|
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) {
|
|
// no detail chart down below, it's hidden, so can't find instance data index
|
|
console.error(
|
|
"handleClickYAxisLabel failed, likely current user wasn't in this instance.",
|
|
params
|
|
);
|
|
} else {
|
|
this.$refs.activityDetailChartRef[detailDataIdx].$el.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
};
|
|
|
|
const options = this.activityData.length ? this.getNewOption() : {};
|
|
|
|
this.echartsInstance.setOption(options, { lazyUpdate: true });
|
|
this.echartsInstance.on('click', 'yAxis', handleClickYAxisLabel);
|
|
this.isLoading = false;
|
|
};
|
|
|
|
const initEchartsInstance = () => {
|
|
this.echartsInstance = this.echarts.init(chartDom, `${this.isDarkMode ? 'dark' : null}`, {
|
|
height: chartsHeight
|
|
});
|
|
this.resizeObserver.observe(chartDom);
|
|
};
|
|
|
|
const loadEchartsWithTimeout = () => {
|
|
const timeout = 5000;
|
|
let time = 0;
|
|
const timer = setInterval(() => {
|
|
if (this.echarts) {
|
|
initEchartsInstance();
|
|
afterInit();
|
|
clearInterval(timer);
|
|
return;
|
|
}
|
|
time += 100;
|
|
if (time >= timeout) {
|
|
clearInterval(timer);
|
|
console.error('echarts init timeout');
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
if (!this.echartsInstance) {
|
|
if (!this.echarts) {
|
|
loadEchartsWithTimeout();
|
|
} else {
|
|
initEchartsInstance();
|
|
afterInit();
|
|
}
|
|
} else {
|
|
afterInit();
|
|
}
|
|
},
|
|
getNewOption() {
|
|
const getTooltip = (params) => {
|
|
const activityData = this.activityData;
|
|
const param = params[1];
|
|
|
|
if (!activityData || !activityData[param.dataIndex]) {
|
|
return '';
|
|
}
|
|
|
|
const instanceData = activityData[param.dataIndex];
|
|
|
|
const format = this.dtHour12 ? 'hh:mm:ss A' : '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 color = param.color;
|
|
const name = param.name;
|
|
const location = parseLocation(instanceData.location);
|
|
|
|
return `
|
|
<div style="display: flex; align-items: center;">
|
|
<div style="width: 10px; height: 55px; background-color: ${color}; margin-right: 5px;"></div>
|
|
<div>
|
|
<div>${name} #${location.instanceName} ${location.accessTypeName}</div>
|
|
<div>${formattedJoinDateTime} - ${formattedLeftDateTime}</div>
|
|
<div>${timeString}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
};
|
|
|
|
const format = this.dtHour12 ? 'hh:mm A' : 'HH:mm';
|
|
|
|
const echartsOption = {
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
axisPointer: {
|
|
type: 'shadow'
|
|
},
|
|
formatter: getTooltip
|
|
},
|
|
grid: {
|
|
top: 50,
|
|
left: 160,
|
|
right: 90
|
|
},
|
|
yAxis: {
|
|
type: 'category',
|
|
axisLabel: {
|
|
interval: 0,
|
|
formatter: (value) => (value.length > 20 ? `${value.slice(0, 20)}...` : value)
|
|
},
|
|
inverse: true,
|
|
data: this.worldNameArray,
|
|
triggerEvent: true
|
|
},
|
|
xAxis: {
|
|
type: 'value',
|
|
min: 0,
|
|
// axisLabel max 24:00
|
|
max: 24 * 60 * 60 * 1000,
|
|
// axisLabel interval 3hr
|
|
interval: 3 * 60 * 60 * 1000,
|
|
axisLine: { show: true },
|
|
axisLabel: {
|
|
formatter: (value) => dayjs(value).utc().format(format)
|
|
},
|
|
splitLine: { lineStyle: { type: 'dashed' } }
|
|
},
|
|
series: [
|
|
{
|
|
name: 'Placeholder',
|
|
type: 'bar',
|
|
stack: 'Total',
|
|
itemStyle: {
|
|
borderColor: 'transparent',
|
|
color: 'transparent'
|
|
},
|
|
emphasis: {
|
|
itemStyle: {
|
|
borderColor: 'transparent',
|
|
color: 'transparent'
|
|
}
|
|
},
|
|
data: this.activityData.map((item, idx) => {
|
|
if (idx === 0) {
|
|
const midnight = dayjs.tz(this.selectedDate).startOf('day');
|
|
if (midnight.isAfter(item.joinTime)) {
|
|
return 0;
|
|
}
|
|
}
|
|
return item.joinTime - dayjs.tz(this.selectedDate).startOf('day');
|
|
})
|
|
},
|
|
{
|
|
name: 'Time',
|
|
type: 'bar',
|
|
stack: 'Total',
|
|
colorBy: 'data',
|
|
barWidth: this.barWidth,
|
|
emphasis: {
|
|
focus: 'self'
|
|
},
|
|
itemStyle: {
|
|
borderRadius: 2,
|
|
shadowBlur: 2,
|
|
shadowOffsetX: 0.7,
|
|
shadowOffsetY: 0.5
|
|
},
|
|
data: this.activityData.map((item, idx) => {
|
|
// If the joinTime of the first data is on the previous day,
|
|
// and the data traverses midnight, the duration starts at midnight
|
|
if (idx === 0) {
|
|
const midnight = dayjs.tz(this.selectedDate).startOf('day');
|
|
if (midnight.isAfter(item.joinTime)) {
|
|
return item.leaveTime - dayjs.tz(midnight);
|
|
}
|
|
}
|
|
return item.time;
|
|
})
|
|
}
|
|
],
|
|
backgroundColor: 'transparent'
|
|
};
|
|
|
|
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_InstanceActivityNoFriendInstanceVisible', 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) {
|
|
if (!this.allDateOfActivityArray || this.allDateOfActivityArray.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const idx = this.allDateOfActivityArray.findIndex((date) => date.isSame(this.selectedDate, 'day'));
|
|
if (idx !== -1) {
|
|
const newIdx = isNext ? idx - 1 : idx + 1;
|
|
|
|
if (newIdx >= 0 && newIdx < this.allDateOfActivityArray.length) {
|
|
this.selectedDate = this.allDateOfActivityArray[newIdx];
|
|
this.reloadData();
|
|
return;
|
|
}
|
|
}
|
|
this.selectedDate = isNext
|
|
? this.allDateOfActivityArray[0]
|
|
: this.allDateOfActivityArray[this.allDateOfActivityArray.length - 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.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
|
|
);
|
|
|
|
if (this.activityDetailData.length) {
|
|
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) {
|
|
if (!entries) {
|
|
console.error('handleIntersection failed');
|
|
return;
|
|
}
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting && this.$refs.activityDetailChartRef[index]) {
|
|
this.$refs.activityDetailChartRef[index].initEcharts();
|
|
this.intersectionObservers[index].unobserve(entry.target);
|
|
}
|
|
});
|
|
}
|
|
// intersection observer - end
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
%flex {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
%flex-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 {
|
|
& > div {
|
|
margin-bottom: 5px;
|
|
font-size: 12px;
|
|
}
|
|
& > div:last-child {
|
|
@extend %flex;
|
|
margin-top: 10px;
|
|
i {
|
|
margin-right: 3px;
|
|
}
|
|
}
|
|
& .el-icon-warning-outline {
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
.settings {
|
|
& > div {
|
|
@extend %flex;
|
|
@extend %flex-between;
|
|
padding: 0 2px;
|
|
height: 30px;
|
|
> span {
|
|
flex-shrink: 0;
|
|
}
|
|
}
|
|
& > div:first-child {
|
|
> div {
|
|
width: 160px;
|
|
padding-left: 20px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.nodata {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-top: 100px;
|
|
color: #5c5c5c;
|
|
}
|
|
.divider {
|
|
padding: 0 400px;
|
|
transition: top 0.3s ease;
|
|
}
|
|
|
|
// override el-ui
|
|
.el-date-editor.el-input,
|
|
.el-date-editor.el-input__inner {
|
|
width: 200px;
|
|
}
|
|
.el-divider__text {
|
|
padding-left: 10px;
|
|
padding-right: 10px;
|
|
}
|
|
</style>
|