Compare commits
281 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ac7ba51e5 | |||
| bdf85cd8ec | |||
| 63b268d9b9 | |||
| d73c8ef112 | |||
| 3d1c66b420 | |||
| b3f7108979 | |||
| feabfd29e0 | |||
| f17fedc976 | |||
| c83c75e014 | |||
| e57143f517 | |||
| fb45a783ee | |||
| 71476e9552 | |||
| 922a338143 | |||
| 231d36e877 | |||
| 27d6ac9f14 | |||
| a6029da2cc | |||
| a3f3790205 | |||
| ebfb24f729 | |||
| e521736618 | |||
| fc7662e431 | |||
| a459fdf178 | |||
| 4e7fba89ee | |||
| 6084e5876d | |||
| 44f548c7b7 | |||
| 59a5fbe5ac | |||
| c252213ed9 | |||
| fb56378f18 | |||
| e9635eae06 | |||
| 1fc98a8f99 | |||
| c9de1a48ce | |||
| fee9774f88 | |||
| 7c974e8d0e | |||
| c84fbbcf42 | |||
| 45af649505 | |||
| 6c1e00d002 | |||
| 69ff85cfb1 | |||
| bdc2ca784c | |||
| dbd73d448d | |||
| 26b1ec246d | |||
| 8190dfa2cb | |||
| 44df685606 | |||
| 785a42b849 | |||
| ccfcca8728 | |||
| d9a7ba122c | |||
| bf8d4a9ef4 | |||
| 6ea1e91d1d | |||
| 813b557455 | |||
| 834b14da69 | |||
| c809b2146d | |||
| 33b98ca313 | |||
| bcb9c63cb0 | |||
| 17d77a80d8 | |||
| 65b159f8fd | |||
| 063d5283e4 | |||
| 29de1b3c4b | |||
| f0c02bf12e | |||
| 8aa23468b3 | |||
| 4c1fcf710b | |||
| a529d6e9eb | |||
| 9fc602e08f | |||
| 56e40bd84b | |||
| a5b5df7452 | |||
| 1a8da02ced | |||
| 7e75fa2516 | |||
| 3ed2c09184 | |||
| 6901c3d2b4 | |||
| 8417754403 | |||
| de5c57181a | |||
| d91d4cc6a8 | |||
| 9a5fd4d670 | |||
| 4202a55673 | |||
| 5181e8f4af | |||
| e117f62fcb | |||
| e0036bf969 | |||
| 1f457d6389 | |||
| eb5b94c9f6 | |||
| 328e8c0573 | |||
| 9f58ae5428 | |||
| ebd0eeb8c4 | |||
| fa656c2f26 | |||
| 0cc3a12d1d | |||
| 392a6437f8 | |||
| 122532f0ed | |||
| 366ff91f60 | |||
| a0496736dd | |||
| f974120e87 | |||
| abd8b8178b | |||
| f1fcde8459 | |||
| b3289d6aab | |||
| 6481a4a3b0 | |||
| 05dc268526 | |||
| 669acc98d2 | |||
| 3371b661c2 | |||
| 871b2c0221 | |||
| d366a877a4 | |||
| 405aab96bd | |||
| f29c160000 | |||
| a2de0e2030 | |||
| 7dd1c06f3f | |||
| ff041b9aaf | |||
| 4782dba444 | |||
| d6b8d032d6 | |||
| c16616330c | |||
| 57cec8bfe7 | |||
| 6bea340e19 | |||
| c181cf7e64 | |||
| 8e4ae64cd3 | |||
| 5750490f01 | |||
| 3ef27e1d69 | |||
| f53993c717 | |||
| 235c16e30f | |||
| c3533f07ad | |||
| d05579c5ee | |||
| c8f53c2f06 | |||
| b44f88ebcd | |||
| 7805d1350c | |||
| b17bd19433 | |||
| c12a6cbacd | |||
| ba650238db | |||
| d5ec9919e2 | |||
| 20cd393e05 | |||
| 31e65c09d6 | |||
| fb2348e774 | |||
| 1ec75bda70 | |||
| 6b6b837dde | |||
| 66a02d76bd | |||
| c7162dbd14 | |||
| 1cfe073bab | |||
| e3b72c81ea | |||
| 5552995564 | |||
| 623d5dd2ce | |||
| 6992b998a8 | |||
| 669975c68e | |||
| 084823de44 | |||
| f62d6982e5 | |||
| 1c9b54b578 | |||
| 0f4e5ee5f3 | |||
| 29b5e715fa | |||
| 91a18b51a0 | |||
| 241648ec49 | |||
| ed7d93e7fc | |||
| 436e3e63f9 | |||
| 17ebdace82 | |||
| 20826d905d | |||
| 41b1ab398c | |||
| 03465a1487 | |||
| a19fdbc19d | |||
| 032f82acd2 | |||
| b4a9d4ca3b | |||
| 986c7ba95e | |||
| 17f6f9c8ef | |||
| 40bbdbe4fa | |||
| 9f5d882119 | |||
| 6f45663c6c | |||
| a7861b361d | |||
| 5f8d7480d1 | |||
| e222dc63eb | |||
| 9c2f0ac797 | |||
| e33ba4af90 | |||
| 7b868a9f28 | |||
| 2a18ba4368 | |||
| fcbd6d0883 | |||
| 20fc4aba5b | |||
| 76ca0d1786 | |||
| 7e3c150815 | |||
| c8d56ec442 | |||
| 5c4c486643 | |||
| 755c729a9b | |||
| 3ac8d60c5c | |||
| dcff3b088f | |||
| 90b2099955 | |||
| fc0c04ec9d | |||
| 41b335555a | |||
| 60f7b3bbb5 | |||
| eaefe955a7 | |||
| edaa4f2684 | |||
| 30fce3787b | |||
| 4716f1c7a4 | |||
| bb7ccf98fd | |||
| c06d75b981 | |||
| c7da8477fa | |||
| e43f1e0819 | |||
| f130e6900b | |||
| db205915be | |||
| 05c38e10e3 | |||
| a8f683a585 | |||
| 68f6fc8a42 | |||
| 6d3b32cd7d | |||
| fadecc9d2c | |||
| 50602cb6db | |||
| 186ce81819 | |||
| f836a075b0 | |||
| 9acf3c740c | |||
| bc1c1bd3d2 | |||
| 2348277b95 | |||
| cd5f489df7 | |||
| f74962222b | |||
| e7f651d2b9 | |||
| 4862328090 | |||
| 87631d1f74 | |||
| 86bb9fcc2e | |||
| b85e3bfe1d | |||
| dd15072813 | |||
| 2f8376c996 | |||
| 514723cf74 | |||
| 0995ce15bc | |||
| 3b3c3bda31 | |||
| 2027b85450 | |||
| 0c6b55146f | |||
| 3c728e3cfa | |||
| adce339392 | |||
| 00a4a840b0 | |||
| 1e705ea496 | |||
| e8ed36df16 | |||
| f4be32aa39 | |||
| e0d3d2585d | |||
| ebfaf06a44 | |||
| 5a651aedf8 | |||
| b66af014b9 | |||
| 634c9e1514 | |||
| c4132a9be2 | |||
| 82a9a9165f | |||
| fcac03c0a4 | |||
| 39c3cf2329 | |||
| 59f4a0cb66 | |||
| e2b42d16a4 | |||
| e23663ed28 | |||
| dc7846c31e | |||
| d875433d56 | |||
| 71e5044cb4 | |||
| e83aa40f82 | |||
| d1c0e0b898 | |||
| 26a7c69886 | |||
| 0dc2c505db | |||
| 188857d335 | |||
| 3dbbb3b4f9 | |||
| 07a77c463b | |||
| 1a8e2231dd | |||
| f630d272ca | |||
| a381cf806c | |||
| 0023ab8cfd | |||
| a392940d52 | |||
| 74984ad653 | |||
| a8991434bc | |||
| 825610b4c2 | |||
| fef3991206 | |||
| a21cb31a0a | |||
| 23a954a0d1 | |||
| b7bc9625b8 | |||
| bc58196804 | |||
| 6550d6973e | |||
| d7a609a4f2 | |||
| 763506d5a9 | |||
| 8dbb32b821 | |||
| b8a21e0f70 | |||
| 7c2b5fbd50 | |||
| ac740c2743 | |||
| 916f19df72 | |||
| de8facfb05 | |||
| 5d5ad44508 | |||
| 647055d2f0 | |||
| 1947555724 | |||
| 86417f3422 | |||
| a224b58d17 | |||
| 69be01fb1e | |||
| 6ef04f0dbd | |||
| 451d90f854 | |||
| b01d3e894b | |||
| 13dc6a0e32 | |||
| 96714550d0 | |||
| 2b6c751f55 | |||
| 08d3a2a03a | |||
| a79ca78781 | |||
| e08333dba1 | |||
| 8705dd1df5 | |||
| 7b4da9d422 | |||
| e51b2fe2f3 | |||
| f8b4ce103f | |||
| e82b4b8817 | |||
| 36e9df82b0 | |||
| cbce9af00b |
@@ -0,0 +1,17 @@
|
|||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
github-releases-to-discord:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Github Releases To Discord
|
||||||
|
uses: SethCohen/github-releases-to-discord@v1.13.1
|
||||||
|
with:
|
||||||
|
webhook_url: ${{ secrets.WEBHOOK_URL }}
|
||||||
|
color: "15844367"
|
||||||
|
footer_title: "Changelog - Stacjownik"
|
||||||
|
footer_timestamp: true
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="pl">
|
<html lang="pl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
|
|
||||||
<meta name="keywords" content="Stacjownik, TD2, Train Driver 2, stacjownik-td2, stacjownik, td2.info.pl" />
|
<meta
|
||||||
|
name="keywords"
|
||||||
|
content="Stacjownik, TD2, Train Driver 2, stacjownik-td2, stacjownik, td2.info.pl"
|
||||||
|
/>
|
||||||
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
||||||
|
|
||||||
<title>Stacjownik</title>
|
<title>Stacjownik</title>
|
||||||
@@ -18,10 +21,6 @@
|
|||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
<meta name="theme-color" content="#222222" />
|
<meta name="theme-color" content="#222222" />
|
||||||
|
|
||||||
<link rel="icon" href="favicon-64.png" sizes="64x64" type="image/png" />
|
|
||||||
<link rel="icon" href="favicon-62.png" sizes="62x62" type="image/png" />
|
|
||||||
<link rel="icon" href="favicon-32.png" sizes="32x32" type="image/png" />
|
|
||||||
<link rel="icon" href="favicon-16.png" sizes="16x16" type="image/png" />
|
|
||||||
<link rel="icon" href="favicon.ico" />
|
<link rel="icon" href="favicon.ico" />
|
||||||
|
|
||||||
<!-- Static OpenGraph meta -->
|
<!-- Static OpenGraph meta -->
|
||||||
@@ -29,18 +28,28 @@
|
|||||||
<meta property="og:url" content="https://stacjownik-td2.web.app/" />
|
<meta property="og:url" content="https://stacjownik-td2.web.app/" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:title" content="Stacjownik" />
|
<meta property="og:title" content="Stacjownik" />
|
||||||
<meta property="og:description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
<meta
|
||||||
<meta property="og:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg" />
|
property="og:description"
|
||||||
|
content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
|
||||||
|
/>
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="630" />
|
<meta property="og:image:height" content="630" />
|
||||||
<meta property="og:site_name" content="Stacjownik" />
|
<meta property="og:site_name" content="Stacjownik" />
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="Stacjownik" />
|
<meta name="twitter:title" content="Stacjownik" />
|
||||||
<meta name="twitter:description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
<meta
|
||||||
<meta name="twitter:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg" />
|
name="twitter:description"
|
||||||
|
content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2"
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap" rel="stylesheet" />
|
/>
|
||||||
|
<meta
|
||||||
|
name="twitter:image"
|
||||||
|
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stacjownik",
|
"name": "stacjownik",
|
||||||
"version": "1.18.2",
|
"version": "1.25.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -14,31 +14,30 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-js": "^3.32.2",
|
"core-js": "^3.32.2",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"firebase": "^10.4.0",
|
|
||||||
"howler": "^2.2.4",
|
|
||||||
"pinia": "^2.1.6",
|
"pinia": "^2.1.6",
|
||||||
"sass": "^1.67.0",
|
"sass": "^1.67.0",
|
||||||
"socket.io-client": "^4.7.2",
|
"showdown": "^2.1.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-i18n": "^9.4.1",
|
"vue-i18n": "^9.4.1",
|
||||||
"vue-router": "^4.2.4"
|
"vue-router": "^4.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@rushstack/eslint-patch": "^1.3.3",
|
||||||
"@types/node": "^20.6.2",
|
"@types/node": "^20.6.2",
|
||||||
"@vite-pwa/assets-generator": "^0.0.10",
|
"@types/showdown": "^2.0.6",
|
||||||
|
"@vite-pwa/assets-generator": "^0.2.4",
|
||||||
"@vitejs/plugin-vue": "^4.3.4",
|
"@vitejs/plugin-vue": "^4.3.4",
|
||||||
"axios": "^1.5.0",
|
|
||||||
"prettier": "^3.0.3",
|
|
||||||
"typescript": "^5.2.2",
|
|
||||||
"vite": "^4.4.9",
|
|
||||||
"vite-plugin-pwa": "^0.16.5",
|
|
||||||
"vue-tsc": "^1.8.11",
|
|
||||||
"@vue/eslint-config-prettier": "^8.0.0",
|
"@vue/eslint-config-prettier": "^8.0.0",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
"@vue/tsconfig": "^0.4.0",
|
"@vue/tsconfig": "^0.4.0",
|
||||||
|
"axios": "^1.5.0",
|
||||||
"eslint": "^8.49.0",
|
"eslint": "^8.49.0",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"@rushstack/eslint-patch": "^1.3.3"
|
"prettier": "^3.0.3",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^4.4.9",
|
||||||
|
"vite-plugin-pwa": "^0.20.0",
|
||||||
|
"vue-tsc": "^1.8.11"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="60" height="60" fill="#898989"/>
|
||||||
|
<path d="M10.5573 31.3793L9.43741 28.0694C9.35445 27.8592 9.26596 27.6131 9.17195 27.3311C9.07793 27.0435 8.98391 26.7338 8.8899 26.402C8.80694 26.7393 8.71845 27.0518 8.62444 27.3394C8.53042 27.6269 8.44193 27.8758 8.35898 28.086L7.24736 31.3793H10.5573ZM15.0121 36H12.8386C12.5953 36 12.3989 35.9447 12.2496 35.8341C12.1003 35.7179 11.9869 35.5714 11.9095 35.3944L11.1961 33.2873H6.6003L5.88688 35.3944C5.82604 35.5493 5.71544 35.6903 5.55505 35.8175C5.4002 35.9392 5.20664 36 4.97436 36H2.78431L7.46305 23.9133H10.3333L15.0121 36ZM22.643 26.3688C22.5601 26.5015 22.4716 26.6011 22.3775 26.6674C22.2891 26.7338 22.1729 26.767 22.0291 26.767C21.9019 26.767 21.7637 26.7283 21.6143 26.6508C21.4706 26.5679 21.3046 26.4766 21.1166 26.3771C20.9341 26.2775 20.724 26.189 20.4861 26.1116C20.2483 26.0287 19.9773 25.9872 19.6732 25.9872C19.1478 25.9872 18.7551 26.1006 18.4952 26.3273C18.2408 26.5485 18.1136 26.8499 18.1136 27.2315C18.1136 27.4749 18.191 27.6767 18.3459 27.8371C18.5007 27.9975 18.7026 28.1357 18.9515 28.2519C19.2059 28.368 19.4934 28.4759 19.8142 28.5754C20.1405 28.6694 20.4723 28.7773 20.8097 28.8989C21.147 29.0151 21.4761 29.1533 21.7969 29.3137C22.1231 29.4741 22.4107 29.6787 22.6596 29.9276C22.914 30.1765 23.1186 30.4806 23.2735 30.8401C23.4283 31.1941 23.5058 31.6227 23.5058 32.1259C23.5058 32.6845 23.409 33.2071 23.2154 33.6938C23.0218 34.1805 22.7398 34.6063 22.3693 34.9713C22.0042 35.3308 21.5508 35.6156 21.0088 35.8258C20.4723 36.0304 19.8612 36.1327 19.1754 36.1327C18.7994 36.1327 18.415 36.094 18.0223 36.0166C17.6352 35.9392 17.2591 35.8313 16.8941 35.6931C16.5291 35.5493 16.1862 35.3806 15.8655 35.187C15.5447 34.9935 15.2654 34.7778 15.0276 34.54L15.8572 33.2293C15.9235 33.1352 16.0093 33.0578 16.1143 32.997C16.225 32.9306 16.3439 32.8974 16.4711 32.8974C16.637 32.8974 16.8029 32.95 16.9688 33.0551C17.1402 33.1601 17.331 33.2763 17.5412 33.4035C17.7569 33.5307 18.003 33.6468 18.2795 33.7519C18.556 33.857 18.8823 33.9095 19.2584 33.9095C19.7672 33.9095 20.1626 33.7989 20.4447 33.5777C20.7267 33.3509 20.8677 32.9942 20.8677 32.5075C20.8677 32.2255 20.7903 31.996 20.6355 31.819C20.4806 31.642 20.276 31.4955 20.0216 31.3793C19.7727 31.2632 19.4879 31.1609 19.1671 31.0724C18.8464 30.9839 18.5173 30.8871 18.18 30.7821C17.8426 30.6714 17.5135 30.5387 17.1928 30.3839C16.872 30.2235 16.5844 30.0161 16.33 29.7617C16.0812 29.5018 15.8793 29.181 15.7245 28.7994C15.5696 28.4123 15.4922 27.9367 15.4922 27.3725C15.4922 26.9191 15.5834 26.4766 15.7659 26.0452C15.9484 25.6139 16.2167 25.2295 16.5706 24.8922C16.9246 24.5548 17.3587 24.2866 17.873 24.0875C18.3874 23.8829 18.9763 23.7805 19.64 23.7805C20.0105 23.7805 20.37 23.811 20.7184 23.8718C21.0724 23.9271 21.407 24.0128 21.7222 24.129C22.0374 24.2396 22.3305 24.3751 22.6015 24.5354C22.8781 24.6903 23.1242 24.8673 23.3398 25.0664L22.643 26.3688ZM36.1186 29.9525C36.1186 30.8263 35.9665 31.6337 35.6623 32.3748C35.3637 33.1104 34.9406 33.7491 34.3931 34.2911C33.8456 34.8276 33.1847 35.2479 32.4105 35.552C31.6417 35.8507 30.7873 36 29.8471 36H25.1518V23.9133H29.8471C30.7873 23.9133 31.6417 24.0654 32.4105 24.3695C33.1847 24.6737 33.8456 25.094 34.3931 25.6305C34.9406 26.1669 35.3637 26.8057 35.6623 27.5468C35.9665 28.2823 36.1186 29.0842 36.1186 29.9525ZM33.2483 29.9525C33.2483 29.3552 33.1709 28.816 33.016 28.3348C32.8612 27.8537 32.6372 27.4472 32.3441 27.1154C32.0565 26.778 31.7026 26.5209 31.2823 26.3439C30.8619 26.1614 30.3836 26.0701 29.8471 26.0701H27.9723V33.8431H29.8471C30.3836 33.8431 30.8619 33.7547 31.2823 33.5777C31.7026 33.3952 32.0565 33.138 32.3441 32.8062C32.6372 32.4688 32.8612 32.0596 33.016 31.5784C33.1709 31.0973 33.2483 30.5553 33.2483 29.9525ZM40.594 26.0701V28.8906H44.3934V30.9646H40.594V33.8431H45.5547V36H37.7735V23.9133H45.5547V26.0701H40.594ZM50.0882 28.8077H50.5361C50.9509 28.8077 51.2496 28.6777 51.4321 28.4178L54.153 24.4691C54.3134 24.2589 54.4849 24.1151 54.6674 24.0377C54.8554 23.9547 55.0877 23.9133 55.3642 23.9133H57.8031L54.2194 28.7994C53.965 29.1368 53.6912 29.3801 53.3981 29.5294C53.6083 29.6068 53.7991 29.7147 53.9705 29.8529C54.142 29.9912 54.3024 30.1709 54.4517 30.3922L58.1018 36H55.5965C55.4361 36 55.2978 35.9889 55.1817 35.9668C55.0711 35.9447 54.9743 35.9115 54.8913 35.8673C54.8084 35.823 54.7365 35.7705 54.6757 35.7097C54.6148 35.6433 54.5568 35.5686 54.5015 35.4857L51.7639 31.2798C51.6643 31.1249 51.5371 31.0171 51.3823 30.9563C51.233 30.8899 51.0284 30.8567 50.7684 30.8567H50.0882V36H47.2843V23.9133H50.0882V28.8077Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Warstwa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 159 38.8" style="enable-background:new 0 0 159 38.8;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#00A962;}
|
||||||
|
.st1{enable-background:new ;}
|
||||||
|
.st2{fill:#FFFFFF;}
|
||||||
|
.st3{fill:#1E3A33;}
|
||||||
|
.st4{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<path class="st0" d="M139.6,38.7H19.4C8.7,38.7,0,30.1,0,19.3l0,0C0,8.6,8.7-0.1,19.4-0.1h120.2c10.7,0,19.4,8.7,19.4,19.4l0,0
|
||||||
|
C159,30.1,150.3,38.7,139.6,38.7z"/>
|
||||||
|
<g class="st1">
|
||||||
|
<path class="st2" d="M48.9,23.7c0,0.5-0.4,0.9-0.9,0.9c-0.5,0-0.9-0.4-0.9-0.9v-9.9c0-0.6,0.4-1,1-1h3.7c2.5,0,3.6,1.9,3.6,3.7
|
||||||
|
s-1,3.7-3.6,3.7h-2.9V23.7z M48.9,14.5v4.1h2.8c1.3,0,1.9-1,1.9-2c0-1-0.5-2.1-1.8-2.1H48.9z"/>
|
||||||
|
<path class="st2" d="M56.3,20.5c0-2.5,1.9-4.1,4.1-4.1c2.3,0,4.2,1.6,4.2,4.1c0,2.5-1.9,4.2-4.2,4.2C58.2,24.7,56.3,23,56.3,20.5z
|
||||||
|
M62.9,20.5c0-1.5-1.1-2.4-2.4-2.4c-1.3,0-2.4,1-2.4,2.4c0,1.5,1.1,2.5,2.4,2.5C61.8,23,62.9,22,62.9,20.5z"/>
|
||||||
|
<path class="st2" d="M66.6,21.9c0.4-0.2,0.7,0.1,0.9,0.3c0.3,0.6,0.9,1,1.7,1c0.8,0,1.4-0.4,1.4-1c0-0.5-0.5-0.7-1.1-0.9l-1.1-0.3
|
||||||
|
c-1.7-0.5-2.3-1.4-2.2-2.7c0.1-1.2,1.4-2.1,2.8-2.1c1.1,0,2,0.3,2.5,1.2c0.2,0.4,0.1,0.8-0.2,1c-0.3,0.2-0.6,0.2-1-0.1
|
||||||
|
c-0.4-0.4-1-0.5-1.4-0.5c-0.4,0-0.9,0.2-1.1,0.4c-0.1,0.2-0.2,0.4-0.1,0.7c0.1,0.3,0.6,0.5,1,0.6l1.2,0.3c1.7,0.4,2.1,1.5,2.1,2.3
|
||||||
|
c0,1.5-1.3,2.4-3.2,2.4c-1.2,0-2.5-0.6-2.9-1.8C66,22.5,66.2,22.1,66.6,21.9z"/>
|
||||||
|
<path class="st2" d="M74.6,18h-0.8c-0.4,0-0.7-0.3-0.7-0.7c0-0.4,0.3-0.7,0.7-0.7h0.8v-2.2c0-0.5,0.4-0.9,0.9-0.9
|
||||||
|
c0.5,0,0.9,0.4,0.9,0.9v2.2h1c0.4,0,0.7,0.3,0.7,0.7c0,0.4-0.3,0.7-0.7,0.7h-1v5.7c0,0.5-0.4,0.9-0.9,0.9c-0.5,0-0.9-0.4-0.9-0.9
|
||||||
|
V18z"/>
|
||||||
|
<path class="st2" d="M85.7,23.4c-0.5,0.9-1.7,1.3-2.7,1.3c-2.1,0-4-1.6-4-4.1c0-2.5,1.9-4.1,4-4.1c1,0,2.1,0.4,2.7,1.3v-0.3
|
||||||
|
c0-0.5,0.4-0.9,0.9-0.9c0.5,0,0.9,0.4,0.9,0.9v6.3c0,0.5-0.4,0.9-0.9,0.9c-0.5,0-0.9-0.4-0.9-0.9V23.4z M83.2,23
|
||||||
|
c1.3,0,2.4-0.9,2.4-2.5c0-1.6-1.3-2.4-2.4-2.4c-1.3,0-2.4,1-2.4,2.4C80.8,22,81.9,23,83.2,23z"/>
|
||||||
|
<path class="st2" d="M95.1,16.6c0.5,0,0.9,0.3,1,0.8l1.6,4.6l1.7-4.8c0.2-0.5,0.6-0.7,1.1-0.6c0.5,0.1,0.7,0.6,0.6,1.1l-2.4,6.3
|
||||||
|
c-0.2,0.5-0.5,0.7-0.9,0.7h-0.1c-0.4,0-0.7-0.2-0.9-0.7L95.1,19l-1.7,4.9c-0.2,0.5-0.5,0.7-0.9,0.7h-0.1c-0.4,0-0.7-0.2-0.9-0.7
|
||||||
|
l-2.4-6.3c-0.2-0.4,0.1-1,0.6-1.1c0.5-0.1,0.9,0.1,1.1,0.6l1.7,4.8l1.6-4.6C94.2,16.8,94.6,16.6,95.1,16.6L95.1,16.6z"/>
|
||||||
|
<path class="st2" d="M112.9,23.2c0.3,0.3,0.3,0.9,0,1.2c-0.4,0.3-0.9,0.3-1.2,0l-3.2-3.5v2.9c0,0.5-0.4,0.9-0.9,0.9
|
||||||
|
c-0.5,0-0.8-0.4-0.8-0.9V13.1c0-0.5,0.4-0.9,0.8-0.9c0.5,0,0.9,0.4,0.9,0.9v6.1l2.4-2.4c0.3-0.3,0.9-0.3,1.2,0
|
||||||
|
c0.3,0.4,0.3,0.9,0,1.2l-2,2.1L112.9,23.2z"/>
|
||||||
|
<path class="st2" d="M120.8,23.4c-0.5,0.9-1.7,1.3-2.7,1.3c-2.1,0-4-1.6-4-4.1c0-2.5,1.9-4.1,4-4.1c1,0,2.1,0.4,2.7,1.3v-0.3
|
||||||
|
c0-0.5,0.4-0.9,0.8-0.9c0.5,0,0.9,0.4,0.9,0.9v6.3c0,0.5-0.4,0.9-0.9,0.9c-0.5,0-0.8-0.4-0.8-0.9V23.4z M118.4,23
|
||||||
|
c1.3,0,2.4-0.9,2.4-2.5c0-1.6-1.3-2.4-2.4-2.4c-1.3,0-2.4,1-2.4,2.4C115.9,22,117,23,118.4,23z"/>
|
||||||
|
<path class="st2" d="M130.3,16.6c0.5,0,0.9,0.3,1,0.8l1.6,4.6l1.7-4.8c0.2-0.5,0.6-0.7,1.1-0.6c0.5,0.1,0.7,0.6,0.6,1.1l-2.4,6.3
|
||||||
|
c-0.2,0.5-0.5,0.7-0.9,0.7h-0.1c-0.4,0-0.7-0.2-0.9-0.7l-1.7-4.9l-1.7,4.9c-0.2,0.5-0.5,0.7-0.9,0.7h-0.1c-0.4,0-0.7-0.2-0.9-0.7
|
||||||
|
l-2.4-6.3c-0.2-0.4,0.1-1,0.6-1.1c0.5-0.1,0.9,0.1,1.1,0.6l1.7,4.8l1.6-4.6C129.3,16.8,129.7,16.6,130.3,16.6L130.3,16.6z"/>
|
||||||
|
<path class="st2" d="M143.6,26.1c0,0.2,0.2,0.5,0.6,0.5c0.1,0,0.4,0,0.5-0.4c0.1-0.4,0.3-0.5,0.7-0.5c0.4,0.1,0.6,0.4,0.5,0.8
|
||||||
|
c-0.2,0.8-0.9,1.4-1.7,1.4c-1.3,0-1.9-0.8-1.9-1.7c0-0.2,0.1-0.6,0.2-0.8l0.5-1c-0.5,0.2-1,0.3-1.6,0.3c-2.3,0-4.1-1.6-4.1-4.2
|
||||||
|
c0-2.5,1.8-4.1,4.1-4.1c2.2,0,3.9,1.5,4,4c0,0.5-0.4,0.8-0.8,0.8h-5.5c0.2,1.2,1.2,2,2.4,2c0.8,0,1.3-0.3,1.9-0.8
|
||||||
|
c0.3-0.2,0.8-0.4,1.1-0.1c0.3,0.3,0.2,0.7,0,1.1C143.5,25.9,143.6,26.1,143.6,26.1z M143.7,19.9c-0.2-1.2-1.1-2-2.3-2
|
||||||
|
c-1.2,0-2.2,0.7-2.3,2H143.7z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st3" d="M21.6,26.7c-4.9,0-7.4-2.6-8.5-4.9c-1.2-2.4-1.4-5-1.4-6c0-2.2,1.8-3.8,4.3-3.8h7c0.7,0,1.3,0.6,1.3,1.3
|
||||||
|
c0,0.7-0.6,1.3-1.3,1.3h-7c-0.5,0-1,0.1-1.3,0.4c-0.3,0.2-0.4,0.4-0.4,0.8c0,1.9,0.5,8.3,7.3,8.3c5.8,0,7.3-4.6,7.9-8.1
|
||||||
|
c0-0.2,0-0.3,0.1-0.4c0.2-0.9,0.4-1.7,0.8-2.3C31,12.4,31.9,12,33,12c0.5,0,0.9,0.2,1.2,0.5c0.5,0.4,0.9,1.3,1.2,4.1
|
||||||
|
c0.1,1.4,0.1,2.6,0.1,2.7c0,0.7-0.6,1.3-1.3,1.3l0,0c-0.7,0-1.3-0.6-1.3-1.3c0,0,0-1.1-0.1-2.3c-0.1-1-0.2-1.7-0.3-2.1
|
||||||
|
c-0.1,0.3-0.2,0.9-0.3,1.1c0,0.1,0,0.2-0.1,0.4c-0.2,1.2-0.7,3.7-2.1,5.9c-0.8,1.3-1.9,2.3-3.2,3.1C25.4,26.3,23.6,26.7,21.6,26.7z
|
||||||
|
"/>
|
||||||
|
<ellipse class="st4" cx="26.7" cy="13.3" rx="1.3" ry="1.3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
<svg width="256" height="213" viewBox="0 0 256 213" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g filter="url(#filter0_d_1571_41)">
|
||||||
|
<path d="M217.048 55.9333L238 80.4039L128 194.6L18 80.4039L38.9524 55.9333L71.6905 18.6H128H184.31L217.048 55.9333Z" fill="#F47FFF"/>
|
||||||
|
<path d="M238 80.4039L217.048 55.9333L184.31 18.6M238 80.4039L128 194.6M238 80.4039H164.536M128 194.6L18 80.4039M128 194.6L91.4641 80.4039M128 194.6L164.536 80.4039M184.31 18.6H128M184.31 18.6L164.536 80.4039M18 80.4039L38.9524 55.9333L71.6905 18.6M18 80.4039H91.4641M71.6905 18.6L91.4641 80.4039M71.6905 18.6H128M91.4641 80.4039H128H164.536M91.4641 80.4039L128 18.6M128 18.6L164.536 80.4039" stroke="#ECECEC" stroke-width="7.45763" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_1571_41" x="0.367179" y="0.967155" width="255.266" height="211.266" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="6.952"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.956863 0 0 0 0 0.498039 0 0 0 0 1 0 0 0 1 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1571_41"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1571_41" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 582 B |
@@ -1,3 +1 @@
|
|||||||
<svg width="144" height="144" viewBox="0 0 144 144" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"/></svg>
|
||||||
<path d="M67.4283 9.79351L67.8252 31.2232H79.5983L79.8628 9.79351H67.4283ZM67.8252 110.989L67.4283 132.816H79.8628L79.466 110.989H67.8252ZM91.6359 51.33L103.938 43.7899C102.439 40.527 100.322 37.3963 97.5886 34.3979C94.9429 31.3995 91.5918 28.9303 87.5352 26.9901C83.5667 25.05 78.8046 24.0799 73.2487 24.0799C67.6929 24.0799 62.7103 25.1382 58.3009 27.2547C53.8915 29.3712 50.4081 32.1932 47.8506 35.7207C45.3814 39.2483 44.1467 43.1726 44.1467 47.4938C44.1467 51.9032 44.9404 55.6512 46.5278 58.7378C48.1152 61.8243 50.1435 64.47 52.6128 66.6747C55.082 68.7912 57.6836 70.5549 60.4174 71.9659C63.2394 73.2888 65.7969 74.347 68.0897 75.1407C71.6173 76.4635 74.9243 77.8745 78.0109 79.3737C81.1856 80.8729 83.7431 82.7249 85.6832 84.9296C87.7115 87.0461 88.7257 89.824 88.7257 93.2633C88.7257 95.9089 88.1966 98.2018 87.1383 100.142C86.1683 101.994 84.625 103.405 82.5085 104.375C80.4801 105.345 77.8786 105.83 74.7038 105.83C71.0881 105.83 67.8693 105.08 65.0473 103.581C62.3134 102.082 59.8001 99.9215 57.5072 97.0995C55.2143 94.2775 53.0978 90.8381 51.1577 86.7815L39.12 94.5861C41.0602 99.3483 43.7499 103.581 47.1892 107.285C50.6286 110.989 54.6411 113.943 59.2269 116.148C63.8126 118.265 68.7512 119.323 74.0424 119.323C80.6565 119.323 86.2564 118.265 90.8422 116.148C95.428 113.943 98.9555 110.813 101.425 106.756C103.894 102.699 105.129 97.9814 105.129 92.6019C105.129 88.4571 104.423 84.8414 103.012 81.7548C101.601 78.6682 99.7492 76.0667 97.4563 73.9502C95.1634 71.7455 92.606 69.8935 89.784 68.3943C87.0501 66.8951 84.3604 65.6605 81.7148 64.6904C78.0109 63.2794 74.5275 61.8684 71.2645 60.4574C68.0016 59.0464 65.3559 57.3709 63.3276 55.4307C61.2993 53.4906 60.2851 51.0213 60.2851 48.0229C60.2851 45.7301 60.7261 43.7899 61.6079 42.2025C62.578 40.527 64.0331 39.2483 65.9732 38.3664C68.0016 37.4845 70.559 37.0436 73.6456 37.0436C76.644 37.0436 79.2455 37.705 81.4502 39.0278C83.6549 40.3506 85.5509 42.1144 87.1383 44.319C88.8139 46.4356 90.3131 48.7725 91.6359 51.33Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 582 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg width="160" height="150" viewBox="0 0 160 150" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="160" height="150" viewBox="0 0 160 150" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M10.163 139L80 12.4204L149.837 139H80H10.163Z" stroke="white" stroke-width="12"/>
|
<path d="M10.163 139L80 12.4204L149.837 139H80H10.163Z" stroke="salmon" stroke-width="15"/>
|
||||||
<path d="M85.4488 50.3354V80.6619C85.4488 83.8784 85.2898 87.0418 84.9717 90.1522C84.6536 93.2273 84.2294 96.4968 83.6992 99.9606H74.8451C74.315 96.4968 73.8908 93.2273 73.5727 90.1522C73.2546 87.0418 73.0955 83.8784 73.0955 80.6619V50.3354H85.4488ZM71.0808 119.789C71.0808 118.694 71.2752 117.651 71.664 116.661C72.0882 115.672 72.6537 114.823 73.3606 114.117C74.1029 113.41 74.9689 112.844 75.9585 112.42C76.9482 111.996 78.0086 111.784 79.1396 111.784C80.2354 111.784 81.278 111.996 82.2677 112.42C83.2574 112.844 84.1057 113.41 84.8126 114.117C85.5195 114.823 86.085 115.672 86.5092 116.661C86.9333 117.651 87.1454 118.694 87.1454 119.789C87.1454 120.921 86.9333 121.981 86.5092 122.971C86.085 123.925 85.5195 124.756 84.8126 125.462C84.1057 126.169 83.2574 126.717 82.2677 127.106C81.278 127.53 80.2354 127.742 79.1396 127.742C78.0086 127.742 76.9482 127.53 75.9585 127.106C74.9689 126.717 74.1029 126.169 73.3606 125.462C72.6537 124.756 72.0882 123.925 71.664 122.971C71.2752 121.981 71.0808 120.921 71.0808 119.789Z" fill="#FFFBFB"/>
|
<path d="M85.4488 50.3354V80.6619C85.4488 83.8784 85.2898 87.0418 84.9717 90.1522C84.6536 93.2273 84.2294 96.4968 83.6992 99.9606H74.8451C74.315 96.4968 73.8908 93.2273 73.5727 90.1522C73.2546 87.0418 73.0955 83.8784 73.0955 80.6619V50.3354H85.4488ZM71.0808 119.789C71.0808 118.694 71.2752 117.651 71.664 116.661C72.0882 115.672 72.6537 114.823 73.3606 114.117C74.1029 113.41 74.9689 112.844 75.9585 112.42C76.9482 111.996 78.0086 111.784 79.1396 111.784C80.2354 111.784 81.278 111.996 82.2677 112.42C83.2574 112.844 84.1057 113.41 84.8126 114.117C85.5195 114.823 86.085 115.672 86.5092 116.661C86.9333 117.651 87.1454 118.694 87.1454 119.789C87.1454 120.921 86.9333 121.981 86.5092 122.971C86.085 123.925 85.5195 124.756 84.8126 125.462C84.1057 126.169 83.2574 126.717 82.2677 127.106C81.278 127.53 80.2354 127.742 79.1396 127.742C78.0086 127.742 76.9482 127.53 75.9585 127.106C74.9689 126.717 74.1029 126.169 73.3606 125.462C72.6537 124.756 72.0882 123.925 71.664 122.971C71.2752 121.981 71.0808 120.921 71.0808 119.789Z" fill="white"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,48 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="512" height="512" rx="256" fill="#121212"/>
|
||||||
|
<rect x="325.818" y="405.665" width="7.11086" height="143.867" rx="3.55543" transform="rotate(90 325.818 405.665)" fill="white"/>
|
||||||
|
<rect x="361.785" y="430.553" width="10.6663" height="208.608" rx="5.33314" transform="rotate(90 361.785 430.553)" fill="white"/>
|
||||||
|
<g filter="url(#filter0_d_272_208)">
|
||||||
|
<rect width="25.0328" height="117.468" rx="12.5164" transform="matrix(0.711174 0.703016 -0.711174 0.703016 211.54 363)" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<g filter="url(#filter1_d_272_208)">
|
||||||
|
<rect width="25.0328" height="117.468" rx="12.5164" transform="matrix(-0.711174 0.703016 0.711174 0.703016 300.46 363)" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<g filter="url(#filter2_d_272_208)">
|
||||||
|
<rect x="139.352" y="65.4912" width="232.66" height="325.893" rx="41.5866" stroke="#7A7A7A" stroke-width="8.31733"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M135.193 107.078C135.193 81.8134 155.674 61.3325 180.938 61.3325H330.425C355.69 61.3325 376.171 81.8134 376.171 107.078V349.797C376.171 375.062 355.69 395.543 330.426 395.543H180.938C155.674 395.543 135.193 375.062 135.193 349.797V107.078ZM208.217 338.656C208.217 352.401 196.774 363.544 182.659 363.544C168.543 363.544 157.1 352.401 157.1 338.656C157.1 324.91 168.543 313.768 182.659 313.768C196.774 313.768 208.217 324.91 208.217 338.656ZM328.706 363.544C342.821 363.544 354.264 352.401 354.264 338.656C354.264 324.91 342.821 313.768 328.706 313.768C314.591 313.768 303.148 324.91 303.148 338.656C303.148 352.401 314.591 363.544 328.706 363.544ZM248.38 136.323C248.38 132.07 244.932 128.622 240.679 128.622H164.802C160.549 128.622 157.101 132.07 157.101 136.323V203.223C157.101 207.476 160.549 210.924 164.802 210.924H240.679C244.932 210.924 248.38 207.476 248.38 203.223V136.323ZM346.563 128.622C350.817 128.622 354.265 132.07 354.265 136.323V203.223C354.265 207.476 350.817 210.924 346.563 210.924H270.686C266.433 210.924 262.985 207.476 262.985 203.223V136.323C262.985 132.07 266.433 128.622 270.686 128.622H346.563Z" fill="white"/>
|
||||||
|
<ellipse cx="255.682" cy="64.888" rx="25.5582" ry="24.888" fill="#F3F1F1"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_272_208" x="106.676" y="341.617" width="143.99" height="142.947" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="13.269"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_272_208"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_272_208" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="filter1_d_272_208" x="261.334" y="341.617" width="143.99" height="142.947" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="13.269"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_272_208"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_272_208" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="filter2_d_272_208" x="110.241" y="15.048" width="290.882" height="405.446" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="12.476"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_272_208"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_272_208" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -1,106 +0,0 @@
|
|||||||
@import './styles/responsive.scss';
|
|
||||||
@import './styles/variables.scss';
|
|
||||||
@import './styles/global.scss';
|
|
||||||
|
|
||||||
// VUE ROUTE CHANGE ANIMATION
|
|
||||||
.view-anim {
|
|
||||||
&-enter-from,
|
|
||||||
&-leave-to {
|
|
||||||
opacity: 0.02;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-active,
|
|
||||||
&-leave-active {
|
|
||||||
transition: all $animDuration $animType;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-anim {
|
|
||||||
&-enter-active,
|
|
||||||
&-leave-active {
|
|
||||||
transition: all $animDuration $animType;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-from,
|
|
||||||
&-leave-to {
|
|
||||||
transform: translateY(-25%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.route {
|
|
||||||
margin: 0 0.2em;
|
|
||||||
|
|
||||||
&-active,
|
|
||||||
&[data-active='true'] {
|
|
||||||
color: $accentCol;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// APP
|
|
||||||
#app {
|
|
||||||
color: white;
|
|
||||||
font-size: 1rem;
|
|
||||||
overflow-x: hidden;
|
|
||||||
|
|
||||||
@include smallScreen() {
|
|
||||||
font-size: calc(0.65rem + 0.8vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
@include screenLandscape() {
|
|
||||||
font-size: calc(0.45rem + 0.8vw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CONTAINER
|
|
||||||
.app_container {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
|
|
||||||
min-height: 100vh;
|
|
||||||
|
|
||||||
header {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
|
|
||||||
padding: 0 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
flex: 0 1 0.2em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
background-color: firebrick;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0.5em 0.4em;
|
|
||||||
max-width: 1100px;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
border-radius: 0 0 1em 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FOOTER
|
|
||||||
footer.app_footer {
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 1.1em;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
background: #111;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app_container">
|
<div class="app_container">
|
||||||
|
<UpdateCard
|
||||||
|
:is-update-card-open="isUpdateCardOpen"
|
||||||
|
@toggle-card="() => (isUpdateCardOpen = false)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip />
|
||||||
|
|
||||||
<transition name="modal-anim">
|
<transition name="modal-anim">
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<TrainModal v-if="store.chosenModalTrainId" />
|
<TrainModal />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
@@ -10,7 +17,7 @@
|
|||||||
|
|
||||||
<main class="app_main">
|
<main class="app_main">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive exclude="JournalView">
|
<keep-alive exclude="SceneryView">
|
||||||
<component :is="Component" :key="$route.name" />
|
<component :is="Component" :key="$route.name" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
@@ -20,7 +27,10 @@
|
|||||||
©
|
©
|
||||||
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
|
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
|
||||||
{{ new Date().getUTCFullYear() }} |
|
{{ new Date().getUTCFullYear() }} |
|
||||||
<a :href="releaseURL" target="_blank">v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}</a>
|
<button class="btn--text" @click="() => (isUpdateCardOpen = true)">
|
||||||
|
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<a href="https://discord.gg/x2mpNN3svk">
|
<a href="https://discord.gg/x2mpNN3svk">
|
||||||
<img src="/images/icon-discord.png" alt="" /> <b>{{ $t('footer.discord') }}</b>
|
<img src="/images/icon-discord.png" alt="" /> <b>{{ $t('footer.discord') }}</b>
|
||||||
@@ -32,98 +42,81 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, watch } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { version } from '.././package.json';
|
||||||
|
import { Status } from './typings/common';
|
||||||
|
import { useMainStore } from './store/mainStore';
|
||||||
|
import { useApiStore } from './store/apiStore';
|
||||||
|
import { useTooltipStore } from './store/tooltipStore';
|
||||||
|
|
||||||
import Clock from './components/App/Clock.vue';
|
import Clock from './components/App/Clock.vue';
|
||||||
|
|
||||||
import packageInfo from '.././package.json';
|
|
||||||
import { regions } from './data/options.json';
|
|
||||||
|
|
||||||
import { useStore } from './store/store';
|
|
||||||
import StatusIndicator from './components/App/StatusIndicator.vue';
|
import StatusIndicator from './components/App/StatusIndicator.vue';
|
||||||
import TrainModal from './components/Global/TrainModal.vue';
|
|
||||||
import StorageManager from './scripts/managers/storageManager';
|
|
||||||
import AppHeader from './components/App/AppHeader.vue';
|
import AppHeader from './components/App/AppHeader.vue';
|
||||||
import axios from 'axios';
|
import TrainModal from './components/TrainsView/TrainModal.vue';
|
||||||
|
import Tooltip from './components/Tooltip/Tooltip.vue';
|
||||||
|
import UpdateCard from './components/App/UpdateCard.vue';
|
||||||
|
|
||||||
|
import StorageManager from './managers/storageManager';
|
||||||
|
|
||||||
|
const STORAGE_VERSION_KEY = 'app_version';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
Clock,
|
Clock,
|
||||||
StatusIndicator,
|
StatusIndicator,
|
||||||
|
AppHeader,
|
||||||
TrainModal,
|
TrainModal,
|
||||||
AppHeader
|
UpdateCard,
|
||||||
|
Tooltip
|
||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
VERSION: packageInfo.version,
|
VERSION: version,
|
||||||
store: useStore(),
|
store: useMainStore(),
|
||||||
|
apiStore: useApiStore(),
|
||||||
|
tooltipStore: useTooltipStore(),
|
||||||
|
|
||||||
|
isUpdateCardOpen: false,
|
||||||
|
|
||||||
currentLang: 'pl',
|
currentLang: 'pl',
|
||||||
releaseURL: '',
|
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
|
||||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
|
|
||||||
|
nextUpdateTime: 0
|
||||||
}),
|
}),
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.loadLang();
|
this.init();
|
||||||
this.store.connectToAPI();
|
|
||||||
|
|
||||||
this.store.isOffline = !window.navigator.onLine;
|
|
||||||
|
|
||||||
window.addEventListener('offline', () => {
|
|
||||||
this.store.isOffline = true;
|
|
||||||
|
|
||||||
this.store.apiData = {
|
|
||||||
stations: [],
|
|
||||||
dispatchers: [],
|
|
||||||
trains: [],
|
|
||||||
connectedSocketCount: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
this.store.setStatuses();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('online', () => {
|
|
||||||
this.store.isOffline = false;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.setReleaseURL();
|
window.addEventListener('mousemove', (e: MouseEvent) => this.tooltipStore.handle(e));
|
||||||
|
|
||||||
watch(
|
|
||||||
() => this.store.blockScroll,
|
|
||||||
(value) => {
|
|
||||||
if (value) document.body.classList.add('no-scroll');
|
|
||||||
else document.body.classList.remove('no-scroll');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
'$route.query.region': {
|
|
||||||
immediate: true,
|
|
||||||
handler(regionQuery: string) {
|
|
||||||
if (regionQuery) {
|
|
||||||
this.store.region.id =
|
|
||||||
regions.find(
|
|
||||||
(reg) =>
|
|
||||||
reg.id == regionQuery.toLocaleLowerCase() ||
|
|
||||||
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
|
|
||||||
)?.id || 'eu';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
changeLang(lang: string) {
|
init() {
|
||||||
this.$i18n.locale = lang;
|
this.loadLang();
|
||||||
this.currentLang = lang;
|
this.setupOfflineHandling();
|
||||||
|
this.checkAppVersion();
|
||||||
|
|
||||||
StorageManager.setStringValue('lang', lang);
|
this.apiStore.setupAPIData();
|
||||||
|
window.requestAnimationFrame(this.update);
|
||||||
|
|
||||||
|
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
|
||||||
},
|
},
|
||||||
|
|
||||||
async setReleaseURL() {
|
update(t: number) {
|
||||||
|
if (t >= this.nextUpdateTime) {
|
||||||
|
this.apiStore.fetchActiveData();
|
||||||
|
this.nextUpdateTime = t + 20000;
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame(this.update);
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkAppVersion() {
|
||||||
|
const storageVersion = StorageManager.getStringValue(STORAGE_VERSION_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const releaseData = await (
|
const releaseData = await (
|
||||||
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
|
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
|
||||||
@@ -131,11 +124,49 @@ export default defineComponent({
|
|||||||
|
|
||||||
if (!releaseData) return;
|
if (!releaseData) return;
|
||||||
|
|
||||||
this.releaseURL = releaseData.html_url;
|
this.store.appUpdate = {
|
||||||
|
version,
|
||||||
|
changelog: releaseData.body,
|
||||||
|
releaseURL: releaseData.html_url
|
||||||
|
};
|
||||||
|
|
||||||
|
this.isUpdateCardOpen =
|
||||||
|
(storageVersion != '' && storageVersion != version) ||
|
||||||
|
import.meta.env.VITE_UPDATE_TEST === 'test';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
|
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StorageManager.setStringValue(STORAGE_VERSION_KEY, version);
|
||||||
|
},
|
||||||
|
|
||||||
|
setupOfflineHandling() {
|
||||||
|
this.store.isOffline = !window.navigator.onLine;
|
||||||
|
|
||||||
|
if (this.store.isOffline) this.handleOfflineMode();
|
||||||
|
|
||||||
|
window.addEventListener('offline', this.handleOfflineMode);
|
||||||
|
window.addEventListener('online', this.handleOnlineMode);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleOfflineMode() {
|
||||||
|
this.store.isOffline = true;
|
||||||
|
|
||||||
|
this.apiStore.activeData = undefined;
|
||||||
|
this.apiStore.dataStatuses.connection = Status.Data.Offline;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleOnlineMode() {
|
||||||
|
this.store.isOffline = false;
|
||||||
|
|
||||||
|
this.apiStore.connectToAPI();
|
||||||
|
},
|
||||||
|
|
||||||
|
changeLang(lang: string) {
|
||||||
|
this.$i18n.locale = lang;
|
||||||
|
this.currentLang = lang;
|
||||||
|
|
||||||
|
StorageManager.setStringValue('lang', lang);
|
||||||
},
|
},
|
||||||
|
|
||||||
loadLang() {
|
loadLang() {
|
||||||
@@ -150,7 +181,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const naviLanguage = window.navigator.language.toString();
|
const naviLanguage = window.navigator.language.toString();
|
||||||
|
|
||||||
if (naviLanguage.includes('en')) {
|
if (naviLanguage.startsWith('en')) {
|
||||||
this.changeLang('en');
|
this.changeLang('en');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -159,4 +190,81 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" src="./App.scss"></style>
|
<style lang="scss">
|
||||||
|
@import './styles/global';
|
||||||
|
@import './styles/animations';
|
||||||
|
|
||||||
|
.route {
|
||||||
|
margin: 0 0.2em;
|
||||||
|
|
||||||
|
&-active,
|
||||||
|
&[data-active='true'] {
|
||||||
|
color: $accentCol;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// APP
|
||||||
|
#app {
|
||||||
|
color: white;
|
||||||
|
font-size: 1rem;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
@include smallScreen() {
|
||||||
|
font-size: calc(0.65rem + 0.85vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include screenLandscape() {
|
||||||
|
font-size: calc(0.45rem + 0.8vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONTAINER
|
||||||
|
.app_container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app_main {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
background-color: firebrick;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5em 0.4em;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
border-radius: 0 0 1em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FOOTER
|
||||||
|
.app_footer {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1.1em;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
background: #111;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -29,11 +29,6 @@
|
|||||||
<img src="/images/icon-dispatcher.svg" alt="icon dispatcher" />
|
<img src="/images/icon-dispatcher.svg" alt="icon dispatcher" />
|
||||||
<span class="text--primary">{{ onlineDispatchersCount }}</span>
|
<span class="text--primary">{{ onlineDispatchersCount }}</span>
|
||||||
|
|
||||||
<!-- <span class="g-tooltip">
|
|
||||||
<b class="text--primary">{{ factorU }}U</b>
|
|
||||||
<div class="content">Test</div>
|
|
||||||
</span> -->
|
|
||||||
|
|
||||||
<span class="text--grayed"> / </span>
|
<span class="text--grayed"> / </span>
|
||||||
<span class="text--primary">{{ onlineTrainsCount }}</span>
|
<span class="text--primary">{{ onlineTrainsCount }}</span>
|
||||||
<img src="/images/icon-train.svg" alt="icon train" />
|
<img src="/images/icon-train.svg" alt="icon train" />
|
||||||
@@ -68,7 +63,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { useStore } from '../../store/store';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import StatusIndicator from './StatusIndicator.vue';
|
import StatusIndicator from './StatusIndicator.vue';
|
||||||
import Clock from './Clock.vue';
|
import Clock from './Clock.vue';
|
||||||
import RegionDropdown from '../Global/RegionDropdown.vue';
|
import RegionDropdown from '../Global/RegionDropdown.vue';
|
||||||
@@ -84,7 +79,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
store: useStore()
|
store: useMainStore()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -96,17 +91,13 @@ export default defineComponent({
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
onlineTrainsCount() {
|
onlineTrainsCount() {
|
||||||
return this.store.trainList.filter((train) => train.online).length;
|
return this.store.trainList.filter((train) => train.region == this.store.region.id).length;
|
||||||
},
|
},
|
||||||
|
|
||||||
onlineDispatchersCount() {
|
onlineDispatchersCount() {
|
||||||
return this.store.onlineSceneryList.length;
|
return this.store.activeSceneryList.filter(
|
||||||
},
|
(scenery) => scenery.region == this.store.region.id && scenery.dispatcherId != -1
|
||||||
|
).length;
|
||||||
factorU() {
|
|
||||||
return this.onlineDispatchersCount == 0
|
|
||||||
? '-'
|
|
||||||
: (this.onlineTrainsCount / this.onlineDispatchersCount).toFixed(2);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { StatusIndicator, Clock, RegionDropdown }
|
components: { StatusIndicator, Clock, RegionDropdown }
|
||||||
|
|||||||
@@ -36,11 +36,11 @@
|
|||||||
<circle id="Ellipse 18" cx="15" cy="17" r="7" fill="#393838" />
|
<circle id="Ellipse 18" cx="15" cy="17" r="7" fill="#393838" />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g v-if="greenLight" filter="url(#filter0_d_843_28)">
|
<g v-if="indicator.lights.greenLight" filter="url(#filter0_d_843_28)">
|
||||||
<circle cx="15" cy="17" r="7" fill="#00FF0A" />
|
<circle cx="15" cy="17" r="7" fill="#00FF0A" />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g v-if="greenBlinkLight" filter="url(#filter0_d_843_28)">
|
<g v-if="indicator.lights.greenBlinkLight" filter="url(#filter0_d_843_28)">
|
||||||
<circle cx="15" cy="17" r="7" fill="#00FF0A" />
|
<circle cx="15" cy="17" r="7" fill="#00FF0A" />
|
||||||
|
|
||||||
<animate
|
<animate
|
||||||
@@ -52,14 +52,14 @@
|
|||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g v-if="redTopLight" filter="url(#filter1_d_843_28)">
|
<g v-if="indicator.lights.redTopLight" filter="url(#filter1_d_843_28)">
|
||||||
<circle cx="15" cy="36" r="7" fill="#F40000" />
|
<circle cx="15" cy="36" r="7" fill="#F40000" />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g v-if="orangeLight" filter="url(#filter2_d_843_28)">
|
<g v-if="indicator.lights.orangeLight" filter="url(#filter2_d_843_28)">
|
||||||
<circle cx="15" cy="55" r="7" fill="#FFB800" />
|
<circle cx="15" cy="55" r="7" fill="#FFB800" />
|
||||||
</g>
|
</g>
|
||||||
<g v-if="redBottomLight" filter="url(#filter3_d_843_28)">
|
<g v-if="indicator.lights.redBottomLight" filter="url(#filter3_d_843_28)">
|
||||||
<circle cx="15" cy="74" r="7" fill="#F40000" />
|
<circle cx="15" cy="74" r="7" fill="#F40000" />
|
||||||
|
|
||||||
<animate
|
<animate
|
||||||
@@ -186,7 +186,11 @@
|
|||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<transition name="tooltip-anim">
|
<transition name="tooltip-anim">
|
||||||
<div v-html="$t(indicator.message)" class="indicator-tooltip" v-if="tooltipActive"></div>
|
<div
|
||||||
|
v-html="$t('data-status.' + indicator.message)"
|
||||||
|
class="indicator-tooltip"
|
||||||
|
v-if="tooltipActive"
|
||||||
|
></div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,124 +198,112 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
import { Status } from '../../typings/common';
|
||||||
import { useStore } from '../../store/store';
|
import { useApiStore } from '../../store/apiStore';
|
||||||
import { StoreState } from '../../scripts/interfaces/store/storeTypes';
|
import { APIDataStatus } from '../../typings/api';
|
||||||
|
|
||||||
|
interface Indicator {
|
||||||
|
// status: Status.Data;
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
lights: {
|
||||||
|
greenLight: boolean;
|
||||||
|
greenBlinkLight: boolean;
|
||||||
|
redTopLight: boolean;
|
||||||
|
orangeLight: boolean;
|
||||||
|
redBottomLight: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tooltipActive: false,
|
tooltipActive: false,
|
||||||
indicator: {
|
apiStore: useApiStore()
|
||||||
offline: false,
|
|
||||||
status: DataStatus.Loading,
|
|
||||||
message: 'data-status.S3'
|
|
||||||
},
|
|
||||||
|
|
||||||
greenLight: false,
|
|
||||||
greenBlinkLight: false,
|
|
||||||
redTopLight: false,
|
|
||||||
orangeLight: false,
|
|
||||||
redBottomLight: false
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.setSignalStatus(DataStatus.Loading);
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
return {
|
|
||||||
dataStatus: store.dataStatuses,
|
|
||||||
store
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
dataStatus: {
|
|
||||||
deep: true,
|
|
||||||
|
|
||||||
handler(statuses: StoreState['dataStatuses']) {
|
|
||||||
const connectionStatus = statuses.connection;
|
|
||||||
const sceneryDataStatus = statuses.sceneries;
|
|
||||||
const trainsDataStatus = statuses.trains;
|
|
||||||
const dispatcherDataStatus = statuses.dispatchers;
|
|
||||||
|
|
||||||
if (this.store.isOffline) {
|
|
||||||
this.setSignalStatus(DataStatus.Initialized);
|
|
||||||
this.indicator.status = DataStatus.Initialized;
|
|
||||||
this.indicator.message = 'data-status.S1-offline';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectionStatus == DataStatus.Error) {
|
|
||||||
this.setSignalStatus(connectionStatus);
|
|
||||||
this.indicator.status = connectionStatus;
|
|
||||||
this.indicator.message = 'data-status.S1a-connection';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sceneryDataStatus == DataStatus.Error) {
|
|
||||||
this.setSignalStatus(sceneryDataStatus);
|
|
||||||
this.indicator.status = sceneryDataStatus;
|
|
||||||
this.indicator.message = 'data-status.S1a-sceneries';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trainsDataStatus == DataStatus.Warning) {
|
|
||||||
this.setSignalStatus(trainsDataStatus);
|
|
||||||
this.indicator.status = trainsDataStatus;
|
|
||||||
this.indicator.message = 'data-status.S5-trains';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dispatcherDataStatus == DataStatus.Warning) {
|
|
||||||
this.setSignalStatus(dispatcherDataStatus);
|
|
||||||
this.indicator.status = dispatcherDataStatus;
|
|
||||||
this.indicator.message = 'data-status.S5-dispatchers';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sceneryDataStatus == DataStatus.Loaded) {
|
|
||||||
this.setSignalStatus(DataStatus.Loaded);
|
|
||||||
|
|
||||||
this.indicator.status = DataStatus.Loaded;
|
|
||||||
this.indicator.message = 'data-status.S2';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
setSignalStatus(status: DataStatus) {
|
setLights(message: string) {
|
||||||
this.greenLight = false;
|
let lights = {
|
||||||
this.greenBlinkLight = false;
|
greenBlinkLight: false,
|
||||||
this.redTopLight = false;
|
greenLight: false,
|
||||||
this.orangeLight = false;
|
orangeLight: false,
|
||||||
this.redBottomLight = false;
|
redBottomLight: false,
|
||||||
|
redTopLight: false
|
||||||
|
};
|
||||||
|
|
||||||
if (status == DataStatus.Initialized) {
|
switch (message) {
|
||||||
this.redTopLight = true;
|
case 'S3':
|
||||||
|
lights.greenBlinkLight = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'S2':
|
||||||
|
lights.greenLight = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'S1-offline':
|
||||||
|
lights.redTopLight = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'S1a-connection':
|
||||||
|
case 'S1a-sceneries':
|
||||||
|
lights.redTopLight = true;
|
||||||
|
lights.redBottomLight = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'S5-dispatchers':
|
||||||
|
case 'S5-trains':
|
||||||
|
lights.orangeLight = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status == DataStatus.Loaded) {
|
return lights;
|
||||||
this.greenLight = true;
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
indicator(): Indicator {
|
||||||
|
const dataStatuses = this.apiStore.dataStatuses;
|
||||||
|
const swdrStatuses = this.apiStore.activeData?.apiStatuses;
|
||||||
|
|
||||||
|
let message = 'S3';
|
||||||
|
|
||||||
|
switch (dataStatuses.connection) {
|
||||||
|
case Status.Data.Loading:
|
||||||
|
message = 'S3';
|
||||||
|
break;
|
||||||
|
case Status.Data.Loaded:
|
||||||
|
message = 'S2';
|
||||||
|
break;
|
||||||
|
case Status.Data.Offline:
|
||||||
|
message = 'S1-offline';
|
||||||
|
break;
|
||||||
|
case Status.Data.Error:
|
||||||
|
message = 'S1a-connection';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status == DataStatus.Warning) {
|
if (swdrStatuses?.dispatchersAPI == APIDataStatus.WARNING) {
|
||||||
this.orangeLight = true;
|
message = 'S5-dispatchers';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status == DataStatus.Error) {
|
if (swdrStatuses?.trainsAPI == APIDataStatus.WARNING) {
|
||||||
this.redTopLight = true;
|
message = 'S5-trains';
|
||||||
this.redBottomLight = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status == DataStatus.Loading) {
|
if (swdrStatuses?.stationsAPI == APIDataStatus.WARNING) {
|
||||||
this.greenBlinkLight = true;
|
message = 'S1a-sceneries';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lights: this.setLights(message),
|
||||||
|
message
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
|
||||||
|
<div class="content">
|
||||||
|
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
|
||||||
|
|
||||||
|
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
|
||||||
|
<div class="no-features" v-else>{{ $t('update.no-data') }}</div>
|
||||||
|
|
||||||
|
<button class="btn btn--action" ref="confirm-btn" @click="toggleCard(false)">
|
||||||
|
{{ $t('update.confirm') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p class="bottom-info">
|
||||||
|
{{ $t('update.info-1') }}
|
||||||
|
<br />
|
||||||
|
<span v-html="$t('update.info-2')"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
import { version } from '../../../package.json';
|
||||||
|
import { Converter } from 'showdown';
|
||||||
|
|
||||||
|
import Card from '../Global/Card.vue';
|
||||||
|
|
||||||
|
const converter = new Converter();
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { Card },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
isUpdateCardOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['toggleCard'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mainStore: useMainStore(),
|
||||||
|
version: version
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
isUpdateCardOpen(val: boolean) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
htmlChangelog() {
|
||||||
|
if (this.mainStore.appUpdate == null) return '';
|
||||||
|
|
||||||
|
return converter.makeHtml(this.mainStore.appUpdate.changelog);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleCard(value: boolean) {
|
||||||
|
this.$emit('toggleCard', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
::v-deep(h1) {
|
||||||
|
text-align: center;
|
||||||
|
color: $accentCol;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(h2) {
|
||||||
|
padding: 0.25em 0;
|
||||||
|
border-bottom: 1px solid #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(ul) {
|
||||||
|
list-style: initial;
|
||||||
|
padding: 1em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 1em;
|
||||||
|
min-height: 700px;
|
||||||
|
overflow: auto;
|
||||||
|
text-align: justify;
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-features {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.bottom-info {
|
||||||
|
text-align: center;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="modal-anim" tag="div">
|
||||||
|
<div class="card" v-if="isOpen">
|
||||||
|
<div class="card-background" @click="toggleCard(false)"></div>
|
||||||
|
<div class="card-body" tabindex="0">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ['toggleCard'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
isOpen: Boolean
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
store: useMainStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
isOpen(v) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (v == false) (this.store.modalLastClickedTarget as HTMLElement)?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleCard(value: boolean) {
|
||||||
|
this.$emit('toggleCard', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/responsive.scss';
|
||||||
|
|
||||||
|
.card {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 200;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
background-color: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
margin: 1em;
|
||||||
|
|
||||||
|
max-height: 95vh;
|
||||||
|
max-height: 95dvh;
|
||||||
|
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
box-shadow: 0 0 15px 10px #0e0e0e;
|
||||||
|
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include smallScreen {
|
||||||
|
.card {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
<template>
|
||||||
|
<Card :isOpen="isCardOpen" @toggleCard="toggleCard" @keydown.esc="toggleCard(false)">
|
||||||
|
<div class="body">
|
||||||
|
<div class="content">
|
||||||
|
<h1 v-html="$t('donations.header')"></h1>
|
||||||
|
<div class="donators-slider" v-if="donatorList.length != 0">
|
||||||
|
<span v-html="$t('donations.donator-title', { count: donatorList.length })"></span>
|
||||||
|
|
||||||
|
<transition mode="out-in" name="slider-anim" class="current-name">
|
||||||
|
<span :key="displayingName">
|
||||||
|
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
|
||||||
|
{{ displayingName }}
|
||||||
|
</span>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<p v-html="$t('donations.p1')"></p>
|
||||||
|
<br />
|
||||||
|
<i18n-t keypath="donations.p2" tag="p">
|
||||||
|
<template v-slot:b1>
|
||||||
|
<b>{{ $t('donations.p2-b1') }}</b>
|
||||||
|
</template>
|
||||||
|
<template v-slot:b2>
|
||||||
|
<b>{{ $t('donations.p2-b2') }}</b>
|
||||||
|
</template>
|
||||||
|
<template v-slot:b3>
|
||||||
|
<b>{{ $t('donations.p2-b3') }}</b>
|
||||||
|
</template>
|
||||||
|
<template v-slot:link>
|
||||||
|
<a class="discord" href="https://discord.gg/x2mpNN3svk" target="_blank">
|
||||||
|
{{ $t('donations.p2-a1') }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
<br />
|
||||||
|
<p v-html="$t('donations.p3')"></p>
|
||||||
|
<br />
|
||||||
|
<i18n-t keypath="donations.p4" tag="p">
|
||||||
|
<template v-slot:img>
|
||||||
|
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:b1>
|
||||||
|
<b>{{ $t('donations.p4-b1') }}</b>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:b2>
|
||||||
|
<b>{{ $t('donations.p4-b2') }}</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
<br />
|
||||||
|
<i
|
||||||
|
v-html="$t('donations.p5')"
|
||||||
|
style="display: flex; justify-content: flex-end; text-align: right"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<a
|
||||||
|
class="action a-button btn--image coffee"
|
||||||
|
href="https://buycoffee.to/spythere"
|
||||||
|
target="_blank"
|
||||||
|
ref="action"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-coffee.png" width="20" alt="buycoffee.to donation" />
|
||||||
|
{{ $t('donations.action-buycoffee') }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="action a-button btn--image paypal"
|
||||||
|
href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-dollar.svg" alt="paypal donation" />
|
||||||
|
{{ $t('donations.action-paypal') }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="action btn--image exit" @click="toggleCard(false)">
|
||||||
|
<img src="/images/icon-exit.svg" alt="dollar donation icon" />
|
||||||
|
{{ $t('donations.action-exit') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import Card from './Card.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { Card },
|
||||||
|
props: {
|
||||||
|
isCardOpen: Boolean
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['toggleCard'],
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
isCardOpen(val: boolean) {
|
||||||
|
this.running = val;
|
||||||
|
this.lastUpdate = Date.now();
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (val) (this.$refs['action'] as HTMLElement).focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.runUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
apiStore: useApiStore(),
|
||||||
|
displayingIndex: 0,
|
||||||
|
lastUpdate: 0,
|
||||||
|
running: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
displayingName() {
|
||||||
|
return this.donatorList[this.displayingIndex];
|
||||||
|
},
|
||||||
|
|
||||||
|
donatorList() {
|
||||||
|
return this.apiStore.donatorsData.slice().sort(() => Math.sign(Math.random() * -2 + 1));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleCard(value: boolean) {
|
||||||
|
this.$emit('toggleCard', value);
|
||||||
|
},
|
||||||
|
|
||||||
|
runUpdate() {
|
||||||
|
if (Date.now() >= this.lastUpdate + 2000 && this.running) {
|
||||||
|
this.displayingIndex = (this.displayingIndex + 1) % this.donatorList.length;
|
||||||
|
this.lastUpdate = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(this.runUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/responsive.scss';
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr auto;
|
||||||
|
gap: 1em;
|
||||||
|
|
||||||
|
font-size: 1.1em;
|
||||||
|
|
||||||
|
max-width: 820px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 20px;
|
||||||
|
margin-right: 5px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.95em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.discord {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
form button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions > .action {
|
||||||
|
&.paypal {
|
||||||
|
$btnColor: #254069;
|
||||||
|
|
||||||
|
background-color: $btnColor;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lighten($btnColor, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.coffee {
|
||||||
|
$btnColor: #009255;
|
||||||
|
background-color: $btnColor;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lighten($btnColor, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.exit {
|
||||||
|
$btnColor: #686868;
|
||||||
|
background-color: $btnColor;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lighten($btnColor, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.donators-slider {
|
||||||
|
text-align: center;
|
||||||
|
line-height: 30px;
|
||||||
|
|
||||||
|
.current-name {
|
||||||
|
backface-visibility: hidden;
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
word-wrap: break-word;
|
||||||
|
color: var(--clr-donator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-anim {
|
||||||
|
&-move,
|
||||||
|
&-enter-active,
|
||||||
|
&-leave-active {
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-from,
|
||||||
|
&-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, Ref, ref } from 'vue';
|
import { defineComponent, Ref, ref } from 'vue';
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
import { regions as regionsJSON } from '../../data/options.json';
|
import { regions as regionsJSON } from '../../data/options.json';
|
||||||
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -41,7 +41,7 @@ interface Item {
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
store: useStore(),
|
store: useMainStore(),
|
||||||
selectedItemIndex: 0,
|
selectedItemIndex: 0,
|
||||||
listOpen: false
|
listOpen: false
|
||||||
};
|
};
|
||||||
@@ -60,6 +60,19 @@ export default defineComponent({
|
|||||||
handler(regionId) {
|
handler(regionId) {
|
||||||
this.selectedItemIndex = this.regionList.findIndex((reg) => reg.id == regionId);
|
this.selectedItemIndex = this.regionList.findIndex((reg) => reg.id == regionId);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'$route.query.region': {
|
||||||
|
immediate: true,
|
||||||
|
handler(regionQuery: string) {
|
||||||
|
if (regionQuery) {
|
||||||
|
this.store.region =
|
||||||
|
regionsJSON.find(
|
||||||
|
(reg) =>
|
||||||
|
reg.id == regionQuery.toLocaleLowerCase() ||
|
||||||
|
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
|
||||||
|
) ?? regionsJSON[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -67,16 +80,15 @@ export default defineComponent({
|
|||||||
selectedItem() {
|
selectedItem() {
|
||||||
return this.regionList[this.selectedItemIndex] || null;
|
return this.regionList[this.selectedItemIndex] || null;
|
||||||
},
|
},
|
||||||
|
|
||||||
regionList() {
|
regionList() {
|
||||||
return regionsJSON.map((region) => {
|
return regionsJSON.map((region) => {
|
||||||
const regionStationCount =
|
const regionStationCount = this.store.activeSceneryList.filter(
|
||||||
this.store.apiData.stations?.filter(
|
(scenery) => scenery.region == region.id && scenery.dispatcherId != -1
|
||||||
(station) => station.region == region.id && station.isOnline
|
).length;
|
||||||
).length || 0;
|
|
||||||
|
|
||||||
const regionTrainCount =
|
const regionTrainCount =
|
||||||
this.store.apiData.trains?.filter((train) => train.region == region.id && train.online)
|
this.store.trainList.filter((train) => train.region == region.id).length || 0;
|
||||||
.length || 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: region.id,
|
id: region.id,
|
||||||
@@ -127,15 +139,10 @@ button.selected-region {
|
|||||||
color: paleturquoise;
|
color: paleturquoise;
|
||||||
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 0.1em 0.5em;
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: #262626;
|
background-color: #262626;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -185,6 +192,8 @@ li.option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5em 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -195,10 +204,6 @@ li.option {
|
|||||||
background-color: #333333f2;
|
background-color: #333333f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
padding: 0.5em 0;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="status-badge" :class="statusID" v-if="isOnline">
|
<span class="status-badge" :class="statusName" v-if="isOnline">
|
||||||
{{ $t(`status.${statusID}`) }}
|
{{ $t(`status.${statusName}`) }}
|
||||||
{{ statusID == 'online' ? timestampToString(statusTimestamp!) : '' }}
|
{{
|
||||||
|
statusName == 'online' && dispatcherStatus && dispatcherStatus > 5
|
||||||
|
? timestampToString(dispatcherStatus)
|
||||||
|
: ''
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="status-badge free" v-else>
|
<span class="status-badge free" v-else>
|
||||||
@@ -10,22 +14,64 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
|
import { Status } from '../../typings/common';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
statusID: {
|
dispatcherStatus: {
|
||||||
type: String
|
type: Number as PropType<Status.ActiveDispatcher | number>
|
||||||
},
|
},
|
||||||
statusTimestamp: {
|
dispatcherTimestamp: {
|
||||||
type: Number
|
type: Number as PropType<number | null>
|
||||||
},
|
},
|
||||||
isOnline: {
|
isOnline: {
|
||||||
type: Boolean
|
type: Boolean
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mixins: [dateMixin]
|
mixins: [dateMixin],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
statusName() {
|
||||||
|
if (this.dispatcherStatus === undefined) return 'free';
|
||||||
|
|
||||||
|
switch (this.dispatcherStatus) {
|
||||||
|
case Status.ActiveDispatcher.AFK:
|
||||||
|
return 'afk';
|
||||||
|
|
||||||
|
case Status.ActiveDispatcher.NO_LIMIT:
|
||||||
|
return 'no-limit';
|
||||||
|
|
||||||
|
case Status.ActiveDispatcher.ENDING:
|
||||||
|
return 'ending';
|
||||||
|
|
||||||
|
case Status.ActiveDispatcher.INVALID:
|
||||||
|
return 'invalid';
|
||||||
|
|
||||||
|
case Status.ActiveDispatcher.NOT_LOGGED_IN:
|
||||||
|
return 'not-signed';
|
||||||
|
|
||||||
|
case Status.ActiveDispatcher.NO_SPACE:
|
||||||
|
return 'no-space';
|
||||||
|
|
||||||
|
case Status.ActiveDispatcher.UNAVAILABLE:
|
||||||
|
return 'unavailable';
|
||||||
|
|
||||||
|
case Status.ActiveDispatcher.UNKNOWN:
|
||||||
|
return 'unknown';
|
||||||
|
|
||||||
|
case Status.ActiveDispatcher.FREE:
|
||||||
|
return 'free';
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (this.dispatcherTimestamp != null && this.dispatcherStatus >= Date.now() + 25500000)
|
||||||
|
return 'no-limit';
|
||||||
|
|
||||||
|
return 'online';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -34,13 +80,13 @@ $free: #8a8a8a;
|
|||||||
$ending: #e6c300;
|
$ending: #e6c300;
|
||||||
$no-limit: #117fc9;
|
$no-limit: #117fc9;
|
||||||
$unav: #ff3d5d;
|
$unav: #ff3d5d;
|
||||||
$brb: #e6a100;
|
$afk: #e6a100;
|
||||||
$no-space: #222;
|
$no-space: #222;
|
||||||
$online: #09a116;
|
$online: #09a116;
|
||||||
$unknown: rgb(185, 60, 60);
|
$unknown: #b93c3c;
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
border-radius: 1rem;
|
border-radius: 1em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
padding: 0.2em 0.55em;
|
padding: 0.2em 0.55em;
|
||||||
@@ -69,8 +115,8 @@ $unknown: rgb(185, 60, 60);
|
|||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.brb {
|
&.afk {
|
||||||
background-color: $brb;
|
background-color: $afk;
|
||||||
color: black;
|
color: black;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
}
|
}
|
||||||
@@ -82,7 +128,8 @@ $unknown: rgb(185, 60, 60);
|
|||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unknown {
|
&.unknown,
|
||||||
|
&.invalid {
|
||||||
background-color: $unknown;
|
background-color: $unknown;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="stock-list">
|
<div class="stock-list">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(stockName, i) in trainStockList" :key="i">
|
<li
|
||||||
<p>
|
v-for="({ vehicleName, vehicleCargo, images, imagesFallbacks }, i) in thumbnailNames"
|
||||||
{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }}
|
:key="i"
|
||||||
{{ stockName.split(':')[1] }}
|
>
|
||||||
</p>
|
<div class="stock-text">
|
||||||
|
<p>{{ vehicleName.replace(/_/g, ' ') }}</p>
|
||||||
|
<small v-if="vehicleCargo">({{ vehicleCargo }})</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<img
|
<img
|
||||||
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${
|
v-for="(thumbnailImage, imageIndex) in images"
|
||||||
/^EN/.test(stockName) ? 'rb' : ''
|
:data-mouseover="vehicleName"
|
||||||
}.png`"
|
data-tooltip-type="VehiclePreviewTooltip"
|
||||||
@error="onImageError($event, stockName)"
|
:data-tooltip-content="vehicleName"
|
||||||
width="400"
|
:src="`https://static.spythere.eu/thumbnails/v2/${thumbnailImage}.png`"
|
||||||
|
@error="onImageError($event, imagesFallbacks[imageIndex])"
|
||||||
|
@click.stop="() => {}"
|
||||||
height="60"
|
height="60"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<img
|
|
||||||
v-if="/^(EN|2EN)/.test(stockName)"
|
|
||||||
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
|
|
||||||
@error="
|
|
||||||
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img
|
|
||||||
class="train-thumbnail"
|
|
||||||
v-if="/^EN71/.test(stockName)"
|
|
||||||
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
|
|
||||||
@error="
|
|
||||||
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img
|
|
||||||
class="train-thumbnail"
|
|
||||||
v-if="/^(EN|2EN)/.test(stockName)"
|
|
||||||
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`"
|
|
||||||
@error="
|
|
||||||
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -50,33 +29,139 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import { useStore } from '../../store/store';
|
import { useApiStore } from '../../store/apiStore';
|
||||||
import { RollingStockInfo } from '../../scripts/interfaces/github_api/StockInfoGithubData';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
trainStockList: {
|
trainStockList: {
|
||||||
type: Array as PropType<string[]>,
|
type: Array as PropType<string[]>,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
tractionOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
store: useStore()
|
apiStore: useApiStore()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
computed: {
|
||||||
onImageError(event: Event, stockName: string) {
|
computedStockList() {
|
||||||
const fallbackName =
|
return this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList;
|
||||||
Object.keys(this.store.rollingStockData!.info).find((type) => {
|
},
|
||||||
return this.store.rollingStockData!.info[type as keyof RollingStockInfo].find(
|
|
||||||
(v) => v[0] === stockName.split(':')[0]
|
|
||||||
);
|
|
||||||
}) || 'vehicle-unknown';
|
|
||||||
|
|
||||||
(event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`;
|
thumbnailNames() {
|
||||||
|
return (this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList)
|
||||||
|
.filter((v) => v.length != 0)
|
||||||
|
.map((vehicleString) => {
|
||||||
|
const [vehicleName, vehicleCargo] = vehicleString.split(':');
|
||||||
|
|
||||||
|
const vehicleThumbnailData = {
|
||||||
|
images: [] as string[],
|
||||||
|
imagesFallbacks: [] as string[],
|
||||||
|
vehicleName,
|
||||||
|
vehicleCargo
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generowanie członów EN57
|
||||||
|
if (vehicleName.startsWith('EN57')) {
|
||||||
|
vehicleThumbnailData['images'] = [
|
||||||
|
vehicleName + 'ra',
|
||||||
|
vehicleName + 's',
|
||||||
|
vehicleName + 'rb'
|
||||||
|
];
|
||||||
|
vehicleThumbnailData['imagesFallbacks'] = [
|
||||||
|
'unknown_ezt-ra',
|
||||||
|
'unknown_ezt-s',
|
||||||
|
'unknown_ezt-rb'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// Generowanie członów EN71
|
||||||
|
else if (vehicleName.startsWith('EN71')) {
|
||||||
|
vehicleThumbnailData['images'] = [
|
||||||
|
vehicleName + 'ra',
|
||||||
|
vehicleName + 'sa',
|
||||||
|
vehicleName + 'sb',
|
||||||
|
vehicleName + 'rb'
|
||||||
|
];
|
||||||
|
vehicleThumbnailData['imagesFallbacks'] = [
|
||||||
|
'unknown_ezt-ra',
|
||||||
|
'unknown_ezt-sa',
|
||||||
|
'unknown_ezt-sb',
|
||||||
|
'unknown_ezt-rb'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// Generowanie pojazdów i członów 2EN57
|
||||||
|
else if (vehicleString.startsWith('2EN57')) {
|
||||||
|
const [firstVehicleNumber, secondVehicleNumber] = vehicleString
|
||||||
|
.replace('2EN57-', '')
|
||||||
|
.split('+');
|
||||||
|
|
||||||
|
vehicleThumbnailData['images'] = [
|
||||||
|
`EN57-${firstVehicleNumber}ra`,
|
||||||
|
`EN57-${firstVehicleNumber}s`,
|
||||||
|
`EN57-${firstVehicleNumber}rb`,
|
||||||
|
`EN57-${secondVehicleNumber}ra`,
|
||||||
|
`EN57-${secondVehicleNumber}s`,
|
||||||
|
`EN57-${secondVehicleNumber}rb`
|
||||||
|
];
|
||||||
|
|
||||||
|
vehicleThumbnailData['imagesFallbacks'] = [
|
||||||
|
'unknown_ezt-ra',
|
||||||
|
'unknown_ezt-s',
|
||||||
|
'unknown_ezt-rb',
|
||||||
|
'unknown_ezt-ra',
|
||||||
|
'unknown_ezt-s',
|
||||||
|
'unknown_ezt-rb'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// Generowanie członów Gor77
|
||||||
|
else if (vehicleString.startsWith('Gor77')) {
|
||||||
|
vehicleThumbnailData['images'] = [
|
||||||
|
vehicleName + '-A',
|
||||||
|
vehicleName + '-B',
|
||||||
|
vehicleName + '-C',
|
||||||
|
vehicleName + '-D'
|
||||||
|
];
|
||||||
|
vehicleThumbnailData['imagesFallbacks'] = [
|
||||||
|
'unknown_Gor77-A',
|
||||||
|
'unknown_Gor77-B',
|
||||||
|
'unknown_Gor77-C',
|
||||||
|
'unknown_Gor77-D'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// Generowanie członów ET41
|
||||||
|
else if (vehicleString.startsWith('ET41')) {
|
||||||
|
vehicleThumbnailData['images'] = [vehicleName + '-A', vehicleName + '-B'];
|
||||||
|
vehicleThumbnailData['imagesFallbacks'] = ['unknown_ET41-A', 'unknown_ET41-B'];
|
||||||
|
}
|
||||||
|
// Generowanie pozostałych pojazdów
|
||||||
|
else {
|
||||||
|
let fallbackVehicleImage = 'unknown_cargo';
|
||||||
|
|
||||||
|
if (/^(EP|EU)/.test(vehicleName)) fallbackVehicleImage = 'unknown_train';
|
||||||
|
else if (/^(SM42)/.test(vehicleName)) fallbackVehicleImage = 'unknown_SM42';
|
||||||
|
else if (/(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(vehicleName))
|
||||||
|
fallbackVehicleImage = 'unknown_passenger';
|
||||||
|
|
||||||
|
vehicleThumbnailData['images'] = [vehicleName];
|
||||||
|
vehicleThumbnailData['imagesFallbacks'] = [fallbackVehicleImage];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.tractionOnly) vehicleThumbnailData['images'].length = 1;
|
||||||
|
|
||||||
|
return vehicleThumbnailData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onImageError(event: Event, fallbackImage: string) {
|
||||||
|
(event.target as HTMLImageElement).src = `/images/${fallbackImage}.png`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -99,6 +184,7 @@ export default defineComponent({
|
|||||||
ul > li > span {
|
ul > li > span {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
cursor: crosshair;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@@ -107,10 +193,14 @@ img {
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
img.traction-only {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-text {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 0.25em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span class="stop-date">
|
|
||||||
<span
|
|
||||||
class="date arrival"
|
|
||||||
v-if="!stop.beginsHere"
|
|
||||||
:class="{
|
|
||||||
delayed: stop.arrivalDelay > 0 && (stop.confirmed || stop.stopped),
|
|
||||||
preponed: stop.arrivalDelay < 0 && (stop.confirmed || stop.stopped),
|
|
||||||
'on-time': stop.arrivalDelay == 0 && stop.confirmed
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span v-if="stop.arrivalDelay != 0 && (stop.confirmed || stop.stopped)">
|
|
||||||
<s>{{ timestampToString(stop.arrivalTimestamp) }}</s>
|
|
||||||
{{ timestampToString(stop.arrivalRealTimestamp) }}
|
|
||||||
({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }})
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-else>
|
|
||||||
{{ timestampToString(stop.arrivalTimestamp) }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="date stop"
|
|
||||||
v-if="stop.stopTime || stop.stopped"
|
|
||||||
:class="stop.stopType.replace(', ', '-')"
|
|
||||||
>
|
|
||||||
{{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="date departure"
|
|
||||||
v-if="!stop.terminatesHere && (stop.stopTime != 0 || stop.stopped)"
|
|
||||||
:class="{
|
|
||||||
delayed: stop.departureDelay > 0 && stop.confirmed,
|
|
||||||
preponed: stop.departureDelay < 0 && stop.confirmed
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span v-if="stop.departureDelay != 0 && stop.confirmed">
|
|
||||||
<s>{{ timestampToString(stop.departureTimestamp) }}</s>
|
|
||||||
{{ timestampToString(stop.departureRealTimestamp) }}
|
|
||||||
|
|
||||||
({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }})
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-else>
|
|
||||||
{{ timestampToString(stop.departureTimestamp) }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
|
||||||
import TrainStop from '../../scripts/interfaces/TrainStop';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
mixins: [dateMixin],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
stop: {
|
|
||||||
type: Object as () => TrainStop,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
$preponedClr: lime;
|
|
||||||
$delayedClr: salmon;
|
|
||||||
$dateClr: #525151;
|
|
||||||
$stopExchangeClr: #db8e29;
|
|
||||||
$stopDefaultClr: #252525;
|
|
||||||
|
|
||||||
.stop-date {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.date {
|
|
||||||
background: $dateClr;
|
|
||||||
padding: 0.3em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stop {
|
|
||||||
&.ph,
|
|
||||||
&.ph-pm,
|
|
||||||
&.pm {
|
|
||||||
background: $stopExchangeClr;
|
|
||||||
}
|
|
||||||
|
|
||||||
background: $stopDefaultClr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrival,
|
|
||||||
.departure {
|
|
||||||
&.delayed {
|
|
||||||
s {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: $delayedClr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.preponed {
|
|
||||||
s {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: $preponedClr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal">
|
|
||||||
<div class="modal_background" @click="closeModal"></div>
|
|
||||||
<div class="modal_content" ref="content" tabindex="0">
|
|
||||||
<button class="btn exit" @click="closeModal">
|
|
||||||
<img src="/images/icon-exit.svg" alt="close card" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<TrainInfo :train="chosenTrain" :extended="false" ref="trainInfo" />
|
|
||||||
<TrainSchedule :train="chosenTrain" tabindex="0" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
|
||||||
import trainInfoMixin from '../../mixins/trainInfoMixin';
|
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
import TrainInfo from '../TrainsView/TrainInfo.vue';
|
|
||||||
import TrainSchedule from '../TrainsView/TrainSchedule.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { TrainInfo, TrainSchedule },
|
|
||||||
mixins: [trainInfoMixin, modalTrainMixin],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isTopBarVisible: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
return {
|
|
||||||
store
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
|
||||||
const contentEl = this.$refs['content'] as HTMLElement;
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
contentEl.focus();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
handleContentScroll(e: Event) {
|
|
||||||
const trainInfoCompHeight: number = (
|
|
||||||
this.$refs['trainInfo'] as any
|
|
||||||
).$el.getBoundingClientRect().height;
|
|
||||||
|
|
||||||
const posTop = (e.target as HTMLElement).scrollTop;
|
|
||||||
this.isTopBarVisible = posTop > trainInfoCompHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../styles/responsive.scss';
|
|
||||||
@import '../../styles/card.scss';
|
|
||||||
|
|
||||||
.top-info-bar-anim {
|
|
||||||
&-enter-active,
|
|
||||||
&-leave-active {
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-from,
|
|
||||||
&-leave-to {
|
|
||||||
transform: translate(-50%, -50%) scale(0.8);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.exit {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
margin: 0.5em 1em;
|
|
||||||
|
|
||||||
padding: 0.25em;
|
|
||||||
|
|
||||||
z-index: 201;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 1.5rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.train-modal {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
z-index: 200;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal_background {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
background-color: rgba(0, 0, 0, 0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal_content {
|
|
||||||
position: relative;
|
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
margin-top: 1em;
|
|
||||||
|
|
||||||
width: 95vw;
|
|
||||||
max-height: 96vh;
|
|
||||||
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
box-shadow: 0 0 15px 10px #0e0e0e;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include midScreen {
|
|
||||||
.exit {
|
|
||||||
margin: 0.5em;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 1.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include smallScreen {
|
|
||||||
.modal_content {
|
|
||||||
max-height: 85vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<template>
|
|
||||||
<img class="train-thumbnail" :src="placeholderUrl" v-if="isNotFound" />
|
|
||||||
|
|
||||||
<img
|
|
||||||
class="train-thumbnail"
|
|
||||||
v-else
|
|
||||||
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${name.split(':')[0]}${
|
|
||||||
stockType == 'loco-ezt' ? 'rb' : ''
|
|
||||||
}.png`"
|
|
||||||
@error="onImageError"
|
|
||||||
@load="onImageLoad"
|
|
||||||
width="220"
|
|
||||||
height="60"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
import { RollingStockInfo } from '../../scripts/interfaces/github_api/StockInfoGithubData';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
|
|
||||||
onlyFirstSegment: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
store: useStore(),
|
|
||||||
isNotFound: false,
|
|
||||||
isLoaded: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
url() {
|
|
||||||
return `https://rj.td2.info.pl/dist/img/thumbnails/${this.name.split(':')[0]}.png`;
|
|
||||||
},
|
|
||||||
|
|
||||||
placeholderUrl() {
|
|
||||||
return `/images/icon-${this.stockType}.png`;
|
|
||||||
},
|
|
||||||
|
|
||||||
stockType() {
|
|
||||||
if (!this.store.rollingStockData) return 'vehicle-unknown';
|
|
||||||
|
|
||||||
return (
|
|
||||||
Object.keys(this.store.rollingStockData.info).find((type) => {
|
|
||||||
return this.store.rollingStockData?.info[type as keyof RollingStockInfo].find(
|
|
||||||
(v) => v[0] === this.name.split(':')[0]
|
|
||||||
);
|
|
||||||
}) || 'vehicle-unknown'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onImageError() {
|
|
||||||
this.isNotFound = true;
|
|
||||||
this.isLoaded = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
onImageLoad() {
|
|
||||||
this.isNotFound = false;
|
|
||||||
this.isLoaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.train-thumbnail {
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-height: 60px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="daily-stats">
|
|
||||||
<span :data-active="statsStatus">
|
|
||||||
<b v-if="statsStatus == DataStatus.Loading">
|
|
||||||
{{ $t('app.loading') }}
|
|
||||||
</b>
|
|
||||||
|
|
||||||
<b v-else-if="stats.distanceSum == null">
|
|
||||||
{{ $t('journal.daily-stats-info') }}
|
|
||||||
</b>
|
|
||||||
|
|
||||||
<span class="stats-list" v-else>
|
|
||||||
<h3>
|
|
||||||
{{ $t('journal.daily-stats-title') }}
|
|
||||||
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
|
||||||
</h3>
|
|
||||||
<hr style="margin-bottom: 0.5em" />
|
|
||||||
|
|
||||||
<div v-if="stats.totalTimetables">
|
|
||||||
•
|
|
||||||
<i18n-t keypath="journal.timetable-stats-total">
|
|
||||||
<template #count>
|
|
||||||
<b class="text--primary">
|
|
||||||
{{ stats.totalTimetables }}
|
|
||||||
{{ $t('journal.timetable-count', stats.totalTimetables) }}
|
|
||||||
</b>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #distance>
|
|
||||||
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="stats.timetableId">
|
|
||||||
•
|
|
||||||
<i18n-t keypath="journal.timetable-stats-longest">
|
|
||||||
<template #id>
|
|
||||||
<router-link :to="`/journal/timetables?timetableId=${stats.timetableId}`">
|
|
||||||
<b>{{ stats.timetableId }}</b>
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
<template #author>
|
|
||||||
<router-link :to="`/journal/dispatchers?dispatcherName=${stats.timetableAuthor}`">
|
|
||||||
<b>{{ stats.timetableAuthor }}</b>
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
<template #driver>
|
|
||||||
<b class="text--primary">{{ stats.timetableDriver }}</b>
|
|
||||||
</template>
|
|
||||||
<template #distance>
|
|
||||||
<b class="text--primary">{{ stats.timetableRouteDistance }} km</b>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="topDispatchers.length == 1">
|
|
||||||
•
|
|
||||||
<i18n-t keypath="journal.timetable-stats-most-active-dr">
|
|
||||||
<template #dispatcher>
|
|
||||||
<router-link :to="`/journal/dispatchers?dispatcherName=${topDispatchers[0].name}`">
|
|
||||||
<b>{{ topDispatchers[0].name }}</b>
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
<template #count>
|
|
||||||
<b class="text--primary">
|
|
||||||
{{ topDispatchers[0].count }}
|
|
||||||
{{ $t('journal.timetable-count', topDispatchers[0].count) }}
|
|
||||||
</b>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="topDispatchers.length > 1">
|
|
||||||
•
|
|
||||||
<i18n-t keypath="journal.timetable-stats-most-active-dr-many">
|
|
||||||
<template #dispatchers>
|
|
||||||
<span v-for="(disp, i) in topDispatchers" :key="i">
|
|
||||||
<span v-if="i == topDispatchers.length - 1"> {{ $t('general.and') }} </span>
|
|
||||||
|
|
||||||
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
|
|
||||||
<b>{{ disp.name }}</b>
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<span v-if="i < topDispatchers.length - 2">, </span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #count>
|
|
||||||
<b class="text--primary">
|
|
||||||
{{ topDispatchers[0].count }}
|
|
||||||
{{ $t('journal.timetable-count', topDispatchers[0].count) }}
|
|
||||||
</b>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="stats.longestDuties.length > 0">
|
|
||||||
•
|
|
||||||
<i18n-t keypath="journal.timetable-stats-longest-duties">
|
|
||||||
<template #dispatcher>
|
|
||||||
<router-link
|
|
||||||
:to="`/journal/dispatchers?dispatcherName=${stats.longestDuties[0].name}`"
|
|
||||||
>
|
|
||||||
<b>{{ stats.longestDuties[0].name }}</b>
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #station>{{ stats.longestDuties[0].station }}</template>
|
|
||||||
|
|
||||||
<template #duration>
|
|
||||||
{{ calculateDuration(stats.longestDuties[0].duration) }}
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="stats.mostActiveDrivers.length > 0">
|
|
||||||
•
|
|
||||||
<i18n-t keypath="journal.timetable-stats-most-active-driver">
|
|
||||||
<template #driver>
|
|
||||||
<b class="text--primary">{{ stats.mostActiveDrivers[0].name }}</b>
|
|
||||||
</template>
|
|
||||||
<template #distance>
|
|
||||||
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import axios from 'axios';
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
|
||||||
import {
|
|
||||||
ITimetablesDailyStats,
|
|
||||||
ITimetablesDailyStatsResponse
|
|
||||||
} from '../../scripts/interfaces/api/StatsAPIData';
|
|
||||||
import { URLs } from '../../scripts/utils/apiURLs';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
mixins: [dateMixin],
|
|
||||||
emits: ['toggleStatsOpen'],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
DataStatus,
|
|
||||||
statsStatus: DataStatus.Loading,
|
|
||||||
intervalId: -1,
|
|
||||||
|
|
||||||
stats: {
|
|
||||||
totalTimetables: 0,
|
|
||||||
distanceSum: 0,
|
|
||||||
distanceAvg: 0,
|
|
||||||
timetableAuthor: '',
|
|
||||||
timetableDriver: '',
|
|
||||||
timetableId: 0,
|
|
||||||
timetableRouteDistance: 0,
|
|
||||||
longestDuties: [],
|
|
||||||
mostActiveDrivers: [],
|
|
||||||
mostActiveDispatchers: []
|
|
||||||
} as ITimetablesDailyStats
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
|
||||||
this.startFetchingDailyStats();
|
|
||||||
this.$emit('toggleStatsOpen', true);
|
|
||||||
},
|
|
||||||
|
|
||||||
deactivated() {
|
|
||||||
this.stopFetchingDailyStats();
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
topDispatchers() {
|
|
||||||
if (this.stats.mostActiveDispatchers.length == 0) return [];
|
|
||||||
const maxCount = this.stats.mostActiveDispatchers[0].count;
|
|
||||||
|
|
||||||
return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async fetchDailyTimetableStats() {
|
|
||||||
try {
|
|
||||||
const res: ITimetablesDailyStatsResponse = await (
|
|
||||||
await axios.get(`${URLs.stacjownikAPI}/api/getDailyTimetableStats`)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
this.stats = {
|
|
||||||
totalTimetables: res.totalTimetables,
|
|
||||||
distanceSum: res.distanceSum,
|
|
||||||
distanceAvg: res.distanceAvg,
|
|
||||||
timetableAuthor: res.maxTimetable?.authorName || '',
|
|
||||||
timetableDriver: res.maxTimetable?.driverName || '',
|
|
||||||
timetableId: res.maxTimetable?.id || 0,
|
|
||||||
timetableRouteDistance: res.maxTimetable?.routeDistance || 0,
|
|
||||||
|
|
||||||
mostActiveDispatchers: res.mostActiveDispatchers,
|
|
||||||
mostActiveDrivers: res.mostActiveDrivers,
|
|
||||||
longestDuties: res.longestDuties
|
|
||||||
};
|
|
||||||
|
|
||||||
this.statsStatus = DataStatus.Loaded;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
|
|
||||||
this.statsStatus = DataStatus.Error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
startFetchingDailyStats() {
|
|
||||||
this.fetchDailyTimetableStats();
|
|
||||||
|
|
||||||
if (this.intervalId != -1) return;
|
|
||||||
|
|
||||||
this.intervalId = setInterval(this.fetchDailyTimetableStats, 60000);
|
|
||||||
},
|
|
||||||
|
|
||||||
stopFetchingDailyStats() {
|
|
||||||
clearInterval(this.intervalId);
|
|
||||||
this.intervalId = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../styles/responsive.scss';
|
|
||||||
|
|
||||||
.daily-stats {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.daily-stats > span[data-active='0'] {
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-list a {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include smallScreen {
|
|
||||||
h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="stats_container" v-click-outside="() => (cardVisible = false)">
|
|
||||||
<button class="stats_button" @click="toggleCard">
|
|
||||||
Statystyki dyżurnego {{ store.dispatcherStatsName }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="stats_card" v-if="store.dispatcherStatsName && cardVisible">
|
|
||||||
<div>
|
|
||||||
<Loading v-if="!store.dispatcherStatsData" />
|
|
||||||
|
|
||||||
<div class="loading" v-else-if="!store.dispatcherStatsData._count._all">
|
|
||||||
Ten dyżurny nie ma jeszcze szczegółowych statystyk!
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3>
|
|
||||||
|
|
||||||
<div class="info-stats" v-if="store.dispatcherStatsData._count._all">
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>LICZBA</span>
|
|
||||||
<span>{{ store.dispatcherStatsData._count._all }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>SUMA (KM)</span>
|
|
||||||
<span>{{ store.dispatcherStatsData._sum.routeDistance.toFixed(2) }}km</span>
|
|
||||||
</span>
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>NAJDŁUŻSZY</span>
|
|
||||||
<span>{{ store.dispatcherStatsData._max.routeDistance.toFixed(2) }}km</span>
|
|
||||||
</span>
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>ŚREDNIO</span>
|
|
||||||
<span>{{ store.dispatcherStatsData._avg.routeDistance.toFixed(2) }}km</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>OSTATNIE WYSTAWIONE ROZKŁADY</h3>
|
|
||||||
<div class="last-timetables">
|
|
||||||
<div class="timetable-row" v-for="timetable in timetables" :key="timetable.id">
|
|
||||||
#{{ timetable.timetableId }} |
|
|
||||||
<b>{{ timetable.trainCategoryCode }} {{ timetable.trainNo }}</b> |
|
|
||||||
{{ timetable.driverName }} ({{ timetable.routeDistance }}km)
|
|
||||||
<div>{{ timetable.route.replace('|', ' > ') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import axios from 'axios';
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { DispatcherStatsAPIData } from '../../scripts/interfaces/api/DispatcherStatsAPIData';
|
|
||||||
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
|
|
||||||
import { URLs } from '../../scripts/utils/apiURLs';
|
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
import Loading from '../Global/Loading.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { Loading },
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
return {
|
|
||||||
store
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
cardVisible: false,
|
|
||||||
lastDispatcherName: '',
|
|
||||||
timetables: [] as TimetableHistory[]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
toggleCard() {
|
|
||||||
if (!this.store.dispatcherStatsName) return;
|
|
||||||
|
|
||||||
this.cardVisible = !this.cardVisible;
|
|
||||||
if (this.cardVisible) this.fetchDispatcherStats();
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchDispatcherStats() {
|
|
||||||
if (this.lastDispatcherName != this.store.dispatcherStatsName) {
|
|
||||||
this.store.dispatcherStatsData = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statsData: DispatcherStatsAPIData = await (
|
|
||||||
await axios.get(
|
|
||||||
`${URLs.stacjownikAPI}/api/getDispatcherInfo?name=${this.store.dispatcherStatsName}`
|
|
||||||
)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
const timetables: TimetableHistory[] = await (
|
|
||||||
await axios.get(
|
|
||||||
`${URLs.stacjownikAPI}/api/getTimetables?authorName=${this.store.dispatcherStatsName}`
|
|
||||||
)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
this.timetables = timetables;
|
|
||||||
this.store.dispatcherStatsData = statsData;
|
|
||||||
this.lastDispatcherName = this.store.dispatcherStatsName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../styles/responsive.scss';
|
|
||||||
@import '../../styles/variables.scss';
|
|
||||||
|
|
||||||
.stats_container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats_card {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 999;
|
|
||||||
top: 120%;
|
|
||||||
right: 0;
|
|
||||||
width: 500px;
|
|
||||||
max-width: 97vw;
|
|
||||||
min-height: 100px;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
border-radius: 1em 0 1em 1em;
|
|
||||||
background-color: #222222f1;
|
|
||||||
box-shadow: 0 3px 10px 5px #131313;
|
|
||||||
padding: 1em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last-timetables {
|
|
||||||
max-height: 400px;
|
|
||||||
margin: 0.5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable-row {
|
|
||||||
width: 95%;
|
|
||||||
margin: 0.5em auto;
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
background-color: #4d4d4d;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2.card-title {
|
|
||||||
font-size: 1.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2,
|
|
||||||
h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last-timetables {
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
<template>
|
||||||
|
<section class="daily-stats">
|
||||||
|
<span :data-active="statsStatus">
|
||||||
|
<span class="stats-list">
|
||||||
|
<h3>
|
||||||
|
{{ $t('journal.daily-stats.title') }}
|
||||||
|
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<hr class="header-separator" />
|
||||||
|
|
||||||
|
<b v-if="statsStatus == Status.Data.Loading">
|
||||||
|
{{ $t('app.loading') }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
|
||||||
|
{{ $t('journal.stats-error') }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<b v-else-if="topDispatchers.length == 0">
|
||||||
|
{{ $t('journal.daily-stats.info') }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="stats.totalTimetables">
|
||||||
|
•
|
||||||
|
<i18n-t keypath="journal.daily-stats.total">
|
||||||
|
<template #count>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ stats.totalTimetables }}
|
||||||
|
{{ $t('journal.daily-stats.count', stats.totalTimetables) }}
|
||||||
|
</b>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #distance>
|
||||||
|
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="stats.maxTimetable">
|
||||||
|
•
|
||||||
|
<i18n-t keypath="journal.daily-stats.longest">
|
||||||
|
<template #id>
|
||||||
|
<router-link :to="`/journal/timetables?search-train=%23${stats.maxTimetable.id}`">
|
||||||
|
<b>{{ stats.maxTimetable.id }}</b>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template #author>
|
||||||
|
<router-link
|
||||||
|
:to="`/journal/timetables?search-dispatcher=${stats.maxTimetable.authorName}`"
|
||||||
|
>
|
||||||
|
<b>{{ stats.maxTimetable.authorName }}</b>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template #driver>
|
||||||
|
<b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
|
||||||
|
</template>
|
||||||
|
<template #distance>
|
||||||
|
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="topDispatchers.length == 1">
|
||||||
|
•
|
||||||
|
<i18n-t keypath="journal.daily-stats.most-active-dr">
|
||||||
|
<template #dispatcher>
|
||||||
|
<router-link
|
||||||
|
:to="`/journal/dispatchers?search-dispatcher=${topDispatchers[0].name}`"
|
||||||
|
>
|
||||||
|
<b>{{ topDispatchers[0].name }}</b>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template #count>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ topDispatchers[0].count }}
|
||||||
|
{{ $t('journal.daily-stats.count', topDispatchers[0].count) }}
|
||||||
|
</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="topDispatchers.length > 1">
|
||||||
|
•
|
||||||
|
<i18n-t keypath="journal.daily-stats.most-active-dr-many">
|
||||||
|
<template #dispatchers>
|
||||||
|
<span v-for="(disp, i) in topDispatchers" :key="i">
|
||||||
|
<span v-if="i == topDispatchers.length - 1"> {{ $t('general.and') }} </span>
|
||||||
|
|
||||||
|
<router-link :to="`/journal/dispatchers?search-dispatcher=${disp.name}`">
|
||||||
|
<b>{{ disp.name }}</b>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<span v-if="i < topDispatchers.length - 2">, </span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #count>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ topDispatchers[0].count }}
|
||||||
|
{{ $t('journal.daily-stats.count', topDispatchers[0].count) }}
|
||||||
|
</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="stats.longestDuties.length > 0">
|
||||||
|
•
|
||||||
|
<i18n-t keypath="journal.daily-stats.longest-duties">
|
||||||
|
<template #dispatcher>
|
||||||
|
<router-link
|
||||||
|
:to="`/journal/dispatchers?search-dispatcher=${stats.longestDuties[0].name}`"
|
||||||
|
>
|
||||||
|
<b>{{ stats.longestDuties[0].name }}</b>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #station>{{ stats.longestDuties[0].station }}</template>
|
||||||
|
|
||||||
|
<template #duration>
|
||||||
|
{{ calculateDuration(stats.longestDuties[0].duration) }}
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="stats.mostActiveDrivers.length > 0">
|
||||||
|
•
|
||||||
|
<i18n-t keypath="journal.daily-stats.most-active-driver">
|
||||||
|
<template #driver>
|
||||||
|
<router-link
|
||||||
|
:to="`/journal/timetables?search-driver=${stats.mostActiveDrivers[0].name}`"
|
||||||
|
>
|
||||||
|
<b>{{ stats.mostActiveDrivers[0].name }}</b>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template #distance>
|
||||||
|
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="section-separator" />
|
||||||
|
|
||||||
|
<div class="stats-badges">
|
||||||
|
<span
|
||||||
|
class="stat-badge"
|
||||||
|
v-for="key in [
|
||||||
|
'rippedSwitches',
|
||||||
|
'derailments',
|
||||||
|
'skippedStopSignals',
|
||||||
|
'radioStops',
|
||||||
|
'kills'
|
||||||
|
]"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
|
||||||
|
<span>{{
|
||||||
|
Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--'
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
|
|
||||||
|
import { API } from '../../typings/api';
|
||||||
|
import { Status } from '../../typings/common';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'journal-daily-stats',
|
||||||
|
|
||||||
|
mixins: [dateMixin],
|
||||||
|
// emits: ['toggleStatsOpen'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
Status,
|
||||||
|
statsStatus: Status.Data.Loading,
|
||||||
|
intervalId: -1,
|
||||||
|
|
||||||
|
stats: {} as API.DailyStats.Response,
|
||||||
|
apiStore: useApiStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
activated() {
|
||||||
|
this.startFetchingDailyStats();
|
||||||
|
// this.$emit('toggleStatsOpen', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
deactivated() {
|
||||||
|
this.stopFetchingDailyStats();
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
topDispatchers() {
|
||||||
|
if (this.stats.mostActiveDispatchers.length == 0) return [];
|
||||||
|
const maxCount = this.stats.mostActiveDispatchers[0].count;
|
||||||
|
|
||||||
|
return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async fetchDailyTimetableStats() {
|
||||||
|
try {
|
||||||
|
const res: API.DailyStats.Response = await (
|
||||||
|
await this.apiStore.client!.get('api/getDailyStats')
|
||||||
|
).data;
|
||||||
|
|
||||||
|
this.stats = res;
|
||||||
|
|
||||||
|
this.statsStatus = Status.Data.Loaded;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
|
||||||
|
this.statsStatus = Status.Data.Error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startFetchingDailyStats() {
|
||||||
|
this.fetchDailyTimetableStats();
|
||||||
|
|
||||||
|
if (this.intervalId != -1) return;
|
||||||
|
|
||||||
|
this.intervalId = window.setInterval(this.fetchDailyTimetableStats, 60000);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopFetchingDailyStats() {
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
this.intervalId = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/responsive.scss';
|
||||||
|
@import '../../styles/JournalStats.scss';
|
||||||
|
@import '../../styles/badge.scss';
|
||||||
|
|
||||||
|
.daily-stats {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.daily-stats > span[data-active='0'] {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-list a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include smallScreen {
|
||||||
|
h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="journal-stats dispatcher" v-if="dispatcherName && stats">
|
||||||
|
<span class="loading" v-if="!stats.issuedTimetables && !stats.services">
|
||||||
|
{{ $t('journal.dispatcher-stats.empty') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else>
|
||||||
|
<h3>
|
||||||
|
<i18n-t keypath="journal.dispatcher-stats.title">
|
||||||
|
<template #name>
|
||||||
|
<span class="text--primary">{{ dispatcherName.toUpperCase() }}</span>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<hr class="header-separator" />
|
||||||
|
|
||||||
|
<div class="info-stats">
|
||||||
|
<span class="stat-badge" v-if="stats.services">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.services-count') }}</span>
|
||||||
|
<span>{{ stats.services.count }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge" v-if="stats.services">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.service-max') }}</span>
|
||||||
|
<span>{{ calculateDuration(stats.services.durationMax) }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge" v-if="stats.services">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.service-avg') }}</span>
|
||||||
|
<span>{{ calculateDuration(stats.services.durationAvg) }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="section-separator" />
|
||||||
|
|
||||||
|
<div class="info-stats">
|
||||||
|
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.timetables-count') }}</span>
|
||||||
|
<span>{{ stats.issuedTimetables.count }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.timetables-sum') }}</span>
|
||||||
|
<span>{{ stats.issuedTimetables.distanceSum.toFixed(2) }}km</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.timetables-max') }}</span>
|
||||||
|
<span>{{ stats.issuedTimetables.distanceMax.toFixed(2) }}km</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.timetables-avg') }}</span>
|
||||||
|
<span>{{ stats.issuedTimetables.distanceAvg.toFixed(2) }}km</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import dateMixin from '../../../mixins/dateMixin';
|
||||||
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'journal-dispatcher-stats',
|
||||||
|
|
||||||
|
mixins: [dateMixin],
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const store = useMainStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
stats: store.dispatcherStatsData,
|
||||||
|
dispatcherName: store.dispatcherStatsName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../styles/JournalStats.scss';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="status-anim" mode="out-in">
|
||||||
|
<div :key="dataStatus">
|
||||||
|
<div class="journal_warning" v-if="store.isOffline">
|
||||||
|
{{ $t('app.offline') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Loading v-else-if="dataStatus == Status.Data.Loading" />
|
||||||
|
|
||||||
|
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
|
||||||
|
{{ $t('app.error') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
|
||||||
|
{{ $t('app.no-result') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<table class="dispatchers-table">
|
||||||
|
<thead>
|
||||||
|
<th>{{ $t('journal.history-name') }}</th>
|
||||||
|
<th>{{ $t('journal.history-hash') }}</th>
|
||||||
|
<th>{{ $t('journal.history-dispatcher') }}</th>
|
||||||
|
<th>{{ $t('journal.history-level') }}</th>
|
||||||
|
<th>{{ $t('journal.history-rate') }}</th>
|
||||||
|
<th>{{ $t('journal.history-region') }}</th>
|
||||||
|
<th>{{ $t('journal.history-date') }}</th>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<transition-group name="list-anim">
|
||||||
|
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
|
||||||
|
<td>
|
||||||
|
<router-link
|
||||||
|
:to="`/journal/dispatchers?search-station=${historyItem.stationName}`"
|
||||||
|
>
|
||||||
|
<b>{{ historyItem.stationName }}</b>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>#{{ historyItem.stationHash }}</td>
|
||||||
|
<td>
|
||||||
|
<router-link
|
||||||
|
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
|
||||||
|
>
|
||||||
|
<b
|
||||||
|
v-if="apiStore.donatorsData.includes(historyItem.dispatcherName)"
|
||||||
|
class="text--donator"
|
||||||
|
:title="$t('donations.dispatcher-message')"
|
||||||
|
>
|
||||||
|
{{ historyItem.dispatcherName }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<b v-else>
|
||||||
|
{{ historyItem.dispatcherName }}
|
||||||
|
</b>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b
|
||||||
|
v-if="historyItem.dispatcherLevel !== null"
|
||||||
|
class="level-badge dispatcher"
|
||||||
|
:style="
|
||||||
|
calculateExpStyle(
|
||||||
|
historyItem.dispatcherLevel,
|
||||||
|
historyItem.dispatcherIsSupporter
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
|
||||||
|
</b>
|
||||||
|
</td>
|
||||||
|
<td class="text--primary">
|
||||||
|
<b>{{ historyItem.dispatcherRate }}</b>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b class="region-badge" :aria-describedby="historyItem.region">{{
|
||||||
|
regions.find((r) => r.id == historyItem.region)?.value || '???'
|
||||||
|
}}</b>
|
||||||
|
</td>
|
||||||
|
<td style="min-width: 200px" class="time">
|
||||||
|
<span v-if="historyItem.timestampTo" class="text--offline">
|
||||||
|
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
||||||
|
{{ timestampToString(historyItem.timestampFrom) }}
|
||||||
|
- {{ timestampToString(historyItem.timestampTo) }} ({{
|
||||||
|
calculateDuration(historyItem.currentDuration)
|
||||||
|
}})
|
||||||
|
</span>
|
||||||
|
<span class="dispatcher-online" v-else>
|
||||||
|
<b class="text--online">
|
||||||
|
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
|
||||||
|
$t('journal.online-since')
|
||||||
|
}}</router-link>
|
||||||
|
{{ timestampToString(historyItem.timestampFrom) }}
|
||||||
|
</b>
|
||||||
|
({{ calculateDuration(historyItem.currentDuration) }})
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</transition-group>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<AddDataButton
|
||||||
|
:list="dispatcherHistory"
|
||||||
|
:scrollDataLoaded="scrollDataLoaded"
|
||||||
|
:scrollNoMoreData="scrollNoMoreData"
|
||||||
|
@addHistoryData="addHistoryData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="journal_warning" v-if="scrollNoMoreData">
|
||||||
|
{{ $t('journal.no-further-data') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
||||||
|
{{ $t('journal.loading-further-data') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
import { regions } from '../../../data/options.json';
|
||||||
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
|
import { Status } from '../../../typings/common';
|
||||||
|
import Loading from '../../Global/Loading.vue';
|
||||||
|
import AddDataButton from '../../Global/AddDataButton.vue';
|
||||||
|
import dateMixin from '../../../mixins/dateMixin';
|
||||||
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { Loading, AddDataButton },
|
||||||
|
|
||||||
|
mixins: [dateMixin, styleMixin],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
dispatcherHistory: {
|
||||||
|
type: Array as PropType<API.DispatcherHistory.Response>,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
scrollNoMoreData: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
scrollDataLoaded: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
addHistoryData: {
|
||||||
|
type: Function as PropType<() => void>
|
||||||
|
},
|
||||||
|
dataStatus: {
|
||||||
|
type: Number as PropType<Status.Data>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
Status,
|
||||||
|
store: useMainStore(),
|
||||||
|
apiStore: useApiStore(),
|
||||||
|
regions
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
computedDispatcherHistory() {
|
||||||
|
return this.dispatcherHistory.reduce(
|
||||||
|
(acc, historyItem, i) => {
|
||||||
|
if (this.isAnotherDay(i - 1, i))
|
||||||
|
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
|
||||||
|
acc.push(historyItem);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as (API.DispatcherHistory.Data | string)[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
navigateToScenery(name: string, isOnline: boolean) {
|
||||||
|
if (!isOnline) return;
|
||||||
|
|
||||||
|
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
isAnotherDay(prevIndex: number, currIndex: number) {
|
||||||
|
if (currIndex == 0) return true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
|
||||||
|
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../styles/animations.scss';
|
||||||
|
@import '../../../styles/responsive.scss';
|
||||||
|
@import '../../../styles/badge.scss';
|
||||||
|
@import '../../../styles/variables.scss';
|
||||||
|
@import '../../../styles/JournalSection.scss';
|
||||||
|
|
||||||
|
table.dispatchers-table {
|
||||||
|
--_bg-table: #111;
|
||||||
|
--_bg-head: #101010;
|
||||||
|
--_bg-row: #2f2f2f;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: var(--_bg-head);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
background-color: var(--_bg-row);
|
||||||
|
border-bottom: 2px solid black;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0.75em;
|
||||||
|
|
||||||
|
.level-badge {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
&--online {
|
||||||
|
color: springgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--offline {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<transition name="status-anim" mode="out-in">
|
|
||||||
<div :key="dataStatus">
|
|
||||||
<div class="journal_warning" v-if="store.isOffline">
|
|
||||||
{{ $t('app.offline') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Loading v-else-if="dataStatus == DataStatus.Loading" />
|
|
||||||
|
|
||||||
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
|
|
||||||
{{ $t('app.error') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
|
|
||||||
{{ $t('app.no-result') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<table class="scenery-history-table">
|
|
||||||
<thead>
|
|
||||||
<th>{{ $t('journal.history-name') }}</th>
|
|
||||||
<th>{{ $t('journal.history-hash') }}</th>
|
|
||||||
<th>{{ $t('journal.history-dispatcher') }}</th>
|
|
||||||
<th>{{ $t('journal.history-level') }}</th>
|
|
||||||
<th>{{ $t('journal.history-rate') }}</th>
|
|
||||||
<th>{{ $t('journal.history-region') }}</th>
|
|
||||||
<th>{{ $t('journal.history-date') }}</th>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<transition-group name="list-anim">
|
|
||||||
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
|
|
||||||
<td>
|
|
||||||
<router-link
|
|
||||||
:to="`/journal/dispatchers?sceneryName=${historyItem.stationName}`"
|
|
||||||
>
|
|
||||||
<b>{{ historyItem.stationName }}</b>
|
|
||||||
</router-link>
|
|
||||||
</td>
|
|
||||||
<td>#{{ historyItem.stationHash }}</td>
|
|
||||||
<td>
|
|
||||||
<router-link
|
|
||||||
:to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`"
|
|
||||||
>
|
|
||||||
<b>{{ historyItem.dispatcherName }}</b>
|
|
||||||
</router-link>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<b
|
|
||||||
v-if="historyItem.dispatcherLevel !== null"
|
|
||||||
class="level-badge dispatcher"
|
|
||||||
:style="
|
|
||||||
calculateExpStyle(
|
|
||||||
historyItem.dispatcherLevel,
|
|
||||||
historyItem.dispatcherIsSupporter
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
|
|
||||||
</b>
|
|
||||||
</td>
|
|
||||||
<td class="text--primary">
|
|
||||||
<b>{{ historyItem.dispatcherRate }}</b>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<b class="region-badge" :aria-describedby="historyItem.region">{{
|
|
||||||
regions.find((r) => r.id == historyItem.region)?.value || '???'
|
|
||||||
}}</b>
|
|
||||||
</td>
|
|
||||||
<td style="min-width: 200px" class="time">
|
|
||||||
<span v-if="historyItem.timestampTo" class="text--offline">
|
|
||||||
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
|
||||||
{{ timestampToString(historyItem.timestampFrom) }}
|
|
||||||
- {{ timestampToString(historyItem.timestampTo) }} ({{
|
|
||||||
calculateDuration(historyItem.currentDuration)
|
|
||||||
}})
|
|
||||||
</span>
|
|
||||||
<span class="dispatcher-online" v-else>
|
|
||||||
<b class="text--online">
|
|
||||||
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
|
|
||||||
$t('journal.online-since')
|
|
||||||
}}</router-link>
|
|
||||||
{{ timestampToString(historyItem.timestampFrom) }}
|
|
||||||
</b>
|
|
||||||
({{ calculateDuration(historyItem.currentDuration) }})
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</transition-group>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<AddDataButton
|
|
||||||
:list="dispatcherHistory"
|
|
||||||
:scrollDataLoaded="scrollDataLoaded"
|
|
||||||
:scrollNoMoreData="scrollNoMoreData"
|
|
||||||
@addHistoryData="addHistoryData"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-if="scrollNoMoreData">
|
|
||||||
{{ $t('journal.no-further-data') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
|
||||||
{{ $t('journal.loading-further-data') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, PropType } from 'vue';
|
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
|
||||||
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
|
|
||||||
import styleMixin from '../../mixins/styleMixin';
|
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
import Loading from '../Global/Loading.vue';
|
|
||||||
import { regions } from '../../data/options.json';
|
|
||||||
import AddDataButton from '../Global/AddDataButton.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { Loading, AddDataButton },
|
|
||||||
|
|
||||||
mixins: [dateMixin, styleMixin],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
dispatcherHistory: {
|
|
||||||
type: Array as PropType<DispatcherHistory[]>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
scrollNoMoreData: {
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
scrollDataLoaded: {
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
addHistoryData: {
|
|
||||||
type: Function as PropType<() => void>
|
|
||||||
},
|
|
||||||
dataStatus: {
|
|
||||||
type: Number as PropType<DataStatus>
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
DataStatus,
|
|
||||||
store: useStore(),
|
|
||||||
regions
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
computedDispatcherHistory() {
|
|
||||||
console.log(this.dispatcherHistory.length);
|
|
||||||
|
|
||||||
return this.dispatcherHistory.reduce(
|
|
||||||
(acc, historyItem, i) => {
|
|
||||||
if (this.isAnotherDay(i - 1, i))
|
|
||||||
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
|
|
||||||
acc.push(historyItem);
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[] as (DispatcherHistory | string)[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
navigateToScenery(name: string, isOnline: boolean) {
|
|
||||||
if (!isOnline) return;
|
|
||||||
|
|
||||||
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
isAnotherDay(prevIndex: number, currIndex: number) {
|
|
||||||
if (currIndex == 0) return true;
|
|
||||||
|
|
||||||
return (
|
|
||||||
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
|
|
||||||
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../styles/animations.scss';
|
|
||||||
@import '../../styles/responsive.scss';
|
|
||||||
@import '../../styles/badge.scss';
|
|
||||||
@import '../../styles/variables.scss';
|
|
||||||
@import '../../styles/JournalSection.scss';
|
|
||||||
|
|
||||||
table.scenery-history-table {
|
|
||||||
--_bg-table: #111;
|
|
||||||
--_bg-head: #101010;
|
|
||||||
--_bg-row: #2f2f2f;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
margin-bottom: 1em;
|
|
||||||
|
|
||||||
thead {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: var(--_bg-head);
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr {
|
|
||||||
background-color: var(--_bg-row);
|
|
||||||
border-bottom: 2px solid black;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 0.75em;
|
|
||||||
|
|
||||||
.level-badge {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 550px) {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
&--online {
|
|
||||||
color: springgreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--offline {
|
|
||||||
color: #ddd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="filters-options" @keydown.esc="showOptions = false">
|
<div class="filters-options dropdown" @keydown.esc="showOptions = false">
|
||||||
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
<div class="dropdown_background" v-if="showOptions" @click="showOptions = false"></div>
|
||||||
|
|
||||||
<div class="actions-bar">
|
<div class="actions-bar">
|
||||||
<button
|
<button
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
ref="button"
|
ref="button"
|
||||||
>
|
>
|
||||||
<img src="/images/icon-filter2.svg" alt="Open filters" />
|
<img src="/images/icon-filter2.svg" alt="Open filters" />
|
||||||
{{ $t('options.filters') }} [F]
|
[F] {{ $t('options.filters') }}
|
||||||
<span class="active-indicator" v-if="currentOptionsActive"></span>
|
<span class="active-indicator" v-if="currentOptionsActive"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -27,13 +27,13 @@
|
|||||||
<option v-for="(sugg, i) in dispatcherSuggestions" :key="i" :value="sugg"></option>
|
<option v-for="(sugg, i) in dispatcherSuggestions" :key="i" :value="sugg"></option>
|
||||||
</datalist>
|
</datalist>
|
||||||
|
|
||||||
<transition name="options-anim">
|
<transition name="dropdown-anim">
|
||||||
<div class="options_wrapper" v-if="showOptions">
|
<div class="dropdown_wrapper" v-if="showOptions">
|
||||||
<div class="options_content">
|
<div class="options_content">
|
||||||
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
|
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
|
||||||
<div class="search_content">
|
<div class="search_content">
|
||||||
<div class="search" v-for="(_, propName) in searchersValues" :key="propName">
|
<div class="search" v-for="(_, propName) in searchersValues" :key="propName">
|
||||||
<label v-if="propName == 'search-date'" for="date">{{
|
<label v-if="propName == 'search-date'" for="search-date">{{
|
||||||
$t(`options.search-${optionsType}-date`)
|
$t(`options.search-${optionsType}-date`)
|
||||||
}}</label>
|
}}</label>
|
||||||
|
|
||||||
@@ -41,12 +41,13 @@
|
|||||||
<input
|
<input
|
||||||
class="search-input"
|
class="search-input"
|
||||||
v-model="searchersValues[propName]"
|
v-model="searchersValues[propName]"
|
||||||
@keydown.enter="onSearchConfirm"
|
@keydown.enter="searchConfirm"
|
||||||
@focus="preventKeyDown = true"
|
@focus="preventKeyDown = true"
|
||||||
@blur="preventKeyDown = false"
|
@blur="preventKeyDown = false"
|
||||||
:placeholder="$t(`options.${propName}`)"
|
:placeholder="$t(`options.${propName}`)"
|
||||||
:type="propName == 'search-date' ? 'date' : 'text'"
|
:type="propName == 'search-date' ? 'date' : 'text'"
|
||||||
:min="propName == 'search-date' ? '2022-02-01' : undefined"
|
:min="propName == 'search-date' ? '2022-02-01' : undefined"
|
||||||
|
:id="`${propName}`"
|
||||||
:list="propName.toString()"
|
:list="propName.toString()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -110,15 +111,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from 'axios';
|
|
||||||
import { defineComponent, inject, PropType } from 'vue';
|
import { defineComponent, inject, PropType } from 'vue';
|
||||||
import keyMixin from '../../mixins/keyMixin';
|
import keyMixin from '../../mixins/keyMixin';
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
|
import { Journal } from './typings';
|
||||||
import { URLs } from '../../scripts/utils/apiURLs';
|
import { Status } from '../../typings/common';
|
||||||
import { useStore } from '../../store/store';
|
import { useApiStore } from '../../store/apiStore';
|
||||||
import { JournalFilterSection } from '../../scripts/enums/JournalFilterType';
|
|
||||||
import { JournalFilter } from '../../scripts/types/JournalTimetablesTypes';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
|
emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
|
||||||
@@ -131,13 +129,13 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
filters: {
|
filters: {
|
||||||
type: Array as PropType<JournalFilter[]>,
|
type: Array as PropType<Journal.TimetableFilter[]>,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
|
||||||
dataStatus: {
|
dataStatus: {
|
||||||
type: Number as PropType<DataStatus>,
|
type: Number as PropType<Status.Data>,
|
||||||
default: DataStatus.Initialized
|
default: -1
|
||||||
},
|
},
|
||||||
|
|
||||||
currentOptionsActive: {
|
currentOptionsActive: {
|
||||||
@@ -154,15 +152,15 @@ export default defineComponent({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showOptions: false,
|
showOptions: false,
|
||||||
JournalFilterSection,
|
|
||||||
|
|
||||||
driverSuggestions: [] as string[],
|
driverSuggestions: [] as string[],
|
||||||
dispatcherSuggestions: [] as string[],
|
dispatcherSuggestions: [] as string[],
|
||||||
|
|
||||||
searchTimeout: 0,
|
searchTimeout: 0,
|
||||||
store: useStore(),
|
store: useMainStore(),
|
||||||
|
apiStore: useApiStore(),
|
||||||
|
|
||||||
DataStatus
|
JournalFilterSection: Journal.FilterSection
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -170,7 +168,7 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
searchersValues: inject('searchersValues') as { [key: string]: string },
|
searchersValues: inject('searchersValues') as { [key: string]: string },
|
||||||
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
|
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
|
||||||
filterList: inject('filterList') as JournalFilter[] | undefined
|
filterList: inject('filterList') as Journal.TimetableFilter[] | undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -184,12 +182,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
async 'store.driverStatsName'() {
|
|
||||||
await this.fetchDriverStats();
|
|
||||||
|
|
||||||
// if (value) this.store.currentStatsTab = 'driver';
|
|
||||||
},
|
|
||||||
|
|
||||||
async 'searchersValues.search-driver'(value: string | undefined) {
|
async 'searchersValues.search-driver'(value: string | undefined) {
|
||||||
clearTimeout(this.searchTimeout);
|
clearTimeout(this.searchTimeout);
|
||||||
|
|
||||||
@@ -208,29 +200,34 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async fetchDriverStats() {
|
// filters & sorters from URL params
|
||||||
this.store.driverStatsData = undefined;
|
handleRouteParams() {
|
||||||
|
this.$router.push({
|
||||||
|
query: {
|
||||||
|
...this.$route.query,
|
||||||
|
'sorter-active':
|
||||||
|
this.sorterOptionIds.indexOf(`${this.sorterActive.id}`) != 0
|
||||||
|
? this.sorterActive.id
|
||||||
|
: undefined,
|
||||||
|
...Object.keys(this.searchersValues).reduce(
|
||||||
|
(acc, k) => {
|
||||||
|
const searchVal = this.searchersValues[k as Journal.TimetableSearchKey];
|
||||||
|
|
||||||
if (!this.store.driverStatsName) {
|
acc[k] = searchVal || undefined;
|
||||||
this.store.driverStatsStatus = DataStatus.Initialized;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
return acc;
|
||||||
this.store.driverStatsStatus = DataStatus.Loading;
|
},
|
||||||
|
{} as { [k: string]: string | undefined }
|
||||||
const statsData: DriverStatsAPIData = await (
|
),
|
||||||
await axios.get(
|
...this.filterList?.reduce(
|
||||||
`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`
|
(acc, f) => {
|
||||||
|
if (f.isActive) acc[f.filterSection] = f.default ? undefined : f.id;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as { [k: string]: string | undefined }
|
||||||
)
|
)
|
||||||
).data;
|
|
||||||
|
|
||||||
this.store.driverStatsData = statsData;
|
|
||||||
this.store.driverStatsStatus = DataStatus.Loaded;
|
|
||||||
} catch (error) {
|
|
||||||
this.store.driverStatsStatus = DataStatus.Error;
|
|
||||||
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshData() {
|
refreshData() {
|
||||||
@@ -242,17 +239,17 @@ export default defineComponent({
|
|||||||
|
|
||||||
window.clearTimeout(this.searchTimeout);
|
window.clearTimeout(this.searchTimeout);
|
||||||
|
|
||||||
this.searchTimeout = setTimeout(async () => {
|
this.searchTimeout = window.setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const suggestions: string[] = await (
|
const suggestions: string[] = await (
|
||||||
await axios.get(`${URLs.stacjownikAPI}/api/get${type}Suggestions?name=${value}`)
|
await this.apiStore.client!.get(`api/get${type}Suggestions?name=${value}`)
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
this[`${type}Suggestions`] = suggestions;
|
this[`${type}Suggestions`] = suggestions;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this[`${type}Suggestions`] = [];
|
this[`${type}Suggestions`] = [];
|
||||||
}
|
}
|
||||||
}, 450);
|
}, 250);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Override keyMixin function
|
// Override keyMixin function
|
||||||
@@ -267,40 +264,43 @@ export default defineComponent({
|
|||||||
onSorterChange(item: { id: string | number; value: string }) {
|
onSorterChange(item: { id: string | number; value: string }) {
|
||||||
this.sorterActive.id = item.id;
|
this.sorterActive.id = item.id;
|
||||||
this.sorterActive.dir = -1;
|
this.sorterActive.dir = -1;
|
||||||
this.$emit('onSearchConfirm');
|
this.searchConfirm();
|
||||||
},
|
},
|
||||||
|
|
||||||
onFilterChange(filter: JournalFilter) {
|
onFilterChange(filter: Journal.TimetableFilter) {
|
||||||
// this.journalFilterActive = filter;
|
// this.journalFilterActive = filter;
|
||||||
this.filterList
|
this.filterList
|
||||||
?.filter((f) => f.filterSection === filter.filterSection)
|
?.filter((f) => f.filterSection === filter.filterSection)
|
||||||
.forEach((f) => (f.isActive = false));
|
.forEach((f) => (f.isActive = false));
|
||||||
filter.isActive = true;
|
filter.isActive = true;
|
||||||
|
|
||||||
this.$emit('onSearchConfirm');
|
this.searchConfirm();
|
||||||
},
|
},
|
||||||
|
|
||||||
onInputClear(id: any) {
|
onInputClear(id: any) {
|
||||||
this.searchersValues[id] = '';
|
this.searchersValues[id] = '';
|
||||||
this.$emit('onSearchConfirm');
|
this.searchConfirm();
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchConfirm() {
|
searchConfirm() {
|
||||||
this.$emit('onSearchConfirm');
|
this.$emit('onSearchConfirm');
|
||||||
|
this.handleRouteParams();
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchButtonConfirm() {
|
onSearchButtonConfirm() {
|
||||||
this.showOptions = false;
|
this.showOptions = false;
|
||||||
this.$emit('onSearchConfirm');
|
this.searchConfirm();
|
||||||
},
|
},
|
||||||
|
|
||||||
onResetButtonClick() {
|
onResetButtonClick() {
|
||||||
this.$emit('onOptionsReset');
|
this.$emit('onOptionsReset');
|
||||||
|
this.handleRouteParams();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/filters_options.scss';
|
@import '../../styles/dropdown';
|
||||||
|
@import '../../styles/dropdown_filters';
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,121 +1,85 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="journal-stats" v-if="!store.isOffline">
|
<div
|
||||||
<div class="tabs">
|
class="journal-stats dropdown"
|
||||||
<button
|
v-if="!mainStore.isOffline"
|
||||||
v-for="tab in data.tabs"
|
@keydown.esc="currentStatsTab = null"
|
||||||
:key="tab.name"
|
|
||||||
class="btn--filled"
|
|
||||||
:data-selected="tab.name == store.currentStatsTab && areStatsOpen"
|
|
||||||
:data-inactive="tab.inactive"
|
|
||||||
:data-disabled="tab.inactive"
|
|
||||||
:disabled="tab.inactive"
|
|
||||||
@click="onTabButtonClick(tab.name)"
|
|
||||||
>
|
>
|
||||||
{{ $t(tab.titlePath) }}
|
<div
|
||||||
|
class="dropdown_background"
|
||||||
|
v-if="currentStatsTab !== null"
|
||||||
|
@click="currentStatsTab = null"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="actions-bar">
|
||||||
|
<button
|
||||||
|
v-for="button in statsButtons"
|
||||||
|
:key="button.tab"
|
||||||
|
class="btn--filled btn--image"
|
||||||
|
:data-selected="button.tab == currentStatsTab"
|
||||||
|
:data-disabled="button.disabled"
|
||||||
|
:disabled="button.disabled"
|
||||||
|
@click="onTabButtonClick(button.tab)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="button.iconName"
|
||||||
|
:src="`/images/icon-${button.iconName}.svg`"
|
||||||
|
:alt="button.iconName"
|
||||||
|
/>
|
||||||
|
{{ $t(button.localeKey) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats-tab" v-show="areStatsOpen">
|
<transition name="dropdown-anim">
|
||||||
|
<div class="dropdown_wrapper" v-if="currentStatsTab !== null">
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<JournalDailyStats
|
<component :is="currentStatsTab" :key="currentStatsTab"></component>
|
||||||
v-if="store.currentStatsTab == 'daily'"
|
|
||||||
@toggleStatsOpen="toggleStatsOpen"
|
|
||||||
/>
|
|
||||||
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" />
|
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script lang="ts">
|
||||||
import { computed, onMounted, reactive, Ref, ref, watch } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import { useStore } from '../../store/store';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import JournalDailyStats from './DailyStats.vue';
|
import StorageManager from '../../managers/storageManager';
|
||||||
import JournalDriverStats from './JournalDriverStats.vue';
|
import { Journal } from './typings';
|
||||||
import StorageManager from '../../scripts/managers/storageManager';
|
import JournalDailyStats from './JournalDailyStats.vue';
|
||||||
|
import JournalDispatcherStats from '../JournalView/JournalDispatchers/JournalDispatcherStats.vue';
|
||||||
|
import JournalDriverStats from '../JournalView/JournalTimetables/JournalDriverStats.vue';
|
||||||
|
|
||||||
// Types
|
export default defineComponent({
|
||||||
type TStatTab = 'daily' | 'driver';
|
components: { JournalDailyStats, JournalDriverStats, JournalDispatcherStats },
|
||||||
|
props: {
|
||||||
// Variables
|
statsButtons: {
|
||||||
const store = useStore();
|
type: Array as PropType<Journal.StatsButton[]>,
|
||||||
|
required: true
|
||||||
const lastDailyStatsOpen = ref(false);
|
}
|
||||||
const areStatsOpen = ref(false);
|
|
||||||
const lastClickedTab: Ref<'daily' | 'driver' | null> = ref(null);
|
|
||||||
|
|
||||||
let data = reactive({
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
name: 'daily',
|
|
||||||
titlePath: 'journal.daily-stats-title'
|
|
||||||
},
|
},
|
||||||
{
|
data() {
|
||||||
name: 'driver',
|
return {
|
||||||
titlePath: 'journal.driver-stats-title'
|
Journal,
|
||||||
// inactive: true,
|
mainStore: useMainStore(),
|
||||||
|
currentStatsTab: null as Journal.StatsTab | null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onTabButtonClick(tab: Journal.StatsTab) {
|
||||||
|
this.currentStatsTab = tab == this.currentStatsTab ? null : tab;
|
||||||
|
|
||||||
|
StorageManager.setStringValue('journalStatsTab', this.currentStatsTab ?? '');
|
||||||
}
|
}
|
||||||
] as { name: TStatTab; titlePath: string; inactive?: boolean }[]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
function onTabButtonClick(tab: TStatTab) {
|
|
||||||
if (lastClickedTab.value == tab || !lastClickedTab.value || !areStatsOpen.value) areStatsOpen.value = !areStatsOpen.value;
|
|
||||||
|
|
||||||
if (tab == 'daily') {
|
|
||||||
StorageManager.setBooleanValue('dailyStatsOpen', areStatsOpen.value);
|
|
||||||
lastDailyStatsOpen.value = areStatsOpen.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.currentStatsTab = tab;
|
|
||||||
lastClickedTab.value = tab;
|
|
||||||
|
|
||||||
if (areStatsOpen.value == false) store.currentStatsTab = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStatsOpen(open: boolean) {
|
|
||||||
areStatsOpen.value = open;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
computed(() => store.driverStatsData),
|
|
||||||
(statsData) => {
|
|
||||||
store.currentStatsTab = statsData ? 'driver' : lastClickedTab.value;
|
|
||||||
areStatsOpen.value = statsData ? true : lastClickedTab.value !== null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (StorageManager.getBooleanValue('dailyStatsOpen')) {
|
|
||||||
areStatsOpen.value = true;
|
|
||||||
store.currentStatsTab = 'daily';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/JournalStats.scss';
|
@import '../../styles/dropdown.scss';
|
||||||
|
@import '../../styles/dropdown_filters.scss';
|
||||||
@import '../../styles/variables.scss';
|
@import '../../styles/variables.scss';
|
||||||
|
|
||||||
.tabs {
|
.dropdown_wrapper {
|
||||||
position: relative;
|
max-width: 100%;
|
||||||
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5em;
|
|
||||||
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 0.5em 0.75em;
|
|
||||||
|
|
||||||
&[data-inactive='true'] {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-selected='true'] {
|
|
||||||
color: $accentCol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="journal-stats">
|
<div class="journal-stats driver" v-if="store.driverStatsData">
|
||||||
<span v-if="store.driverStatsData">
|
<span>
|
||||||
<h3>
|
<h3>
|
||||||
{{ $t('journal.stats-title') }}
|
<i18n-t keypath="journal.driver-stats.title">
|
||||||
|
<template #name>
|
||||||
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
|
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<hr class="header-separator" />
|
||||||
|
|
||||||
<div class="info-stats">
|
<div class="info-stats">
|
||||||
<span class="stat-badge">
|
<span class="stat-badge">
|
||||||
<span>{{ $t('journal.stats-timetables') }}</span>
|
<span>{{ $t('journal.driver-stats.timetables') }}</span>
|
||||||
<span
|
<span
|
||||||
>{{ store.driverStatsData._count.fulfilled }} /
|
>{{ store.driverStatsData._count.fulfilled }} /
|
||||||
{{ store.driverStatsData._count._all }}</span
|
{{ store.driverStatsData._count._all }}</span
|
||||||
@@ -16,17 +21,17 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stat-badge">
|
<span class="stat-badge">
|
||||||
<span>{{ $t('journal.stats-longest-timetable') }}</span>
|
<span>{{ $t('journal.driver-stats.longest-timetable') }}</span>
|
||||||
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
|
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stat-badge">
|
<span class="stat-badge">
|
||||||
<span>{{ $t('journal.stats-avg-timetable') }}</span>
|
<span>{{ $t('journal.driver-stats.avg-timetable') }}</span>
|
||||||
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
|
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stat-badge">
|
<span class="stat-badge">
|
||||||
<span>{{ $t('journal.stats-distance') }}</span>
|
<span>{{ $t('journal.driver-stats.distance') }}</span>
|
||||||
<span>
|
<span>
|
||||||
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
|
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
|
||||||
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
|
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
|
||||||
@@ -34,7 +39,7 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stat-badge">
|
<span class="stat-badge">
|
||||||
<span>{{ $t('journal.stats-stations') }}</span>
|
<span>{{ $t('journal.driver-stats.stations') }}</span>
|
||||||
<span>
|
<span>
|
||||||
{{ store.driverStatsData._sum.confirmedStopsCount }} /
|
{{ store.driverStatsData._sum.confirmedStopsCount }} /
|
||||||
{{ store.driverStatsData._sum.allStopsCount }}
|
{{ store.driverStatsData._sum.allStopsCount }}
|
||||||
@@ -42,32 +47,26 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<b v-else-if="store.driverStatsStatus == DataStatus.Loading">{{
|
|
||||||
$t('journal.stats-loading')
|
|
||||||
}}</b>
|
|
||||||
<b v-else-if="store.driverStatsStatus == DataStatus.Error">
|
|
||||||
{{ $t('journal.stats-error ') }}
|
|
||||||
</b>
|
|
||||||
<b v-else>{{ $t('journal.driver-stats-info') }}</b>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
import { useStore } from '../../store/store';
|
import { Status } from '../../../typings/common';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
name: 'journal-driver-stats',
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
store: useStore(),
|
store: useMainStore(),
|
||||||
DataStatus
|
Status: Status
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/JournalStats.scss';
|
@import '../../../styles/JournalStats.scss';
|
||||||
</style>
|
</style>
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
{{ $t('app.offline') }}
|
{{ $t('app.offline') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Loading v-else-if="dataStatus == DataStatus.Loading" />
|
<Loading v-else-if="dataStatus == Status.Data.Loading" />
|
||||||
|
|
||||||
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
|
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
|
||||||
{{ $t('app.error') }}
|
{{ $t('app.error') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -17,7 +17,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<TimetableHistoryList :timetableHistory="timetableHistory" />
|
<ul class="journal-list">
|
||||||
|
<transition-group name="list-anim">
|
||||||
|
<li
|
||||||
|
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
|
||||||
|
class="journal_item"
|
||||||
|
:key="timetable.id"
|
||||||
|
@click="showExtraInfo.value = !showExtraInfo.value"
|
||||||
|
>
|
||||||
|
<div class="journal_item-info">
|
||||||
|
<!-- General -->
|
||||||
|
<TimetableGeneral :timetable="timetable" />
|
||||||
|
<!-- Route -->
|
||||||
|
<span class="item-route">
|
||||||
|
<b>{{ timetable.route.replace('|', ' - ') }}</b>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<!-- Stops -->
|
||||||
|
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
||||||
|
<!-- Status -->
|
||||||
|
<TimetableStatus :timetable="timetable" />
|
||||||
|
|
||||||
|
<!-- Extra -->
|
||||||
|
<TimetableDetails :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</transition-group>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<AddDataButton
|
<AddDataButton
|
||||||
:list="timetableHistory"
|
:list="timetableHistory"
|
||||||
@@ -37,21 +64,33 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from 'vue';
|
import { defineComponent, PropType, ref } from 'vue';
|
||||||
import { DataStatus } from '../../../scripts/enums/DataStatus';
|
|
||||||
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
|
|
||||||
import { useStore } from '../../../store/store';
|
|
||||||
|
|
||||||
import Loading from '../../Global/Loading.vue';
|
import Loading from '../../Global/Loading.vue';
|
||||||
import AddDataButton from '../../Global/AddDataButton.vue';
|
import AddDataButton from '../../Global/AddDataButton.vue';
|
||||||
import TimetableHistoryList from './TimetableHistoryList.vue';
|
|
||||||
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
|
import { Status } from '../../../typings/common';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
|
|
||||||
|
import TimetableGeneral from './TimetableGeneral.vue';
|
||||||
|
import TimetableStops from './TimetableStops.vue';
|
||||||
|
import TimetableStatus from './TimetableStatus.vue';
|
||||||
|
import TimetableDetails from './TimetableDetails.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { Loading, AddDataButton, TimetableHistoryList },
|
components: {
|
||||||
|
Loading,
|
||||||
|
AddDataButton,
|
||||||
|
TimetableDetails,
|
||||||
|
TimetableGeneral,
|
||||||
|
TimetableStatus,
|
||||||
|
TimetableStops
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
timetableHistory: {
|
timetableHistory: {
|
||||||
type: Array as PropType<TimetableHistory[]>,
|
type: Array as PropType<API.TimetableHistory.Response>,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
scrollNoMoreData: {
|
scrollNoMoreData: {
|
||||||
@@ -64,15 +103,24 @@ export default defineComponent({
|
|||||||
type: Function as PropType<() => void>
|
type: Function as PropType<() => void>
|
||||||
},
|
},
|
||||||
dataStatus: {
|
dataStatus: {
|
||||||
type: Number as PropType<DataStatus>
|
type: Number as PropType<Status.Data>
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
DataStatus,
|
Status,
|
||||||
store: useStore()
|
store: useMainStore()
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
computedTimetableHistory() {
|
||||||
|
return this.timetableHistory.map((timetable) => ({
|
||||||
|
timetable,
|
||||||
|
showExtraInfo: ref(false)
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -80,4 +128,15 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../../styles/JournalSection.scss';
|
@import '../../../styles/JournalSection.scss';
|
||||||
@import '../../../styles/animations.scss';
|
@import '../../../styles/animations.scss';
|
||||||
|
|
||||||
|
@include smallScreen {
|
||||||
|
.journal_item-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-route {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="details-actions">
|
||||||
|
<button class="btn--action">
|
||||||
|
<b>{{ $t('journal.stock-info') }}</b>
|
||||||
|
<img :src="`/images/icon-arrow-${showExtraInfo ? 'asc' : 'desc'}.svg`" alt="Arrow icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details-body" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="stock-specs">
|
||||||
|
<span class="badge">
|
||||||
|
<span>{{ $t('journal.dispatcher-name') }}</span>
|
||||||
|
<span>{{ timetable.authorName }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stock-specs">
|
||||||
|
<span class="badge">
|
||||||
|
<span>{{ $t('journal.stock-max-speed') }}</span>
|
||||||
|
<span>{{ timetable.maxSpeed }}km/h</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="badge">
|
||||||
|
<span>{{ $t('journal.stock-length') }}</span>
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
currentHistoryIndex == 0
|
||||||
|
? timetable.stockLength
|
||||||
|
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
|
||||||
|
}}m
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="badge">
|
||||||
|
<span>{{ $t('journal.stock-mass') }}</span>
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
Math.floor(
|
||||||
|
(currentHistoryIndex == 0
|
||||||
|
? timetable.stockMass!
|
||||||
|
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
|
||||||
|
)
|
||||||
|
}}t
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Historia zmian w składzie -->
|
||||||
|
<div class="stock-history" v-if="stockHistory.length > 1">
|
||||||
|
<button
|
||||||
|
v-for="(sh, i) in stockHistory"
|
||||||
|
:key="i"
|
||||||
|
class="btn--action"
|
||||||
|
:data-checked="i == currentHistoryIndex"
|
||||||
|
@click.stop="currentHistoryIndex = i"
|
||||||
|
>
|
||||||
|
{{ sh.updatedAt }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StockList
|
||||||
|
:trainStockList="
|
||||||
|
(currentHistoryIndex == 0
|
||||||
|
? timetable.stockString
|
||||||
|
: stockHistory[currentHistoryIndex].stockString
|
||||||
|
).split(';')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { PropType, defineComponent } from 'vue';
|
||||||
|
import StockList from '../../Global/StockList.vue';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { StockList },
|
||||||
|
props: {
|
||||||
|
showExtraInfo: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
timetable: {
|
||||||
|
type: Object as PropType<API.TimetableHistory.Data>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentHistoryIndex: 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
stockHistory() {
|
||||||
|
return this.timetable.stockHistory
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((h) => {
|
||||||
|
const historyData = h.split('@');
|
||||||
|
return {
|
||||||
|
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}),
|
||||||
|
stockString: historyData[1],
|
||||||
|
stockMass: Number(historyData[2]) || undefined,
|
||||||
|
stockLength: Number(historyData[3]) || undefined
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onImageError(e: Event) {
|
||||||
|
const imageEl = e.target as HTMLImageElement;
|
||||||
|
imageEl.src = '/images/icon-unknown.png';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../styles/variables.scss';
|
||||||
|
@import '../../../styles/responsive.scss';
|
||||||
|
@import '../../../styles/badge.scss';
|
||||||
|
|
||||||
|
.details-body {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-actions {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
button img {
|
||||||
|
height: 1.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-history {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
button[data-checked='true'] {
|
||||||
|
color: $accentCol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-specs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
span:last-child {
|
||||||
|
color: black;
|
||||||
|
background-color: $accentCol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.stock-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
|
||||||
|
li > div {
|
||||||
|
margin: 1em 0;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include smallScreen() {
|
||||||
|
.stock-specs {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item-extra" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<div class="stock-specs">
|
|
||||||
<span class="badge">
|
|
||||||
<span>{{ $t('journal.dispatcher-name') }}</span>
|
|
||||||
<span>{{ timetable.authorName }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stock-specs">
|
|
||||||
<span class="badge">
|
|
||||||
<span>{{ $t('journal.stock-max-speed') }}</span>
|
|
||||||
<span>{{ timetable.maxSpeed }}km/h</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge">
|
|
||||||
<span>{{ $t('journal.stock-length') }}</span>
|
|
||||||
<span>
|
|
||||||
{{
|
|
||||||
currentHistoryIndex == 0
|
|
||||||
? timetable.stockLength
|
|
||||||
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
|
|
||||||
}}m
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge">
|
|
||||||
<span>{{ $t('journal.stock-mass') }}</span>
|
|
||||||
<span>
|
|
||||||
{{
|
|
||||||
Math.floor(
|
|
||||||
(currentHistoryIndex == 0
|
|
||||||
? timetable.stockMass!
|
|
||||||
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
|
|
||||||
)
|
|
||||||
}}t
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Historia zmian w składzie -->
|
|
||||||
<div class="stock-history" v-if="stockHistory.length > 1">
|
|
||||||
<button
|
|
||||||
v-for="(sh, i) in stockHistory"
|
|
||||||
:key="i"
|
|
||||||
class="btn--action"
|
|
||||||
:data-checked="i == currentHistoryIndex"
|
|
||||||
@click.stop="currentHistoryIndex = i"
|
|
||||||
>
|
|
||||||
{{ sh.updatedAt }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- <StockList :trainStockList="currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';')" /> -->
|
|
||||||
<StockList
|
|
||||||
:trainStockList="
|
|
||||||
(currentHistoryIndex == 0
|
|
||||||
? timetable.stockString
|
|
||||||
: stockHistory[currentHistoryIndex].stockString
|
|
||||||
).split(';')
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- <ul class="stock-list">
|
|
||||||
<li
|
|
||||||
v-for="(stockName, i) in (currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';')"
|
|
||||||
:key="i"
|
|
||||||
>
|
|
||||||
<div>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</div>
|
|
||||||
<TrainThumbnail :name="stockName" />
|
|
||||||
</li>
|
|
||||||
</ul> -->
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { PropType, defineComponent } from 'vue';
|
|
||||||
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
|
|
||||||
import StockList from '../../Global/StockList.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { StockList },
|
|
||||||
props: {
|
|
||||||
showExtraInfo: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
timetable: {
|
|
||||||
type: Object as PropType<TimetableHistory>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
currentHistoryIndex: 0
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
stockHistory() {
|
|
||||||
return this.timetable.stockHistory
|
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.map((h) => {
|
|
||||||
const historyData = h.split('@');
|
|
||||||
return {
|
|
||||||
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
}),
|
|
||||||
stockString: historyData[1],
|
|
||||||
stockMass: Number(historyData[2]) || undefined,
|
|
||||||
stockLength: Number(historyData[3]) || undefined
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onImageError(e: Event) {
|
|
||||||
const imageEl = e.target as HTMLImageElement;
|
|
||||||
imageEl.src = '/images/icon-unknown.png';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../../styles/variables.scss';
|
|
||||||
@import '../../../styles/responsive.scss';
|
|
||||||
@import '../../../styles/badge.scss';
|
|
||||||
|
|
||||||
.item-extra {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stock-history {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5em;
|
|
||||||
margin-top: 1em;
|
|
||||||
|
|
||||||
button[data-checked='true'] {
|
|
||||||
color: $accentCol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stock-specs {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5em;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
span:last-child {
|
|
||||||
color: black;
|
|
||||||
background-color: $accentCol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include smallScreen() {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.stock-list {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
|
|
||||||
li > div {
|
|
||||||
margin: 1em 0;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
color: #aaa;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="item-general">
|
<div class="item-general">
|
||||||
<span
|
<span class="general-train">
|
||||||
class="general-train"
|
|
||||||
tabindex="0"
|
|
||||||
@click.stop="showTimetable(timetable, $event.currentTarget)"
|
|
||||||
@keydown.enter="showTimetable(timetable, $event.currentTarget)"
|
|
||||||
>
|
|
||||||
<span class="text--grayed">#{{ timetable.id }}</span>
|
<span class="text--grayed">#{{ timetable.id }}</span>
|
||||||
|
|
||||||
<span class="badges" v-if="timetable.skr || timetable.twr">
|
<span class="badges" v-if="timetable.skr || timetable.twr">
|
||||||
@@ -28,7 +23,17 @@
|
|||||||
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
|
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
|
||||||
</strong>
|
</strong>
|
||||||
|
|
||||||
<strong>{{ timetable.driverName }}</strong>
|
<strong
|
||||||
|
v-if="apiStore.donatorsData.includes(timetable.driverName)"
|
||||||
|
class="text--donator"
|
||||||
|
:title="$t('donations.driver-message')"
|
||||||
|
>
|
||||||
|
{{ timetable.driverName }}
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
<strong v-else>
|
||||||
|
{{ timetable.driverName }}
|
||||||
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="general-time">
|
<span class="general-time">
|
||||||
@@ -56,41 +61,57 @@
|
|||||||
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
|
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
|
||||||
}}
|
}}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="timetable.terminated == false"
|
||||||
|
class="btn--action btn-timetable"
|
||||||
|
@click.stop="showTimetable(timetable, $event.currentTarget)"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-train.svg" alt="train icon" />
|
||||||
|
<b>{{ $t('journal.timetable-online-button') }}</b>
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
|
|
||||||
|
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
import dateMixin from '../../../mixins/dateMixin';
|
import dateMixin from '../../../mixins/dateMixin';
|
||||||
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
||||||
import styleMixin from '../../../mixins/styleMixin';
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [dateMixin, modalTrainMixin, styleMixin],
|
mixins: [dateMixin, modalTrainMixin, styleMixin],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
apiStore: useApiStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
timetable: {
|
timetable: {
|
||||||
type: Object as PropType<TimetableHistory>,
|
type: Object as PropType<API.TimetableHistory.Data>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
showTimetable(timetable: TimetableHistory, target: EventTarget | null) {
|
showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
|
||||||
if (timetable?.terminated) return;
|
if (timetable?.terminated) return;
|
||||||
|
|
||||||
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target);
|
this.selectModalTrainById(`${timetable.driverName}${timetable.trainNo}`, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../../styles/responsive.scss';
|
@import '../../../styles/responsive';
|
||||||
@import '../../../styles/badge.scss';
|
@import '../../../styles/badge';
|
||||||
|
|
||||||
.item-general {
|
.item-general {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -100,14 +121,29 @@ export default defineComponent({
|
|||||||
|
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
@include smallScreen() {
|
.general-train {
|
||||||
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25em;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-date {
|
.general-time {
|
||||||
margin-right: 0.5em;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-badge {
|
.info-badge {
|
||||||
@@ -127,11 +163,18 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.general-train {
|
.btn-timetable {
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
padding: 0.2em 0.5em;
|
||||||
align-items: center;
|
|
||||||
gap: 0.25em;
|
img {
|
||||||
|
height: 1.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include smallScreen {
|
||||||
|
.item-general {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -44,14 +44,14 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
|
|
||||||
import ProgressBar from '../../Global/ProgressBar.vue';
|
import ProgressBar from '../../Global/ProgressBar.vue';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { ProgressBar },
|
components: { ProgressBar },
|
||||||
props: {
|
props: {
|
||||||
timetable: {
|
timetable: {
|
||||||
type: Object as PropType<TimetableHistory>,
|
type: Object as PropType<API.TimetableHistory.Data>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="stop-list" v-if="showExtraInfo == true">
|
<div class="timetable-stops">
|
||||||
|
<div class="stop-list">
|
||||||
<span
|
<span
|
||||||
v-for="(stop, i) in timetableStops.filter((_, i) =>
|
v-for="(stop, i) in timetableStops.filter((_, i) =>
|
||||||
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
|
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
|
||||||
@@ -19,13 +20,29 @@
|
|||||||
<span v-html="stop.html"></span>
|
<span v-html="stop.html"></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="path-details" v-if="showExtraInfo && timetablePathDetails">
|
||||||
|
<span
|
||||||
|
v-for="(pathData, i) in timetablePathDetails"
|
||||||
|
:data-visited="pathData.isVisited"
|
||||||
|
:data-next-visited="
|
||||||
|
i < timetablePathDetails.length - 1 && timetablePathDetails[i + 1].isVisited
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span class="path-arrival" v-if="pathData.arrival">/ {{ pathData.arrival }} → </span>
|
||||||
|
<b class="path-scenery">{{ pathData.sceneryName }}</b>
|
||||||
|
<span class="path-departure" v-if="pathData.departure">
|
||||||
|
→ {{ pathData.departure }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import dateMixin from '../../../mixins/dateMixin';
|
import dateMixin from '../../../mixins/dateMixin';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [dateMixin],
|
mixins: [dateMixin],
|
||||||
@@ -37,12 +54,30 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
timetable: {
|
timetable: {
|
||||||
type: Object as PropType<TimetableHistory>,
|
type: Object as PropType<API.TimetableHistory.Data>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
timetablePathDetails() {
|
||||||
|
if (!this.timetable.path || this.timetable.path == '') return null;
|
||||||
|
|
||||||
|
return this.timetable.path.split(';').map((pathEl, i) => {
|
||||||
|
const [arrival, name, departure] = pathEl.split(',');
|
||||||
|
const sceneryName = name.split(' ').slice(0, -1).join(' ');
|
||||||
|
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
arrival,
|
||||||
|
sceneryName,
|
||||||
|
sceneryHash,
|
||||||
|
departure,
|
||||||
|
isVisited: this.timetable.visitedSceneries?.includes(sceneryHash) ?? false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
timetableStops() {
|
timetableStops() {
|
||||||
const timetable = this.timetable;
|
const timetable = this.timetable;
|
||||||
|
|
||||||
@@ -95,13 +130,14 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.stop-list {
|
.timetable-stops {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
|
|
||||||
color: #adadad;
|
color: #adadad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-list {
|
||||||
&-item[data-confirmed='true'] {
|
&-item[data-confirmed='true'] {
|
||||||
color: lightgreen;
|
color: lightgreen;
|
||||||
|
|
||||||
@@ -110,4 +146,19 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.path-details {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-details > span[data-visited='true'] {
|
||||||
|
.path-arrival,
|
||||||
|
.path-scenery {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-next-visited='true'] .path-departure {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<TimetableStatus :timetable="timetable" />
|
<TimetableStatus :timetable="timetable" />
|
||||||
|
|
||||||
<button class="btn--option btn--show">
|
<button class="btn--action btn--show">
|
||||||
{{ $t('journal.stock-info') }}
|
{{ $t('journal.stock-info') }}
|
||||||
<img
|
<img
|
||||||
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
|
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
|
||||||
@@ -38,17 +38,19 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent, ref } from 'vue';
|
import { PropType, defineComponent, ref } from 'vue';
|
||||||
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
|
|
||||||
|
|
||||||
import TimetableGeneral from './TimetableGeneral.vue';
|
import TimetableGeneral from './TimetableGeneral.vue';
|
||||||
import TimetableStops from './TimetableStops.vue';
|
import TimetableStops from './TimetableStops.vue';
|
||||||
import TimetableStatus from './TimetableStatus.vue';
|
import TimetableStatus from './TimetableStatus.vue';
|
||||||
import TimetableExtra from './TimetableExtra.vue';
|
import TimetableExtra from './TimetableExtra.vue';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
timetableHistory: {
|
timetableHistory: {
|
||||||
type: Array as PropType<TimetableHistory[]>,
|
type: Array as PropType<API.TimetableHistory.Response>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -59,16 +61,14 @@ export default defineComponent({
|
|||||||
showExtraInfo: ref(false)
|
showExtraInfo: ref(false)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
methods: {},
|
|
||||||
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra }
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../../styles/variables.scss';
|
@import '../../../styles/variables';
|
||||||
@import '../../../styles/responsive.scss';
|
@import '../../../styles/responsive';
|
||||||
@import '../../../styles/JournalSection.scss';
|
@import '../../../styles/JournalSection';
|
||||||
|
|
||||||
.btn--show {
|
.btn--show {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
export namespace Journal {
|
||||||
|
export type DispatcherSearchKey = 'search-dispatcher' | 'search-station' | 'search-date';
|
||||||
|
|
||||||
|
export type TimetableSearchKey =
|
||||||
|
| 'search-driver'
|
||||||
|
| 'search-train'
|
||||||
|
| 'search-date'
|
||||||
|
| 'search-dispatcher'
|
||||||
|
| 'search-issuedFrom'
|
||||||
|
| 'search-terminatingAt'
|
||||||
|
| 'search-via';
|
||||||
|
|
||||||
|
export type TimetableSearchType = {
|
||||||
|
[key in TimetableSearchKey]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DispatcherSearchType = {
|
||||||
|
[key in DispatcherSearchKey]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
|
||||||
|
export type DispatcherSorterKey = 'timestampFrom' | 'duration';
|
||||||
|
|
||||||
|
export interface DispatcherSorter {
|
||||||
|
id: DispatcherSorterKey;
|
||||||
|
dir: -1 | 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimetableSorter {
|
||||||
|
id: TimetableSorterKey;
|
||||||
|
dir: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum TimetableFilterId {
|
||||||
|
ALL_STATUSES = 'all-statuses',
|
||||||
|
ACTIVE = 'active',
|
||||||
|
FULFILLED = 'fulfilled',
|
||||||
|
ABANDONED = 'abandoned',
|
||||||
|
ALL_SPECIALS = 'all-specials',
|
||||||
|
TWR = 'twr',
|
||||||
|
SKR = 'skr',
|
||||||
|
TWR_SKR = 'twr-skr'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FilterSection {
|
||||||
|
TIMETABLE_STATUS = 'timetable-status',
|
||||||
|
SPECIAL = 'special'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimetableFilter {
|
||||||
|
id: TimetableFilterId;
|
||||||
|
filterSection: string;
|
||||||
|
isActive: boolean;
|
||||||
|
default: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum StatsTab {
|
||||||
|
DRIVER_STATS = 'journal-driver-stats',
|
||||||
|
DISPATCHER_STATS = 'journal-dispatcher-stats',
|
||||||
|
DAILY_STATS = 'journal-daily-stats'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatsButton {
|
||||||
|
tab: StatsTab;
|
||||||
|
localeKey: string;
|
||||||
|
iconName: string;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="scenery-table-section">
|
<div class="scenery-dispatchers-history">
|
||||||
|
<div class="history-wrapper">
|
||||||
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
|
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
|
||||||
|
|
||||||
<div class="no-history" v-else-if="historyList.length == 0">
|
<div v-else-if="historyList.length == 0" class="no-history">
|
||||||
{{ $t('scenery.history-list-empty') }}
|
{{ $t('scenery.history-list-empty') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="scenery-history-table" v-else>
|
<div v-else class="history-list">
|
||||||
<thead>
|
<div v-for="historyItem in historyList" :key="historyItem.id">
|
||||||
<th>{{ $t('scenery.dispatchers-history-hash') }}</th>
|
<span>
|
||||||
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th>
|
<span class="text--grayed" style="margin-right: 10px">
|
||||||
<th>{{ $t('scenery.dispatchers-history-level') }}</th>
|
#{{ historyItem.stationHash }}
|
||||||
<th>{{ $t('scenery.dispatchers-history-rate') }}</th>
|
</span>
|
||||||
<th>{{ $t('scenery.dispatchers-history-date') }}</th>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="historyItem in historyList" :key="historyItem.id">
|
|
||||||
<td>#{{ historyItem.stationHash }}</td>
|
|
||||||
<td>
|
|
||||||
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
|
|
||||||
<b>{{ historyItem.dispatcherName }}</b>
|
|
||||||
</router-link>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<b
|
<b
|
||||||
v-if="historyItem.dispatcherLevel !== null"
|
v-if="historyItem.dispatcherLevel !== null"
|
||||||
class="level-badge dispatcher"
|
class="level-badge dispatcher"
|
||||||
@@ -33,71 +22,83 @@
|
|||||||
>
|
>
|
||||||
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
|
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
|
||||||
</b>
|
</b>
|
||||||
</td>
|
<b style="margin-left: 5px">
|
||||||
<td class="text--primary">
|
<router-link
|
||||||
<b>{{ historyItem.dispatcherRate }}</b>
|
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
|
||||||
</td>
|
>
|
||||||
<td style="min-width: 300px">
|
{{ historyItem.dispatcherName }}
|
||||||
<div v-if="historyItem.timestampTo">
|
</router-link>
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{{ $t('scenery.dispatcher-rate') }}
|
||||||
|
<b class="text--primary"> {{ historyItem.dispatcherRate }}</b>
|
||||||
|
</span>
|
||||||
|
|
|
||||||
|
<span>
|
||||||
|
{{ $t('scenery.dispatcher-status-changes') }}
|
||||||
|
<b class="text--primary">{{ historyItem.statusHistory.length }}</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<span v-if="historyItem.timestampTo">
|
||||||
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
||||||
|
|
||||||
{{ timestampToString(historyItem.timestampFrom) }}
|
{{ timestampToString(historyItem.timestampFrom) }}
|
||||||
- {{ timestampToString(historyItem.timestampTo) }} ({{
|
- {{ timestampToString(historyItem.timestampTo) }} ({{
|
||||||
calculateDuration(historyItem.currentDuration)
|
calculateDuration(historyItem.currentDuration)
|
||||||
}})
|
}})
|
||||||
</div>
|
</span>
|
||||||
|
|
||||||
<div class="dispatcher-online" v-else>
|
<span class="dispatcher-online" v-else>
|
||||||
{{ $t('journal.online-since') }}
|
{{ $t('journal.online-since') }}
|
||||||
<b>{{ timestampToString(historyItem.timestampFrom) }}</b>
|
<b>{{ timestampToString(historyItem.timestampFrom) }}</b>
|
||||||
({{ calculateDuration(historyItem.currentDuration) }})
|
({{ calculateDuration(historyItem.currentDuration) }})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="bottom-info">
|
<div class="bottom-info">
|
||||||
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
|
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
|
||||||
{{ $t('scenery.bottom-info') }}
|
{{ $t('scenery.bottom-info') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from 'axios';
|
|
||||||
import { defineComponent, PropType } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
|
||||||
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
|
|
||||||
import Station from '../../scripts/interfaces/Station';
|
|
||||||
import { URLs } from '../../scripts/utils/apiURLs';
|
|
||||||
import Loading from '../Global/Loading.vue';
|
import Loading from '../Global/Loading.vue';
|
||||||
import styleMixin from '../../mixins/styleMixin';
|
import styleMixin from '../../mixins/styleMixin';
|
||||||
import listObserverMixin from '../../mixins/listObserverMixin';
|
import { API } from '../../typings/api';
|
||||||
import { OnlineScenery } from '../../scripts/interfaces/store/storeTypes';
|
import { ActiveScenery, Station, Status } from '../../typings/common';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SceneryDispatchersHistory',
|
name: 'SceneryDispatchersHistory',
|
||||||
mixins: [dateMixin, styleMixin, listObserverMixin],
|
mixins: [dateMixin, styleMixin],
|
||||||
components: { Loading },
|
components: { Loading },
|
||||||
props: {
|
props: {
|
||||||
station: {
|
station: {
|
||||||
type: Object as PropType<Station>,
|
type: Object as PropType<Station>
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
onlineScenery: {
|
onlineScenery: {
|
||||||
type: Object as PropType<OnlineScenery>,
|
type: Object as PropType<ActiveScenery>
|
||||||
required: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
historyList: [] as DispatcherHistory[],
|
historyList: [] as API.DispatcherHistory.Response,
|
||||||
dataStatus: DataStatus.Loading,
|
dataStatus: Status.Data.Loading,
|
||||||
DataStatus
|
DataStatus: Status.Data,
|
||||||
|
apiStore: useApiStore()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -109,23 +110,38 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async fetchAPIData(countFrom = 0, countLimit = 30): Promise<DispatcherHistory[] | null> {
|
async fetchAPIData(
|
||||||
|
countFrom = 0,
|
||||||
|
countLimit = 30
|
||||||
|
): Promise<API.DispatcherHistory.Response | null> {
|
||||||
|
if (!this.station && !this.onlineScenery) {
|
||||||
|
this.dataStatus = Status.Data.Loaded;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.dataStatus = DataStatus.Loading;
|
this.dataStatus = Status.Data.Loading;
|
||||||
|
|
||||||
const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
|
const requestString = `api/getDispatchers?stationName=${
|
||||||
const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data;
|
this.station?.name || this.onlineScenery?.name
|
||||||
|
}&countFrom=${countFrom}&countLimit=${countLimit}`;
|
||||||
|
|
||||||
this.dataStatus = DataStatus.Loaded;
|
const historyAPIData: API.DispatcherHistory.Response = await (
|
||||||
|
await this.apiStore.client!.get(requestString)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
this.dataStatus = Status.Data.Loaded;
|
||||||
return historyAPIData;
|
return historyAPIData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.dataStatus = DataStatus.Error;
|
this.dataStatus = Status.Data.Error;
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigateToHistory() {
|
navigateToHistory() {
|
||||||
this.$router.push(`/journal/dispatchers?sceneryName=${this.station.name}`);
|
this.$router.push(
|
||||||
|
`/journal/dispatchers?search-station=${this.station?.name || this.onlineScenery?.name}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -135,8 +151,43 @@ export default defineComponent({
|
|||||||
@import '../../styles/responsive.scss';
|
@import '../../styles/responsive.scss';
|
||||||
@import '../../styles/sceneryViewTables.scss';
|
@import '../../styles/sceneryViewTables.scss';
|
||||||
|
|
||||||
|
.scenery-dispatchers-history {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5em;
|
||||||
|
grid-template-rows: auto 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-wrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
background-color: #2b2b2b;
|
||||||
|
line-height: 1.75em;
|
||||||
|
}
|
||||||
|
|
||||||
.level-badge {
|
.level-badge {
|
||||||
margin: 0 auto;
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dispatcher-online {
|
.dispatcher-online {
|
||||||
@@ -144,12 +195,10 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
@include smallScreen {
|
||||||
.history-list {
|
.history-list > div {
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
.list-item {
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-header">
|
<section class="info-header">
|
||||||
<a class="scenery-name" :href="station.generalInfo?.url" target="_blank">
|
<a class="scenery-name" :href="station?.generalInfo?.url" target="_blank">
|
||||||
{{ station.name }}
|
{{ stationName.replace(/_/g, ' ') }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="scenery-abbrev">
|
<div class="scenery-abbrev" v-if="station?.generalInfo?.abbr">
|
||||||
{{ $t('scenery.abbrev') }} <b>{{ station.generalInfo?.abbr }}</b>
|
{{ $t('scenery.abbrev') }} <b>{{ station.generalInfo.abbr }}</b>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scenery-hash" v-if="onlineScenery?.hash">#{{ onlineScenery.hash }}</div>
|
<div class="scenery-hash" v-if="onlineScenery?.hash">#{{ onlineScenery.hash }}</div>
|
||||||
@@ -14,19 +14,21 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import Station from '../../scripts/interfaces/Station';
|
import { ActiveScenery, Station } from '../../typings/common';
|
||||||
import { OnlineScenery } from '../../scripts/interfaces/store/storeTypes';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
station: {
|
station: {
|
||||||
type: Object as PropType<Station>,
|
type: Object as PropType<Station>
|
||||||
|
},
|
||||||
|
|
||||||
|
stationName: {
|
||||||
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
|
||||||
onlineScenery: {
|
onlineScenery: {
|
||||||
type: Object as PropType<OnlineScenery>,
|
type: Object as PropType<ActiveScenery>
|
||||||
required: false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="scenery-info">
|
<div class="scenery-info">
|
||||||
<section>
|
<section>
|
||||||
<div class="scenery-info-general" v-if="station.generalInfo">
|
<div class="scenery-info-general">
|
||||||
<SceneryInfoIcons :station="station" />
|
<SceneryInfoIcons :station="station" />
|
||||||
|
|
||||||
<div class="scenery-general-list">
|
<div class="scenery-general-list" v-if="station?.generalInfo">
|
||||||
<span>
|
<span>
|
||||||
<b>{{ $t('availability.title') }}:</b>
|
<b>{{ $t('availability.title') }}:</b>
|
||||||
{{ $t(`availability.${station.generalInfo.availability}`) }}
|
{{ $t(`availability.${station.generalInfo.availability}`) }}
|
||||||
@@ -46,11 +46,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SceneryInfoRoutes :station="station" />
|
<SceneryInfoRoutes v-if="station" :station="station" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="scenery-authors"
|
class="scenery-authors"
|
||||||
v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0"
|
v-if="station?.generalInfo?.authors && station.generalInfo.authors.length > 0"
|
||||||
>
|
>
|
||||||
<b>
|
<b>
|
||||||
{{
|
{{
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
|
|
||||||
<div class="info-lists">
|
<div class="info-lists">
|
||||||
<!-- user list -->
|
<!-- user list -->
|
||||||
<SceneryInfoUserList :onlineScenery="onlineScenery" />
|
<SceneryInfoUserList :onlineScenery="onlineScenery" :station="station" />
|
||||||
|
|
||||||
<!-- spawn list -->
|
<!-- spawn list -->
|
||||||
<SceneryInfoSpawnList :onlineScenery="onlineScenery" />
|
<SceneryInfoSpawnList :onlineScenery="onlineScenery" />
|
||||||
@@ -89,8 +89,7 @@ import SceneryInfoIcons from './SceneryInfo/SceneryInfoIcons.vue';
|
|||||||
import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue';
|
import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue';
|
||||||
import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
|
import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
|
||||||
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
||||||
import Station from '../../scripts/interfaces/Station';
|
import { ActiveScenery, Station } from '../../typings/common';
|
||||||
import { OnlineScenery } from '../../scripts/interfaces/store/storeTypes';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -102,15 +101,13 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
station: {
|
station: {
|
||||||
type: Object as PropType<Station>,
|
type: Object as PropType<Station>
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onlineScenery: {
|
onlineScenery: {
|
||||||
type: Object as PropType<OnlineScenery>,
|
type: Object as PropType<ActiveScenery>
|
||||||
required: false
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -127,11 +124,6 @@ h3.section-header {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
|
|
||||||
img {
|
|
||||||
width: 1.1em;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-lists {
|
.info-lists {
|
||||||
|
|||||||
@@ -1,31 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-dispatcher">
|
<section class="info-dispatcher">
|
||||||
<div class="dispatcher" v-if="onlineScenery">
|
<div class="info-top" v-if="onlineScenery && onlineScenery.dispatcherExp != -1">
|
||||||
<span
|
<span
|
||||||
class="dispatcher_level"
|
class="dispatcher-level"
|
||||||
:style="calculateExpStyle(onlineScenery.dispatcherExp, onlineScenery.dispatcherIsSupporter)"
|
:style="calculateExpStyle(onlineScenery.dispatcherExp, onlineScenery.dispatcherIsSupporter)"
|
||||||
>
|
>
|
||||||
{{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
|
{{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
class="dispatcher_name"
|
class="dispatcher-name"
|
||||||
:to="`/journal/dispatchers?dispatcherName=${onlineScenery.dispatcherName}`"
|
:to="`/journal/dispatchers?search-dispatcher=${onlineScenery.dispatcherName}`"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text--donator"
|
||||||
|
v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
|
||||||
|
:title="$t('donations.dispatcher-message')"
|
||||||
>
|
>
|
||||||
{{ onlineScenery.dispatcherName }}
|
{{ onlineScenery.dispatcherName }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ onlineScenery.dispatcherName }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span class="dispatcher_likes text--primary">
|
<div class="info-bottom">
|
||||||
|
<span
|
||||||
|
class="dispatcher-likes text--primary"
|
||||||
|
v-if="onlineScenery && onlineScenery.dispatcherExp != -1"
|
||||||
|
>
|
||||||
<img src="/images/icon-like.svg" alt="Likes count icon" />
|
<img src="/images/icon-like.svg" alt="Likes count icon" />
|
||||||
<span>{{ onlineScenery?.dispatcherRate || '0' }}</span>
|
<span>{{ onlineScenery?.dispatcherRate || '0' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<span class="dispatcher-badge">
|
||||||
<StationStatusBadge
|
<StationStatusBadge
|
||||||
:statusID="onlineScenery?.statusID"
|
|
||||||
:isOnline="onlineScenery ? true : false"
|
:isOnline="onlineScenery ? true : false"
|
||||||
:statusTimestamp="onlineScenery?.statusTimestamp"
|
:dispatcherStatus="onlineScenery?.dispatcherStatus"
|
||||||
|
:dispatcherTimestamp="onlineScenery?.dispatcherTimestamp"
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -35,13 +49,21 @@ import dateMixin from '../../../mixins/dateMixin';
|
|||||||
import routerMixin from '../../../mixins/routerMixin';
|
import routerMixin from '../../../mixins/routerMixin';
|
||||||
import styleMixin from '../../../mixins/styleMixin';
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
||||||
import { OnlineScenery } from '../../../scripts/interfaces/store/storeTypes';
|
import { ActiveScenery } from '../../../typings/common';
|
||||||
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [styleMixin, dateMixin, routerMixin],
|
mixins: [styleMixin, dateMixin, routerMixin],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
apiStore: useApiStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
onlineScenery: {
|
onlineScenery: {
|
||||||
type: Object as PropType<OnlineScenery>,
|
type: Object as PropType<ActiveScenery>,
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -51,19 +73,26 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.info-dispatcher {
|
.info-dispatcher {
|
||||||
display: flex;
|
font-size: 1.8em;
|
||||||
align-items: center;
|
}
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
flex-wrap: wrap;
|
.info-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
|
||||||
.dispatcher {
|
margin-top: 0.5em;
|
||||||
font-size: 2em;
|
}
|
||||||
|
|
||||||
&_level {
|
.dispatcher-level {
|
||||||
display: inline-block;
|
|
||||||
margin-right: 0.3em;
|
|
||||||
background: firebrick;
|
background: firebrick;
|
||||||
|
|
||||||
border-radius: 0.1em;
|
border-radius: 0.1em;
|
||||||
@@ -74,22 +103,16 @@ export default defineComponent({
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
&_name {
|
.dispatcher-likes {
|
||||||
cursor: pointer;
|
display: flex;
|
||||||
margin-right: 0.25em;
|
gap: 0.25em;
|
||||||
}
|
|
||||||
|
|
||||||
&_likes {
|
|
||||||
img {
|
img {
|
||||||
height: 0.7em;
|
width: 1em;
|
||||||
margin: 0 0.25em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
.dispatcher-badge {
|
||||||
font-size: 1.25em;
|
font-size: 0.7em;
|
||||||
margin: 0.5em 0.25em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,92 +1,101 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-icons">
|
<section class="info-icons">
|
||||||
<span
|
<span v-if="!station || !station.generalInfo">
|
||||||
v-if="station.generalInfo && station.generalInfo.reqLevel >= 0"
|
|
||||||
class="scenery-icon icon-info level"
|
|
||||||
:style="calculateExpStyle(station.generalInfo.reqLevel)"
|
|
||||||
>
|
|
||||||
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="station.generalInfo"
|
|
||||||
class="scenery-icon icon-info"
|
|
||||||
:class="station.generalInfo.controlType.replace('+', '-')"
|
|
||||||
:title="$t('desc.control-type') + $t(`controls.${station.generalInfo.controlType}`)"
|
|
||||||
v-html="getControlTypeAbbrev(station.generalInfo.controlType)"
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="station.generalInfo?.SUP"
|
|
||||||
class="icon-info"
|
|
||||||
src="/images/icon-SUP.svg"
|
|
||||||
alt="SUP (RASP-UZK)"
|
|
||||||
:title="$t('desc.SUP')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img
|
|
||||||
v-if="station.generalInfo?.signalType"
|
|
||||||
class="icon-info"
|
|
||||||
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
|
|
||||||
:alt="station.generalInfo.signalType"
|
|
||||||
:title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img
|
|
||||||
v-if="station.generalInfo?.availability == 'nonPublic'"
|
|
||||||
class="icon-info"
|
|
||||||
src="/images/icon-lock.svg"
|
|
||||||
alt="Non-public scenery"
|
|
||||||
:title="$t('desc.non-public')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img
|
|
||||||
v-if="station.generalInfo?.availability == 'unavailable'"
|
|
||||||
class="icon-info"
|
|
||||||
src="/images/icon-unavailable.svg"
|
|
||||||
alt="Unavailable scenery"
|
|
||||||
:title="$t('desc.unavailable')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img
|
|
||||||
v-if="station.generalInfo?.availability == 'abandoned'"
|
|
||||||
class="icon-info"
|
|
||||||
src="/images/icon-abandoned.svg"
|
|
||||||
alt="Abandoned scenery"
|
|
||||||
:title="$t('desc.abandoned')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img
|
|
||||||
v-if="station.generalInfo?.lines"
|
|
||||||
class="icon-info"
|
|
||||||
src="/images/icon-real.svg"
|
|
||||||
alt="real scenery"
|
|
||||||
:title="`${$t('desc.real')} ${station.generalInfo.lines}`"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img
|
|
||||||
v-if="!station.generalInfo"
|
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
src="/images/icon-unknown.svg"
|
src="/images/icon-unknown.svg"
|
||||||
alt="icon-unknown"
|
alt="icon-unknown"
|
||||||
:title="$t('desc.unknown')"
|
:title="$t('sceneries.info.unknown')"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="station?.generalInfo && station?.generalInfo.reqLevel >= 0"
|
||||||
|
class="scenery-icon icon-info level"
|
||||||
|
:style="calculateExpStyle(station?.generalInfo.reqLevel)"
|
||||||
|
>
|
||||||
|
{{ station?.generalInfo.reqLevel >= 2 ? station?.generalInfo.reqLevel : 'L' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="station?.generalInfo"
|
||||||
|
class="scenery-icon icon-info"
|
||||||
|
:class="station?.generalInfo.controlType.replace('+', '-')"
|
||||||
|
:title="
|
||||||
|
$t('sceneries.info.control-type') + $t(`controls.${station?.generalInfo.controlType}`)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="station?.generalInfo?.signalType"
|
||||||
|
class="icon-info"
|
||||||
|
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
|
||||||
|
:alt="station.generalInfo.signalType"
|
||||||
|
:title="$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="station?.generalInfo?.availability == 'nonPublic'"
|
||||||
|
class="icon-info"
|
||||||
|
src="/images/icon-lock.svg"
|
||||||
|
alt="Non-public scenery"
|
||||||
|
:title="$t('sceneries.info.non-public')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="station?.generalInfo?.availability == 'unavailable'"
|
||||||
|
class="icon-info"
|
||||||
|
src="/images/icon-unavailable.svg"
|
||||||
|
alt="Unavailable scenery"
|
||||||
|
:title="$t('sceneries.info.unavailable')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="station?.generalInfo?.availability == 'abandoned'"
|
||||||
|
class="icon-info"
|
||||||
|
src="/images/icon-abandoned.svg"
|
||||||
|
alt="Abandoned scenery"
|
||||||
|
:title="$t('sceneries.info.abandoned')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="station?.generalInfo?.SUP"
|
||||||
|
class="icon-info"
|
||||||
|
src="/images/icon-SUP.svg"
|
||||||
|
alt="SUP (RASP-UZK)"
|
||||||
|
:title="$t('sceneries.info.SUP')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="station?.generalInfo?.ASDEK"
|
||||||
|
class="icon-info"
|
||||||
|
src="/images/icon-ASDEK.svg"
|
||||||
|
alt="dSAT ASDEK"
|
||||||
|
:title="$t('sceneries.info.ASDEK')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="station?.generalInfo?.lines"
|
||||||
|
class="icon-info"
|
||||||
|
src="/images/icon-real.svg"
|
||||||
|
alt="real scenery"
|
||||||
|
:title="`${$t('sceneries.info.real')} ${station.generalInfo.lines}`"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import stationInfoMixin from '../../../mixins/stationInfoMixin';
|
|
||||||
import styleMixin from '../../../mixins/styleMixin';
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
import Station from '../../../scripts/interfaces/Station';
|
import { Station } from '../../../typings/common';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [stationInfoMixin, styleMixin],
|
mixins: [styleMixin],
|
||||||
props: {
|
props: {
|
||||||
station: {
|
station: {
|
||||||
type: Object as PropType<Station>,
|
type: Object as PropType<Station>
|
||||||
required: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -94,6 +103,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../../styles/icons.scss';
|
@import '../../../styles/icons.scss';
|
||||||
|
|
||||||
.info-icons {
|
.info-icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -101,6 +111,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-info {
|
.icon-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,41 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-routes" v-if="station.generalInfo">
|
<section class="info-routes" v-if="station.generalInfo">
|
||||||
<div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0">
|
<div class="routes one-way" v-if="oneWayRoutes.length > 0">
|
||||||
<b>{{ $t('scenery.one-way-routes') }}</b>
|
<b>{{ $t('scenery.one-way-routes') }}</b>
|
||||||
|
|
||||||
<ul class="routes-list">
|
<ul class="routes-list">
|
||||||
<li
|
<li
|
||||||
v-for="route in station.generalInfo.routes.oneWay"
|
v-for="route in oneWayRoutes"
|
||||||
:key="route.name"
|
:key="route.routeName"
|
||||||
@click="setActiveShowLength(route.name)"
|
@click="setActiveShowLength(route.routeName)"
|
||||||
>
|
>
|
||||||
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">
|
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">
|
||||||
{{ route.name }}</span
|
{{ route.routeName }}</span
|
||||||
>
|
>
|
||||||
<span v-if="route.speed" class="speed">
|
<span v-if="route.routeSpeed" class="speed">
|
||||||
{{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
|
{{
|
||||||
|
activeShowLength.includes(route.routeName)
|
||||||
|
? route.routeLength + 'm'
|
||||||
|
: route.routeSpeed
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="route.SBL" class="sbl">SBL</span>
|
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0">
|
<div class="routes two-way" v-if="twoWayRoutes.length > 0">
|
||||||
<b>{{ $t('scenery.two-way-routes') }}</b>
|
<b>{{ $t('scenery.two-way-routes') }}</b>
|
||||||
|
|
||||||
<ul class="routes-list">
|
<ul class="routes-list">
|
||||||
<li
|
<li
|
||||||
v-for="route in station.generalInfo.routes.twoWay"
|
v-for="route in twoWayRoutes"
|
||||||
:key="route.name"
|
:key="route.routeName"
|
||||||
@click="setActiveShowLength(route.name)"
|
@click="setActiveShowLength(route.routeName)"
|
||||||
>
|
>
|
||||||
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">{{
|
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">{{
|
||||||
route.name
|
route.routeName
|
||||||
}}</span>
|
}}</span>
|
||||||
<span v-if="route.speed" class="speed">
|
<span v-if="route.routeSpeed" class="speed">
|
||||||
{{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
|
{{
|
||||||
|
activeShowLength.includes(route.routeName)
|
||||||
|
? route.routeLength + 'm'
|
||||||
|
: route.routeSpeed
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="route.SBL" class="sbl">SBL</span>
|
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +52,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import Station from '../../../scripts/interfaces/Station';
|
import { Station } from '../../../typings/common';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -66,6 +74,16 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
activeShowLength: [] as string[]
|
activeShowLength: [] as string[]
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
oneWayRoutes() {
|
||||||
|
return this.station.generalInfo?.routes.single ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
twoWayRoutes() {
|
||||||
|
return this.station.generalInfo?.routes.double ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,18 +7,22 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<transition-group name="spawns-anim" tag="ul">
|
<transition-group name="spawns-anim" tag="ul">
|
||||||
<li class="badge spawn badge-none" v-if="!onlineScenery || onlineScenery.spawns.length == 0" key="no-spawns">
|
<li
|
||||||
|
class="badge badge-none"
|
||||||
|
v-if="!onlineScenery || onlineScenery.spawns.length == 0"
|
||||||
|
key="no-spawns"
|
||||||
|
>
|
||||||
{{ $t('scenery.no-spawns') }}
|
{{ $t('scenery.no-spawns') }}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
class="badge spawn"
|
class="badge spawn-badge"
|
||||||
v-for="(spawn, i) in sortedSpawns"
|
v-for="(spawn, i) in sortedSpawns"
|
||||||
:key="spawn.spawnName + onlineScenery?.dispatcherName + i"
|
:key="spawn.spawnName + onlineScenery?.dispatcherName + i"
|
||||||
:data-electrified="spawn.isElectrified"
|
:data-electrified="spawn.isElectrified"
|
||||||
>
|
>
|
||||||
<span class="spawn_name">{{ spawn.spawnName }}</span>
|
<span class="name">{{ spawn.spawnName }}</span>
|
||||||
<span class="spawn_length">{{ spawn.spawnLength }}m</span>
|
<span class="length">{{ spawn.spawnLength }}m</span>
|
||||||
</li>
|
</li>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</section>
|
</section>
|
||||||
@@ -26,12 +30,12 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import { OnlineScenery } from '../../../scripts/interfaces/store/storeTypes';
|
import { ActiveScenery } from '../../../typings/common';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
onlineScenery: {
|
onlineScenery: {
|
||||||
type: Object as PropType<OnlineScenery>,
|
type: Object as PropType<ActiveScenery>,
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -55,19 +59,6 @@ ul {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spawn {
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
&_length {
|
|
||||||
background-color: #404040;
|
|
||||||
color: #cfcfcf;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-electrified='true'] > &_name {
|
|
||||||
background-color: #007599;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spawns-anim {
|
.spawns-anim {
|
||||||
&-move,
|
&-move,
|
||||||
&-enter-active,
|
&-enter-active,
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="info-stats" :class="!station.onlineInfo ? 'no-stats' : ''">
|
|
||||||
<span class="likes">
|
|
||||||
<img src="/images/icon-like" alt="Likes count icon" />
|
|
||||||
<span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="users">
|
|
||||||
<img src="/images/icon-user" alt="Users count icon" />
|
|
||||||
<span>{{ station.onlineInfo?.currentUsers || '0' }}</span>
|
|
||||||
/
|
|
||||||
<span>{{ station.onlineInfo?.maxUsers || '0' }}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="spawns">
|
|
||||||
<img src="/images/icon-spawn" alt="Spawns count icon" />
|
|
||||||
<span>{{ station.onlineInfo?.spawns.length || '0' }}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="schedules">
|
|
||||||
<img src="/images/icon-timetable" alt="Timetables count icon" />
|
|
||||||
<span>
|
|
||||||
<span style="color: #eee">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span>
|
|
||||||
/
|
|
||||||
<span style="color: #bbb"
|
|
||||||
>{{
|
|
||||||
station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed)
|
|
||||||
.length || '0'
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { PropType, defineComponent } from 'vue';
|
|
||||||
import Station from '../../../scripts/interfaces/Station';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
station: {
|
|
||||||
type: Object as PropType<Station>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../../styles/variables.scss';
|
|
||||||
|
|
||||||
.info-stats {
|
|
||||||
padding: 1rem 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
font-size: 1.65em;
|
|
||||||
|
|
||||||
&.no-stats {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > span {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
margin: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.likes,
|
|
||||||
.spawns {
|
|
||||||
color: $accentCol;
|
|
||||||
}
|
|
||||||
|
|
||||||
span > img {
|
|
||||||
width: 1.2em;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -13,13 +13,13 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
v-for="train in onlineScenery?.stationTrains"
|
v-for="{ train, status } in stationTrains"
|
||||||
class="badge user"
|
class="badge user"
|
||||||
:class="train.stopStatus"
|
|
||||||
:key="train.trainId"
|
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click.prevent="selectModalTrain(train.trainId, $event.currentTarget)"
|
:key="train.id"
|
||||||
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
|
:data-status="status"
|
||||||
|
@click.prevent="selectModalTrain(train, $event.currentTarget)"
|
||||||
|
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
|
||||||
>
|
>
|
||||||
<span class="user_train">{{ train.trainNo }}</span>
|
<span class="user_train">{{ train.trainNo }}</span>
|
||||||
<span class="user_name">{{ train.driverName }}</span>
|
<span class="user_name">{{ train.driverName }}</span>
|
||||||
@@ -32,15 +32,51 @@
|
|||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
||||||
import routerMixin from '../../../mixins/routerMixin';
|
import routerMixin from '../../../mixins/routerMixin';
|
||||||
import { OnlineScenery } from '../../../scripts/interfaces/store/storeTypes';
|
import { ActiveScenery, Station, StopStatus } from '../../../typings/common';
|
||||||
|
import { getTrainStopStatus } from '../utils';
|
||||||
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [routerMixin, modalTrainMixin],
|
mixins: [routerMixin, modalTrainMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
onlineScenery: {
|
onlineScenery: {
|
||||||
type: Object as PropType<OnlineScenery>,
|
type: Object as PropType<ActiveScenery>,
|
||||||
required: false
|
required: false
|
||||||
|
},
|
||||||
|
station: {
|
||||||
|
type: Object as PropType<Station>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mainStore: useMainStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
stationTrains() {
|
||||||
|
if (!this.onlineScenery) return;
|
||||||
|
|
||||||
|
const name = this.station?.generalInfo?.checkpoints[0] ?? this.onlineScenery.name;
|
||||||
|
|
||||||
|
return this.onlineScenery.stationTrains.map((train) => {
|
||||||
|
const stop = train.timetableData?.followingStops.find(
|
||||||
|
(stop) =>
|
||||||
|
stop.stopNameRAW.toLowerCase() == name.toLowerCase() ||
|
||||||
|
this.station?.generalInfo?.checkpoints.includes(stop.stopNameRAW)
|
||||||
|
);
|
||||||
|
|
||||||
|
const status = stop
|
||||||
|
? getTrainStopStatus(stop, train.currentStationName, this.onlineScenery!.name)
|
||||||
|
: 'no-timetable';
|
||||||
|
|
||||||
|
return {
|
||||||
|
train,
|
||||||
|
status
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -74,31 +110,31 @@ ul {
|
|||||||
-webkit-transition: background-color 200ms;
|
-webkit-transition: background-color 200ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.no-timetable .user_train {
|
&[data-status='no-timetable'] .user_train {
|
||||||
background-color: $no-timetable;
|
background-color: $no-timetable;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.departed > &_train {
|
&[data-status='departed'] > &_train {
|
||||||
background-color: $departed;
|
background-color: $departed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stopped > &_train {
|
&[data-status='stopped'] > &_train {
|
||||||
background-color: $stopped;
|
background-color: $stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.online > &_train {
|
&[data-status='online'] > &_train {
|
||||||
background-color: $online;
|
background-color: $online;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.terminated > &_train {
|
&[data-status='terminated'] > &_train {
|
||||||
background-color: $terminated;
|
background-color: $terminated;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disconnected > &_train {
|
&[data-status='disconnected'] > &_train {
|
||||||
background-color: $disconnected;
|
background-color: $disconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.offline {
|
&[data-status='offline'] {
|
||||||
background: firebrick;
|
background: firebrick;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,22 +6,14 @@
|
|||||||
<span>{{ $t('scenery.timetables') }}</span>
|
<span>{{ $t('scenery.timetables') }}</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<span class="text--primary">{{ onlineScenery?.scheduledTrainCount.all || 0 }}</span>
|
<span class="text--primary">{{ onlineScenery?.scheduledTrainCount.all ?? 0 }}</span>
|
||||||
<span> / </span>
|
<span> / </span>
|
||||||
<span class="text--grayed">
|
<span class="text--grayed">
|
||||||
{{ onlineScenery?.scheduledTrainCount.confirmed || '0' }}
|
{{ onlineScenery?.scheduledTrainCount.confirmed ?? 0 }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="header_links">
|
<span class="header_links" v-if="station">
|
||||||
<a
|
|
||||||
:href="`https://pragotron-td2.web.app/board?name=${station.name}`"
|
|
||||||
target="_blank"
|
|
||||||
:title="$t('scenery.pragotron-link')"
|
|
||||||
>
|
|
||||||
<img src="/images/icon-pragotron.svg" alt="icon-pragotron" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
|
<a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
|
||||||
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
|
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
|
||||||
</a>
|
</a>
|
||||||
@@ -33,12 +25,12 @@
|
|||||||
{{ (i > 0 && '•') || '' }}
|
{{ (i > 0 && '•') || '' }}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
:key="cp.checkpointName"
|
:key="cp"
|
||||||
class="checkpoint_item"
|
class="checkpoint_item"
|
||||||
:class="{ current: chosenCheckpoint === cp.checkpointName }"
|
:class="{ current: chosenCheckpoint === cp }"
|
||||||
@click="setCheckpoint(cp)"
|
@click="setCheckpoint(cp)"
|
||||||
>
|
>
|
||||||
{{ cp.checkpointName }}
|
{{ cp }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,8 +39,8 @@
|
|||||||
<div class="timetable-list">
|
<div class="timetable-list">
|
||||||
<transition-group name="list-anim">
|
<transition-group name="list-anim">
|
||||||
<div
|
<div
|
||||||
|
v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0"
|
||||||
style="padding-bottom: 5em"
|
style="padding-bottom: 5em"
|
||||||
v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0"
|
|
||||||
key="list-loading"
|
key="list-loading"
|
||||||
>
|
>
|
||||||
<Loading />
|
<Loading />
|
||||||
@@ -56,7 +48,7 @@
|
|||||||
|
|
||||||
<span
|
<span
|
||||||
class="timetable-item empty"
|
class="timetable-item empty"
|
||||||
v-else-if="computedScheduledTrains.length == 0 && !onlineScenery"
|
v-else-if="sceneryTimetables.length == 0 && !onlineScenery"
|
||||||
key="list-offline"
|
key="list-offline"
|
||||||
>
|
>
|
||||||
{{ $t('scenery.offline') }}
|
{{ $t('scenery.offline') }}
|
||||||
@@ -64,7 +56,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="timetable-item empty"
|
class="timetable-item empty"
|
||||||
v-else-if="computedScheduledTrains.length == 0"
|
v-else-if="sceneryTimetables.length == 0"
|
||||||
key="list-no-timetables"
|
key="list-no-timetables"
|
||||||
>
|
>
|
||||||
{{ $t('scenery.no-timetables') }}
|
{{ $t('scenery.no-timetables') }}
|
||||||
@@ -73,57 +65,56 @@
|
|||||||
<div
|
<div
|
||||||
class="timetable-item"
|
class="timetable-item"
|
||||||
v-else
|
v-else
|
||||||
v-for="scheduledTrain in computedScheduledTrains"
|
v-for="(row, i) in sceneryTimetables"
|
||||||
:key="scheduledTrain.trainId"
|
:key="row.train.id + i"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
|
@click.prevent.stop="selectModalTrain(row.train, $event.currentTarget)"
|
||||||
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
|
@keydown.enter.prevent="selectModalTrain(row.train, $event.currentTarget)"
|
||||||
>
|
>
|
||||||
<span class="timetable-general">
|
<span class="timetable-general">
|
||||||
<span class="general-info">
|
<span class="general-info">
|
||||||
<span class="info-number">
|
<span class="info-number">
|
||||||
<strong>{{ scheduledTrain.category }}</strong>
|
<strong>{{ row.train.timetableData!.category }}</strong>
|
||||||
{{ scheduledTrain.trainNo }}
|
{{ row.train.trainNo }}
|
||||||
|
|
||||||
<span class="g-tooltip" v-if="scheduledTrain.stopInfo.comments">
|
<span v-if="row.checkpointStop.comments" :title="row.checkpointStop.comments">
|
||||||
<img src="/images/icon-warning.svg" />
|
<img src="/images/icon-warning.svg" />
|
||||||
<span class="content" v-html="scheduledTrain.stopInfo.comments"> </span>
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
|
||||||
<span>
|
<span>
|
||||||
{{ scheduledTrain.driverName }}
|
{{ row.train.driverName }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="info-route">
|
<div class="info-route">
|
||||||
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
|
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScheduledTrainStatus :scheduledTrain="scheduledTrain" />
|
<ScheduledTrainStatus :sceneryTimetableRow="row" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="timetable-schedule">
|
<span class="timetable-schedule">
|
||||||
<span class="schedule-arrival">
|
<span class="schedule-arrival">
|
||||||
<span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
|
<span class="arrival-time begins" v-if="row.checkpointStop.beginsHere">
|
||||||
{{ $t('timetables.begins') }}
|
{{ $t('timetables.begins') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="arrival-time" v-else>
|
<span class="arrival-time" v-else>
|
||||||
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
|
<div v-if="row.checkpointStop.arrivalDelay == 0">
|
||||||
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
|
<span>{{ timestampToString(row.checkpointStop.arrivalTimestamp) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div>
|
<div>
|
||||||
<s style="margin-right: 0.2em" class="text--grayed">{{
|
<s style="margin-right: 0.2em" class="text--grayed">{{
|
||||||
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
|
timestampToString(row.checkpointStop.arrivalTimestamp)
|
||||||
}}</s>
|
}}</s>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
|
{{ timestampToString(row.checkpointStop.arrivalRealTimestamp) }}
|
||||||
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : ''
|
({{ row.checkpointStop.arrivalDelay > 0 ? '+' : ''
|
||||||
}}{{ scheduledTrain.stopInfo.arrivalDelay }})
|
}}{{ row.checkpointStop.arrivalDelay }})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -131,41 +122,39 @@
|
|||||||
|
|
||||||
<span class="schedule-stop">
|
<span class="schedule-stop">
|
||||||
<span class="stop-connection">
|
<span class="stop-connection">
|
||||||
{{ scheduledTrain.arrivingLine }}
|
{{ row.arrivingLine }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stop-time">
|
<span class="stop-time">
|
||||||
{{ scheduledTrain.stopInfo.stopTime || '' }}
|
{{ row.checkpointStop.stopTime || '' }}
|
||||||
{{
|
{{ row.checkpointStop.stopTime ? row.checkpointStop.stopType || 'pt' : '' }}
|
||||||
scheduledTrain.stopInfo.stopTime ? scheduledTrain.stopInfo.stopType || 'pt' : ''
|
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stop-connection">
|
<span class="stop-connection">
|
||||||
{{ scheduledTrain.departureLine }}
|
{{ row.departureLine }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="schedule-departure">
|
<span class="schedule-departure">
|
||||||
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere">
|
<span class="departure-time terminates" v-if="row.checkpointStop.terminatesHere">
|
||||||
{{ $t('timetables.terminates') }}
|
{{ $t('timetables.terminates') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="departure-time" v-else>
|
<span class="departure-time" v-else>
|
||||||
<div v-if="scheduledTrain.stopInfo.departureDelay == 0">
|
<div v-if="row.checkpointStop.departureDelay == 0">
|
||||||
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span>
|
<span>{{ timestampToString(row.checkpointStop.departureTimestamp) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div>
|
<div>
|
||||||
<s style="margin-right: 0.2em" class="text--grayed">{{
|
<s style="margin-right: 0.2em" class="text--grayed">{{
|
||||||
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
|
timestampToString(row.checkpointStop.departureTimestamp)
|
||||||
}}</s>
|
}}</s>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
|
{{ timestampToString(row.checkpointStop.departureRealTimestamp) }}
|
||||||
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : ''
|
({{ row.checkpointStop.departureDelay > 0 ? '+' : ''
|
||||||
}}{{ scheduledTrain.stopInfo.departureDelay }})
|
}}{{ row.checkpointStop.departureDelay }})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -184,11 +173,13 @@ import { useRoute } from 'vue-router';
|
|||||||
import Loading from '../Global/Loading.vue';
|
import Loading from '../Global/Loading.vue';
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
import routerMixin from '../../mixins/routerMixin';
|
import routerMixin from '../../mixins/routerMixin';
|
||||||
import Station from '../../scripts/interfaces/Station';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||||
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
||||||
import { OnlineScenery } from '../../scripts/interfaces/store/storeTypes';
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import { ActiveScenery, Station } from '../../typings/common';
|
||||||
|
import { SceneryTimetableRow } from './typings';
|
||||||
|
import { getTrainStopStatus, stopStatusPriority } from './utils';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SceneryTimetable',
|
name: 'SceneryTimetable',
|
||||||
@@ -199,12 +190,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
props: {
|
props: {
|
||||||
station: {
|
station: {
|
||||||
type: Object as PropType<Station>,
|
type: Object as PropType<Station>
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
onlineScenery: {
|
onlineScenery: {
|
||||||
type: Object as PropType<OnlineScenery>,
|
type: Object as PropType<ActiveScenery>
|
||||||
required: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -212,10 +201,6 @@ export default defineComponent({
|
|||||||
listOpen: false
|
listOpen: false
|
||||||
}),
|
}),
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.loadSelectedOption();
|
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
activated() {
|
||||||
this.loadSelectedOption();
|
this.loadSelectedOption();
|
||||||
},
|
},
|
||||||
@@ -224,58 +209,88 @@ export default defineComponent({
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const currentURL = computed(() => `${location.origin}${route.fullPath}`);
|
const currentURL = computed(() => `${location.origin}${route.fullPath}`);
|
||||||
|
|
||||||
const store = useStore();
|
const apiStore = useApiStore();
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
|
||||||
const chosenCheckpoint = ref(
|
const chosenCheckpoint = ref(
|
||||||
props.station?.generalInfo?.checkpoints?.length == 0
|
props.station?.generalInfo?.checkpoints[0] ??
|
||||||
? ''
|
props.station?.name ??
|
||||||
: props.station?.generalInfo?.checkpoints[0].checkpointName || null
|
route.query['station']?.toString() ??
|
||||||
|
''
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentURL,
|
currentURL,
|
||||||
chosenCheckpoint,
|
chosenCheckpoint,
|
||||||
store
|
apiStore,
|
||||||
|
mainStore
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
tabliceZbiorczeHref() {
|
tabliceZbiorczeHref() {
|
||||||
let url = `https://tablice-td2.web.app/?station=${this.station.name}`;
|
let url = `https://tablice-td2.web.app/?station=${this.station!.name}`;
|
||||||
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
|
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
|
|
||||||
computedScheduledTrains() {
|
sceneryTimetables(): SceneryTimetableRow[] {
|
||||||
return (
|
if (!this.onlineScenery) return [];
|
||||||
this.onlineScenery?.scheduledTrains
|
|
||||||
?.filter(
|
const sceneryName = this.$route.query['station']?.toString().replace(/_/g, ' ') ?? '';
|
||||||
(train) =>
|
|
||||||
train.checkpointName.toLocaleLowerCase() ==
|
return this.onlineScenery.scheduledTrains
|
||||||
(this.chosenCheckpoint || this.station.name).toLocaleLowerCase()
|
.filter(
|
||||||
|
(ct) =>
|
||||||
|
ct.timetablePathElement.stationName == sceneryName &&
|
||||||
|
ct.train.region == this.mainStore.region.id &&
|
||||||
|
this.chosenCheckpoint &&
|
||||||
|
ct.checkpointStop.stopNameRAW.toLowerCase() == this.chosenCheckpoint.toLowerCase()
|
||||||
)
|
)
|
||||||
.sort((a, b) => {
|
.map((ct) => {
|
||||||
if (a.stopStatusID > b.stopStatusID) return 1;
|
const trainStopStatus = getTrainStopStatus(
|
||||||
if (a.stopStatusID < b.stopStatusID) return -1;
|
ct.checkpointStop,
|
||||||
|
ct.train.currentStationName,
|
||||||
if (a.stopInfo.arrivalTimestamp > b.stopInfo.arrivalTimestamp) return 1;
|
sceneryName
|
||||||
if (a.stopInfo.arrivalTimestamp < b.stopInfo.arrivalTimestamp) return -1;
|
|
||||||
|
|
||||||
return a.stopInfo.departureTimestamp > b.stopInfo.departureTimestamp ? 1 : -1;
|
|
||||||
}) || []
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
checkpointStop: ct.checkpointStop,
|
||||||
|
train: ct.train,
|
||||||
|
prevDepartureLine: ct.previousSceneryElement?.departureRouteExt ?? null,
|
||||||
|
nextArrivalLine: ct.nextSceneryElement?.arrivalRouteExt ?? null,
|
||||||
|
departureLine: ct.timetablePathElement.departureRouteExt ?? null,
|
||||||
|
arrivingLine: ct.timetablePathElement.arrivalRouteExt ?? null,
|
||||||
|
prevStationName: ct.previousSceneryElement?.stationName ?? null,
|
||||||
|
nextStationName: ct.nextSceneryElement?.stationName ?? null,
|
||||||
|
status: trainStopStatus
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) > 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
if (a.checkpointStop.arrivalTimestamp > b.checkpointStop.arrivalTimestamp) return 1;
|
||||||
|
if (a.checkpointStop.arrivalTimestamp < b.checkpointStop.arrivalTimestamp) return -1;
|
||||||
|
|
||||||
|
return a.checkpointStop.departureTimestamp > b.checkpointStop.departureTimestamp ? 1 : -1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
loadSelectedOption() {
|
loadSelectedOption() {
|
||||||
this.chosenCheckpoint =
|
if (!this.station) return;
|
||||||
this.station.generalInfo?.checkpoints[0]?.checkpointName || this.station.name;
|
|
||||||
|
this.chosenCheckpoint = this.station.generalInfo?.checkpoints[0] ?? this.station.name;
|
||||||
},
|
},
|
||||||
|
|
||||||
setCheckpoint(cp: { checkpointName: string }) {
|
setCheckpoint(cp: string) {
|
||||||
this.chosenCheckpoint = cp.checkpointName;
|
this.chosenCheckpoint = cp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -407,13 +422,6 @@ export default defineComponent({
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.g-tooltip > .content {
|
|
||||||
z-index: 100;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
left: 110%;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 1.1em;
|
width: 1.1em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +1,186 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="scenery-table-section">
|
<div class="scenery-timetables-history">
|
||||||
|
<div class="history-modes">
|
||||||
|
<button
|
||||||
|
class="btn btn--option"
|
||||||
|
v-for="mode in historyModeList"
|
||||||
|
:key="mode"
|
||||||
|
:class="{ checked: checkedHistoryMode == mode }"
|
||||||
|
@click="checkHistoryMode(mode)"
|
||||||
|
>
|
||||||
|
{{ $t(`scenery.timetable-${mode}`) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-wrapper">
|
||||||
<Loading v-if="dataStatus != DataStatus.Loaded" />
|
<Loading v-if="dataStatus != DataStatus.Loaded" />
|
||||||
<div class="no-history" v-else-if="historyList.length == 0">
|
|
||||||
|
<div v-else-if="historyList.length == 0" class="no-history">
|
||||||
{{ $t('scenery.history-list-empty') }}
|
{{ $t('scenery.history-list-empty') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="scenery-history-table" v-else>
|
<div v-else class="history-list">
|
||||||
<thead>
|
<div v-for="timetableHistory in historyList" :key="timetableHistory.id">
|
||||||
<th>{{ $t('scenery.timetables-history-id') }}</th>
|
<span>
|
||||||
<th>{{ $t('scenery.timetables-history-number') }}</th>
|
<div>
|
||||||
<th>{{ $t('scenery.timetables-history-route') }}</th>
|
<span
|
||||||
<th>{{ $t('scenery.timetables-history-driver') }}</th>
|
class="timetable-status-indicator"
|
||||||
<th>{{ $t('scenery.timetables-history-author') }}</th>
|
:data-terminated="timetableHistory.terminated"
|
||||||
<th>{{ $t('scenery.timetables-history-date') }}</th>
|
:data-fulfilled="timetableHistory.fulfilled"
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="historyItem in historyList" :key="historyItem.id">
|
|
||||||
<td>
|
|
||||||
<router-link :to="`/journal/timetables?timetableId=${historyItem.id}`"
|
|
||||||
>#{{ historyItem.id }}</router-link
|
|
||||||
>
|
>
|
||||||
</td>
|
⦿
|
||||||
<td>
|
</span>
|
||||||
<b class="text--primary">{{ historyItem.trainCategoryCode }}</b> <br />
|
#{{ timetableHistory.id }} |
|
||||||
{{ historyItem.trainNo }}
|
<b class="text--primary">{{ timetableHistory.trainCategoryCode }}</b>
|
||||||
</td>
|
{{ timetableHistory.trainNo }}
|
||||||
<td>{{ historyItem.route.replace('|', ' -> ') }}</td>
|
{{ timetableHistory.route.replace('|', ' ⇒ ') }}
|
||||||
<td>{{ historyItem.driverName }}</td>
|
</div>
|
||||||
<td>
|
|
||||||
|
<div class="text--grayed">
|
||||||
|
<span>
|
||||||
|
{{ $t('scenery.timetable-issued-date') }}
|
||||||
|
<b>
|
||||||
|
{{
|
||||||
|
localeDateTime(
|
||||||
|
timetableHistory.createdAt > timetableHistory.beginDate
|
||||||
|
? timetableHistory.beginDate
|
||||||
|
: timetableHistory.createdAt,
|
||||||
|
$i18n.locale
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</b></span
|
||||||
|
>
|
||||||
|
<span v-if="timetableHistory.authorName">
|
||||||
|
{{ $t('scenery.timetable-issued-by') }}
|
||||||
|
<b>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="historyItem.authorName"
|
:to="`/journal/timetables?search-dispatcher=${timetableHistory.authorName}`"
|
||||||
:to="`/journal/timetables?authorName=${historyItem.authorName}`"
|
>
|
||||||
>{{ historyItem.authorName }}
|
{{ timetableHistory.authorName }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
|
</b>
|
||||||
</td>
|
</span>
|
||||||
<td>
|
|
||||||
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
|
<span>
|
||||||
{{ localeTime(historyItem.beginDate, $i18n.locale) }}
|
{{ $t('scenery.timetable-issued-for') }}
|
||||||
</td>
|
<b>
|
||||||
</tr>
|
<router-link
|
||||||
</tbody>
|
:to="`/journal/timetables?search-driver=${timetableHistory.driverName}`"
|
||||||
</table>
|
>
|
||||||
</section>
|
{{ timetableHistory.driverName }}
|
||||||
|
</router-link>
|
||||||
|
</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
navigateTo(`/journal/timetables`, {
|
||||||
|
'search-train': `#${timetableHistory.id}`
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-back.svg" alt="icon navigate to timetable" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bottom-info">
|
<div class="bottom-info">
|
||||||
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
|
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
|
||||||
{{ $t('scenery.bottom-info') }}
|
{{ $t('scenery.bottom-info') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from 'axios';
|
|
||||||
import { defineComponent, PropType } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
|
||||||
import {
|
|
||||||
TimetableHistory,
|
|
||||||
SceneryTimetableHistory
|
|
||||||
} from '../../scripts/interfaces/api/TimetablesAPIData';
|
|
||||||
import Station from '../../scripts/interfaces/Station';
|
|
||||||
import { URLs } from '../../scripts/utils/apiURLs';
|
|
||||||
import Loading from '../Global/Loading.vue';
|
import Loading from '../Global/Loading.vue';
|
||||||
import listObserverMixin from '../../mixins/listObserverMixin';
|
import { API } from '../../typings/api';
|
||||||
import { OnlineScenery } from '../../scripts/interfaces/store/storeTypes';
|
import { ActiveScenery, Station, Status } from '../../typings/common';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import routerMixin from '../../mixins/routerMixin';
|
||||||
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
|
||||||
|
const historyModeList = ['via', 'issuedFrom', 'terminatingAt'] as const;
|
||||||
|
type HistoryMode = (typeof historyModeList)[number];
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SceneryTimetablesHistory',
|
name: 'SceneryTimetablesHistory',
|
||||||
mixins: [dateMixin, listObserverMixin],
|
mixins: [dateMixin, routerMixin],
|
||||||
props: {
|
props: {
|
||||||
station: {
|
station: {
|
||||||
type: Object as PropType<Station>,
|
type: Object as PropType<Station>
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
onlineScenery: {
|
onlineScenery: {
|
||||||
type: Object as PropType<OnlineScenery>,
|
type: Object as PropType<ActiveScenery>
|
||||||
required: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
historyList: [] as TimetableHistory[],
|
historyList: [] as API.TimetableHistory.Response,
|
||||||
dataStatus: DataStatus.Loading,
|
historyModeList,
|
||||||
DataStatus
|
|
||||||
|
apiStore: useApiStore(),
|
||||||
|
mainStore: useMainStore(),
|
||||||
|
dataStatus: Status.Data.Loading,
|
||||||
|
DataStatus: Status.Data,
|
||||||
|
|
||||||
|
checkedHistoryMode: 'via' as HistoryMode
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async activated() {
|
async activated() {
|
||||||
const fetchedHistory = await this.fetchAPIData();
|
this.fetchAPIData();
|
||||||
if (fetchedHistory) this.historyList = fetchedHistory.timetables;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async fetchAPIData(countFrom = 0, countLimit = 15): Promise<SceneryTimetableHistory | null> {
|
async fetchAPIData() {
|
||||||
try {
|
const stationName = this.$route.query['station'];
|
||||||
const requestString = `${URLs.stacjownikAPI}/api/getIssuedTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
|
|
||||||
const historyAPIData: SceneryTimetableHistory = await (await axios.get(requestString)).data;
|
|
||||||
|
|
||||||
this.dataStatus = DataStatus.Loaded;
|
if (!stationName) {
|
||||||
return historyAPIData;
|
this.historyList = [];
|
||||||
|
this.dataStatus = Status.Data.Loaded;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestFilters: Record<string, any> = {};
|
||||||
|
requestFilters[this.checkedHistoryMode] = stationName.toString();
|
||||||
|
requestFilters.countLimit = 30;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response: API.TimetableHistory.Response = await (
|
||||||
|
await this.apiStore.client!.get('api/getTimetables', {
|
||||||
|
params: requestFilters
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
this.historyList = response;
|
||||||
|
|
||||||
|
this.dataStatus = Status.Data.Loaded;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
checkHistoryMode(mode: HistoryMode) {
|
||||||
|
this.checkedHistoryMode = mode;
|
||||||
|
this.dataStatus = Status.Data.Loading;
|
||||||
|
this.fetchAPIData();
|
||||||
|
},
|
||||||
|
|
||||||
navigateToHistory() {
|
navigateToHistory() {
|
||||||
this.$router.push(`/journal/timetables?issuedFrom=${this.station.name}`);
|
this.$router.push({
|
||||||
|
path: '/journal/timetables',
|
||||||
|
query: {
|
||||||
|
[`search-${this.checkedHistoryMode}`]: this.station?.name || this.onlineScenery?.name
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { Loading }
|
components: { Loading }
|
||||||
@@ -119,4 +190,67 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@import '../../styles/responsive.scss';
|
||||||
@import '../../styles/sceneryViewTables.scss';
|
@import '../../styles/sceneryViewTables.scss';
|
||||||
|
|
||||||
|
.scenery-timetables-history {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
gap: 1em;
|
||||||
|
grid-template-rows: auto 1fr 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-wrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-modes {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 0.25em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.35em;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5em;
|
||||||
|
background-color: #2b2b2b;
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list > div > button > img {
|
||||||
|
width: 2em;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-status-indicator {
|
||||||
|
&[data-fulfilled='true'] {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-terminated='false'] {
|
||||||
|
color: lightblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-terminated='true'][data-fulfilled='false'] {
|
||||||
|
color: lightcoral;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="general-status">
|
<div class="general-status">
|
||||||
<span
|
<span
|
||||||
:class="computedScheduledTrain.stopStatus"
|
:class="computedScheduledTrain.status"
|
||||||
:title="computedScheduledTrain.stopStatusDescription"
|
:title="computedScheduledTrain.stopStatusDescription"
|
||||||
>
|
>
|
||||||
{{ computedScheduledTrain.stopStatusIndicator }}
|
{{ computedScheduledTrain.stopStatusIndicator }}
|
||||||
@@ -11,25 +11,21 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import { ScheduledTrain, StopStatus } from '../../scripts/interfaces/ScheduledTrain';
|
import { StopStatus } from '../../typings/common';
|
||||||
|
import { SceneryTimetableRow } from './typings';
|
||||||
interface ScheduledTrainComp extends ScheduledTrain {
|
|
||||||
stopStatusIndicator: string;
|
|
||||||
stopStatusDescription: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
scheduledTrain: {
|
sceneryTimetableRow: {
|
||||||
type: Object as PropType<ScheduledTrain>,
|
type: Object as PropType<SceneryTimetableRow>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
computedScheduledTrain(): ScheduledTrainComp {
|
computedScheduledTrain() {
|
||||||
const { prevDepartureLine, prevStationName, stopStatus, nextArrivalLine, nextStationName } =
|
const { prevDepartureLine, prevStationName, nextArrivalLine, nextStationName, status } =
|
||||||
this.scheduledTrain;
|
this.sceneryTimetableRow;
|
||||||
|
|
||||||
const prevDepartureIndicator = prevDepartureLine
|
const prevDepartureIndicator = prevDepartureLine
|
||||||
? `(${prevDepartureLine}) ${prevStationName}`
|
? `(${prevDepartureLine}) ${prevStationName}`
|
||||||
@@ -41,8 +37,8 @@ export default defineComponent({
|
|||||||
let stopStatusDescription = '',
|
let stopStatusDescription = '',
|
||||||
stopStatusIndicator = '';
|
stopStatusIndicator = '';
|
||||||
|
|
||||||
switch (stopStatus) {
|
switch (status) {
|
||||||
case StopStatus.arriving:
|
case StopStatus.ARRIVING:
|
||||||
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
|
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
|
||||||
stopStatusDescription = this.$t('timetables.desc-arriving', {
|
stopStatusDescription = this.$t('timetables.desc-arriving', {
|
||||||
prevStationName,
|
prevStationName,
|
||||||
@@ -50,17 +46,17 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StopStatus.online:
|
case StopStatus.ONLINE:
|
||||||
case StopStatus.stopped:
|
case StopStatus.STOPPED:
|
||||||
stopStatusIndicator = nextArrivalLine
|
stopStatusIndicator = nextArrivalLine
|
||||||
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
|
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
|
||||||
: `${this.$t('timetables.desc-end')}`;
|
: `${this.$t('timetables.desc-end')}`;
|
||||||
stopStatusDescription = nextArrivalLine
|
stopStatusDescription = nextArrivalLine
|
||||||
? this.$t(`timetables.desc-${stopStatus}`, { nextStationName, nextArrivalLine })
|
? this.$t(`timetables.desc-${status}`, { nextStationName, nextArrivalLine })
|
||||||
: '';
|
: '';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StopStatus.departed:
|
case StopStatus.DEPARTED:
|
||||||
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
|
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
|
||||||
stopStatusDescription = this.$t('timetables.desc-departed', {
|
stopStatusDescription = this.$t('timetables.desc-departed', {
|
||||||
nextStationName,
|
nextStationName,
|
||||||
@@ -68,7 +64,7 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StopStatus['departed-away']:
|
case StopStatus.DEPARTED_AWAY:
|
||||||
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
|
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
|
||||||
stopStatusDescription = this.$t('timetables.desc-departed-away', {
|
stopStatusDescription = this.$t('timetables.desc-departed-away', {
|
||||||
nextStationName,
|
nextStationName,
|
||||||
@@ -76,7 +72,7 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StopStatus.terminated:
|
case StopStatus.TERMINATED:
|
||||||
stopStatusIndicator = `X ${this.$t('timetables.desc-terminated')}`;
|
stopStatusIndicator = `X ${this.$t('timetables.desc-terminated')}`;
|
||||||
stopStatusDescription = this.$t('timetables.desc-terminated');
|
stopStatusDescription = this.$t('timetables.desc-terminated');
|
||||||
break;
|
break;
|
||||||
@@ -85,7 +81,7 @@ export default defineComponent({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...this.scheduledTrain,
|
...this.sceneryTimetableRow,
|
||||||
stopStatusDescription,
|
stopStatusDescription,
|
||||||
stopStatusIndicator
|
stopStatusIndicator
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { StopStatus, Train, TrainStop } from '../../typings/common';
|
||||||
|
|
||||||
|
export interface SceneryTimetableRow {
|
||||||
|
checkpointStop: TrainStop;
|
||||||
|
train: Train;
|
||||||
|
prevDepartureLine: string | null;
|
||||||
|
nextArrivalLine: string | null;
|
||||||
|
departureLine: string | null;
|
||||||
|
arrivingLine: string | null;
|
||||||
|
prevStationName: string | null;
|
||||||
|
nextStationName: string | null;
|
||||||
|
status: StopStatus;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { StopStatus, TrainStop } from '../../typings/common';
|
||||||
|
|
||||||
|
export const stopStatusPriority = [
|
||||||
|
StopStatus.ONLINE,
|
||||||
|
StopStatus.STOPPED,
|
||||||
|
StopStatus.DEPARTED,
|
||||||
|
StopStatus.ARRIVING,
|
||||||
|
StopStatus.DEPARTED_AWAY,
|
||||||
|
StopStatus.TERMINATED
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getTrainStopStatus(
|
||||||
|
stopInfo: TrainStop,
|
||||||
|
currentStationName: string,
|
||||||
|
sceneryName: string
|
||||||
|
) {
|
||||||
|
if (stopInfo.terminatesHere && stopInfo.confirmed) {
|
||||||
|
return StopStatus.TERMINATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
|
||||||
|
return StopStatus.DEPARTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
|
||||||
|
return StopStatus.DEPARTED_AWAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStationName == sceneryName && !stopInfo.stopped) {
|
||||||
|
return StopStatus.ONLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStationName == sceneryName && stopInfo.stopped) {
|
||||||
|
return StopStatus.STOPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStationName != sceneryName) {
|
||||||
|
return StopStatus.ARRIVING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StopStatus.ONLINE;
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { useStationFiltersStore } from '../../store/stationFiltersStore';
|
|
||||||
|
|
||||||
interface FilterOption {
|
interface FilterOption {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -40,15 +39,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
emits: ['update:optionValue'],
|
emits: ['update:optionValue'],
|
||||||
|
|
||||||
setup() {
|
|
||||||
return {
|
|
||||||
filterStore: useStationFiltersStore()
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
'option.value'() {
|
'option.value'() {
|
||||||
this.filterStore.changeFilterValue(this.option.name, !this.option.value);
|
// this.filterStore.changeFilterValue(this.option.name, !this.option.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -56,17 +49,17 @@ export default defineComponent({
|
|||||||
handleDbClick(e: Event) {
|
handleDbClick(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.filterStore.lastClickedFilterId = this.option.id;
|
// this.filterStore.lastClickedFilterId = this.option.id;
|
||||||
// this.option.value = true;
|
// this.option.value = true;
|
||||||
this.$emit('update:optionValue', true);
|
this.$emit('update:optionValue', true);
|
||||||
|
|
||||||
this.filterStore.inputs.options
|
// this.filterStore.inputs.options
|
||||||
.filter((option) => {
|
// .filter((option) => {
|
||||||
return option.section == this.option.section && option.id != this.option.id;
|
// return option.section == this.option.section && option.id != this.option.id;
|
||||||
})
|
// })
|
||||||
.forEach((option) => {
|
// .forEach((option) => {
|
||||||
option.value = !this.option.value;
|
// option.value = !this.option.value;
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="filter-card" v-click-outside="closeCard" @keydown.esc="closeCard">
|
<section class="filter-card" v-click-outside="closeCard" @keydown.esc="closeCard">
|
||||||
<div class="card_controls">
|
<div class="card_controls">
|
||||||
<button class="btn--filled btn--image" @click="toggleCard">
|
<button class="card-button btn--filled btn--image" @click="toggleCard">
|
||||||
<img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" />
|
<img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" />
|
||||||
{{ $t('options.filters') }} [F]
|
<p>[F] {{ $t('options.filters') }}</p>
|
||||||
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span>
|
<span class="active-indicator" v-if="changedFilters.length != 0"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<label for="scenery-search">
|
<label for="scenery-search">
|
||||||
@@ -28,46 +28,63 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition name="card-anim">
|
<transition name="card-anim">
|
||||||
<div class="card" v-if="isVisible" tabindex="0" ref="cardEl">
|
<div class="card" v-if="isVisible" tabindex="0" ref="cardRef" @keydown.r="resetFilters">
|
||||||
<div class="card_content">
|
<div class="card_content" @scroll="onScroll" ref="cardContentRef">
|
||||||
<div class="card_title flex">{{ $t('filters.title') }}</div>
|
<div class="card_title flex">{{ $t('filters.title') }}</div>
|
||||||
<p class="card_info" v-html="$t('filters.desc')"></p>
|
<p class="card_info" v-html="$t('filters.desc')"></p>
|
||||||
|
|
||||||
|
<div class="changed-filters" :data-active="changedFilters.length > 0">
|
||||||
|
<template v-if="changedFilters.length > 0">
|
||||||
|
{{ $t('filters.changed-filters-count') }} <b>{{ changedFilters.length }}</b>
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="card_options">
|
<section class="card_options">
|
||||||
<div
|
<div
|
||||||
class="option-section"
|
class="option-section"
|
||||||
v-for="section in filterStore.inputs.optionSections"
|
v-for="(sectionFilters, sectionKey) in filtersSections"
|
||||||
:key="section"
|
:key="sectionKey"
|
||||||
>
|
>
|
||||||
<h3 class="text--primary">
|
<h3 class="text--primary">
|
||||||
{{ $t(`filters.sections.${section}`) }}
|
<span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
|
||||||
|
{{ $t(`filters.sections.${sectionKey}`) }}
|
||||||
<button @click="filterStore.resetSectionOptions(section)">RESET</button>
|
<button @click="resetSectionFilters(sectionKey)">RESET</button>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="section-inputs">
|
<div class="section-filters">
|
||||||
<FilterOption
|
<label
|
||||||
v-for="(option, i) in filterStore.inputs.options.filter(
|
v-for="filterKey in sectionFilters"
|
||||||
(o) => o.section == section
|
@click="() => (filters[filterKey] = !filters[filterKey])"
|
||||||
)"
|
@dblclick="setSingleSectionFilter(sectionKey, filterKey)"
|
||||||
v-model:optionValue="option.value"
|
:for="filterKey"
|
||||||
:option="option"
|
>
|
||||||
:key="i"
|
<input
|
||||||
|
:checked="filters[filterKey]"
|
||||||
|
v-model="filters[filterKey]"
|
||||||
|
type="checkbox"
|
||||||
|
:class="sectionKey"
|
||||||
|
:name="filterKey"
|
||||||
/>
|
/>
|
||||||
|
<span>
|
||||||
|
{{ $t(`filters.${filterKey}`) }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_timestamp" style="text-align: center">
|
<section class="card_timestamp">
|
||||||
<div>{{ $t('filters.minimum-hours-title') }}</div>
|
<h3 class="section-header">{{ $t('filters.minimum-hours-title') }}</h3>
|
||||||
|
|
||||||
<span class="clock">
|
<span class="clock">
|
||||||
<button class="btn--action" @click="subHour">-</button>
|
<button class="btn--action" @click="subHour">-</button>
|
||||||
<span>{{
|
<span>{{
|
||||||
minimumHours == 0
|
minimumHours == 0
|
||||||
? $t('filters.now')
|
? $t('filters.now')
|
||||||
: minimumHours < 8
|
: minimumHours < 7
|
||||||
? minimumHours + $t('filters.hour')
|
? minimumHours + $t('filters.hour')
|
||||||
: $t('filters.no-limit')
|
: $t('filters.no-limit')
|
||||||
}}</span>
|
}}</span>
|
||||||
@@ -76,30 +93,41 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_authors-search">
|
<section class="card_authors-search">
|
||||||
|
<h3 class="section-header">{{ $t('filters.authors-search') }}</h3>
|
||||||
|
|
||||||
|
<datalist id="authors" name="authors">
|
||||||
|
<option v-for="(author, i) in authorsHint" :key="i" :value="author"></option>
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<form action="javascript:void(0);" @submit="handleAuthorsInput">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('filters.authors-search')"
|
id="author"
|
||||||
|
list="authors"
|
||||||
name="authors"
|
name="authors"
|
||||||
v-model="authorsInputValue"
|
v-model="authors"
|
||||||
@input="handleAuthorsInput"
|
:placeholder="$t('filters.authors-placeholder')"
|
||||||
@focus="preventKeyDown = true"
|
@focus="preventKeyDown = true"
|
||||||
@blur="preventKeyDown = false"
|
@blur="preventKeyDown = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<button class="btn--action">{{ $t('filters.authors-button-title') }}</button>
|
||||||
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_sliders">
|
<section class="card_sliders">
|
||||||
<div class="slider" v-for="(slider, i) in filterStore.inputs.sliders" :key="i">
|
<div class="slider" v-for="(slider, i) in initSliders" :key="i">
|
||||||
<input
|
<input
|
||||||
class="slider-input"
|
class="slider-input"
|
||||||
type="range"
|
type="range"
|
||||||
:name="slider.name"
|
:name="slider.id"
|
||||||
:id="slider.id"
|
:id="slider.id"
|
||||||
:min="slider.minRange"
|
:min="slider.minRange"
|
||||||
:max="slider.maxRange"
|
:max="slider.maxRange"
|
||||||
v-model="slider.value"
|
:step="slider.step"
|
||||||
@change="handleInput"
|
v-model="filters[slider.id]"
|
||||||
/>
|
/>
|
||||||
<span class="slider-value">{{ slider.value }}</span>
|
<span class="slider-value">{{ filters[slider.id] }}</span>
|
||||||
<div class="slider-content">
|
<div class="slider-content">
|
||||||
{{ $t(`filters.sliders.${slider.id}`) }}
|
{{ $t(`filters.sliders.${slider.id}`) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -120,11 +148,11 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn--action"
|
class="btn--action"
|
||||||
|
:disabled="changedFilters.length == 0"
|
||||||
|
:data-disabled="changedFilters.length == 0"
|
||||||
@click="resetFilters"
|
@click="resetFilters"
|
||||||
:disabled="filterStore.areFiltersAtDefault"
|
|
||||||
:data-disabled="filterStore.areFiltersAtDefault"
|
|
||||||
>
|
>
|
||||||
{{ $t('filters.reset') }}
|
[R] {{ $t('filters.reset') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
|
<button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,11 +166,23 @@
|
|||||||
import { defineComponent, inject } from 'vue';
|
import { defineComponent, inject } from 'vue';
|
||||||
import keyMixin from '../../mixins/keyMixin';
|
import keyMixin from '../../mixins/keyMixin';
|
||||||
import routerMixin from '../../mixins/routerMixin';
|
import routerMixin from '../../mixins/routerMixin';
|
||||||
import StorageManager from '../../scripts/managers/storageManager';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import { useStationFiltersStore } from '../../store/stationFiltersStore';
|
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
|
|
||||||
import FilterOption from './FilterOption.vue';
|
import FilterOption from './FilterOption.vue';
|
||||||
|
import StorageManager from '../../managers/storageManager';
|
||||||
|
|
||||||
|
import {
|
||||||
|
filtersSections,
|
||||||
|
initSliders,
|
||||||
|
initFilters,
|
||||||
|
getChangedFilters
|
||||||
|
} from '../../managers/stationFilterManager';
|
||||||
|
|
||||||
|
import { StationFilterSection } from '../../managers/stationFilterManager';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'options_saved';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { FilterOption },
|
components: { FilterOption },
|
||||||
@@ -150,36 +190,52 @@ export default defineComponent({
|
|||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
saveOptions: false,
|
saveOptions: false,
|
||||||
STORAGE_KEY: 'options_saved',
|
|
||||||
|
|
||||||
authorsInputValue: '',
|
filtersSections,
|
||||||
|
initSliders,
|
||||||
|
|
||||||
minimumHours: 0,
|
minimumHours: 0,
|
||||||
|
authors: '',
|
||||||
|
|
||||||
currentRegion: { id: '', value: '' },
|
currentRegion: { id: '', value: '' },
|
||||||
|
|
||||||
delayInputTimer: -1,
|
delayInputTimer: -1,
|
||||||
chosenSearchScenery: ''
|
chosenSearchScenery: '',
|
||||||
|
|
||||||
|
scrollTop: 0,
|
||||||
|
lastFocusedEl: null as HTMLElement | null
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const isVisible = inject('isFilterCardVisible');
|
const isVisible = inject('isFilterCardVisible');
|
||||||
const store = useStore();
|
const store = useMainStore();
|
||||||
const filterStore = useStationFiltersStore();
|
|
||||||
|
const filters = inject('StationsView_filters') as Record<string, any>;
|
||||||
|
|
||||||
|
const changedFilters = computed(() => getChangedFilters(filters));
|
||||||
|
|
||||||
|
// Save filters to persistent storage
|
||||||
|
watch(filters, (value) => {
|
||||||
|
if (!StorageManager.isRegistered(STORAGE_KEY)) return;
|
||||||
|
|
||||||
|
Object.keys(value).forEach((filterKey) => {
|
||||||
|
StorageManager.setValue(filterKey, filters[filterKey]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isVisible,
|
isVisible,
|
||||||
store,
|
store,
|
||||||
filterStore
|
filters,
|
||||||
|
changedFilters
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.saveOptions = StorageManager.isRegistered(this.STORAGE_KEY);
|
this.saveOptions = StorageManager.isRegistered(STORAGE_KEY);
|
||||||
|
|
||||||
if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) {
|
if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) {
|
||||||
this.minimumHours = StorageManager.getNumericValue('onlineFromHours');
|
this.minimumHours = StorageManager.getNumericValue('onlineFromHours');
|
||||||
|
|
||||||
this.changeNumericFilterValue('onlineFromHours', this.minimumHours);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentRegion = this.store.region;
|
this.currentRegion = this.store.region;
|
||||||
@@ -196,6 +252,19 @@ export default defineComponent({
|
|||||||
|
|
||||||
currentOptionsActive() {
|
currentOptionsActive() {
|
||||||
return true;
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
authorsHint() {
|
||||||
|
return this.store.stationList
|
||||||
|
.reduce((acc, station) => {
|
||||||
|
station.generalInfo?.authors?.forEach((author) => {
|
||||||
|
if (author.trim() != '' && !acc.includes(author.toLocaleLowerCase()))
|
||||||
|
acc.push(author.toLocaleLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [] as string[])
|
||||||
|
.sort((a, b) => a.localeCompare(b));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -211,7 +280,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
isVisible(value: boolean) {
|
isVisible(value: boolean) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (value) (this.$refs['cardEl'] as HTMLDivElement).focus();
|
if (value) {
|
||||||
|
(this.$refs['cardRef'] as HTMLDivElement).focus();
|
||||||
|
(this.$refs['cardContentRef'] as HTMLDivElement).scrollTop = this.scrollTop;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -222,63 +294,67 @@ export default defineComponent({
|
|||||||
this.isVisible = !this.isVisible;
|
this.isVisible = !this.isVisible;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleInput(e: Event) {
|
onScroll(e: Event) {
|
||||||
const target = e.target as HTMLInputElement;
|
this.scrollTop = (e.target as HTMLElement).scrollTop;
|
||||||
|
|
||||||
this.filterStore.changeFilterValue(target.name, target.value);
|
|
||||||
|
|
||||||
if (this.saveOptions) StorageManager.setStringValue(target.name, target.value);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleAuthorsInput(e: Event) {
|
handleAuthorsInput() {
|
||||||
clearTimeout(this.delayInputTimer);
|
this.filters['authors'] = this.authors;
|
||||||
|
// if (this.saveOptions) StorageManager.setStringValue('authors', target.value);
|
||||||
this.delayInputTimer = window.setTimeout(() => {
|
|
||||||
this.handleInput(e);
|
|
||||||
}, 400);
|
|
||||||
},
|
|
||||||
|
|
||||||
changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
|
|
||||||
this.filterStore.changeFilterValue(name, value);
|
|
||||||
if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
subHour() {
|
subHour() {
|
||||||
this.minimumHours = this.minimumHours < 1 ? 8 : this.minimumHours - 1;
|
this.minimumHours = this.minimumHours < 1 ? 7 : this.minimumHours - 1;
|
||||||
|
this.filters['onlineFromHours'] = this.minimumHours;
|
||||||
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
addHour() {
|
addHour() {
|
||||||
this.minimumHours = this.minimumHours > 7 ? 0 : this.minimumHours + 1;
|
this.minimumHours = this.minimumHours > 6 ? 0 : this.minimumHours + 1;
|
||||||
|
this.filters['onlineFromHours'] = this.minimumHours;
|
||||||
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
saveFilters() {
|
saveFilters() {
|
||||||
this.saveOptions = !this.saveOptions;
|
this.saveOptions = !this.saveOptions;
|
||||||
|
|
||||||
if (!this.saveOptions) {
|
if (!this.saveOptions) {
|
||||||
StorageManager.unregisterStorage(this.STORAGE_KEY);
|
StorageManager.unregisterStorage(STORAGE_KEY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StorageManager.registerStorage(this.STORAGE_KEY);
|
StorageManager.registerStorage(STORAGE_KEY);
|
||||||
|
|
||||||
this.filterStore.inputs.options.forEach((option) =>
|
Object.keys(this.filters).forEach((filterKey) => {
|
||||||
StorageManager.setBooleanValue(option.name, !option.value)
|
StorageManager.setValue(filterKey, this.filters[filterKey]);
|
||||||
);
|
});
|
||||||
this.filterStore.inputs.sliders.forEach((slider) =>
|
|
||||||
StorageManager.setNumericValue(slider.name, slider.value)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
resetFilters() {
|
resetFilters() {
|
||||||
this.authorsInputValue = '';
|
// Reset local model values
|
||||||
|
|
||||||
this.minimumHours = 0;
|
this.minimumHours = 0;
|
||||||
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
|
this.authors = '';
|
||||||
this.filterStore.resetFilters();
|
|
||||||
|
// Reset global filters
|
||||||
|
Object.keys(this.filters).forEach((filterKey) => {
|
||||||
|
this.filters[filterKey] = (initFilters as any)[filterKey];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
areSectionFiltersDefault(sectionKey: StationFilterSection) {
|
||||||
|
return filtersSections[sectionKey].every((filterKey) => {
|
||||||
|
return this.filters[filterKey] == initFilters[filterKey];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
resetSectionFilters(sectionKey: StationFilterSection) {
|
||||||
|
filtersSections[sectionKey].forEach((filterKey) => {
|
||||||
|
this.filters[filterKey] = initFilters[filterKey];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setSingleSectionFilter(sectionKey: StationFilterSection, chosenKey: string) {
|
||||||
|
filtersSections[sectionKey].forEach((filterKey) => {
|
||||||
|
if (filterKey != chosenKey) this.filters[filterKey] = initFilters[filterKey];
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
closeCard() {
|
closeCard() {
|
||||||
@@ -293,32 +369,36 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@import '../../styles/responsive';
|
||||||
@import '../../styles/card.scss';
|
@import '../../styles/card';
|
||||||
|
@import '../../styles/animations';
|
||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
.card-anim {
|
h3.section-header {
|
||||||
&-enter-active,
|
text-align: center;
|
||||||
&-leave-active {
|
margin: 0.5em 0;
|
||||||
transition: all $animDuration $animType;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-from,
|
|
||||||
&-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate(-50%, -50%) scale(0.45);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr auto;
|
grid-template-rows: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
&_info {
|
.card_info {
|
||||||
background-color: #111;
|
background-color: #111;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&_controls {
|
.changed-filters {
|
||||||
|
background-color: #111;
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
&[data-active='true'] {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card_controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
|
||||||
@@ -328,7 +408,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_content {
|
.card_content {
|
||||||
padding: 1em 0.5em;
|
padding: 1em 0.5em;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -338,7 +418,7 @@ export default defineComponent({
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&_title {
|
.card_title {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: $accentCol;
|
color: $accentCol;
|
||||||
@@ -346,29 +426,7 @@ export default defineComponent({
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&_regions {
|
.card_timestamp {
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
label > input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
label > span {
|
|
||||||
padding: 0.25em 0.5em;
|
|
||||||
margin: 0 0.25em;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
background-color: gray;
|
|
||||||
|
|
||||||
&.checked {
|
|
||||||
background-color: seagreen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&_timestamp {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -379,7 +437,7 @@ export default defineComponent({
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
margin-top: 0.5em;
|
text-align: center;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
@@ -393,46 +451,83 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_modes {
|
.card_authors-search {
|
||||||
|
margin: 1em 0;
|
||||||
|
|
||||||
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
.option {
|
gap: 0.5em;
|
||||||
margin: 0 1em;
|
width: 100%;
|
||||||
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&_authors-search {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 60%;
|
|
||||||
min-width: 240px;
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 70%;
|
||||||
|
max-width: 400px;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border: 1px solid white;
|
outline: 1px solid white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_actions {
|
.section-filters {
|
||||||
width: 100%;
|
display: grid;
|
||||||
padding: 0.5em;
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.5em;
|
||||||
.filter-option {
|
margin: 1em 0;
|
||||||
max-width: 50%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-filters > label {
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
|
||||||
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.25em;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: forestgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:hover {
|
||||||
|
background-color: #22aa22;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:checked + span {
|
||||||
|
background-color: #444;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible + span {
|
||||||
|
outline: 1px solid $accentCol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card_actions {
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
width: 50%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
|
||||||
@@ -442,7 +537,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.option-section h3 {
|
.option-section h3 {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -457,35 +551,18 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-inputs {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 0.5em;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-actions div {
|
|
||||||
display: flex;
|
|
||||||
margin: 1em 0;
|
|
||||||
gap: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
|
||||||
&-value {
|
&-value {
|
||||||
color: $accentCol;
|
color: $accentCol;
|
||||||
margin-right: 0.5em;
|
|
||||||
padding: 0.1em 0.2em;
|
padding: 0.1em 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-content {
|
|
||||||
flex-grow: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-input {
|
&-input {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
@@ -494,7 +571,6 @@ export default defineComponent({
|
|||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
min-width: 25%;
|
min-width: 25%;
|
||||||
max-width: 120px;
|
|
||||||
|
|
||||||
&:focus-visible ~ * {
|
&:focus-visible ~ * {
|
||||||
color: gold;
|
color: gold;
|
||||||
@@ -564,4 +640,19 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include smallScreen {
|
||||||
|
.card_controls > button.card-button > p {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&-input {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<div class="station-stats">
|
||||||
|
<div class="separator" />
|
||||||
|
|
||||||
|
<div class="stats-row">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
>{{ $t('station-stats.u-factor') }}
|
||||||
|
<a
|
||||||
|
href="https://td2.info.pl/dyskusje/wspolczynnik-ugla-czy-to-ma-sens/msg81011/#msg81011"
|
||||||
|
target="_blank"
|
||||||
|
:data-tooltip="$t('station-stats.u-factor-tooltip')"
|
||||||
|
>(?)</a
|
||||||
|
>:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<b class="u-factor" :style="calculateFactorStyle()">
|
||||||
|
{{ uFactor.toFixed(2) }}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
•
|
||||||
|
{{ $t('station-stats.avg-timetable-count') }}
|
||||||
|
<b>{{ avgTimetableCount.toFixed(2) }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
•
|
||||||
|
{{ $t('station-stats.single-track-count') }}
|
||||||
|
<b>{{ trackCount.oneWay }}</b> (<b>{{ trackCount.oneWayElectric }} ⚡</b>)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
•
|
||||||
|
{{ $t('station-stats.double-track-count') }}
|
||||||
|
<b>{{ trackCount.twoWay }}</b>
|
||||||
|
(<b>{{ trackCount.twoWayElectric }} ⚡</b>)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
• {{ $t('station-stats.cross-sceneries') }} <b>{{ trackCount.crossTrack }}</b> (<b
|
||||||
|
>{{ trackCount.crossTrackElectric }} ⚡</b
|
||||||
|
>)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
•
|
||||||
|
{{ $t('station-stats.open-spawns') }} <b>{{ spawnCount.passenger }}</b> - PAS /
|
||||||
|
<b>{{ spawnCount.freight }}</b> - TOW / <b>{{ spawnCount.loco }}</b> - LUZ /
|
||||||
|
<b>{{ spawnCount.all }}</b> - ALL
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mainStore: useMainStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
calculateFactorStyle() {
|
||||||
|
if (this.uFactor == 0) return '';
|
||||||
|
|
||||||
|
const norm = this.uFactor == 0 ? 1 : Math.max(Math.min(this.uFactor / 2, 1), 0);
|
||||||
|
const lerp = 120 * norm;
|
||||||
|
|
||||||
|
return `color: hsl(${lerp}, 100%, 60%)`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
uFactor() {
|
||||||
|
const activeDispatchers = this.mainStore.activeSceneryList.filter(
|
||||||
|
(scenery) => scenery.region == this.mainStore.region.id && scenery.dispatcherId != -1
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeTrains = this.mainStore.trainList.filter(
|
||||||
|
(train) => train.region == this.mainStore.region.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return activeDispatchers.length != 0 ? activeTrains.length / activeDispatchers.length : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
avgTimetableCount() {
|
||||||
|
const regionSceneries = this.mainStore.activeSceneryList.filter((sc) => {
|
||||||
|
return sc.region == this.mainStore.region.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const timetableCountSum = regionSceneries.reduce((acc, sc) => {
|
||||||
|
acc += sc.scheduledTrainCount.all;
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (regionSceneries.length == 0) return 0;
|
||||||
|
|
||||||
|
return timetableCountSum / regionSceneries.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
trackCount() {
|
||||||
|
return this.mainStore.allStationInfo
|
||||||
|
.filter(
|
||||||
|
(st) =>
|
||||||
|
st.onlineInfo?.dispatcherId != -1 &&
|
||||||
|
st.onlineInfo?.region == this.mainStore.region.id &&
|
||||||
|
st.generalInfo?.routes
|
||||||
|
)
|
||||||
|
.reduce(
|
||||||
|
(acc, st) => {
|
||||||
|
const { routes } = st.generalInfo!;
|
||||||
|
|
||||||
|
if (
|
||||||
|
routes.single.filter((r) => !r.isInternal).length > 0 &&
|
||||||
|
routes.double.filter((r) => !r.isInternal).length > 0
|
||||||
|
) {
|
||||||
|
acc.crossTrack++;
|
||||||
|
|
||||||
|
if (
|
||||||
|
routes.single.some((r) => r.isElectric) &&
|
||||||
|
routes.double.some((r) => r.isElectric)
|
||||||
|
)
|
||||||
|
acc.crossTrackElectric++;
|
||||||
|
}
|
||||||
|
|
||||||
|
[...routes.single, ...routes.double].forEach((r) => {
|
||||||
|
if (r.isInternal) return;
|
||||||
|
|
||||||
|
acc[r.routeTracks == 2 ? 'twoWay' : 'oneWay'] += 1;
|
||||||
|
if (r.isElectric) acc[r.routeTracks == 2 ? 'twoWayElectric' : 'oneWayElectric'] += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
oneWay: 0,
|
||||||
|
oneWayElectric: 0,
|
||||||
|
twoWay: 0,
|
||||||
|
twoWayElectric: 0,
|
||||||
|
crossTrack: 0,
|
||||||
|
crossTrackElectric: 0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
spawnCount() {
|
||||||
|
return this.mainStore.activeSceneryList.reduce(
|
||||||
|
(acc, scenery) => {
|
||||||
|
if (scenery.region != this.mainStore.region.id) return acc;
|
||||||
|
|
||||||
|
scenery.spawns.forEach((spawn) => {
|
||||||
|
if (/EZT|POS|OSOB/i.test(spawn.spawnName)) acc['passenger'] += 1;
|
||||||
|
if (/TOW/i.test(spawn.spawnName)) acc['freight'] += 1;
|
||||||
|
if (/LUZ|SM/i.test(spawn.spawnName)) acc['loco'] += 1;
|
||||||
|
if (/ALL/i.test(spawn.spawnName)) acc['all'] += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ passenger: 0, freight: 0, loco: 0, all: 0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.separator {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
background-color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-stats {
|
||||||
|
text-align: center;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
text-wrap: pretty;
|
||||||
|
gap: 0.25em;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-factor {
|
||||||
|
[data-factor-low='true'] {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-factor-mediocre='true'] {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-factor-high='true'] {
|
||||||
|
color: greenyellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-factor-highest='true'] {
|
||||||
|
color: rgb(22, 245, 22);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="station_table">
|
<section class="station_table">
|
||||||
<div class="table_wrapper">
|
<Loading
|
||||||
<table>
|
v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<table v-else-if="filteredStationList.length > 0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
@@ -9,14 +12,15 @@
|
|||||||
:key="headerName"
|
:key="headerName"
|
||||||
@click="changeSorter(headerName)"
|
@click="changeSorter(headerName)"
|
||||||
class="header-text"
|
class="header-text"
|
||||||
|
:class="headerName"
|
||||||
>
|
>
|
||||||
<span class="header_wrapper">
|
<span class="header_wrapper">
|
||||||
<div v-html="$t(`sceneries.${headerName}`)"></div>
|
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
class="sort-icon"
|
class="sort-icon"
|
||||||
v-if="sorterActive.headerName == headerName"
|
v-if="activeSorter.headerName == headerName"
|
||||||
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
|
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
|
||||||
alt="sort icon"
|
alt="sort icon"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -27,18 +31,19 @@
|
|||||||
:key="headerName"
|
:key="headerName"
|
||||||
@click="changeSorter(headerName)"
|
@click="changeSorter(headerName)"
|
||||||
class="header-image"
|
class="header-image"
|
||||||
|
:class="headerName"
|
||||||
>
|
>
|
||||||
<span class="header_wrapper">
|
<span class="header_wrapper">
|
||||||
<img
|
<img
|
||||||
:src="`/images/icon-${headerName}.svg`"
|
:src="`/images/icon-${headerName}.svg`"
|
||||||
:alt="headerName"
|
:alt="headerName"
|
||||||
:title="$t(`sceneries.${headerName}`)"
|
:title="$t(`sceneries.headers.${headerName}`)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
class="sort-icon"
|
class="sort-icon"
|
||||||
v-if="sorterActive.headerName == headerName"
|
v-if="activeSorter.headerName == headerName"
|
||||||
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
|
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
|
||||||
alt="sort icon"
|
alt="sort icon"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -48,24 +53,23 @@
|
|||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
class="station"
|
v-for="station in filteredStationList"
|
||||||
:class="{ 'last-selected': lastSelectedStationName == station.name }"
|
:class="{ 'last-selected': lastSelectedStationName == station.name }"
|
||||||
v-for="(station, i) in stations"
|
:key="station.name"
|
||||||
:key="i + station.name"
|
|
||||||
@click.left="setScenery(station.name)"
|
@click.left="setScenery(station.name)"
|
||||||
@click.right="openForumSite($event, station.generalInfo?.url)"
|
@click.right="openForumSite($event, station.generalInfo?.url)"
|
||||||
@keydown.enter="setScenery(station.name)"
|
@keydown.enter="setScenery(station.name)"
|
||||||
@keydown.space="openForumSite($event, station.generalInfo?.url)"
|
@keydown.space="openForumSite($event, station.generalInfo?.url)"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<td class="station_name" :class="station.generalInfo?.availability">
|
<td class="station-name" :class="station.generalInfo?.availability">
|
||||||
<b v-if="station.generalInfo?.project" style="color: salmon">{{
|
<b v-if="station.generalInfo?.project" style="color: salmon">{{
|
||||||
station.generalInfo.project
|
station.generalInfo.project
|
||||||
}}</b>
|
}}</b>
|
||||||
{{ station.name }}
|
{{ station.name }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_level">
|
<td class="station-level">
|
||||||
<span v-if="station.generalInfo">
|
<span v-if="station.generalInfo">
|
||||||
<span
|
<span
|
||||||
v-if="
|
v-if="
|
||||||
@@ -82,7 +86,7 @@
|
|||||||
<img
|
<img
|
||||||
src="/images/icon-abandoned.svg"
|
src="/images/icon-abandoned.svg"
|
||||||
alt="non-public"
|
alt="non-public"
|
||||||
:title="$t('desc.abandoned')"
|
:title="$t('sceneries.info.abandoned')"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -90,7 +94,7 @@
|
|||||||
<img
|
<img
|
||||||
src="/images/icon-lock.svg"
|
src="/images/icon-lock.svg"
|
||||||
alt="non-public"
|
alt="non-public"
|
||||||
:title="$t('desc.non-public')"
|
:title="$t('sceneries.info.non-public')"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -98,7 +102,7 @@
|
|||||||
<img
|
<img
|
||||||
src="/images/icon-unavailable.svg"
|
src="/images/icon-unavailable.svg"
|
||||||
alt="unavailable"
|
alt="unavailable"
|
||||||
:title="$t('desc.unavailable')"
|
:title="$t('sceneries.info.unavailable')"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -106,21 +110,34 @@
|
|||||||
<span v-else> ? </span>
|
<span v-else> ? </span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_status">
|
<td class="station-status">
|
||||||
<StationStatusBadge
|
<StationStatusBadge
|
||||||
:statusID="station.onlineInfo?.statusID"
|
|
||||||
:isOnline="station.onlineInfo ? true : false"
|
:isOnline="station.onlineInfo ? true : false"
|
||||||
:statusTimestamp="station.onlineInfo?.statusTimestamp"
|
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_dispatcher-name">
|
<td class="station-dispatcher-name">
|
||||||
{{ station.onlineInfo ? station.onlineInfo.dispatcherName : '' }}
|
<span v-if="station.onlineInfo?.dispatcherName">
|
||||||
|
<b
|
||||||
|
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
|
||||||
|
@click.stop="openDonationCard"
|
||||||
|
data-tooltip-type="DonatorTooltip"
|
||||||
|
:data-tooltip-content="$t('donations.dispatcher-message')"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-diamond.svg" alt="" />
|
||||||
|
{{ station.onlineInfo.dispatcherName }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
{{ station.onlineInfo.dispatcherName }}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_dispatcher-exp">
|
<td class="station-dispatcher-exp">
|
||||||
<span
|
<span
|
||||||
v-if="station.onlineInfo"
|
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
|
||||||
:style="
|
:style="
|
||||||
calculateExpStyle(
|
calculateExpStyle(
|
||||||
station.onlineInfo.dispatcherExp,
|
station.onlineInfo.dispatcherExp,
|
||||||
@@ -128,208 +145,232 @@
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }}
|
{{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_tracks twoway">
|
<td class="station-tracks">
|
||||||
|
<div v-if="station.generalInfo">
|
||||||
<span
|
<span
|
||||||
v-if="
|
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
|
||||||
station.generalInfo &&
|
|
||||||
station.generalInfo.routes.twoWayCatenaryRouteNames.length > 0
|
|
||||||
"
|
|
||||||
class="track catenary"
|
class="track catenary"
|
||||||
:title="`Liczba zelektryfikowanych szlaków dwutorowych: ${station.generalInfo.routes.twoWayCatenaryRouteNames.length}`"
|
:title="`${$t('sceneries.info.single-track-routes-catenary')}${
|
||||||
|
station.generalInfo.routes.singleElectrifiedNames.length
|
||||||
|
}`"
|
||||||
>
|
>
|
||||||
{{ station.generalInfo.routes.twoWayCatenaryRouteNames.length }}
|
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="
|
v-if="station.generalInfo.routes.singleOtherNames.length != 0"
|
||||||
station.generalInfo &&
|
|
||||||
station.generalInfo.routes.twoWayNoCatenaryRouteNames.length > 0
|
|
||||||
"
|
|
||||||
class="track no-catenary"
|
class="track no-catenary"
|
||||||
:title="`Liczba niezelektryfikowanych szlaków dwutorowych: ${station.generalInfo.routes.twoWayNoCatenaryRouteNames.length}`"
|
:title="`${$t('sceneries.info.single-track-routes-other')}${
|
||||||
|
station.generalInfo.routes.singleOtherNames.length
|
||||||
|
}`"
|
||||||
>
|
>
|
||||||
{{ station.generalInfo.routes.twoWayNoCatenaryRouteNames.length }}
|
{{ station.generalInfo.routes.singleOtherNames.length }}
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="separator"></span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
station.generalInfo &&
|
|
||||||
station.generalInfo.routes.oneWayCatenaryRouteNames.length > 0
|
|
||||||
"
|
|
||||||
class="track catenary"
|
|
||||||
:title="`Liczba zelektryfikowanych szlaków jednotorowych: ${station.generalInfo.routes.oneWayCatenaryRouteNames.length}`"
|
|
||||||
>
|
|
||||||
{{ station.generalInfo.routes.oneWayCatenaryRouteNames.length }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
station.generalInfo &&
|
|
||||||
station.generalInfo.routes.oneWayNoCatenaryRouteNames.length > 0
|
|
||||||
"
|
|
||||||
class="track no-catenary"
|
|
||||||
:title="`Liczba niezelektryfikowanych szlaków jednotorowych: ${station.generalInfo.routes.oneWayNoCatenaryRouteNames.length}`"
|
|
||||||
>
|
|
||||||
{{ station.generalInfo.routes.oneWayNoCatenaryRouteNames.length }}
|
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_info" v-if="station.generalInfo">
|
<td class="station-tracks">
|
||||||
|
<div v-if="station.generalInfo">
|
||||||
<span
|
<span
|
||||||
|
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
|
||||||
|
class="track catenary"
|
||||||
|
:title="`${$t('sceneries.info.double-track-routes-catenary')}${
|
||||||
|
station.generalInfo.routes.doubleElectrifiedNames.length
|
||||||
|
}`"
|
||||||
|
>
|
||||||
|
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
|
||||||
|
class="track no-catenary"
|
||||||
|
:title="`${$t('sceneries.info.double-track-routes-other')}${
|
||||||
|
station.generalInfo.routes.doubleOtherNames.length
|
||||||
|
}`"
|
||||||
|
>
|
||||||
|
{{ station.generalInfo.routes.doubleOtherNames.length }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="station-info">
|
||||||
|
<span
|
||||||
|
v-if="station.generalInfo?.signalType"
|
||||||
class="scenery-icon icon-info"
|
class="scenery-icon icon-info"
|
||||||
:class="station.generalInfo.controlType.replace('+', '-')"
|
:class="station.generalInfo?.controlType.replace('+', '-')"
|
||||||
:title="$t('desc.control-type') + $t(`controls.${station.generalInfo.controlType}`)"
|
:title="
|
||||||
v-html="getControlTypeAbbrev(station.generalInfo.controlType)"
|
$t('sceneries.info.control-type') +
|
||||||
|
$t(`controls.${station.generalInfo?.controlType}`)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
|
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>
|
|
||||||
<img
|
<img
|
||||||
|
v-if="station.generalInfo?.signalType"
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
v-if="station.generalInfo.SUP"
|
|
||||||
src="/images/icon-SUP.svg"
|
|
||||||
alt="SUP (RASP-UZK)"
|
|
||||||
:title="$t('desc.SUP')"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<img
|
|
||||||
class="icon-info"
|
|
||||||
v-if="station.generalInfo.signalType"
|
|
||||||
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
|
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
|
||||||
:alt="station.generalInfo.signalType"
|
:alt="station.generalInfo.signalType"
|
||||||
:title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
|
:title="
|
||||||
|
$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<img
|
<img
|
||||||
|
v-if="station.generalInfo?.SUP"
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
v-if="station.generalInfo && station.generalInfo.routes.sblRouteNames.length > 0"
|
src="/images/icon-SUP.svg"
|
||||||
src="/images/icon-SBL.svg"
|
alt="SUP (RASP-UZK)"
|
||||||
alt="SBL"
|
:title="$t('sceneries.info.SUP')"
|
||||||
:title="$t('desc.SBL') + `${station.generalInfo.routes.sblRouteNames.join(',')}`"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="station_info" v-else>
|
|
||||||
<img
|
<img
|
||||||
|
v-if="station.generalInfo?.ASDEK"
|
||||||
|
class="icon-info"
|
||||||
|
src="/images/icon-ASDEK.svg"
|
||||||
|
alt="dSAT ASDEK"
|
||||||
|
:title="$t('sceneries.info.ASDEK')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="!station.generalInfo"
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
src="/images/icon-unknown.svg"
|
src="/images/icon-unknown.svg"
|
||||||
alt="icon-unknown"
|
alt="icon-unknown"
|
||||||
:title="$t('desc.unknown')"
|
:title="$t('sceneries.info.unknown')"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_users" :class="{ inactive: !station.onlineInfo }">
|
<td
|
||||||
<span>{{ station.onlineInfo?.currentUsers || 0 }}</span>
|
class="station-users"
|
||||||
|
:class="{ inactive: !station.onlineInfo }"
|
||||||
|
data-tooltip-type="UsersTooltip"
|
||||||
|
:data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
|
||||||
|
>
|
||||||
|
<span class="text--primary">{{
|
||||||
|
station.onlineInfo?.stationTrains?.length ?? '-'
|
||||||
|
}}</span>
|
||||||
/
|
/
|
||||||
<span>{{ station.onlineInfo?.maxUsers || 0 }}</span>
|
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_spawns" :class="{ inactive: !station.onlineInfo }">
|
<td class="station-likes" :class="{ inactive: !station.onlineInfo }">
|
||||||
<span>{{ station.onlineInfo?.spawns.length || 0 }}</span>
|
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td
|
<td
|
||||||
class="station_schedules all"
|
class="station-spawns"
|
||||||
style="width: 30px"
|
|
||||||
:class="{ inactive: !station.onlineInfo }"
|
:class="{ inactive: !station.onlineInfo }"
|
||||||
|
data-tooltip-type="SpawnsTooltip"
|
||||||
|
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])"
|
||||||
>
|
>
|
||||||
{{ station.onlineInfo?.scheduledTrainCount.all }}
|
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td
|
<td
|
||||||
class="station_schedules unconfirmed"
|
class="station-schedules all"
|
||||||
style="width: 30px"
|
style="width: 30px"
|
||||||
:class="{ inactive: !station.onlineInfo }"
|
:class="{ inactive: !station.onlineInfo }"
|
||||||
>
|
>
|
||||||
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed }}
|
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td
|
<td
|
||||||
class="station_schedules confirmed"
|
class="station-schedules unconfirmed"
|
||||||
style="width: 30px"
|
style="width: 30px"
|
||||||
:class="{ inactive: !station.onlineInfo }"
|
:class="{ inactive: !station.onlineInfo }"
|
||||||
>
|
>
|
||||||
{{ station.onlineInfo?.scheduledTrainCount.confirmed }}
|
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td
|
||||||
|
class="station-schedules confirmed"
|
||||||
|
style="width: 30px"
|
||||||
|
:class="{ inactive: !station.onlineInfo }"
|
||||||
|
>
|
||||||
|
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div class="no-stations" v-else>
|
||||||
|
<div>
|
||||||
|
{{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b
|
||||||
|
>)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Loading v-if="!isDataLoaded && stations.length == 0" />
|
<div class="text--primary" v-if="getChangedFilters(filters).length != 0">
|
||||||
|
⚠ {{ $t('sceneries.active-filters') }}
|
||||||
<div class="no-stations" v-else-if="stations.length == 0">
|
</div>
|
||||||
{{ $t('sceneries.no-stations') }}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed, PropType } from 'vue';
|
import { defineComponent, inject, computed } from 'vue';
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
|
||||||
import stationInfoMixin from '../../mixins/stationInfoMixin';
|
|
||||||
import styleMixin from '../../mixins/styleMixin';
|
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
|
||||||
import Station from '../../scripts/interfaces/Station';
|
|
||||||
import { useStationFiltersStore } from '../../store/stationFiltersStore';
|
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
import Loading from '../Global/Loading.vue';
|
|
||||||
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
|
|
||||||
import StationStatusBadge from '../Global/StationStatusBadge.vue';
|
import StationStatusBadge from '../Global/StationStatusBadge.vue';
|
||||||
|
import Loading from '../Global/Loading.vue';
|
||||||
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
|
import styleMixin from '../../mixins/styleMixin';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
import { Status } from '../../typings/common';
|
||||||
|
import { useTooltipStore } from '../../store/tooltipStore';
|
||||||
|
import { getChangedFilters } from '../../managers/stationFilterManager';
|
||||||
|
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
|
||||||
|
import { filterStations, sortStations } from './utils';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
emits: ['toggleDonationCard'],
|
||||||
stations: {
|
|
||||||
type: Array as PropType<Station[]>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: { Loading, StationStatusBadge },
|
components: { Loading, StationStatusBadge },
|
||||||
mixins: [styleMixin, dateMixin, stationInfoMixin],
|
mixins: [styleMixin, dateMixin],
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
headIconsIds,
|
headIconsIds,
|
||||||
headIds,
|
headIds,
|
||||||
lastSelectedStationName: ''
|
lastSelectedStationName: '',
|
||||||
|
getChangedFilters
|
||||||
}),
|
}),
|
||||||
|
|
||||||
computed: {
|
|
||||||
sorterActive() {
|
|
||||||
return this.stationFiltersStore.sorterActive;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore();
|
const mainStore = useMainStore();
|
||||||
const stationFiltersStore = useStationFiltersStore();
|
const apiStore = useApiStore();
|
||||||
|
const tooltipStore = useTooltipStore();
|
||||||
|
|
||||||
|
const filters = inject('StationsView_filters') as Record<string, any>;
|
||||||
|
const activeSorter = inject('StationsView_activeSorter') as ActiveSorter;
|
||||||
|
|
||||||
|
const filteredStationList = computed(() =>
|
||||||
|
mainStore.allStationInfo
|
||||||
|
.filter((station) => filterStations(station, filters))
|
||||||
|
.sort((a, b) => sortStations(a, b, activeSorter))
|
||||||
|
);
|
||||||
|
|
||||||
const isDataLoaded = computed(() => {
|
|
||||||
return store.dataStatuses.sceneries != DataStatus.Loading;
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
isDataLoaded,
|
Status: Status.Data,
|
||||||
stationFiltersStore
|
mainStore,
|
||||||
|
apiStore,
|
||||||
|
tooltipStore,
|
||||||
|
filters,
|
||||||
|
filteredStationList,
|
||||||
|
activeSorter
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
setScenery(name: string) {
|
setScenery(name: string) {
|
||||||
const station = this.stations.find((station) => station.name === name);
|
const station = this.filteredStationList.find((station) => station.name === name);
|
||||||
|
|
||||||
if (!station) return;
|
if (!station) return;
|
||||||
|
|
||||||
this.lastSelectedStationName = station.name;
|
this.lastSelectedStationName = station.name;
|
||||||
|
this.tooltipStore.hide();
|
||||||
|
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'SceneryView',
|
name: 'SceneryView',
|
||||||
@@ -340,16 +381,26 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openDonationCard(e: Event) {
|
||||||
|
this.$emit('toggleDonationCard', true);
|
||||||
|
this.mainStore.modalLastClickedTarget = e.target;
|
||||||
|
this.tooltipStore.hide();
|
||||||
|
},
|
||||||
|
|
||||||
openForumSite(e: Event, url: string | undefined) {
|
openForumSite(e: Event, url: string | undefined) {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
},
|
},
|
||||||
|
|
||||||
changeSorter(headerName: HeadIdsTypes) {
|
changeSorter(headerName: HeadIdsType) {
|
||||||
if (headerName == 'general' || headerName == 'routes') return;
|
if (headerName == 'general') return;
|
||||||
|
|
||||||
this.stationFiltersStore.changeSorter(headerName);
|
if (headerName == this.activeSorter.headerName)
|
||||||
|
this.activeSorter.dir = -1 * this.activeSorter.dir;
|
||||||
|
else this.activeSorter.dir = 1;
|
||||||
|
|
||||||
|
this.activeSorter.headerName = headerName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -362,37 +413,28 @@ export default defineComponent({
|
|||||||
|
|
||||||
$rowCol: #424242;
|
$rowCol: #424242;
|
||||||
|
|
||||||
.change-anim {
|
.station_table {
|
||||||
&-enter-active,
|
height: 80vh;
|
||||||
&-leave-active {
|
max-height: 2000px;
|
||||||
transition: opacity 100ms ease-in;
|
min-height: 700px;
|
||||||
}
|
|
||||||
|
|
||||||
&-enter,
|
|
||||||
&-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section.station_table {
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-y: hidden;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table_wrapper {
|
.no-stations {
|
||||||
overflow: auto;
|
text-align: center;
|
||||||
overflow-y: hidden;
|
font-size: 1.25em;
|
||||||
|
padding: 1em;
|
||||||
|
background: #1a1a1a;
|
||||||
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
white-space: nowrap;
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
min-width: 1350px;
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
@include smallScreen() {
|
min-width: 1250px;
|
||||||
min-width: auto;
|
white-space: wrap;
|
||||||
}
|
|
||||||
|
|
||||||
thead tr {
|
thead tr {
|
||||||
background-color: $bgCol;
|
background-color: $bgCol;
|
||||||
@@ -402,12 +444,41 @@ table {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
||||||
&.header-text {
|
&.station {
|
||||||
min-width: 140px;
|
width: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.min-lvl {
|
||||||
|
width: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dispatcher {
|
||||||
|
width: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dispatcher-lvl {
|
||||||
|
width: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.routes-double,
|
||||||
|
&.routes-single {
|
||||||
|
width: 7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.general {
|
||||||
|
width: 11em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.header-image {
|
&.header-image {
|
||||||
min-width: 60px;
|
width: 3.5em;
|
||||||
|
|
||||||
|
&.user {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
padding: 0.5em 0.25em;
|
padding: 0.5em 0.25em;
|
||||||
@@ -432,7 +503,7 @@ table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.station {
|
tr {
|
||||||
background-color: $rowCol;
|
background-color: $rowCol;
|
||||||
|
|
||||||
&:nth-child(even) {
|
&:nth-child(even) {
|
||||||
@@ -446,10 +517,15 @@ tr.station {
|
|||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 0.25em 1em;
|
padding: 0.15em 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&.inactive {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
@include smallScreen() {
|
@include smallScreen() {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -459,9 +535,9 @@ tr.station {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
td.station {
|
.station-name {
|
||||||
&_name {
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
max-width: 200px;
|
||||||
|
|
||||||
&.default {
|
&.default {
|
||||||
color: $accentCol;
|
color: $accentCol;
|
||||||
@@ -477,8 +553,8 @@ td.station {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_level,
|
.station-level,
|
||||||
&_dispatcher-exp {
|
.station-dispatcher-exp {
|
||||||
span {
|
span {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@@ -494,42 +570,36 @@ td.station {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_level {
|
.station-dispatcher-name {
|
||||||
|
img {
|
||||||
|
max-width: 1.35em;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-level {
|
||||||
span {
|
span {
|
||||||
background-color: #888;
|
background-color: #888;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_info {
|
.station-info {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
/* Images */
|
|
||||||
.icon-info {
|
.icon-info {
|
||||||
display: flex;
|
vertical-align: middle;
|
||||||
justify-content: center;
|
line-height: 2.5em;
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
width: 32px;
|
width: 2.5em;
|
||||||
height: 32px;
|
height: 2.5em;
|
||||||
font-size: 12px;
|
font-size: 0.8em;
|
||||||
|
margin: 0 3px;
|
||||||
|
|
||||||
margin: 0 0.2em;
|
outline: 2px solid #2b2b2b;
|
||||||
|
border-radius: 5px;
|
||||||
outline: 2px solid #444;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
|
|
||||||
@include smallScreen() {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_tracks {
|
.station-tracks {
|
||||||
.no-catenary {
|
.no-catenary {
|
||||||
background-color: #939393;
|
background-color: #939393;
|
||||||
}
|
}
|
||||||
@@ -538,30 +608,22 @@ td.station {
|
|||||||
background-color: #009dce;
|
background-color: #009dce;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
background-color: #b3b3b3;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.track {
|
.track {
|
||||||
margin: 0 0.35em;
|
display: inline-block;
|
||||||
padding: 0.35em;
|
text-align: center;
|
||||||
font-size: 1.05em;
|
width: 1.3em;
|
||||||
white-space: pre-wrap;
|
padding: 0.35em 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin: 0 0.2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_users,
|
.station-schedules {
|
||||||
&_spawns,
|
|
||||||
&_schedules {
|
|
||||||
&.inactive {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.station_users {
|
|
||||||
span {
|
|
||||||
color: gold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.station_schedules {
|
|
||||||
&.all {
|
&.all {
|
||||||
color: gold;
|
color: gold;
|
||||||
}
|
}
|
||||||
@@ -574,18 +636,4 @@ td.station {
|
|||||||
color: lime;
|
color: lime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
|
||||||
border-left: 3px solid #b3b3b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-stations {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5em;
|
|
||||||
|
|
||||||
padding: 1em;
|
|
||||||
margin: 1em 0;
|
|
||||||
|
|
||||||
background: #333;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export interface FilterOption {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: boolean;
|
||||||
|
defaultValue: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const headIds = [
|
||||||
|
'station',
|
||||||
|
'min-lvl',
|
||||||
|
'status',
|
||||||
|
'dispatcher',
|
||||||
|
'dispatcher-lvl',
|
||||||
|
'routes-single',
|
||||||
|
'routes-double',
|
||||||
|
'general'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const headIconsIds = [
|
||||||
|
'user',
|
||||||
|
'like',
|
||||||
|
'spawn',
|
||||||
|
'timetableAll',
|
||||||
|
'timetableUnconfirmed',
|
||||||
|
'timetableConfirmed'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type HeadIdsType = (typeof headIds)[number] | (typeof headIconsIds)[number];
|
||||||
|
|
||||||
|
export interface ActiveSorter {
|
||||||
|
headerName: HeadIdsType;
|
||||||
|
dir: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
import { ActiveSorter } from '../../components/StationsView/typings';
|
||||||
|
import { ActiveScenery, StationGeneralInfo, Status } from '../../typings/common';
|
||||||
|
import { Station } from '../../typings/common';
|
||||||
|
|
||||||
|
const dispatcherStatusPriority = [
|
||||||
|
Status.ActiveDispatcher.UNKNOWN,
|
||||||
|
Status.ActiveDispatcher.INVALID,
|
||||||
|
Status.ActiveDispatcher.NOT_LOGGED_IN,
|
||||||
|
Status.ActiveDispatcher.UNAVAILABLE,
|
||||||
|
Status.ActiveDispatcher.AFK,
|
||||||
|
Status.ActiveDispatcher.ENDING,
|
||||||
|
Status.ActiveDispatcher.NO_SPACE,
|
||||||
|
undefined
|
||||||
|
];
|
||||||
|
|
||||||
|
const filtersAssociations: Record<string, string> = {
|
||||||
|
mechaniczne: 'mechanical',
|
||||||
|
ręczne: 'manual',
|
||||||
|
'mechaniczne+SPK': 'SPK-M',
|
||||||
|
'ręczne+SPK': 'SPK-R',
|
||||||
|
'mechaniczne+SCS': 'SCS-M',
|
||||||
|
'ręczne+SCS': 'SCS-R',
|
||||||
|
współczesna: 'modern',
|
||||||
|
historyczna: 'historical',
|
||||||
|
kształtowa: 'semaphores',
|
||||||
|
mieszana: 'mixed'
|
||||||
|
};
|
||||||
|
|
||||||
|
function filterStatusSection(
|
||||||
|
filters: Record<string, any>,
|
||||||
|
{ dispatcherStatus, dispatcherTimestamp }: ActiveScenery
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
(filters['endingStatus'] && dispatcherStatus == Status.ActiveDispatcher.ENDING) ||
|
||||||
|
(filters['unavailableStatus'] &&
|
||||||
|
(dispatcherStatus == Status.ActiveDispatcher.UNAVAILABLE ||
|
||||||
|
dispatcherStatus == Status.ActiveDispatcher.NOT_LOGGED_IN)) ||
|
||||||
|
(filters['afkStatus'] && dispatcherStatus == Status.ActiveDispatcher.AFK) ||
|
||||||
|
(filters['noSpaceStatus'] && dispatcherStatus == Status.ActiveDispatcher.NO_SPACE) ||
|
||||||
|
(filters['occupied'] && dispatcherStatus != Status.ActiveDispatcher.FREE) ||
|
||||||
|
(filters['onlineFromHours'] > 0 &&
|
||||||
|
(dispatcherTimestamp ?? 0) <= Date.now() + filters['onlineFromHours'] * 3600000)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterTimetablesSection(filters: Record<string, any>, station: Station) {
|
||||||
|
return (
|
||||||
|
(filters['withoutActiveTimetables'] &&
|
||||||
|
(!station.onlineInfo || station.onlineInfo.scheduledTrainCount.all == 0)) ||
|
||||||
|
(filters['withActiveTimetables'] &&
|
||||||
|
station.onlineInfo &&
|
||||||
|
(station.onlineInfo.scheduledTrainCount.all != 0 ||
|
||||||
|
station.onlineInfo.dispatcherStatus == Status.ActiveDispatcher.FREE))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterAccessibilitySection(filters: Record<string, any>, station: Station) {
|
||||||
|
if (
|
||||||
|
filters['nonPublic'] &&
|
||||||
|
(!station.generalInfo || station.generalInfo.availability == 'nonPublic')
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!station.generalInfo) return false;
|
||||||
|
|
||||||
|
const { availability } = station.generalInfo;
|
||||||
|
|
||||||
|
return (
|
||||||
|
(filters['unavailable'] && availability == 'unavailable' && !station.onlineInfo) ||
|
||||||
|
(filters['abandoned'] && availability == 'abandoned' && !station.onlineInfo) ||
|
||||||
|
(filters['default'] && availability == 'default') ||
|
||||||
|
(filters['notDefault'] &&
|
||||||
|
availability != 'default' &&
|
||||||
|
availability != 'abandoned' &&
|
||||||
|
availability != 'unavailable')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterRealitySection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
|
||||||
|
return (filters['real'] && generalInfo.lines) || (filters['fictional'] && !generalInfo.lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterProgramsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
|
||||||
|
return (
|
||||||
|
(filters['SUP'] && generalInfo.SUP) ||
|
||||||
|
(filters['noSUP'] && !generalInfo.SUP) ||
|
||||||
|
(filters['ASDEK'] && generalInfo.ASDEK) ||
|
||||||
|
(filters['noASDEK'] && !generalInfo.ASDEK)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterControlsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
|
||||||
|
return (
|
||||||
|
filters[generalInfo.controlType] == true ||
|
||||||
|
filters[filtersAssociations[generalInfo.controlType]] == true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterSignalsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
|
||||||
|
return (
|
||||||
|
filters[generalInfo.signalType] == true ||
|
||||||
|
filters[filtersAssociations[generalInfo.signalType]] == true ||
|
||||||
|
(filters['SBL'] && generalInfo.routes.sblNames.length > 0) ||
|
||||||
|
(filters['PBL'] && generalInfo.routes.sblNames.length == 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterStationType(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
|
||||||
|
const singleTracks = generalInfo.routes.single.filter((r) => !r.isInternal);
|
||||||
|
const doubleTracks = generalInfo.routes.double.filter((r) => !r.isInternal);
|
||||||
|
|
||||||
|
let isJunction = singleTracks.length > 0 && doubleTracks.length > 0;
|
||||||
|
|
||||||
|
return (filters['junction'] && isJunction) || (filters['nonJunction'] && !isJunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterSliderValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
|
||||||
|
const { availability, reqLevel, routes } = generalInfo;
|
||||||
|
|
||||||
|
const otherAvailability =
|
||||||
|
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
|
||||||
|
|
||||||
|
return (
|
||||||
|
filters['minLevel'] > reqLevel + (otherAvailability ? 1 : 0) ||
|
||||||
|
filters['maxLevel'] < reqLevel + (otherAvailability ? 1 : 0) ||
|
||||||
|
filters['minVmax'] > routes.maxRouteSpeed ||
|
||||||
|
filters['maxVmax'] < routes.minRouteSpeed ||
|
||||||
|
(filters['no-1track'] && routes.single.length != 0) ||
|
||||||
|
(filters['no-2track'] && routes.double.length != 0) ||
|
||||||
|
filters['minOneWayCatenary'] > routes.singleElectrifiedNames.length ||
|
||||||
|
filters['minOneWay'] > routes.singleOtherNames.length ||
|
||||||
|
filters['minTwoWayCatenary'] > routes.doubleElectrifiedNames.length ||
|
||||||
|
filters['minTwoWay'] > routes.doubleOtherNames.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterInputValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
|
||||||
|
return (
|
||||||
|
filters['authors'].length > 3 &&
|
||||||
|
!generalInfo.authors
|
||||||
|
?.map((a) => a.toLocaleLowerCase())
|
||||||
|
.includes(filters['authors'].toLocaleLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sortStations = (a: Station, b: Station, sorter: ActiveSorter) => {
|
||||||
|
let diff = 0;
|
||||||
|
|
||||||
|
switch (sorter.headerName) {
|
||||||
|
case 'station':
|
||||||
|
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
||||||
|
|
||||||
|
case 'min-lvl':
|
||||||
|
diff = (a.generalInfo?.reqLevel || 0) - (b.generalInfo?.reqLevel || 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
diff =
|
||||||
|
(a.onlineInfo?.dispatcherTimestamp ??
|
||||||
|
dispatcherStatusPriority.indexOf(a.onlineInfo?.dispatcherStatus)) -
|
||||||
|
(b.onlineInfo?.dispatcherTimestamp ??
|
||||||
|
dispatcherStatusPriority.indexOf(b.onlineInfo?.dispatcherStatus));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dispatcher':
|
||||||
|
if (
|
||||||
|
(a.onlineInfo?.dispatcherName.toLowerCase() || '') >
|
||||||
|
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
|
||||||
|
)
|
||||||
|
return sorter.dir;
|
||||||
|
if (
|
||||||
|
(a.onlineInfo?.dispatcherName.toLowerCase() || '') <
|
||||||
|
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
|
||||||
|
)
|
||||||
|
return -sorter.dir;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dispatcher-lvl':
|
||||||
|
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'routes-single':
|
||||||
|
diff =
|
||||||
|
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
|
||||||
|
(b.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'routes-double':
|
||||||
|
diff =
|
||||||
|
(a.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
|
||||||
|
(b.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'user':
|
||||||
|
diff =
|
||||||
|
(b.onlineInfo?.stationTrains ? b.onlineInfo.stationTrains.length : -1) -
|
||||||
|
(a.onlineInfo?.stationTrains ? a.onlineInfo.stationTrains.length : -1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'like':
|
||||||
|
diff =
|
||||||
|
(a.onlineInfo ? a.onlineInfo.dispatcherRate : -Infinity) -
|
||||||
|
(b.onlineInfo ? b.onlineInfo.dispatcherRate : -Infinity);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'spawn':
|
||||||
|
diff =
|
||||||
|
(a.onlineInfo ? a.onlineInfo.spawns.length : -1) -
|
||||||
|
(b.onlineInfo ? b.onlineInfo.spawns.length : -1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'timetableConfirmed':
|
||||||
|
diff =
|
||||||
|
(a.onlineInfo?.scheduledTrainCount.confirmed ?? -1) -
|
||||||
|
(b.onlineInfo?.scheduledTrainCount.confirmed ?? -1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'timetableUnconfirmed':
|
||||||
|
diff =
|
||||||
|
(a.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1) -
|
||||||
|
(b.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'timetableAll':
|
||||||
|
diff =
|
||||||
|
(a.onlineInfo?.scheduledTrainCount.all ?? -1) -
|
||||||
|
(b.onlineInfo?.scheduledTrainCount.all ?? -1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff != 0) return Math.sign(diff) * sorter.dir;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterStations = (station: Station, filters: Record<string, any>) => {
|
||||||
|
if (filters['free'] && (!station.onlineInfo || station.onlineInfo.dispatcherId == -1))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Scenery Timetables section
|
||||||
|
if (filterTimetablesSection(filters, station)) return false;
|
||||||
|
|
||||||
|
// Scenery Accessibility section
|
||||||
|
if (filterAccessibilitySection(filters, station)) return false;
|
||||||
|
|
||||||
|
// Scenery Status section
|
||||||
|
if (station.onlineInfo && filterStatusSection(filters, station.onlineInfo)) return false;
|
||||||
|
|
||||||
|
if (station.generalInfo) {
|
||||||
|
// Scenery Reality section
|
||||||
|
if (filterRealitySection(filters, station.generalInfo)) return false;
|
||||||
|
|
||||||
|
// Scenery Additional Programs section
|
||||||
|
if (filterProgramsSection(filters, station.generalInfo)) return false;
|
||||||
|
|
||||||
|
// Scenery Controls section
|
||||||
|
if (filterControlsSection(filters, station.generalInfo)) return false;
|
||||||
|
|
||||||
|
// Scenery Signalling section(s)
|
||||||
|
if (filterSignalsSection(filters, station.generalInfo)) return false;
|
||||||
|
|
||||||
|
// Scenery Station Type section
|
||||||
|
if (filterStationType(filters, station.generalInfo)) return false;
|
||||||
|
|
||||||
|
// Scenery sliders
|
||||||
|
if (filterSliderValues(filters, station.generalInfo)) return false;
|
||||||
|
|
||||||
|
// Scenery Authors section
|
||||||
|
if (filterInputValues(filters, station.generalInfo)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tooltip-content">
|
||||||
|
<span>{{ tooltipStore.content }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useTooltipStore } from '../../store/tooltipStore';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tooltipStore: useTooltipStore()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tooltip-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
background-color: #333;
|
||||||
|
box-shadow: 0 0 5px 2px #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tooltip-content">
|
||||||
|
<img src="/images/icon-diamond.svg" alt="" />
|
||||||
|
<span>{{ tooltipStore.content }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useTooltipStore } from '../../store/tooltipStore';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tooltipStore: useTooltipStore()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tooltip-content {
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
padding: 0.5em;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
background-color: #333;
|
||||||
|
box-shadow: 0 0 10px 2px #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 1em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tooltip-content" v-if="spawns.length != 0">
|
||||||
|
<span v-for="(spawn, i) in spawns">
|
||||||
|
<template v-if="i > 0"> | </template>
|
||||||
|
<b>{{ spawn.spawnName }}</b> ({{ spawn.spawnLength }}m)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useTooltipStore } from '../../store/tooltipStore';
|
||||||
|
import { ScenerySpawn } from '../../typings/common';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tooltipStore: useTooltipStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
spawns() {
|
||||||
|
if (this.tooltipStore.content == '') return [];
|
||||||
|
|
||||||
|
const parsedSpawns = JSON.parse(this.tooltipStore.content) as ScenerySpawn[];
|
||||||
|
return parsedSpawns ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tooltip-content {
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
background-color: #1b1b1b;
|
||||||
|
box-shadow: 0 0 5px 2px #aaa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tooltip" ref="preview">
|
||||||
|
<component v-if="tooltipStore.type" :is="tooltipStore.type" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useTooltipStore } from '../../store/tooltipStore';
|
||||||
|
import DonatorTooltip from './DonatorTooltip.vue';
|
||||||
|
import VehiclePreviewTooltip from './VehiclePreviewTooltip.vue';
|
||||||
|
import BaseTooltip from './BaseTooltip.vue';
|
||||||
|
import SpawnsTooltip from './SpawnsTooltip.vue';
|
||||||
|
import UsersTooltip from './UsersTooltip.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { DonatorTooltip, VehiclePreviewTooltip, BaseTooltip, SpawnsTooltip, UsersTooltip },
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tooltipStore: useTooltipStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'tooltipStore.mousePos': {
|
||||||
|
deep: true,
|
||||||
|
// [x, y]
|
||||||
|
handler(val: [number, number]) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const previewEl = this.$refs['preview'] as HTMLElement;
|
||||||
|
const clientWidth = document.body.clientWidth;
|
||||||
|
const boxWidth = previewEl.getBoundingClientRect().width;
|
||||||
|
|
||||||
|
let translateX = '0',
|
||||||
|
translateY = '30px';
|
||||||
|
|
||||||
|
if (val[0] <= boxWidth / 2) {
|
||||||
|
previewEl.style.left = '0';
|
||||||
|
translateX = '0px';
|
||||||
|
} else if (val[0] >= clientWidth - boxWidth / 2) {
|
||||||
|
previewEl.style.left = '100%';
|
||||||
|
translateX = '-100%';
|
||||||
|
} else {
|
||||||
|
previewEl.style.left = `${val[0]}px`;
|
||||||
|
translateX = '-50%';
|
||||||
|
}
|
||||||
|
|
||||||
|
previewEl.style.top = `${val[1]}px`;
|
||||||
|
|
||||||
|
const isOutside =
|
||||||
|
val[1] + previewEl.getBoundingClientRect().height + 30 >=
|
||||||
|
window.innerHeight + window.scrollY;
|
||||||
|
|
||||||
|
if (isOutside) translateY = 'calc(-100% - 30px)';
|
||||||
|
previewEl.style.transform = `translate(${translateX}, ${translateY})`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 250;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tooltip-content" v-if="trains.length != 0">
|
||||||
|
<span v-for="(train, i) in trains">
|
||||||
|
<template v-if="i > 0"> | </template>
|
||||||
|
<b>{{ train.trainNo }}</b> {{ train.driverName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useTooltipStore } from '../../store/tooltipStore';
|
||||||
|
import { Train } from '../../typings/common';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tooltipStore: useTooltipStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
trains() {
|
||||||
|
if (this.tooltipStore.content == '') return [];
|
||||||
|
|
||||||
|
const parsedTrains = JSON.parse(this.tooltipStore.content) as Train[];
|
||||||
|
return (parsedTrains ?? []).sort((a, b) => a.trainNo - b.trainNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tooltip-content {
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
background-color: #1b1b1b;
|
||||||
|
box-shadow: 0 0 5px 2px #aaa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tooltip-content">
|
||||||
|
<div v-if="imageState == 'loading'" class="loading-info">
|
||||||
|
{{ $t('vehicle-preview.loading') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="imageState == 'error'">{{ $t('vehicle-preview.error') }}</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="tooltipStore.type"
|
||||||
|
@load="onImageLoad"
|
||||||
|
@error="onImageError"
|
||||||
|
width="300"
|
||||||
|
height="176"
|
||||||
|
class="rounded-md w-full h-auto"
|
||||||
|
:src="`https://static.spythere.eu/images/${vehicleName}--300px.jpg`"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="imageState == 'error'" class="error-placeholder"></div>
|
||||||
|
|
||||||
|
<div class="vehicle-name">
|
||||||
|
{{ vehicleName.replace(/_/g, ' ') }}
|
||||||
|
<span v-if="vehicleCargo">({{ vehicleCargo.id }})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vehicle-props" v-if="vehicleData">
|
||||||
|
{{ vehicleData.group.speed }}km/h • {{ vehicleData.group.length }}m •
|
||||||
|
{{ (vehicleData.group.weight / 1000).toFixed(1) }}t
|
||||||
|
<span v-if="vehicleCargo">(+{{ (vehicleCargo.weight / 1000).toFixed(1) }}t)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useTooltipStore } from '../../store/tooltipStore';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tooltipStore: useTooltipStore(),
|
||||||
|
apiStore: useApiStore(),
|
||||||
|
imageState: 'loading'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.imageState = 'loading';
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'tooltipStore.type'(prev, val) {
|
||||||
|
if (prev != val) this.imageState = 'loading';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onImageLoad() {
|
||||||
|
this.imageState = 'loaded';
|
||||||
|
},
|
||||||
|
|
||||||
|
onImageError(e: Event) {
|
||||||
|
this.imageState = 'error';
|
||||||
|
|
||||||
|
(e.target as HTMLElement).style.display = 'none';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
vehicleName() {
|
||||||
|
return this.tooltipStore.content.split(':')[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
vehicleData() {
|
||||||
|
return this.apiStore.vehiclesData?.find((v) => v.name == this.vehicleName);
|
||||||
|
},
|
||||||
|
|
||||||
|
vehicleCargo() {
|
||||||
|
return this.vehicleData?.group.cargoTypes?.find(
|
||||||
|
(c) => c.id == this.tooltipStore.content.split(':')[1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// vehicleProps() {
|
||||||
|
// const vehicleDataArray = this.apiStore.vehiclesData?.vehicleList.find(
|
||||||
|
// ([name]) => name === this.vehicleName
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (!vehicleDataArray) return null;
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// this.apiStore.vehiclesData!.vehicleProps.find((v) => v.type == vehicleDataArray[1]) ?? null
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tooltip-content {
|
||||||
|
width: 300px;
|
||||||
|
min-height: 200px;
|
||||||
|
background-color: #333;
|
||||||
|
box-shadow: 0 0 10px 2px #aaa;
|
||||||
|
|
||||||
|
padding: 0.5em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-info {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-name {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
text-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-props {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-placeholder {
|
||||||
|
height: 176px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="stop-label"
|
||||||
|
:data-minor="stop.isSBL || (stop.nameRaw.endsWith(', po.') && !stop.duration)"
|
||||||
|
>
|
||||||
|
<span class="name" v-html="stop.nameHtml"></span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="stop.position != 'begin'"
|
||||||
|
class="date arrival"
|
||||||
|
:data-status="
|
||||||
|
stop.arrivalDelay > 0 && stop.status != 'unconfirmed'
|
||||||
|
? 'delayed'
|
||||||
|
: stop.arrivalDelay < 0 && stop.status != 'unconfirmed'
|
||||||
|
? 'preponed'
|
||||||
|
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
|
||||||
|
? 'on-time'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
p.
|
||||||
|
<span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'">
|
||||||
|
<s>{{ timestampToString(stop.arrivalScheduled) }}</s>
|
||||||
|
{{ timestampToString(stop.arrivalReal) }}
|
||||||
|
({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }})
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else>
|
||||||
|
{{ timestampToString(stop.arrivalScheduled) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
stop.duration ||
|
||||||
|
(stop.status == 'stopped' && stop.position != 'begin' && stop.departureDelay > 0)
|
||||||
|
"
|
||||||
|
class="date stop"
|
||||||
|
:data-stop-types="stop.type.replace(', ', '-')"
|
||||||
|
:data-stop-status="stop.departureDelay > 0 && !stop.duration ? 'delayed' : ''"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
stop.duration == 0 && stop.departureDelay > 0
|
||||||
|
? stop.departureDelay - stop.arrivalDelay
|
||||||
|
: stop.duration
|
||||||
|
}}
|
||||||
|
{{ stop.type == '' ? 'pt' : stop.type }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
stop.position != 'end' &&
|
||||||
|
(stop.duration != 0 || stop.status == 'stopped' || stop.departureDelay != stop.arrivalDelay)
|
||||||
|
"
|
||||||
|
class="date departure"
|
||||||
|
:data-status="
|
||||||
|
stop.departureDelay > 0 && stop.status == 'confirmed'
|
||||||
|
? 'delayed'
|
||||||
|
: stop.departureDelay < 0 && stop.status == 'confirmed'
|
||||||
|
? 'preponed'
|
||||||
|
: stop.departureDelay == 0 && stop.status == 'confirmed'
|
||||||
|
? 'on-time'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
o.
|
||||||
|
<span
|
||||||
|
v-if="stop.departureDelay != 0 && (stop.status == 'confirmed' || stop.status == 'stopped')"
|
||||||
|
>
|
||||||
|
<s>{{ timestampToString(stop.departureScheduled) }}</s>
|
||||||
|
{{ timestampToString(stop.departureReal) }}
|
||||||
|
|
||||||
|
({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }})
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else>
|
||||||
|
{{ timestampToString(stop.departureScheduled) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { PropType, defineComponent } from 'vue';
|
||||||
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
|
import { TrainScheduleStop } from './TrainSchedule.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
mixins: [dateMixin],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
stop: {
|
||||||
|
type: Object as PropType<TrainScheduleStop>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$preponedClr: lime;
|
||||||
|
$delayedClr: salmon;
|
||||||
|
$dateClr: #525151;
|
||||||
|
$stopExchangeClr: #db8e29;
|
||||||
|
$stopDefaultClr: #252525;
|
||||||
|
$stopNameClr: #303030;
|
||||||
|
|
||||||
|
.stop-label {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&[data-minor='true'] {
|
||||||
|
.date {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
background: none;
|
||||||
|
color: #aaa;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
background: $stopNameClr;
|
||||||
|
border-radius: 0.5em 0 0 0.5em;
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.misc {
|
||||||
|
background: gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
background: $dateClr;
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 0.5em 0.5em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop {
|
||||||
|
&[data-stop-types='ph'],
|
||||||
|
&[data-stop-types='ph-pm'],
|
||||||
|
&[data-stop-types='pm'] {
|
||||||
|
background: $stopExchangeClr;
|
||||||
|
}
|
||||||
|
|
||||||
|
background: $stopDefaultClr;
|
||||||
|
|
||||||
|
&[data-stop-status='delayed'] {
|
||||||
|
color: $delayedClr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrival,
|
||||||
|
.departure {
|
||||||
|
&[data-status='delayed'] {
|
||||||
|
s {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $delayedClr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-status='preponed'] {
|
||||||
|
s {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $preponedClr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,22 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="train-info">
|
<div class="train-info" :data-extended="extended">
|
||||||
<section class="train-route">
|
<section class="train-general">
|
||||||
<div class="train_general">
|
<div class="general-top-bar">
|
||||||
|
<div>
|
||||||
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
|
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
|
||||||
<span class="timetable-id" v-if="train.timetableData"
|
<span class="timetable-id" v-if="train.timetableData">
|
||||||
>#{{ train.timetableData.timetableId }}</span
|
#{{ train.timetableData.timetableId }}
|
||||||
>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="timetable_warnings"
|
class="timetable-warnings"
|
||||||
v-if="train.timetableData?.TWR || train.timetableData?.SKR"
|
v-if="train.timetableData?.TWR || train.timetableData?.SKR"
|
||||||
>
|
>
|
||||||
<span class="train-badge twr" v-if="train.timetableData?.TWR" :title="$t('general.TWR')"
|
<span
|
||||||
>TWR</span
|
class="train-badge twr"
|
||||||
|
v-if="train.timetableData?.TWR"
|
||||||
|
:title="$t('general.TWR')"
|
||||||
>
|
>
|
||||||
<span class="train-badge skr" v-if="train.timetableData?.SKR" :title="$t('general.SKR')"
|
TWR
|
||||||
>SKR</span
|
</span>
|
||||||
|
<span
|
||||||
|
class="train-badge skr"
|
||||||
|
v-if="train.timetableData?.SKR"
|
||||||
|
:title="$t('general.SKR')"
|
||||||
>
|
>
|
||||||
|
SKR
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<strong>
|
<strong>
|
||||||
@@ -32,74 +41,132 @@
|
|||||||
>
|
>
|
||||||
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
|
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
|
||||||
</b>
|
</b>
|
||||||
<span>{{ train.driverName }}</span>
|
|
||||||
|
<div class="train-driver">
|
||||||
|
<b
|
||||||
|
v-if="apiStore.donatorsData.includes(train.driverName)"
|
||||||
|
data-tooltip-type="DonatorTooltip"
|
||||||
|
:data-tooltip-content="$t('donations.driver-message')"
|
||||||
|
>
|
||||||
|
{{ train.driverName }}
|
||||||
|
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<span v-else>{{ train.driverName }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timetable_route" v-if="train.timetableData">
|
<div v-if="extended">
|
||||||
|
<button class="btn-timetable btn--image btn--action" @click="navigateToJournal">
|
||||||
|
<img src="/images/icon-train.svg" alt="train icon" />
|
||||||
|
<span>
|
||||||
|
{{ $t('trains.journal-button') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn-exit btn--image btn--action" @click="closeModal">
|
||||||
|
<img src="/images/icon-exit.svg" alt="modal exit icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="general-timetable" v-if="train.timetableData">
|
||||||
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
|
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
|
||||||
<img
|
<span
|
||||||
v-if="getSceneriesWithComments(train.timetableData).length > 0"
|
v-if="getSceneriesWithComments(train.timetableData).length > 0"
|
||||||
class="image-warning"
|
data-tooltip-type="BaseTooltip"
|
||||||
src="/images/icon-warning.svg"
|
:data-tooltip-content="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
|
||||||
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
|
|
||||||
train.timetableData
|
train.timetableData
|
||||||
)})`"
|
)})`"
|
||||||
/>
|
>
|
||||||
|
<img class="image-warning" src="/images/icon-warning.svg" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr style="margin: 0.25em 0" />
|
<hr style="margin: 0.25em 0" />
|
||||||
|
|
||||||
<div class="timetable_stops" v-if="train.timetableData">
|
<div class="general-stops" v-if="train.timetableData">
|
||||||
<span v-if="train.timetableData.followingStops.length > 2">
|
<span v-if="train.timetableData.followingStops.length > 2">
|
||||||
{{ $t('trains.via-title') }}
|
{{ $t('trains.via-title') }}
|
||||||
<span v-html="displayStopList(train.timetableData.followingStops)"></span>
|
<span v-html="displayStopList(train.timetableData.followingStops)"></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
|
<div class="general-status">
|
||||||
|
<div class="status-timetable-progress" v-if="train.timetableData">
|
||||||
<ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
|
<ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
|
||||||
|
|
||||||
<span class="timetable_progress-distance">
|
<span class="progress-distance">
|
||||||
{{ currentDistance(train.timetableData.followingStops) }} km /
|
{{ currentDistance(train.timetableData.followingStops) }} km /
|
||||||
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span>
|
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span>
|
||||||
|
|
|
|
||||||
<span v-html="currentDelay(train.timetableData.followingStops)"></span>
|
<span v-html="currentDelay(train.timetableData.followingStops)"></span>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="train-status-badges">
|
<div class="status-badges">
|
||||||
<div v-if="!train.currentStationHash" class="train-badge offline">
|
<div v-if="!train.currentStationHash" class="train-badge offline">
|
||||||
|
<img src="/images/icon-offline.svg" alt="" />
|
||||||
{{ $t('trains.scenery-offline') }}
|
{{ $t('trains.scenery-offline') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!train.online" class="train-badge offline">
|
<div v-if="!train.online" class="train-badge offline">
|
||||||
|
<img src="/images/icon-offline.svg" alt="" />
|
||||||
Offline {{ lastSeenMessage(train.lastSeen) }}
|
Offline {{ lastSeenMessage(train.lastSeen) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="driver_position text--grayed" style="margin-top: 0.25em">
|
<div class="general-stats" v-if="extended">
|
||||||
|
<div>
|
||||||
|
<img src="/images/icon-length.svg" alt="length icon" />
|
||||||
|
{{ train.length }}m
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img src="/images/icon-mass.svg" alt="mass icon" />
|
||||||
|
{{ (train.mass / 1000).toFixed(1) }}t
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img src="/images/icon-speed.svg" alt="speed icon" />
|
||||||
|
{{ train.speed }} km/h
|
||||||
|
|
||||||
|
<span v-if="stockSpeedLimit != Infinity">
|
||||||
|
•
|
||||||
|
<em
|
||||||
|
class="text--grayed"
|
||||||
|
style="text-decoration: underline dotted"
|
||||||
|
tabindex="0"
|
||||||
|
:data-tooltip="$t('trains.vmax-tooltip')"
|
||||||
|
>
|
||||||
|
{{ stockSpeedLimit }} km/h
|
||||||
|
</em>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text--grayed" style="margin-top: 0.25em">
|
||||||
{{ displayTrainPosition(train) }}
|
{{ displayTrainPosition(train) }}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="train-stats">
|
<section class="train-stats" v-if="!extended">
|
||||||
<TrainThumbnail :name="train.locoType" :onlyFirstSegment="true" />
|
<StockList :trainStockList="train.stockList" :tractionOnly="true" />
|
||||||
|
|
||||||
<div class="text--grayed">
|
|
||||||
{{ train.locoType }}
|
|
||||||
<span v-if="train.stockList.length > 1">
|
|
||||||
• {{ $t('trains.cars') }}:
|
|
||||||
<span class="count">{{ train.stockList.length - 1 }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span v-for="(stat, i) in STATS.main" :key="stat.name">
|
<span>{{ train.speed }}km/h</span>
|
||||||
<span v-if="i > 0"> • </span>
|
|
||||||
<span
|
<div>
|
||||||
>{{ `${~~((train as any)[stat.name] * (stat.multiplier || 1))}${stat.unit}` }}
|
<span> {{ train.length }}m</span>
|
||||||
</span>
|
•
|
||||||
|
<span> {{ (train.mass / 1000).toFixed(1) }}t</span>
|
||||||
|
<span v-if="train.stockList.length > 1">
|
||||||
|
•
|
||||||
|
{{ $t('trains.cars') }}: {{ train.stockList.length - 1 }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -108,13 +175,16 @@
|
|||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import styleMixin from '../../mixins/styleMixin';
|
import styleMixin from '../../mixins/styleMixin';
|
||||||
import trainInfoMixin from '../../mixins/trainInfoMixin';
|
import trainInfoMixin from '../../mixins/trainInfoMixin';
|
||||||
import Train from '../../scripts/interfaces/Train';
|
|
||||||
import ProgressBar from '../Global/ProgressBar.vue';
|
import ProgressBar from '../Global/ProgressBar.vue';
|
||||||
import TrainThumbnail from '../Global/TrainThumbnail.vue';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import StockList from '../Global/StockList.vue';
|
||||||
|
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||||
|
import { Train } from '../../typings/common';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [trainInfoMixin, styleMixin],
|
mixins: [trainInfoMixin, styleMixin, modalTrainMixin],
|
||||||
components: { ProgressBar, TrainThumbnail },
|
components: { ProgressBar, StockList },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
train: {
|
train: {
|
||||||
@@ -122,28 +192,52 @@ export default defineComponent({
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
extended: {
|
extended: {
|
||||||
type: Boolean,
|
type: Boolean
|
||||||
default: true
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
store: useMainStore(),
|
||||||
|
apiStore: useApiStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
stockSpeedLimit() {
|
||||||
|
return this.train.stockList.reduce((acc, stockName) => {
|
||||||
|
const vehicleSpeed =
|
||||||
|
this.apiStore.vehiclesData?.find((v) => v.name == stockName.split(':')[0])?.group.speed ??
|
||||||
|
300;
|
||||||
|
|
||||||
|
return Math.min(vehicleSpeed, acc);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
navigateToJournal() {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/journal/timetables',
|
||||||
|
query: {
|
||||||
|
'search-driver': this.train.driverName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.closeModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Global style for TrainThumbnail -->
|
|
||||||
<style lang="scss">
|
|
||||||
.train-stats .train-thumbnail {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@import '../../styles/responsive.scss';
|
||||||
@import '../../styles/badge.scss';
|
@import '../../styles/badge.scss';
|
||||||
|
|
||||||
.image-warning {
|
.image-warning {
|
||||||
height: 1em;
|
height: 1em;
|
||||||
|
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.train-stats {
|
.train-stats {
|
||||||
@@ -154,7 +248,7 @@ export default defineComponent({
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
gap: 0.25em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.train-info {
|
.train-info {
|
||||||
@@ -162,12 +256,21 @@ export default defineComponent({
|
|||||||
grid-template-columns: 2fr 1fr;
|
grid-template-columns: 2fr 1fr;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
|
|
||||||
|
&[data-extended='true'] {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.train-driver img {
|
||||||
|
max-height: 20px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
.timetable-id {
|
.timetable-id {
|
||||||
color: #d2d2d2;
|
color: #d2d2d2;
|
||||||
}
|
}
|
||||||
@@ -181,103 +284,102 @@ export default defineComponent({
|
|||||||
padding: 0 0.25em;
|
padding: 0 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timetable_stops {
|
.train-general {
|
||||||
font-size: 0.75em;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.train_general {
|
.general-stops {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-top-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
& > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
margin-right: 1.5em;
|
|
||||||
}
|
}
|
||||||
.train-status-badges {
|
}
|
||||||
|
|
||||||
|
.btn-timetable {
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-exit {
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.train-driver {
|
.general-stats {
|
||||||
&.supporter {
|
display: flex;
|
||||||
color: orange;
|
gap: 0.5em;
|
||||||
text-shadow: orange 0 0 5px;
|
flex-wrap: wrap;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable_route {
|
& > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable_warnings {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timetable_progress {
|
img {
|
||||||
|
width: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-timetable {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
gap: 0.25em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-timetable-progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timetable_progress-distance {
|
.progress-distance {
|
||||||
margin-right: 0.25em;
|
margin-right: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comments {
|
.timetable-warnings {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
gap: 0.25em;
|
||||||
|
|
||||||
font-size: 0.9em;
|
|
||||||
|
|
||||||
margin-top: 1em;
|
|
||||||
|
|
||||||
img {
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen() {
|
@include smallScreen() {
|
||||||
.train-info {
|
.train-info {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 1em 0;
|
gap: 1em 0;
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
font-size: 1.15em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.train-stats {
|
.btn-timetable > span {
|
||||||
font-size: 1.1em;
|
display: none;
|
||||||
}
|
|
||||||
|
|
||||||
.train_general {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.train-status-badges {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable_route {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable_progress {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comments {
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
margin: 0 0 0.5em 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal">
|
||||||
|
<div class="modal-background" @click="closeModal"></div>
|
||||||
|
<div class="modal-content" ref="content" tabindex="0">
|
||||||
|
<TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" />
|
||||||
|
<TrainSchedule :train="chosenTrain" tabindex="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||||
|
import TrainInfo from './TrainInfo.vue';
|
||||||
|
import TrainSchedule from './TrainSchedule.vue';
|
||||||
|
import { Train } from '../../typings/common';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { TrainInfo, TrainSchedule },
|
||||||
|
mixins: [modalTrainMixin],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
chosenTrain() {
|
||||||
|
return this.store.trainList.find((train) => train.modalId == this.store.chosenModalTrainId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
chosenTrain(train: Train | undefined) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (train) {
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
|
const contentEl = this.$refs['content'] as HTMLElement;
|
||||||
|
contentEl.focus();
|
||||||
|
} else {
|
||||||
|
(this.store.modalLastClickedTarget as any)?.focus();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
}, 90);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/responsive.scss';
|
||||||
|
|
||||||
|
.train-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
z-index: 200;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
background-color: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
position: relative;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
width: 95vw;
|
||||||
|
max-height: 95vh;
|
||||||
|
max-height: 95dvh;
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
box-shadow: 0 0 15px 10px #0e0e0e;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include midScreen {
|
||||||
|
.exit {
|
||||||
|
margin: 0.5em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="filters-options" @keydown.esc="showOptions = false">
|
<div class="dropdown" @keydown.esc="showOptions = false">
|
||||||
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
<div class="dropdown_background" v-if="showOptions" @click="showOptions = false"></div>
|
||||||
|
|
||||||
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
|
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
|
||||||
<img src="/images/icon-filter2.svg" alt="Open filters icon" />
|
<img src="/images/icon-filter2.svg" alt="Open filters icon" />
|
||||||
{{ $t('options.filters') }} [F]
|
[F] {{ $t('options.filters') }}
|
||||||
<span class="active-indicator" v-if="currentOptionsActive"></span>
|
<span class="active-indicator" v-if="currentOptionsActive"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="options-anim">
|
<transition name="dropdown-anim">
|
||||||
<div class="options_wrapper" v-if="showOptions">
|
<div class="dropdown_wrapper" v-if="showOptions">
|
||||||
<div class="options_content">
|
<div class="options_content">
|
||||||
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
|
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
|
||||||
<div class="search_content">
|
<div class="search_content">
|
||||||
@@ -23,7 +23,11 @@
|
|||||||
v-model="searchedTrain"
|
v-model="searchedTrain"
|
||||||
/>
|
/>
|
||||||
<button class="search-exit">
|
<button class="search-exit">
|
||||||
<img src="/images/icon-exit.svg" alt="Trains search clear icon" @click="onInputClear('train')" />
|
<img
|
||||||
|
src="/images/icon-exit.svg"
|
||||||
|
alt="Trains search clear icon"
|
||||||
|
@click="onInputClear('train')"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -36,7 +40,11 @@
|
|||||||
v-model="searchedDriver"
|
v-model="searchedDriver"
|
||||||
/>
|
/>
|
||||||
<button class="search-exit">
|
<button class="search-exit">
|
||||||
<img src="/images/icon-exit.svg" alt="Driver search clear icon" @click="onInputClear('driver')" />
|
<img
|
||||||
|
src="/images/icon-exit.svg"
|
||||||
|
alt="Driver search clear icon"
|
||||||
|
@click="onInputClear('driver')"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,12 +81,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
<div></div>
|
|
||||||
<button class="btn--action" @click="resetAllFilters">
|
<button class="btn--action" @click="resetAllFilters">
|
||||||
{{ $t('options.filter-reset') }}
|
{{ $t('options.filter-reset') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div tabindex="0" @focus="showOptions = false"></div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,8 +96,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, inject, PropType } from 'vue';
|
import { defineComponent, inject, PropType } from 'vue';
|
||||||
import keyMixin from '../../mixins/keyMixin';
|
import keyMixin from '../../mixins/keyMixin';
|
||||||
import { TrainFilterSection } from '../../scripts/enums/TrainFilterType';
|
import { TrainFilter, TrainFilterSection } from './typings';
|
||||||
import { TrainFilter } from '../../scripts/interfaces/Trains/TrainFilter';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [keyMixin],
|
mixins: [keyMixin],
|
||||||
@@ -152,9 +160,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onFilterChange(filter: TrainFilter) {
|
onFilterChange(filter: TrainFilter) {
|
||||||
// if (this.lastSelectedFilter?.id === filter.id)
|
|
||||||
// this.trainFilterList.forEach((tf) => (tf.isActive = filter.id === tf.id));
|
|
||||||
|
|
||||||
filter.isActive = !filter.isActive;
|
filter.isActive = !filter.isActive;
|
||||||
this.lastSelectedFilter = filter;
|
this.lastSelectedFilter = filter;
|
||||||
},
|
},
|
||||||
@@ -180,7 +185,8 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/filters_options.scss';
|
@import '../../styles/dropdown.scss';
|
||||||
|
@import '../../styles/dropdown_filters.scss';
|
||||||
|
|
||||||
.search_content > div {
|
.search_content > div {
|
||||||
margin: 0.5em auto;
|
margin: 0.5em auto;
|
||||||
@@ -216,9 +222,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
.filter-actions {
|
.filter-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
|
|||||||
@@ -2,83 +2,149 @@
|
|||||||
<div class="train-schedule" @click="toggleShowState">
|
<div class="train-schedule" @click="toggleShowState">
|
||||||
<StockList :trainStockList="train.stockList" />
|
<StockList :trainStockList="train.stockList" />
|
||||||
|
|
||||||
<!-- <div class="train-stock"> -->
|
|
||||||
<!-- <ul>
|
|
||||||
<li v-for="(stockName, i) in train.stockList" :key="i">
|
|
||||||
<p>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</p>
|
|
||||||
<TrainThumbnail :name="stockName" />
|
|
||||||
</li>
|
|
||||||
</ul> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
|
|
||||||
<div class="schedule-wrapper" v-if="train.timetableData">
|
<div class="schedule-wrapper" v-if="train.timetableData">
|
||||||
<ul class="stop_list">
|
<div class="stops">
|
||||||
<li
|
<div
|
||||||
v-for="(stop, i) in train.timetableData.followingStops"
|
v-for="(stop, i) in scheduleStops"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="stop"
|
class="stop"
|
||||||
:class="addClasses(stop, i)"
|
:data-status="stop.status"
|
||||||
|
:data-position="stop.position"
|
||||||
|
:data-delayed="stop.departureDelay > 0"
|
||||||
|
:data-stop-type="stop.type"
|
||||||
|
:data-minor-stop-active="stop.isActive"
|
||||||
|
:data-last-confirmed="stop.isLastConfirmed"
|
||||||
>
|
>
|
||||||
<span class="stop_info">
|
<span class="stop_info">
|
||||||
<div class="indicator"></div>
|
<span class="distance">
|
||||||
|
{{ stop.distance ? stop.distance.toFixed(1) : '' }}
|
||||||
<div class="progress-bar"></div>
|
|
||||||
|
|
||||||
<div class="stop-bar"></div>
|
|
||||||
|
|
||||||
<span class="distance" v-if="stop.stopDistance">
|
|
||||||
{{ Math.floor(stop.stopDistance) }}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stop-name" v-html="stop.stopName"> </span>
|
<div class="progress">
|
||||||
|
<div class="line line_node line_node-top"></div>
|
||||||
<StopDate :stop="stop" />
|
<div class="node"></div>
|
||||||
</span>
|
<div class="line line_node line_node-bottom"></div>
|
||||||
|
|
||||||
<div class="stop_line" v-if="i < train.timetableData!.followingStops.length - 1">
|
|
||||||
<div class="progress-bar"></div>
|
|
||||||
|
|
||||||
<div v-if="stop.comments" style="color: salmon">
|
|
||||||
<b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<StopLabel :stop="stop" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="stop_line">
|
||||||
|
<!-- Grid placeholder -->
|
||||||
|
<div></div>
|
||||||
|
|
||||||
|
<div class="progress">
|
||||||
|
<div class="line line_connection" v-if="i < scheduleStops.length - 1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bottom-line-info">
|
||||||
|
<div class="info-comments" v-if="stop.comments" style="color: salmon">
|
||||||
|
<img src="/images/icon-warning.svg" alt="icon-warning" width="20" />
|
||||||
|
<b v-html="stop.comments"></b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Routes -->
|
||||||
<span
|
<span
|
||||||
v-if="
|
v-if="
|
||||||
stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine &&
|
stop.departureLine &&
|
||||||
!/sbl/gi.test(stop.departureLine!)
|
scheduleStops[i + 1] != undefined &&
|
||||||
|
!/-|_|(^it\d+)|(^sbl)/gi.test(stop.departureLine)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ stop.departureLine }}
|
<div class="scenery-route">
|
||||||
</span>
|
<span>{{ stop.departureLine }}</span>
|
||||||
|
<span v-if="stop.departureLineInfo">
|
||||||
<span v-else-if="!/sbl/gi.test(stop.departureLine!)">
|
| {{ stop.departureLineInfo.routeSpeed }}
|
||||||
{{ stop.departureLine }} /
|
<span v-if="stop.departureLineInfo.isElectric">⚡</span>
|
||||||
{{ train.timetableData!.followingStops[i + 1].arrivalLine }}
|
<img
|
||||||
|
v-else
|
||||||
|
src="/images/icon-we4a.png"
|
||||||
|
:title="$t('trains.we4a-tooltip')"
|
||||||
|
width="12"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stop_line" v-else>
|
<div
|
||||||
<div v-if="stop.comments" style="color: salmon">
|
v-if="stop.sceneryName != scheduleStops[i + 1]?.sceneryName"
|
||||||
<b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span>
|
class="scenery-change-name"
|
||||||
|
>
|
||||||
|
<span>{{ scheduleStops[i + 1].sceneryName }}</span>
|
||||||
|
<span v-if="stop.departureLineInfo?.routeTracks == 1"> ↕</span>
|
||||||
|
<span v-else> ⇅</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scenery-route">
|
||||||
|
<span> {{ scheduleStops[i + 1].arrivalLine }}</span>
|
||||||
|
|
||||||
|
<span v-if="scheduleStops[i + 1].arrivalLineInfo">
|
||||||
|
| {{ scheduleStops[i + 1].arrivalLineInfo!.routeSpeed }}
|
||||||
|
<span v-if="scheduleStops[i + 1].arrivalLineInfo!.isElectric">⚡</span>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
src="/images/icon-we4a.png"
|
||||||
|
:title="$t('trains.we4a-tooltip')"
|
||||||
|
width="12"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, PropType } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
import Train from '../../scripts/interfaces/Train';
|
import StopLabel from './StopLabel.vue';
|
||||||
import TrainStop from '../../scripts/interfaces/TrainStop';
|
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
import StopDate from '../Global/StopDate.vue';
|
|
||||||
import StockList from '../Global/StockList.vue';
|
import StockList from '../Global/StockList.vue';
|
||||||
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import { StationRoutesInfo, Train } from '../../typings/common';
|
||||||
|
|
||||||
|
export interface TrainScheduleStop {
|
||||||
|
nameHtml: string;
|
||||||
|
nameRaw: string;
|
||||||
|
|
||||||
|
status: 'confirmed' | 'unconfirmed' | 'stopped';
|
||||||
|
type: string;
|
||||||
|
position: 'begin' | 'end' | 'en-route';
|
||||||
|
|
||||||
|
arrivalScheduled: number;
|
||||||
|
arrivalReal: number;
|
||||||
|
|
||||||
|
departureScheduled: number;
|
||||||
|
departureReal: number;
|
||||||
|
|
||||||
|
departureDelay: number;
|
||||||
|
arrivalDelay: number;
|
||||||
|
|
||||||
|
duration: number | null;
|
||||||
|
|
||||||
|
isActive: boolean;
|
||||||
|
isLastConfirmed: boolean;
|
||||||
|
isSBL: boolean;
|
||||||
|
|
||||||
|
sceneryName: string | null;
|
||||||
|
distance: number;
|
||||||
|
|
||||||
|
arrivalLine: string | null;
|
||||||
|
departureLine: string | null;
|
||||||
|
|
||||||
|
arrivalLineInfo?: StationRoutesInfo;
|
||||||
|
departureLineInfo?: StationRoutesInfo;
|
||||||
|
|
||||||
|
isExternal: boolean;
|
||||||
|
|
||||||
|
comments: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { StopDate, StockList },
|
components: { StopLabel, StockList },
|
||||||
props: {
|
props: {
|
||||||
train: {
|
train: {
|
||||||
type: Object as PropType<Train>,
|
type: Object as PropType<Train>,
|
||||||
@@ -90,62 +156,111 @@ export default defineComponent({
|
|||||||
|
|
||||||
emits: ['click'],
|
emits: ['click'],
|
||||||
|
|
||||||
setup(props) {
|
data() {
|
||||||
return {
|
return {
|
||||||
store: useStore(),
|
store: useMainStore(),
|
||||||
|
apiStore: useApiStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
lastConfirmed: computed(() => {
|
computed: {
|
||||||
return props.train.timetableData!.followingStops.findIndex(
|
scheduleStops(): TrainScheduleStop[] {
|
||||||
|
let currentSceneryIndex = 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.train.timetableData?.followingStops.map((stop, i, arr) => {
|
||||||
|
const isExternal =
|
||||||
|
i > 0 &&
|
||||||
|
stop.arrivalLine != null &&
|
||||||
|
(stop.arrivalLine != arr[i - 1].departureLine ||
|
||||||
|
(stop.arrivalLine == arr[i - 1].departureLine &&
|
||||||
|
!/-|_|(^it\d+)|(^sbl)/gi.test(stop.arrivalLine)));
|
||||||
|
|
||||||
|
if (isExternal) currentSceneryIndex++;
|
||||||
|
|
||||||
|
const sceneryName = this.train.timetableData!.sceneryNames[currentSceneryIndex];
|
||||||
|
const sceneryInfo = this.apiStore.sceneryData.find((st) => st.name == sceneryName);
|
||||||
|
|
||||||
|
const arrivalLineInfo = sceneryInfo?.routesInfo.find(
|
||||||
|
(r) => r.routeName == stop.arrivalLine
|
||||||
|
);
|
||||||
|
|
||||||
|
const departureLineInfo = sceneryInfo?.routesInfo.find(
|
||||||
|
(r) => r.routeName == stop.departureLine
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nameHtml: stop.stopName,
|
||||||
|
nameRaw: stop.stopNameRAW,
|
||||||
|
|
||||||
|
arrivalScheduled: stop.arrivalTimestamp,
|
||||||
|
arrivalReal: stop.arrivalRealTimestamp,
|
||||||
|
|
||||||
|
departureScheduled: stop.departureTimestamp,
|
||||||
|
departureReal: stop.departureRealTimestamp,
|
||||||
|
|
||||||
|
departureDelay: stop.departureDelay,
|
||||||
|
arrivalDelay: stop.arrivalDelay,
|
||||||
|
|
||||||
|
duration: stop.stopTime,
|
||||||
|
|
||||||
|
comments: stop.comments ?? null,
|
||||||
|
|
||||||
|
arrivalLine: stop.arrivalLine,
|
||||||
|
departureLine: stop.departureLine,
|
||||||
|
|
||||||
|
arrivalLineInfo: arrivalLineInfo,
|
||||||
|
departureLineInfo: departureLineInfo,
|
||||||
|
|
||||||
|
isExternal,
|
||||||
|
|
||||||
|
type: stop.stopType,
|
||||||
|
distance: stop.stopDistance,
|
||||||
|
isActive: this.activeMinorStops.includes(i),
|
||||||
|
isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere,
|
||||||
|
isSBL: /sbl/gi.test(stop.stopName),
|
||||||
|
position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route',
|
||||||
|
sceneryName,
|
||||||
|
status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed'
|
||||||
|
};
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
lastConfirmed() {
|
||||||
|
return this.train.timetableData?.followingStops.findIndex(
|
||||||
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped
|
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
activeMinorStops: computed(() => {
|
|
||||||
const lastMajorConfirmed = props.train.timetableData!.followingStops.findIndex(
|
activeMinorStops() {
|
||||||
|
if (!this.train.timetableData) return [];
|
||||||
|
|
||||||
|
const lastMajorConfirmed = this.train.timetableData.followingStops.findIndex(
|
||||||
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed
|
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeMinorStopList: number[] = [];
|
const activeMinorStopList: number[] = [];
|
||||||
if (lastMajorConfirmed + 1 >= props.train.timetableData!.followingStops.length)
|
if (lastMajorConfirmed + 1 >= this.train.timetableData.followingStops.length)
|
||||||
return activeMinorStopList;
|
return activeMinorStopList;
|
||||||
|
|
||||||
for (
|
for (
|
||||||
let i = lastMajorConfirmed + 1;
|
let i = lastMajorConfirmed + 1;
|
||||||
i < props.train.timetableData!.followingStops.length;
|
i < this.train.timetableData!.followingStops.length;
|
||||||
i++
|
i++
|
||||||
) {
|
) {
|
||||||
if (/po\.|sbl/gi.test(props.train.timetableData!.followingStops[i].stopNameRAW))
|
if (/po\.|sbl/gi.test(this.train.timetableData!.followingStops[i].stopNameRAW))
|
||||||
activeMinorStopList.push(i);
|
activeMinorStopList.push(i);
|
||||||
else break;
|
else break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return activeMinorStopList;
|
return activeMinorStopList;
|
||||||
})
|
}
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
toggleShowState() {
|
toggleShowState() {
|
||||||
this.$emit('click');
|
this.$emit('click');
|
||||||
},
|
|
||||||
|
|
||||||
addClasses(stop: TrainStop, index: number) {
|
|
||||||
return {
|
|
||||||
confirmed: stop.confirmed,
|
|
||||||
stopped: stop.stopped,
|
|
||||||
begin: stop.beginsHere,
|
|
||||||
end: stop.terminatesHere,
|
|
||||||
delayed: stop.departureDelay > 0,
|
|
||||||
sbl: /sbl/gi.test(stop.stopName),
|
|
||||||
[stop.stopType.replaceAll(', ', '-')]:
|
|
||||||
stop.stopType.match(new RegExp('ph|pm|pt')) && !stop.confirmed && !stop.beginsHere,
|
|
||||||
'minor-stop-active': this.activeMinorStops.includes(index),
|
|
||||||
'last-confirmed': index == this.lastConfirmed && !stop.terminatesHere
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
onImageError(e: Event) {
|
|
||||||
const imageEl = e.target as HTMLImageElement;
|
|
||||||
imageEl.src = '/images/icon-unknown.png';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -155,17 +270,18 @@ export default defineComponent({
|
|||||||
@import '../../styles/responsive.scss';
|
@import '../../styles/responsive.scss';
|
||||||
|
|
||||||
$barClr: #b1b1b1;
|
$barClr: #b1b1b1;
|
||||||
$confirmedClr: #18d818;
|
$confirmedClr: #4ae24a;
|
||||||
$stoppedClr: #f55f31;
|
$stoppedClr: #f55f31;
|
||||||
$haltClr: #f8bb36;
|
$haltClr: #f8bb36;
|
||||||
$stopNameClr: #22a8d1;
|
|
||||||
|
$blinkAnim: 0.5s ease-in-out alternate infinite blink;
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
from {
|
from {
|
||||||
background-color: $barClr;
|
border-color: $barClr;
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
background-color: $confirmedClr;
|
border-color: $confirmedClr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,216 +297,251 @@ $stopNameClr: #22a8d1;
|
|||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.stops {
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
top: -1px;
|
|
||||||
left: -17px;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
width: 3px;
|
|
||||||
|
|
||||||
background-color: $barClr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stop-name {
|
|
||||||
background: $stopNameClr;
|
|
||||||
padding: 0.3em 0.5em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&.misc {
|
|
||||||
background: gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stop-comment {
|
|
||||||
background: forestgreen;
|
|
||||||
padding: 0.3em 0.5em;
|
|
||||||
|
|
||||||
max-width: 250px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
width: 2em;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.stop_list {
|
|
||||||
margin-left: 2.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.stop_list > li.stop {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow-y: hidden;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
padding: 0 0.5em;
|
padding: 5px 0;
|
||||||
|
|
||||||
&.sbl {
|
|
||||||
.stop-date {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop-name {
|
.stop {
|
||||||
background: none;
|
// Begin stop
|
||||||
color: #aaa;
|
&[data-position='begin'] {
|
||||||
padding: 0;
|
.node {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[class*='ph'] > .stop_info > .indicator {
|
|
||||||
border-color: $stopNameClr;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[class*='pt'] > .stop_info > .indicator {
|
|
||||||
border-color: #818181;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.begin {
|
|
||||||
.stop_info > .indicator {
|
|
||||||
border-color: lightgreen;
|
border-color: lightgreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop_info > .progress-bar {
|
.line_node-top {
|
||||||
background: lightgreen;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.end {
|
// End stop
|
||||||
.stop_info > .indicator {
|
&[data-position='end'] {
|
||||||
|
.node {
|
||||||
border-color: salmon;
|
border-color: salmon;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop_info > .progress-bar {
|
.line_node-bottom {
|
||||||
background: salmon;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.minor-stop-active {
|
// Stop types
|
||||||
.stop_info > .progress-bar {
|
&[data-stop-type*='pt'] .node {
|
||||||
animation: 0.5s ease-in-out alternate infinite blink;
|
border-color: #818181;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop_line > .progress-bar {
|
&[data-stop-type*='ph'] .node {
|
||||||
animation: 0.5s ease-in-out alternate infinite blink;
|
border-color: $haltClr;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-minor-stop-active='true'] {
|
||||||
|
.progress > .line {
|
||||||
|
animation: $blinkAnim;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + div {
|
||||||
|
.progress > .line_node-top {
|
||||||
|
animation: $blinkAnim;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.last-confirmed {
|
// Last confirmed outpost / checkpoint
|
||||||
.stop_line > .progress-bar {
|
&[data-last-confirmed='true'] {
|
||||||
animation: 0.5s ease-in-out alternate infinite blink;
|
.progress > .line_connection {
|
||||||
|
animation: $blinkAnim;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress > .line_node-bottom {
|
||||||
|
animation: $blinkAnim;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + div {
|
||||||
|
.progress > .line_node-top {
|
||||||
|
animation: $blinkAnim;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.confirmed {
|
// Confirmed status
|
||||||
.stop_info {
|
&[data-status='confirmed'] {
|
||||||
> .progress-bar {
|
.progress > .node {
|
||||||
background-color: $confirmedClr;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .indicator {
|
|
||||||
border-color: $confirmedClr;
|
border-color: $confirmedClr;
|
||||||
}
|
}
|
||||||
}
|
.progress > .line {
|
||||||
|
border-left: 2px solid $confirmedClr;
|
||||||
.stop_line > .progress-bar {
|
border-right: 2px solid $confirmedClr;
|
||||||
background-color: $confirmedClr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stopped {
|
// Stopped status
|
||||||
.stop_info {
|
&[data-status='stopped'] {
|
||||||
> .indicator {
|
.progress > .node {
|
||||||
border-color: $stoppedClr;
|
border-color: $stoppedClr;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .stop-bar {
|
.progress > .line_node {
|
||||||
background: $stoppedClr;
|
border-color: $stoppedClr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unused so far
|
||||||
|
&[data-track-count-departure='2'] {
|
||||||
|
.progress > .line {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-track-count-arrival='2'] {
|
||||||
|
.progress > .line_node-top {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-track-count-arrival='1'] {
|
||||||
|
.progress > .line_node-top {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-electrified-departure] {
|
||||||
|
.stop_line > .line-speed > .speed-departure {
|
||||||
|
color: #00c1c7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-electrified-arrival] {
|
||||||
|
.stop_line > .line-speed > .speed-next-arrival {
|
||||||
|
color: #00c1c7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop_info,
|
||||||
|
.stop_line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 30px 40px auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-speed {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #9b9b9b;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop_info {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop_line {
|
.stop_line {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
|
margin-top: 5px;
|
||||||
padding: 0.35em 0;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.line-segment {
|
|
||||||
color: $barClr;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stop_info {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stop-bar {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -17px;
|
|
||||||
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
width: 3px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.distance {
|
.distance {
|
||||||
position: absolute;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
top: 50%;
|
justify-content: center;
|
||||||
transform: translate(-100%, -50%);
|
|
||||||
|
|
||||||
margin-left: -1.75rem;
|
|
||||||
|
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
color: #d6d6d6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator {
|
.progress {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& > .node {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 11;
|
|
||||||
|
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: -1rem;
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
transform: translate(-47%, -50%);
|
z-index: 15;
|
||||||
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
|
|
||||||
background: var(--clr-secondary);
|
background-color: var(--clr-secondary);
|
||||||
border: 3px solid $barClr;
|
border: 4px solid $barClr;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .line {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
// background-color: $barClr;
|
||||||
|
border-left: 2px solid $barClr;
|
||||||
|
border-right: 2px solid $barClr;
|
||||||
|
|
||||||
|
&.line_connection {
|
||||||
|
transform: translate(-50%, -6px);
|
||||||
|
height: calc(100% + 12px);
|
||||||
|
// height: calc(100% + 0.25em);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.line_node-top {
|
||||||
|
top: 0;
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.line_node-bottom {
|
||||||
|
top: 50%;
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.line_stop {
|
||||||
|
border-color: $stoppedClr;
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-comments {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
|
||||||
|
margin: 0.25em 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenery-route {
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenery-change-name {
|
||||||
|
position: relative;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: 2px;
|
||||||
|
width: 30px;
|
||||||
|
background-color: #aaa;
|
||||||
|
|
||||||
|
top: 50%;
|
||||||
|
right: calc(100% + 5px);
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dropdown" @keydown.esc="showOptions = false" @focusout="showOptions = false">
|
||||||
|
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
||||||
|
|
||||||
|
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
|
||||||
|
<img src="/images/icon-stats.svg" alt="Open filters icon" />
|
||||||
|
{{ $t('train-stats.stats-button') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<transition name="dropdown-anim">
|
||||||
|
<div class="dropdown_wrapper" v-if="showOptions">
|
||||||
|
<h1 class="text--primary">
|
||||||
|
<img src="/images/icon-stats.svg" alt="Open filters icon" />
|
||||||
|
{{ $t('train-stats.title') }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<hr style="margin: 0.5em 0" />
|
||||||
|
|
||||||
|
<div v-if="apiStore.dataStatuses.connection == Status.Loaded && regionTrains.length > 0">
|
||||||
|
<div class="top-list general">
|
||||||
|
<transition-group tag="ul" name="stats-anim">
|
||||||
|
<li class="badge" key="timetable-count">
|
||||||
|
<span>{{ $t('train-stats.timetable-count') }}</span>
|
||||||
|
<span>
|
||||||
|
<b>{{ regionTrainsWithTT.length }}</b>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="badge" key="avg-speed">
|
||||||
|
<span>{{ $t('train-stats.avg-speed') }}</span>
|
||||||
|
<span>
|
||||||
|
<b>{{ stats.avgSpeed.toFixed(1) }} km/h</b>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="badge" key="avg-distance">
|
||||||
|
<span>{{ $t('train-stats.avg-timetable') }}</span>
|
||||||
|
<span>
|
||||||
|
<b>{{ stats.avgDistance.toFixed(1) }} km</b>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="top-list categories">
|
||||||
|
<h3>{{ $t('train-stats.top-categories') }}</h3>
|
||||||
|
|
||||||
|
<transition-group tag="ul" name="stats-anim">
|
||||||
|
<li class="badge" v-for="top in stats.topCategories" :key="top.name">
|
||||||
|
<span>{{ top.name }}</span>
|
||||||
|
<span>{{ top.count }}</span>
|
||||||
|
</li>
|
||||||
|
</transition-group>
|
||||||
|
|
||||||
|
<span class="no-data" v-if="stats.topCategories.length == 0">
|
||||||
|
{{ $t('train-stats.no-timetables') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="top-list vehicles">
|
||||||
|
<h3>{{ $t('train-stats.top-vehicles') }}</h3>
|
||||||
|
|
||||||
|
<transition-group tag="ul" name="stats-anim">
|
||||||
|
<li class="badge" v-for="top in stats.topVehicles" :key="top.name">
|
||||||
|
<span>{{ top.name }}</span>
|
||||||
|
<span>{{ top.count }}</span>
|
||||||
|
</li>
|
||||||
|
</transition-group>
|
||||||
|
|
||||||
|
<span class="no-data" v-if="stats.topVehicles.length == 0">
|
||||||
|
{{ $t('train-stats.no-vehicles') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="top-list vehicle-types">
|
||||||
|
<h3>{{ $t('train-stats.top-units') }}</h3>
|
||||||
|
|
||||||
|
<transition-group tag="ul" name="stats-anim">
|
||||||
|
<li class="badge" v-for="top in stats.topUnits.slice(0, 7)" :key="top.name">
|
||||||
|
<span>{{ top.name }}</span>
|
||||||
|
<span>{{ top.count }}</span>
|
||||||
|
</li>
|
||||||
|
</transition-group>
|
||||||
|
|
||||||
|
<span class="no-data" v-if="stats.topUnits.length == 0">
|
||||||
|
{{ $t('train-stats.no-units') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="apiStore.dataStatuses.connection != Status.Loaded">
|
||||||
|
{{ $t('train-stats.stats-loading') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="no-data" v-else>
|
||||||
|
{{ $t('train-stats.no-stats') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
import { Status } from '../../typings/common';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
|
||||||
|
interface ITop {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IStats {
|
||||||
|
timetableCount: number;
|
||||||
|
avgSpeed: number;
|
||||||
|
avgDistance: number;
|
||||||
|
topCategories: ITop[];
|
||||||
|
topVehicles: ITop[];
|
||||||
|
topUnits: ITop[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareTop(top1: ITop, top2: ITop) {
|
||||||
|
return Math.sign(top2.count - top1.count) || top1.name.localeCompare(top2.name, 'pl-PL');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showOptions: false,
|
||||||
|
store: useMainStore(),
|
||||||
|
apiStore: useApiStore(),
|
||||||
|
Status: Status.Data
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
regionTrains() {
|
||||||
|
return this.store.trainList.filter((train) => train.region == this.store.region.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
regionTrainsWithTT() {
|
||||||
|
return this.regionTrains.filter((train) => train.timetableData);
|
||||||
|
},
|
||||||
|
|
||||||
|
stats() {
|
||||||
|
const stats = this.regionTrains.reduce(
|
||||||
|
(acc, train, i, arr) => {
|
||||||
|
// AVG SPEED
|
||||||
|
acc.avgSpeed += train.speed / arr.length;
|
||||||
|
|
||||||
|
// TOP VEHICLES
|
||||||
|
const locoType = train.locoType.split('-')[0];
|
||||||
|
const topVehicle = acc.topVehicles.find((top) => top.name == locoType);
|
||||||
|
|
||||||
|
if (!topVehicle) acc.topVehicles.push({ name: locoType, count: 1 });
|
||||||
|
else topVehicle.count++;
|
||||||
|
|
||||||
|
// TOP UNITS
|
||||||
|
const unitType = train.locoType;
|
||||||
|
const topUnit = acc.topUnits.find((top) => top.name == unitType);
|
||||||
|
|
||||||
|
if (!topUnit) acc.topUnits.push({ name: unitType, count: 1 });
|
||||||
|
else topUnit.count++;
|
||||||
|
|
||||||
|
if (train.timetableData !== undefined) {
|
||||||
|
acc.timetableCount++;
|
||||||
|
// AVG DISTANCE
|
||||||
|
acc.avgDistance += train.timetableData.routeDistance;
|
||||||
|
|
||||||
|
// TOP CATEGORIES
|
||||||
|
const topCategory = acc.topCategories.find(
|
||||||
|
(top) => top.name == train.timetableData!.category
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!topCategory)
|
||||||
|
acc.topCategories.push({ name: train.timetableData!.category, count: 1 });
|
||||||
|
else topCategory.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == arr.length - 1 && acc.timetableCount != 0) {
|
||||||
|
acc.avgDistance /= acc.timetableCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timetableCount: 0,
|
||||||
|
avgDistance: 0,
|
||||||
|
avgSpeed: 0,
|
||||||
|
topCategories: [],
|
||||||
|
topUnits: [],
|
||||||
|
topVehicles: []
|
||||||
|
} as IStats
|
||||||
|
);
|
||||||
|
|
||||||
|
stats.topCategories.sort(compareTop);
|
||||||
|
stats.topUnits.sort(compareTop);
|
||||||
|
stats.topVehicles.sort(compareTop);
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleShowOptions() {
|
||||||
|
this.showOptions = !this.showOptions;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/dropdown.scss';
|
||||||
|
@import '../../styles/badge.scss';
|
||||||
|
@import '../../styles/responsive.scss';
|
||||||
|
|
||||||
|
h1 img {
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-list ul {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
// @include smallScreen {
|
||||||
|
// justify-content: center;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
& > span:first-child {
|
||||||
|
background-color: $accentCol;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown_wrapper {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-anim {
|
||||||
|
&-move,
|
||||||
|
&-enter-active,
|
||||||
|
&-leave-active {
|
||||||
|
transition: all 250ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-from,
|
||||||
|
&-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include smallScreen {
|
||||||
|
h1,
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||