feat: Instance Activity Chart (#1141)

* feat: Instance Activity Chart

* fix: chart data handling
This commit is contained in:
pa
2025-02-18 14:32:47 +09:00
committed by GitHub
parent 42d623c932
commit 4002e944b9
21 changed files with 1145 additions and 87 deletions
+3 -2
View File
@@ -1,13 +1,14 @@
{ {
"root": true, "root": true,
"extends": ["eslint:all", "prettier"], "extends": ["eslint:all", "plugin:vue/recommended", "prettier"],
"env": { "env": {
"browser": true, "browser": true,
"commonjs": true, "commonjs": true,
"es2021": true "es2021": true
}, },
"parser": "@babel/eslint-parser", "parser": "vue-eslint-parser",
"parserOptions": { "parserOptions": {
"parser": "@babel/eslint-parser",
"ecmaVersion": "latest", "ecmaVersion": "latest",
"sourceType": "module", "sourceType": "module",
"ecmaFeatures": { "ecmaFeatures": {
+251 -8
View File
@@ -25,12 +25,15 @@
"copy-webpack-plugin": "^12.0.2", "copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"dayjs": "^1.11.13",
"default-passive-events": "^2.0.0", "default-passive-events": "^2.0.0",
"echarts": "^5.6.0",
"electron": "^34.0.2", "electron": "^34.0.2",
"electron-builder": "^25.1.8", "electron-builder": "^25.1.8",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"eslint": "^9.19.0", "eslint": "^9.19.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-vue": "^9.32.0",
"html-webpack-plugin": "^5.6.3", "html-webpack-plugin": "^5.6.3",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.2",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
@@ -1967,9 +1970,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.19.0", "version": "9.20.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz",
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -4811,6 +4814,13 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"dev": true,
"license": "MIT"
},
"node_modules/de-indent": { "node_modules/de-indent": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@@ -5164,6 +5174,24 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"dev": true,
"license": "0BSD"
},
"node_modules/ejs": { "node_modules/ejs": {
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@@ -5439,18 +5467,18 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.19.0", "version": "9.20.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz",
"integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.0", "@eslint/config-array": "^0.19.0",
"@eslint/core": "^0.10.0", "@eslint/core": "^0.11.0",
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
"@eslint/js": "9.19.0", "@eslint/js": "9.20.0",
"@eslint/plugin-kit": "^0.2.5", "@eslint/plugin-kit": "^0.2.5",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
@@ -5511,6 +5539,85 @@
"eslint": ">=7.0.0" "eslint": ">=7.0.0"
} }
}, },
"node_modules/eslint-plugin-vue": {
"version": "9.32.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.32.0.tgz",
"integrity": "sha512-b/Y05HYmnB/32wqVcjxjHZzNpwxj1onBOvqW89W+V+XNG1dRuaFbNd3vT9CLbr2LXjEoq+3vn8DanWf7XU22Ug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"globals": "^13.24.0",
"natural-compare": "^1.4.0",
"nth-check": "^2.1.1",
"postcss-selector-parser": "^6.0.15",
"semver": "^7.6.3",
"vue-eslint-parser": "^9.4.3",
"xml-name-validator": "^4.0.0"
},
"engines": {
"node": "^14.17.0 || >=16.0.0"
},
"peerDependencies": {
"eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
}
},
"node_modules/eslint-plugin-vue/node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
"integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"type-fest": "^0.20.2"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-vue/node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/eslint-plugin-vue/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/eslint-plugin-vue/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@@ -5533,6 +5640,19 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/eslint/node_modules/@eslint/core": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz",
"integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/eslint/node_modules/eslint-scope": { "node_modules/eslint/node_modules/eslint-scope": {
"version": "8.2.0", "version": "8.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
@@ -10331,6 +10451,102 @@
"npm": ">= 3.0.0" "npm": ">= 3.0.0"
} }
}, },
"node_modules/vue-eslint-parser": {
"version": "9.4.3",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
"integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4",
"eslint-scope": "^7.1.1",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.3.1",
"esquery": "^1.4.0",
"lodash": "^4.17.21",
"semver": "^7.3.6"
},
"engines": {
"node": "^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
},
"peerDependencies": {
"eslint": ">=6.0.0"
}
},
"node_modules/vue-eslint-parser/node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
"integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-eslint-parser/node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-eslint-parser/node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/vue-eslint-parser/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/vue-hot-reload-api": { "node_modules/vue-hot-reload-api": {
"version": "2.3.4", "version": "2.3.4",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
@@ -10751,6 +10967,16 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12"
}
},
"node_modules/xmlbuilder": { "node_modules/xmlbuilder": {
"version": "15.1.1", "version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
@@ -10887,6 +11113,23 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
},
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"dev": true,
"license": "0BSD"
} }
} }
} }
+3
View File
@@ -41,12 +41,15 @@
"copy-webpack-plugin": "^12.0.2", "copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"dayjs": "^1.11.13",
"default-passive-events": "^2.0.0", "default-passive-events": "^2.0.0",
"echarts": "^5.6.0",
"electron": "^34.0.2", "electron": "^34.0.2",
"electron-builder": "^25.1.8", "electron-builder": "^25.1.8",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"eslint": "^9.19.0", "eslint": "^9.19.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-vue": "^9.32.0",
"html-webpack-plugin": "^5.6.3", "html-webpack-plugin": "^5.6.3",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.2",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
+22 -1
View File
@@ -16,6 +16,10 @@ import VueLazyload from 'vue-lazyload';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import { DataTables } from 'vue-data-tables'; import { DataTables } from 'vue-data-tables';
import ElementUI from 'element-ui'; import ElementUI from 'element-ui';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import * as workerTimers from 'worker-timers'; import * as workerTimers from 'worker-timers';
import 'default-passive-events'; import 'default-passive-events';
@@ -33,6 +37,7 @@ import _vrcxJsonStorage from './classes/vrcxJsonStorage.js';
// tabs // tabs
import ModerationTab from './views/tabs/Moderation.vue'; import ModerationTab from './views/tabs/Moderation.vue';
import ChartsTab from './views/tabs/Charts.vue';
// components // components
import SimpleSwitch from './components/settings/SimpleSwitch.vue'; import SimpleSwitch from './components/settings/SimpleSwitch.vue';
@@ -109,6 +114,13 @@ console.log(`isLinux: ${LINUX}`);
}); });
// #endregion // #endregion
// #region | date utility library
// - dayjs plugin init
dayjs.extend(duration);
dayjs.extend(utc);
dayjs.extend(timezone);
// #endregion
// everything in this program is global stored in $app, I hate it, it is what it is // everything in this program is global stored in $app, I hate it, it is what it is
let $app = {}; let $app = {};
const API = new _apiInit($app); const API = new _apiInit($app);
@@ -164,6 +176,7 @@ console.log(`isLinux: ${LINUX}`);
components: { components: {
// tabs // tabs
ModerationTab, ModerationTab,
ChartsTab,
// components // components
// - settings // - settings
@@ -173,6 +186,12 @@ console.log(`isLinux: ${LINUX}`);
// - sidebar(friendsListSidebar) // - sidebar(friendsListSidebar)
GroupsSidebar GroupsSidebar
}, },
provide() {
return {
API,
showUserDialog: this.showUserDialog
};
},
el: '#x-app', el: '#x-app',
async mounted() { async mounted() {
await this.initLanguage(); await this.initLanguage();
@@ -4824,7 +4843,9 @@ console.log(`isLinux: ${LINUX}`);
}); });
worldName = args.ref.name; worldName = args.ref.name;
} }
} catch (err) {} } catch (e) {
throw e;
}
return worldName; return worldName;
}; };
+10 -2
View File
@@ -89,7 +89,8 @@ export default class extends baseClass {
link: { link: {
type: Boolean, type: Boolean,
default: true default: true
} },
isOpenPreviousInstanceInfoDialog: Boolean
}, },
data() { data() {
return { return {
@@ -177,7 +178,14 @@ export default class extends baseClass {
API.$emit('SHOW_WORLD_DIALOG_SHORTNAME', this.hint); API.$emit('SHOW_WORLD_DIALOG_SHORTNAME', this.hint);
return; return;
} }
API.$emit('SHOW_WORLD_DIALOG', instanceId); if (this.isOpenPreviousInstanceInfoDialog) {
this.$emit(
'open-previous-instance-info-dialog',
instanceId
);
} else {
API.$emit('SHOW_WORLD_DIALOG', instanceId);
}
} }
}, },
showGroupDialog() { showGroupDialog() {
+4 -4
View File
@@ -40,13 +40,13 @@ export default {
return obj; return obj;
}, },
timeToText(sec) { timeToText(sec, isNeedSeconds = false) {
var n = Number(sec); let n = Number(sec);
if (isNaN(n)) { if (isNaN(n)) {
return this.escapeTag(sec); return this.escapeTag(sec);
} }
n = Math.floor(n / 1000); n = Math.floor(n / 1000);
var arr = []; const arr = [];
if (n < 0) { if (n < 0) {
n = -n; n = -n;
} }
@@ -62,7 +62,7 @@ export default {
arr.push(`${Math.floor(n / 60)}m`); arr.push(`${Math.floor(n / 60)}m`);
n %= 60; n %= 60;
} }
if (arr.length === 0 && n < 60) { if (isNeedSeconds || (arr.length === 0 && n < 60)) {
arr.push(`${n}s`); arr.push(`${n}s`);
} }
return arr.join(' '); return arr.join(' ');
+421
View File
@@ -0,0 +1,421 @@
<template>
<div>
<div class="options-container flex-between" style="margin-top: 0">
<span style="margin-top: 10px">Instance Activity</span>
<el-date-picker
v-model="selectedDate"
type="date"
:clearable="false"
align="right"
:picker-options="{
disabledDate: (time) => getDatePickerDisabledDate(time)
}"
@change="handleSelectDate"
></el-date-picker>
</div>
<div style="position: relative">
<el-statistic title="Total 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="!isLoading && activityData.length !== 0" class="divider"><el-divider>·</el-divider></div>
</transition>
<instance-activity-detail
v-for="arr in activityDetailData"
:key="arr[0].location"
ref="activityDetailChartRef"
:activity-data="activityData"
:activity-detail-data="arr"
:is-dark-mode="isDarkMode"
style="width: 100%"
@open-previous-instance-info-dialog="$emit('open-previous-instance-info-dialog', $event)"
/>
</div>
</template>
<script>
import dayjs from 'dayjs';
import database from '../../repository/database';
import utils from '../../classes/utils';
import InstanceActivityDetail from './InstanceActivityDetail.vue';
let echarts = null;
export default {
name: 'InstanceActivity',
components: {
InstanceActivityDetail
},
inject: ['API'],
props: {
getWorldName: Function,
isDarkMode: Boolean
},
data() {
return {
echartsInstance: null,
resizeObserver: null,
intersectionObservers: [],
selectedDate: dayjs().add(-1, 'day'),
activityData: [],
activityDetailData: [],
// previousDarkMode: this.isDarkMode,
allDateOfActivity: null,
firstDateOfActivity: null,
worldNameArray: [],
isLoading: true
};
},
computed: {
totalOnlineTime() {
return utils.timeToText(
this.activityData.reduce((acc, item) => acc + item.time, 0),
true
);
}
},
activated() {
// first time also call activated
if (!this.echartsInstance) {
return;
}
// if (this.isDarkMode === this.previousDarkMode) {
// when tab activated, play animation
this.echartsInstance.clear();
this.initEcharts();
// }
},
deactivated() {
// prevent switch tab play resize animation
this.resizeObserver.disconnect();
},
created() {
this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
this.echartsInstance.resize({
width: entry.contentRect.width,
animation: {
duration: 300
}
});
}
});
},
async mounted() {
try {
const [echartsModule] = await Promise.all([
// lazy load echarts
// reduce the VRCX initial screen load times
// TODO: export lazy load func to a single file
import('echarts').catch((error) => {
console.error('lazy load echarts failed', error);
return null;
}),
this.getActivityData()
]);
if (echartsModule) {
echarts = echartsModule;
}
if (this.activityData.length && echarts) {
// actvity data is ready, but world name data isn't ready
// so init echarts with empty data, redcuce the render time of init screen
// TODO: move to created lifecycle, init screen faster
this.initEcharts(true);
this.getAllDateOfActivity();
this.getWorldNameData();
} else {
this.isLoading = false;
}
} catch (error) {
console.error('error in mounted', error);
}
},
methods: {
// reload data
async handleSelectDate() {
this.isLoading = true;
await this.getActivityData();
this.getWorldNameData();
},
initEcharts(isFirstTime = false) {
const chartsHeight = this.activityData.length * 40 + 200;
const chartDom = this.$refs.activityChartRef;
if (!this.echartsInstance) {
this.echartsInstance = echarts.init(chartDom, `${this.isDarkMode ? 'dark' : null}`, {
height: chartsHeight
});
this.resizeObserver.observe(chartDom);
}
this.echartsInstance.resize({
height: chartsHeight,
animation: {
duration: 300
}
});
requestAnimationFrame(() => {
this.echartsInstance.setOption(this.getNewOption(isFirstTime), { lazyUpdate: true });
this.echartsInstance.on('click', 'yAxis', this.hanleClickYAxisLable);
this.isLoading = false;
});
},
hanleClickYAxisLable(params) {
const detailDataIdx = this.activityDetailData.findIndex(
(arr) => arr[0]?.location === this.activityData[params?.dataIndex]?.location
);
if (detailDataIdx !== -1) {
this.$refs.activityDetailChartRef[detailDataIdx].$el.scrollIntoView({
behavior: 'smooth',
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.firstDateOfActivity = dayjs.utc(utcDateStrings[0]).startOf('day');
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) {
const getTooltip = (params) => {
const activityData = this.activityData;
const param = params[1];
if (!activityData || !activityData[param.dataIndex]) {
return '';
}
const instanceData = activityData[param.dataIndex];
const formattedLeftDateTime = dayjs(instanceData.leaveTime).format('HH:mm:ss');
const formattedJoinDateTime = dayjs(instanceData.joinTime).format('HH:mm:ss');
const timeString = utils.timeToText(param.data, true);
const color = param.color;
const name = param.name;
const location = utils.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 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) =>
value === 24 * 60 * 60 * 1000 ? '24:00' : dayjs(value).utc().format('HH:mm')
},
splitLine: { lineStyle: { type: 'dashed' } }
},
series: [
{
name: 'Placeholder',
type: 'bar',
stack: 'Total',
itemStyle: {
borderColor: 'transparent',
color: 'transparent'
},
emphasis: {
itemStyle: {
borderColor: 'transparent',
color: 'transparent'
}
},
data: isFirstTime
? []
: 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: 30,
itemStyle: {
borderRadius: 2,
shadowBlur: 2,
shadowOffsetX: 0.7,
shadowOffsetY: 0.5
},
data: isFirstTime
? []
: 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;
})
}
]
};
if (this.isDarkMode) {
echartsOption.backgroundColor = 'rgba(0, 0, 0, 0)';
}
return echartsOption;
}
}
};
</script>
<style scoped>
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.nodata {
display: flex;
align-items: center;
justify-content: center;
margin-top: 200px;
color: #5c5c5c;
}
.divider {
padding: 0 400px;
transition: top 0.3s ease;
}
.el-date-editor.el-input,
.el-date-editor.el-input__inner {
width: 200px;
}
.el-divider__text {
padding-left: 10px;
padding-right: 10px;
}
</style>
@@ -0,0 +1,216 @@
<template>
<div>
<div style="height: 25px; margin-top: 60px">
<transition name="el-fade-in-linear">
<location
v-show="!isLoading"
class="location"
:location="activityDetailData[0].location"
is-open-previous-instance-info-dialog
@open-previous-instance-info-dialog="$emit('open-previous-instance-info-dialog', $event)"
></location>
</transition>
</div>
<div ref="activityDetailChart"></div>
</div>
</template>
<script>
import dayjs from 'dayjs';
import utils from '../../classes/utils';
let echarts = null;
export default {
name: 'InstanceActivityDetail',
inject: ['API'],
props: {
activityDetailData: {
type: Array,
required: true
},
activityData: {
type: Array,
required: true
},
isDarkMode: {
type: Boolean,
required: true
}
},
data() {
return {
isLoading: true,
echartsInstance: null,
resizeObserver: null
};
},
computed: {
startTimeStamp() {
return this.activityData
.find((item) => item.location === this.activityDetailData[0].location)
?.joinTime.valueOf();
},
endTimeStamp() {
return this.activityData
.findLast((item) => item.location === this.activityDetailData[0].location)
?.leaveTime.valueOf();
}
},
created() {
this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
this.echartsInstance.resize({
width: entry.contentRect.width,
animation: {
duration: 300
}
});
}
});
},
deactivated() {
// prevent switch tab play resize animation
this.resizeObserver.disconnect();
},
methods: {
async initEcharts() {
// TODO: unnecessary import, import from individual js file
await import('echarts').then((echartsModule) => {
echarts = echartsModule;
});
const chartDom = this.$refs.activityDetailChart;
if (!this.echartsInstance) {
this.echartsInstance = echarts.init(chartDom, `${this.isDarkMode ? 'dark' : null}`, {
height: this.activityDetailData.length * 40 + 200,
useDirtyRect: this.activityDetailData.length > 30
});
this.resizeObserver.observe(chartDom);
}
this.echartsInstance.resize({
height: this.activityDetailData.length * 40 + 200,
animation: {
duration: 300
}
});
this.echartsInstance.setOption(this.getNewOption(), { lazyUpdate: true });
setTimeout(() => {
this.isLoading = false;
}, 200);
},
getNewOption() {
const getTooltip = (params) => {
const activityDetailData = this.activityDetailData;
const param = params[1];
if (!activityDetailData || !activityDetailData[param.dataIndex]) {
return '';
}
const instanceData = activityDetailData[param.dataIndex];
const formattedLeftDateTime = dayjs(instanceData.leaveTime).format('HH:mm:ss');
const formattedJoinDateTime = dayjs(instanceData.joinTime).format('HH:mm:ss');
const timeString = utils.timeToText(instanceData.time, true);
const color = param.color;
return `
<div style="display: flex; align-items: center;">
<div style="width: 10px; height: 55px; background-color: ${color}; margin-right: 5px;"></div>
<div>
<div>${instanceData.display_name}</div>
<div>${formattedJoinDateTime} - ${formattedLeftDateTime}</div>
<div>${timeString}</div>
</div>
</div>
`;
};
const echartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: getTooltip
},
grid: {
top: 60,
left: 160,
right: 90
},
yAxis: {
type: 'category',
axisLabel: {
interval: 0,
formatter: (value) => (value.length > 20 ? `${value.slice(0, 20)}...` : value)
},
inverse: true,
data: this.activityDetailData.map((item) => item.display_name)
},
xAxis: {
type: 'value',
min: 0,
max: this.endTimeStamp - this.startTimeStamp,
axisLine: { show: true },
axisLabel: {
formatter: (value) => dayjs(value + this.startTimeStamp).format('HH:mm')
},
splitLine: { lineStyle: { type: 'dashed' } }
},
series: [
{
name: 'Placeholder',
type: 'bar',
stack: 'Total',
itemStyle: {
borderColor: 'transparent',
color: 'transparent'
},
emphasis: {
itemStyle: {
borderColor: 'transparent',
color: 'transparent'
}
},
data: this.activityDetailData.map((item) => item.joinTime.valueOf() - this.startTimeStamp)
},
{
name: 'Time',
type: 'bar',
stack: 'Total',
colorBy: 'data',
barWidth: 30,
itemStyle: {
borderRadius: 2,
shadowBlur: 2,
shadowOffsetX: 0.7,
shadowOffsetY: 0.5
},
data: this.activityDetailData.map((item) => item.time)
}
]
};
if (this.isDarkMode) {
echartsOption.backgroundColor = 'rgba(0, 0, 0, 0)';
}
return echartsOption;
}
}
};
</script>
<style scoped>
.location {
display: flex;
align-items: center;
justify-content: center;
}
</style>
+11 -3
View File
@@ -51,6 +51,7 @@ html
+menuitem('moderation', "{{ $t('nav_tooltip.moderation') }}", 'el-icon-finished') +menuitem('moderation', "{{ $t('nav_tooltip.moderation') }}", 'el-icon-finished')
+menuitem('notification', "{{ $t('nav_tooltip.notification') }}", 'el-icon-bell') +menuitem('notification', "{{ $t('nav_tooltip.notification') }}", 'el-icon-bell')
+menuitem('friendsList', "{{ $t('nav_tooltip.friend_list') }}", 'el-icon-s-management') +menuitem('friendsList', "{{ $t('nav_tooltip.friend_list') }}", 'el-icon-s-management')
+menuitem('charts', "{{ $t('nav_tooltip.charts') }}", 'el-icon-data-analysis')
+menuitem('profile', "{{ $t('nav_tooltip.profile') }}", 'el-icon-user') +menuitem('profile', "{{ $t('nav_tooltip.profile') }}", 'el-icon-user')
+menuitem('settings', "{{ $t('nav_tooltip.settings') }}", 'el-icon-s-tools') +menuitem('settings', "{{ $t('nav_tooltip.settings') }}", 'el-icon-s-tools')
@@ -83,10 +84,9 @@ html
//- moderation //- moderation
moderation-tab( moderation-tab(
v-if='$refs.menu?.activeIndex === "moderation"' v-if='$refs.menu?.activeIndex === "moderation"'
:Api='API'
:table-data='playerModerationTable' :table-data='playerModerationTable'
:show-user-dialog='showUserDialog' :shift-held='shiftHeld'
:shift-held='shiftHeld') :hide-tooltips='hideTooltips')
//- notification //- notification
include ./mixins/tabs/notifications.pug include ./mixins/tabs/notifications.pug
@@ -100,6 +100,14 @@ html
include ./mixins/tabs/friendsList.pug include ./mixins/tabs/friendsList.pug
+friendsListTab +friendsListTab
//- charts
keep-alive
charts-tab(
v-if='$refs.menu?.activeIndex === "charts"'
:get-world-name='getWorldName'
:is-dark-mode='isDarkMode'
@open-previous-instance-info-dialog='showPreviousInstanceInfoDialog')
//- settings //- settings
include ./mixins/tabs/settings.pug include ./mixins/tabs/settings.pug
+settingsTab +settingsTab
+1
View File
@@ -11,6 +11,7 @@
"moderation": "Moderation", "moderation": "Moderation",
"notification": "Notification", "notification": "Notification",
"friend_list": "Friend List", "friend_list": "Friend List",
"charts": "Charts",
"profile": "Profile", "profile": "Profile",
"settings": "Settings" "settings": "Settings"
}, },
@@ -398,3 +398,36 @@ mixin openSourceSoftwareNotice
OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE. DEALINGS IN THE FONT SOFTWARE.
div(style='margin-top: 15px')
p(style='font-weight: bold') Apache ECharts
pre(style='font-size: 12px; white-space: pre-line').
Apache License 2.0
Copyright 2017-2025 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (https://www.apache.org/).
div(style='margin-top: 15px')
p(style='font-weight: bold') dayjs
pre(style='font-size: 12px; white-space: pre-line').
MIT License
Copyright (c) 2018-present, iamkun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -1
View File
@@ -1,5 +1,6 @@
mixin friendsListSidebar mixin friendsListSidebar
#aside.x-aside-container(v-show='$refs.menu && $refs.menu.activeIndex !== \'friendsList\'') #aside.x-aside-container(
v-show='$refs.menu && !($refs.menu.activeIndex == "friendsList" || $refs.menu.activeIndex == "charts") ')
div(style='display: flex; align-items: baseline') div(style='display: flex; align-items: baseline')
el-select( el-select(
v-model='quickSearch' v-model='quickSearch'
+74
View File
@@ -2772,6 +2772,80 @@ class Database {
); );
return instances; return instances;
} }
/**
*
* @param {string} startDate: utc string of startOfDay
* @param {string} endDate: utc string endOfDay
* @returns
*/
async getInstanceActivity(startDate, endDate) {
const currentUserData = [];
const detailData = new Map();
await sqliteService.execute(
(row) => {
const rowData = {
id: row[0],
created_at: row[1],
type: row[2],
display_name: row[3],
location: row[4],
user_id: row[5],
time: row[6]
};
// skip dirty data
if (!rowData.location || rowData.location === 'traveling') {
return;
}
if (rowData.user_id === Database.userId) {
currentUserData.push(rowData);
}
const instanceData = detailData.get(rowData.location);
detailData.set(rowData.location, [
...(instanceData || []),
rowData
]);
},
`SELECT
*
FROM
gamelog_join_leave
WHERE type = "OnPlayerLeft"
AND (
strftime('%Y-%m-%dT%H:%M:%SZ', created_at, '-' || (time * 1.0 / 1000) || ' seconds') BETWEEN @utc_start_date AND @utc_end_date
OR created_at BETWEEN @utc_start_date AND @utc_end_date
);`,
{
'@utc_start_date': startDate,
'@utc_end_date': endDate
}
);
return { currentUserData, detailData };
}
/**
* Get the All Date of Instance Activity for the current user
* @returns {Promise<null>}
*/
async getDateOfInstanceActivity() {
let result = [];
await sqliteService.execute(
(row) => {
result.push(row[0]);
},
`SELECT created_at
FROM gamelog_join_leave
WHERE user_id = @userId`,
{
'@userId': Database.userId
}
);
return result;
}
} }
var self = new Database(); var self = new Database();
+7
View File
@@ -432,3 +432,10 @@ button {
padding: 2px 2px; padding: 2px 2px;
border-radius: 4px; border-radius: 4px;
} }
.el-divider {
background-color: #606266;
}
.el-divider__text {
background: #222;
color: #ffffff;
}
+8
View File
@@ -696,3 +696,11 @@ i[class='el-icon-star-off']:not(.el-menu-item div.el-tooltip i) {
.el-color-picker__panel { .el-color-picker__panel {
background-color: var(--dv_bg-top); background-color: var(--dv_bg-top);
} }
.el-divider {
background-color: #606266;
}
.el-divider__text {
background: var(--dv_bg-top);
color: #efefef;
padding-bottom: 2px;
}
+7
View File
@@ -318,3 +318,10 @@ path[stroke='#e5e9f2'] {
border: transparent; border: transparent;
background-color: #333 !important; background-color: #333 !important;
} }
.el-divider {
background-color: #606266;
}
.el-divider__text {
background: var(--farback);
color: #efefef;
}
+7
View File
@@ -2012,3 +2012,10 @@ i.x-user-status {
.el-dialog__body .el-tag--mini { .el-dialog__body .el-tag--mini {
line-height: 28px; line-height: 28px;
} }
.el-divider {
background-color: #606266;
}
.el-divider__text {
background: rgba(var(--md-sys-color-background));
color: #efefef;
}
+7
View File
@@ -366,3 +366,10 @@ input[type='checkbox']:checked + .el-switch__core {
border: transparent; border: transparent;
background-color: #333; background-color: #333;
} }
.el-divider {
background-color: #606266;
}
.el-divider__text {
background: var(--bg);
color: #efefef;
}
+29
View File
@@ -0,0 +1,29 @@
<template>
<div class="x-container" id="chart">
<div class="options-container" style="margin-top: 0">
<span class="header">Charts</span>
</div>
<instance-activity
:get-world-name="getWorldName"
:is-dark-mode="isDarkMode"
@open-previous-instance-info-dialog="$emit('open-previous-instance-info-dialog', $event)"
id="instance-activity"
></instance-activity>
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop>
</div>
</template>
<script>
import InstanceActivity from '../../components/charts/InstanceActivity.vue';
export default {
name: 'ChartsTab',
inject: ['API'],
props: {
getWorldName: Function,
isDarkMode: Boolean
},
components: {
InstanceActivity
}
};
</script>
+27 -65
View File
@@ -6,7 +6,7 @@
:filters="filters" :filters="filters"
:tableProps="tableProps" :tableProps="tableProps"
:paginationProps="paginationProps" :paginationProps="paginationProps"
v-loading="Api.isPlayerModerationsLoading" v-loading="API.isPlayerModerationsLoading"
> >
<template slot="tool"> <template slot="tool">
<div class="tool-slot"> <div class="tool-slot">
@@ -37,48 +37,30 @@
> >
<el-button <el-button
type="default" type="default"
:loading="Api.isPlayerModerationsLoading" :loading="API.isPlayerModerationsLoading"
@click="Api.refreshPlayerModerations()" @click="API.refreshPlayerModerations()"
icon="el-icon-refresh" icon="el-icon-refresh"
circle circle
/> />
</el-tooltip> </el-tooltip>
</div> </div>
</template> </template>
<el-table-column <el-table-column :label="$t('table.moderation.date')" prop="created" sortable="custom" width="120">
:label="$t('table.moderation.date')"
prop="created"
sortable="custom"
width="120"
>
<template slot-scope="scope"> <template slot-scope="scope">
<el-tooltip placement="right"> <el-tooltip placement="right">
<template slot="content"> <template slot="content">
<span>{{ <span>{{ scope.row.created | formatDate('long') }}</span>
scope.row.created | formatDate('long')
}}</span>
</template> </template>
<span>{{ <span>{{ scope.row.created | formatDate('short') }}</span>
scope.row.created | formatDate('short')
}}</span>
</el-tooltip> </el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column :label="$t('table.moderation.type')" prop="type" width="100">
:label="$t('table.moderation.type')"
prop="type"
width="100"
>
<template slot-scope="scope"> <template slot-scope="scope">
<span <span v-text="$t('view.moderation.filters.' + scope.row.type)"></span>
v-text="$t('view.moderation.filters.' + scope.row.type)"
></span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column :label="$t('table.moderation.source')" prop="sourceDisplayName">
:label="$t('table.moderation.source')"
prop="sourceDisplayName"
>
<template slot-scope="scope"> <template slot-scope="scope">
<span <span
class="x-link" class="x-link"
@@ -87,10 +69,7 @@
></span> ></span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column :label="$t('table.moderation.target')" prop="targetDisplayName">
:label="$t('table.moderation.target')"
prop="targetDisplayName"
>
<template slot-scope="scope"> <template slot-scope="scope">
<span <span
class="x-link" class="x-link"
@@ -99,15 +78,9 @@
></span> ></span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column :label="$t('table.moderation.action')" width="80" align="right">
:label="$t('table.moderation.action')"
width="80"
align="right"
>
<template slot-scope="scope"> <template slot-scope="scope">
<template <template v-if="scope.row.sourceUserId === API.currentUser.id">
v-if="scope.row.sourceUserId === Api.currentUser.id"
>
<el-button <el-button
v-if="shiftHeld" v-if="shiftHeld"
style="color: #f56c6c" style="color: #f56c6c"
@@ -135,18 +108,15 @@
export default { export default {
name: 'ModerationTab', name: 'ModerationTab',
inject: ['API', 'showUserDialog'],
props: { props: {
Api: Object,
tableData: Object, tableData: Object,
showUserDialog: Function, shiftHeld: Boolean,
shiftHeld: Boolean hideTooltips: Boolean
}, },
created: async function () { created: async function () {
this.filters[0].value = JSON.parse( this.filters[0].value = JSON.parse(
await configRepository.getString( await configRepository.getString('VRCX_playerModerationTableFilters', '[]')
'VRCX_playerModerationTableFilters',
'[]'
)
); );
}, },
data() { data() {
@@ -155,8 +125,7 @@
{ {
prop: 'type', prop: 'type',
value: [], value: [],
filterFn: (row, filter) => filterFn: (row, filter) => filter.value.some((v) => v === row.type)
filter.value.some((v) => v === row.type)
}, },
{ {
prop: ['sourceDisplayName', 'targetDisplayName'], prop: ['sourceDisplayName', 'targetDisplayName'],
@@ -191,32 +160,25 @@
}, },
methods: { methods: {
saveTableFilters() { saveTableFilters() {
configRepository.setString( configRepository.setString('VRCX_playerModerationTableFilters', JSON.stringify(this.filters[0].value));
'VRCX_playerModerationTableFilters',
JSON.stringify(this.filters[0].value)
);
}, },
deletePlayerModeration(row) { deletePlayerModeration(row) {
this.Api.deletePlayerModeration({ this.API.deletePlayerModeration({
moderated: row.targetUserId, moderated: row.targetUserId,
type: row.type type: row.type
}); });
}, },
deletePlayerModerationPrompt(row) { deletePlayerModerationPrompt(row) {
this.$confirm( this.$confirm(`Continue? Delete Moderation ${row.type}`, 'Confirm', {
`Continue? Delete Moderation ${row.type}`, confirmButtonText: 'Confirm',
'Confirm', cancelButtonText: 'Cancel',
{ type: 'info',
confirmButtonText: 'Confirm', callback: (action) => {
cancelButtonText: 'Cancel', if (action === 'confirm') {
type: 'info', this.deletePlayerModeration(row);
callback: (action) => {
if (action === 'confirm') {
this.deletePlayerModeration(row);
}
} }
} }
); });
} }
} }
}; };
+2 -1
View File
@@ -13,7 +13,8 @@ module.exports = {
'noty', 'noty',
'vue', 'vue',
'vue-data-tables', 'vue-data-tables',
'vue-lazyload' 'vue-lazyload',
'dayjs'
], ],
app: { app: {
import: ['./src/app.js', './src/app.scss'], import: ['./src/app.js', './src/app.scss'],