admin-dashboard.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. <template lang='pug'>
  2. v-container(fluid, grid-list-lg)
  3. v-layout(row, wrap)
  4. v-flex(xs12)
  5. .admin-header
  6. img.animated.fadeInUp(src='/_assets/svg/icon-browse-page.svg', alt='Dashboard', style='width: 80px;')
  7. .admin-header-title
  8. .headline.primary--text.animated.fadeInLeft {{ $t('admin:dashboard.title') }}
  9. .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{ $t('admin:dashboard.subtitle') }}
  10. v-flex(xs12 md6 lg4 xl3 d-flex)
  11. v-card.primary.dashboard-card.animated.fadeInUp(dark)
  12. v-card-text
  13. v-icon.dashboard-icon mdi-file-document-outline
  14. .overline {{$t('admin:dashboard.pages')}}
  15. animated-number.display-1(
  16. :value='info.pagesTotal'
  17. :duration='2000'
  18. :formatValue='round'
  19. easing='easeOutQuint'
  20. )
  21. v-flex(xs12 md6 lg4 xl3 d-flex)
  22. v-card.blue.darken-3.dashboard-card.animated.fadeInUp.wait-p2s(dark)
  23. v-card-text
  24. v-icon.dashboard-icon mdi-account
  25. .overline {{$t('admin:dashboard.users')}}
  26. animated-number.display-1(
  27. :value='info.usersTotal'
  28. :duration='2000'
  29. :formatValue='round'
  30. easing='easeOutQuint'
  31. )
  32. v-flex(xs12 md6 lg4 xl3 d-flex)
  33. v-card.blue.darken-4.dashboard-card.animated.fadeInUp.wait-p4s(dark)
  34. v-card-text
  35. v-icon.dashboard-icon mdi-account-group
  36. .overline {{$t('admin:dashboard.groups')}}
  37. animated-number.display-1(
  38. :value='info.groupsTotal'
  39. :duration='2000'
  40. :formatValue='round'
  41. easing='easeOutQuint'
  42. )
  43. v-flex(xs12 md6 lg12 xl3 d-flex)
  44. v-card.dashboard-card.animated.fadeInUp.wait-p6s(
  45. :class='isLatestVersion ? "green" : "red lighten-2"'
  46. dark
  47. )
  48. v-btn.btn-animate-wrench(fab, absolute, :right='!$vuetify.rtl', :left='$vuetify.rtl', top, small, light, to='system', v-if='hasPermission(`manage:system`)')
  49. v-icon(:color='isLatestVersion ? `green` : `red darken-4`', small) mdi-wrench
  50. v-card-text
  51. v-icon.dashboard-icon mdi-blur
  52. .subtitle-1 Wiki.js {{info.currentVersion}}
  53. .body-2(v-if='isLatestVersion') {{$t('admin:dashboard.versionLatest')}}
  54. .body-2(v-else) {{$t('admin:dashboard.versionNew', { version: info.latestVersion })}}
  55. v-flex(xs12, xl6)
  56. v-card.radius-7.animated.fadeInUp.wait-p2s
  57. v-toolbar(:color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-5`', dense, flat)
  58. v-spacer
  59. .overline {{$t('admin:dashboard.recentPages')}}
  60. v-spacer
  61. v-data-table.pb-2(
  62. :items='recentPages'
  63. :headers='recentPagesHeaders'
  64. :loading='recentPagesLoading'
  65. hide-default-footer
  66. hide-default-header
  67. )
  68. template(slot='item', slot-scope='props')
  69. tr.is-clickable(:active='props.selected', @click='$router.push(`/pages/` + props.item.id)')
  70. td
  71. .body-2: strong {{ props.item.title }}
  72. td.admin-pages-path
  73. v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}
  74. span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}
  75. td.text-right.caption(width='250') {{ props.item.updatedAt | moment('calendar') }}
  76. v-flex(xs12, xl6)
  77. v-card.radius-7.animated.fadeInUp.wait-p4s
  78. v-toolbar(:color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-5`', dense, flat)
  79. v-spacer
  80. .overline {{$t('admin:dashboard.lastLogins')}}
  81. v-spacer
  82. v-data-table.pb-2(
  83. :items='lastLogins'
  84. :headers='lastLoginsHeaders'
  85. :loading='lastLoginsLoading'
  86. hide-default-footer
  87. hide-default-header
  88. )
  89. template(slot='item', slot-scope='props')
  90. tr.is-clickable(:active='props.selected', @click='$router.push(`/users/` + props.item.id)')
  91. td
  92. .body-2: strong {{ props.item.name }}
  93. td.text-right.caption(width='250') {{ props.item.lastLoginAt | moment('calendar') }}
  94. v-flex(xs12)
  95. v-card.dashboard-contribute.animated.fadeInUp.wait-p4s
  96. v-card-text
  97. img(src='/_assets/svg/icon-heart-health.svg', alt='Contribute', style='height: 80px;')
  98. .pl-5
  99. .subtitle-1 {{$t('admin:contribute.title')}}
  100. .body-2.mt-3: strong {{$t('admin:dashboard.contributeSubtitle')}}
  101. .body-2 {{$t('admin:dashboard.contributeHelp')}}
  102. v-btn.mx-0.mt-4(:color='$vuetify.theme.dark ? `indigo lighten-3` : `indigo`', outlined, small, to='/contribute')
  103. .caption: strong {{$t('admin:dashboard.contributeLearnMore')}}
  104. </template>
  105. <script>
  106. import _ from 'lodash'
  107. import AnimatedNumber from 'animated-number-vue'
  108. import { get } from 'vuex-pathify'
  109. import gql from 'graphql-tag'
  110. import semverLte from 'semver/functions/lte'
  111. export default {
  112. components: {
  113. AnimatedNumber
  114. },
  115. data() {
  116. return {
  117. recentPages: [],
  118. recentPagesLoading: false,
  119. recentPagesHeaders: [
  120. { text: 'Title', value: 'title' },
  121. { text: 'Path', value: 'path' },
  122. { text: 'Last Updated', value: 'updatedAt', width: 250 }
  123. ],
  124. lastLogins: [],
  125. lastLoginsLoading: false,
  126. lastLoginsHeaders: [
  127. { text: 'User', value: 'displayName' },
  128. { text: 'Last Login', value: 'lastLoginAt', width: 250 }
  129. ]
  130. }
  131. },
  132. computed: {
  133. isLatestVersion() {
  134. if (this.info.latestVersion === 'n/a' || this.info.currentVersion === 'n/a') {
  135. return true
  136. } else {
  137. return semverLte(this.info.latestVersion, this.info.currentVersion)
  138. }
  139. },
  140. info: get('admin/info'),
  141. permissions: get('user/permissions')
  142. },
  143. methods: {
  144. round(val) { return Math.round(val) },
  145. hasPermission(prm) {
  146. if (_.isArray(prm)) {
  147. return _.some(prm, p => {
  148. return _.includes(this.permissions, p)
  149. })
  150. } else {
  151. return _.includes(this.permissions, prm)
  152. }
  153. }
  154. },
  155. apollo: {
  156. recentPages: {
  157. query: gql`
  158. query {
  159. pages {
  160. list(limit: 10, orderBy: UPDATED, orderByDirection: DESC) {
  161. id
  162. locale
  163. path
  164. title
  165. description
  166. contentType
  167. isPublished
  168. isPrivate
  169. privateNS
  170. createdAt
  171. updatedAt
  172. }
  173. }
  174. }
  175. `,
  176. update: (data) => data.pages.list,
  177. watchLoading (isLoading) {
  178. this.recentPagesLoading = isLoading
  179. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dashboard-recentpages')
  180. }
  181. },
  182. lastLogins: {
  183. query: gql`
  184. query {
  185. users {
  186. lastLogins {
  187. id
  188. name
  189. lastLoginAt
  190. }
  191. }
  192. }
  193. `,
  194. fetchPolicy: 'network-only',
  195. update: (data) => data.users.lastLogins,
  196. watchLoading (isLoading) {
  197. this.lastLoginsLoading = isLoading
  198. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dashboard-lastlogins')
  199. }
  200. }
  201. }
  202. }
  203. </script>
  204. <style lang='scss'>
  205. .dashboard-card {
  206. display: flex;
  207. width: 100%;
  208. border-radius: 7px;
  209. .v-card__text {
  210. overflow: hidden;
  211. position: relative;
  212. }
  213. }
  214. .dashboard-contribute {
  215. background-color: #FFF;
  216. background-image: linear-gradient(to bottom, #FFF 0%, lighten(mc('indigo', '50'), 3%) 100%);
  217. border-radius: 7px;
  218. @at-root .theme--dark & {
  219. background-color: mc('grey', '800');
  220. background-image: linear-gradient(to bottom, mc('grey', '800') 0%, darken(mc('grey', '800'), 6%) 100%);
  221. }
  222. .v-card__text {
  223. display: flex;
  224. align-items: center;
  225. color: mc('indigo', '500') !important;
  226. @at-root .theme--dark & {
  227. color: mc('grey', '300') !important;
  228. }
  229. }
  230. }
  231. .v-icon.dashboard-icon {
  232. position: absolute !important;
  233. right: 0;
  234. top: 12px;
  235. font-size: 100px !important;
  236. opacity: .25;
  237. @at-root .v-application--is-rtl & {
  238. left: 0;
  239. right: initial;
  240. }
  241. }
  242. </style>