mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-14 12:23:52 +02:00
feat: Instance Activity Chart (#1141)
* feat: Instance Activity Chart * fix: chart data handling
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["eslint:all", "prettier"],
|
||||
"extends": ["eslint:all", "plugin:vue/recommended", "prettier"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es2021": true
|
||||
},
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser",
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
|
||||
259
package-lock.json
generated
259
package-lock.json
generated
@@ -25,12 +25,15 @@
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"default-passive-events": "^2.0.0",
|
||||
"echarts": "^5.6.0",
|
||||
"electron": "^34.0.2",
|
||||
"electron-builder": "^25.1.8",
|
||||
"element-ui": "^2.15.14",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
@@ -1967,9 +1970,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
|
||||
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
|
||||
"version": "9.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz",
|
||||
"integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4811,6 +4814,13 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
@@ -5164,6 +5174,24 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||
@@ -5439,18 +5467,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.19.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
|
||||
"integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
|
||||
"version": "9.20.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz",
|
||||
"integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.19.0",
|
||||
"@eslint/core": "^0.10.0",
|
||||
"@eslint/core": "^0.11.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "9.19.0",
|
||||
"@eslint/js": "9.20.0",
|
||||
"@eslint/plugin-kit": "^0.2.5",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@@ -5511,6 +5539,85 @@
|
||||
"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": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
@@ -5533,6 +5640,19 @@
|
||||
"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": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
|
||||
@@ -10331,6 +10451,102 @@
|
||||
"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": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
|
||||
@@ -10751,6 +10967,16 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "15.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
|
||||
@@ -10887,6 +11113,23 @@
|
||||
"funding": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +41,15 @@
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"default-passive-events": "^2.0.0",
|
||||
"echarts": "^5.6.0",
|
||||
"electron": "^34.0.2",
|
||||
"electron-builder": "^25.1.8",
|
||||
"element-ui": "^2.15.14",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
|
||||
23
src/app.js
23
src/app.js
@@ -16,6 +16,10 @@ import VueLazyload from 'vue-lazyload';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import { DataTables } from 'vue-data-tables';
|
||||
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 'default-passive-events';
|
||||
|
||||
@@ -33,6 +37,7 @@ import _vrcxJsonStorage from './classes/vrcxJsonStorage.js';
|
||||
|
||||
// tabs
|
||||
import ModerationTab from './views/tabs/Moderation.vue';
|
||||
import ChartsTab from './views/tabs/Charts.vue';
|
||||
|
||||
// components
|
||||
import SimpleSwitch from './components/settings/SimpleSwitch.vue';
|
||||
@@ -109,6 +114,13 @@ console.log(`isLinux: ${LINUX}`);
|
||||
});
|
||||
// #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
|
||||
let $app = {};
|
||||
const API = new _apiInit($app);
|
||||
@@ -164,6 +176,7 @@ console.log(`isLinux: ${LINUX}`);
|
||||
components: {
|
||||
// tabs
|
||||
ModerationTab,
|
||||
ChartsTab,
|
||||
|
||||
// components
|
||||
// - settings
|
||||
@@ -173,6 +186,12 @@ console.log(`isLinux: ${LINUX}`);
|
||||
// - sidebar(friendsListSidebar)
|
||||
GroupsSidebar
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
API,
|
||||
showUserDialog: this.showUserDialog
|
||||
};
|
||||
},
|
||||
el: '#x-app',
|
||||
async mounted() {
|
||||
await this.initLanguage();
|
||||
@@ -4824,7 +4843,9 @@ console.log(`isLinux: ${LINUX}`);
|
||||
});
|
||||
worldName = args.ref.name;
|
||||
}
|
||||
} catch (err) {}
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
return worldName;
|
||||
};
|
||||
|
||||
|
||||
@@ -89,7 +89,8 @@ export default class extends baseClass {
|
||||
link: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
isOpenPreviousInstanceInfoDialog: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -177,7 +178,14 @@ export default class extends baseClass {
|
||||
API.$emit('SHOW_WORLD_DIALOG_SHORTNAME', this.hint);
|
||||
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() {
|
||||
|
||||
@@ -40,13 +40,13 @@ export default {
|
||||
return obj;
|
||||
},
|
||||
|
||||
timeToText(sec) {
|
||||
var n = Number(sec);
|
||||
timeToText(sec, isNeedSeconds = false) {
|
||||
let n = Number(sec);
|
||||
if (isNaN(n)) {
|
||||
return this.escapeTag(sec);
|
||||
}
|
||||
n = Math.floor(n / 1000);
|
||||
var arr = [];
|
||||
const arr = [];
|
||||
if (n < 0) {
|
||||
n = -n;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export default {
|
||||
arr.push(`${Math.floor(n / 60)}m`);
|
||||
n %= 60;
|
||||
}
|
||||
if (arr.length === 0 && n < 60) {
|
||||
if (isNeedSeconds || (arr.length === 0 && n < 60)) {
|
||||
arr.push(`${n}s`);
|
||||
}
|
||||
return arr.join(' ');
|
||||
|
||||
421
src/components/charts/InstanceActivity.vue
Normal file
421
src/components/charts/InstanceActivity.vue
Normal 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>
|
||||
216
src/components/charts/InstanceActivityDetail.vue
Normal file
216
src/components/charts/InstanceActivityDetail.vue
Normal file
@@ -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>
|
||||
@@ -51,6 +51,7 @@ html
|
||||
+menuitem('moderation', "{{ $t('nav_tooltip.moderation') }}", 'el-icon-finished')
|
||||
+menuitem('notification', "{{ $t('nav_tooltip.notification') }}", 'el-icon-bell')
|
||||
+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('settings', "{{ $t('nav_tooltip.settings') }}", 'el-icon-s-tools')
|
||||
|
||||
@@ -83,10 +84,9 @@ html
|
||||
//- moderation
|
||||
moderation-tab(
|
||||
v-if='$refs.menu?.activeIndex === "moderation"'
|
||||
:Api='API'
|
||||
:table-data='playerModerationTable'
|
||||
:show-user-dialog='showUserDialog'
|
||||
:shift-held='shiftHeld')
|
||||
:shift-held='shiftHeld'
|
||||
:hide-tooltips='hideTooltips')
|
||||
|
||||
//- notification
|
||||
include ./mixins/tabs/notifications.pug
|
||||
@@ -100,6 +100,14 @@ html
|
||||
include ./mixins/tabs/friendsList.pug
|
||||
+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
|
||||
include ./mixins/tabs/settings.pug
|
||||
+settingsTab
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"moderation": "Moderation",
|
||||
"notification": "Notification",
|
||||
"friend_list": "Friend List",
|
||||
"charts": "Charts",
|
||||
"profile": "Profile",
|
||||
"settings": "Settings"
|
||||
},
|
||||
|
||||
@@ -398,3 +398,36 @@ mixin openSourceSoftwareNotice
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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')
|
||||
el-select(
|
||||
v-model='quickSearch'
|
||||
|
||||
@@ -2772,6 +2772,80 @@ class Database {
|
||||
);
|
||||
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();
|
||||
|
||||
@@ -432,3 +432,10 @@ button {
|
||||
padding: 2px 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.el-divider {
|
||||
background-color: #606266;
|
||||
}
|
||||
.el-divider__text {
|
||||
background: #222;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -696,3 +696,11 @@ i[class='el-icon-star-off']:not(.el-menu-item div.el-tooltip i) {
|
||||
.el-color-picker__panel {
|
||||
background-color: var(--dv_bg-top);
|
||||
}
|
||||
.el-divider {
|
||||
background-color: #606266;
|
||||
}
|
||||
.el-divider__text {
|
||||
background: var(--dv_bg-top);
|
||||
color: #efefef;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
@@ -318,3 +318,10 @@ path[stroke='#e5e9f2'] {
|
||||
border: transparent;
|
||||
background-color: #333 !important;
|
||||
}
|
||||
.el-divider {
|
||||
background-color: #606266;
|
||||
}
|
||||
.el-divider__text {
|
||||
background: var(--farback);
|
||||
color: #efefef;
|
||||
}
|
||||
|
||||
@@ -2012,3 +2012,10 @@ i.x-user-status {
|
||||
.el-dialog__body .el-tag--mini {
|
||||
line-height: 28px;
|
||||
}
|
||||
.el-divider {
|
||||
background-color: #606266;
|
||||
}
|
||||
.el-divider__text {
|
||||
background: rgba(var(--md-sys-color-background));
|
||||
color: #efefef;
|
||||
}
|
||||
|
||||
@@ -366,3 +366,10 @@ input[type='checkbox']:checked + .el-switch__core {
|
||||
border: transparent;
|
||||
background-color: #333;
|
||||
}
|
||||
.el-divider {
|
||||
background-color: #606266;
|
||||
}
|
||||
.el-divider__text {
|
||||
background: var(--bg);
|
||||
color: #efefef;
|
||||
}
|
||||
|
||||
29
src/views/tabs/Charts.vue
Normal file
29
src/views/tabs/Charts.vue
Normal 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>
|
||||
@@ -6,7 +6,7 @@
|
||||
:filters="filters"
|
||||
:tableProps="tableProps"
|
||||
:paginationProps="paginationProps"
|
||||
v-loading="Api.isPlayerModerationsLoading"
|
||||
v-loading="API.isPlayerModerationsLoading"
|
||||
>
|
||||
<template slot="tool">
|
||||
<div class="tool-slot">
|
||||
@@ -37,48 +37,30 @@
|
||||
>
|
||||
<el-button
|
||||
type="default"
|
||||
:loading="Api.isPlayerModerationsLoading"
|
||||
@click="Api.refreshPlayerModerations()"
|
||||
:loading="API.isPlayerModerationsLoading"
|
||||
@click="API.refreshPlayerModerations()"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-table-column
|
||||
:label="$t('table.moderation.date')"
|
||||
prop="created"
|
||||
sortable="custom"
|
||||
width="120"
|
||||
>
|
||||
<el-table-column :label="$t('table.moderation.date')" prop="created" sortable="custom" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template slot="content">
|
||||
<span>{{
|
||||
scope.row.created | formatDate('long')
|
||||
}}</span>
|
||||
<span>{{ scope.row.created | formatDate('long') }}</span>
|
||||
</template>
|
||||
<span>{{
|
||||
scope.row.created | formatDate('short')
|
||||
}}</span>
|
||||
<span>{{ scope.row.created | formatDate('short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.moderation.type')"
|
||||
prop="type"
|
||||
width="100"
|
||||
>
|
||||
<el-table-column :label="$t('table.moderation.type')" prop="type" width="100">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
v-text="$t('view.moderation.filters.' + scope.row.type)"
|
||||
></span>
|
||||
<span v-text="$t('view.moderation.filters.' + scope.row.type)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.moderation.source')"
|
||||
prop="sourceDisplayName"
|
||||
>
|
||||
<el-table-column :label="$t('table.moderation.source')" prop="sourceDisplayName">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
class="x-link"
|
||||
@@ -87,10 +69,7 @@
|
||||
></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.moderation.target')"
|
||||
prop="targetDisplayName"
|
||||
>
|
||||
<el-table-column :label="$t('table.moderation.target')" prop="targetDisplayName">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
class="x-link"
|
||||
@@ -99,15 +78,9 @@
|
||||
></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.moderation.action')"
|
||||
width="80"
|
||||
align="right"
|
||||
>
|
||||
<el-table-column :label="$t('table.moderation.action')" width="80" align="right">
|
||||
<template slot-scope="scope">
|
||||
<template
|
||||
v-if="scope.row.sourceUserId === Api.currentUser.id"
|
||||
>
|
||||
<template v-if="scope.row.sourceUserId === API.currentUser.id">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c"
|
||||
@@ -135,18 +108,15 @@
|
||||
|
||||
export default {
|
||||
name: 'ModerationTab',
|
||||
inject: ['API', 'showUserDialog'],
|
||||
props: {
|
||||
Api: Object,
|
||||
tableData: Object,
|
||||
showUserDialog: Function,
|
||||
shiftHeld: Boolean
|
||||
shiftHeld: Boolean,
|
||||
hideTooltips: Boolean
|
||||
},
|
||||
created: async function () {
|
||||
this.filters[0].value = JSON.parse(
|
||||
await configRepository.getString(
|
||||
'VRCX_playerModerationTableFilters',
|
||||
'[]'
|
||||
)
|
||||
await configRepository.getString('VRCX_playerModerationTableFilters', '[]')
|
||||
);
|
||||
},
|
||||
data() {
|
||||
@@ -155,8 +125,7 @@
|
||||
{
|
||||
prop: 'type',
|
||||
value: [],
|
||||
filterFn: (row, filter) =>
|
||||
filter.value.some((v) => v === row.type)
|
||||
filterFn: (row, filter) => filter.value.some((v) => v === row.type)
|
||||
},
|
||||
{
|
||||
prop: ['sourceDisplayName', 'targetDisplayName'],
|
||||
@@ -191,32 +160,25 @@
|
||||
},
|
||||
methods: {
|
||||
saveTableFilters() {
|
||||
configRepository.setString(
|
||||
'VRCX_playerModerationTableFilters',
|
||||
JSON.stringify(this.filters[0].value)
|
||||
);
|
||||
configRepository.setString('VRCX_playerModerationTableFilters', JSON.stringify(this.filters[0].value));
|
||||
},
|
||||
deletePlayerModeration(row) {
|
||||
this.Api.deletePlayerModeration({
|
||||
this.API.deletePlayerModeration({
|
||||
moderated: row.targetUserId,
|
||||
type: row.type
|
||||
});
|
||||
},
|
||||
deletePlayerModerationPrompt(row) {
|
||||
this.$confirm(
|
||||
`Continue? Delete Moderation ${row.type}`,
|
||||
'Confirm',
|
||||
{
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
this.deletePlayerModeration(row);
|
||||
}
|
||||
this.$confirm(`Continue? Delete Moderation ${row.type}`, 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
this.deletePlayerModeration(row);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,7 +13,8 @@ module.exports = {
|
||||
'noty',
|
||||
'vue',
|
||||
'vue-data-tables',
|
||||
'vue-lazyload'
|
||||
'vue-lazyload',
|
||||
'dayjs'
|
||||
],
|
||||
app: {
|
||||
import: ['./src/app.js', './src/app.scss'],
|
||||
|
||||
Reference in New Issue
Block a user