浏览代码

feat: search page UI (wip)

NGPixel 1 年之前
父节点
当前提交
a806aa3466
共有 5 个文件被更改,包括 390 次插入24 次删除
  1. 21 4
      server/graph/schemas/page.graphql
  2. 3 0
      server/locales/en.json
  3. 32 20
      ux/src/components/HeaderNav.vue
  4. 330 0
      ux/src/pages/Search.vue
  5. 4 0
      ux/src/router/routes.js

+ 21 - 4
server/graph/schemas/page.graphql

@@ -15,10 +15,18 @@ extend type Query {
   ): PageVersion
   ): PageVersion
 
 
   searchPages(
   searchPages(
+    siteId: UUID!
     query: String!
     query: String!
     path: String
     path: String
-    locale: String
-  ): PageSearchResponse!
+    locale: [String]
+    tags: [String]
+    editor: String
+    publishState: PagePublishState
+    orderBy: PageSearchSort
+    orderByDirection: OrderByDirection
+    offset: Int
+    limit: Int
+  ): PageSearchResponse
 
 
   pages(
   pages(
     limit: Int
     limit: Int
@@ -28,7 +36,7 @@ extend type Query {
     locale: String
     locale: String
     creatorId: Int
     creatorId: Int
     authorId: Int
     authorId: Int
-  ): [PageListItem!]!
+  ): [PageListItem]
 
 
   pageById(
   pageById(
     id: UUID!
     id: UUID!
@@ -261,10 +269,13 @@ type PageSearchResponse {
 }
 }
 
 
 type PageSearchResult {
 type PageSearchResult {
-  id: String
+  id: UUID
   title: String
   title: String
   description: String
   description: String
+  icon: String
   path: String
   path: String
+  tags: [String]
+  updatedAt: Date
   locale: String
   locale: String
 }
 }
 
 
@@ -378,6 +389,12 @@ input PageTocDepthInput {
   max: Int!
   max: Int!
 }
 }
 
 
+enum PageSearchSort {
+  relevancy
+  title
+  updated
+}
+
 enum PageOrderBy {
 enum PageOrderBy {
   CREATED
   CREATED
   ID
   ID

+ 3 - 0
server/locales/en.json

@@ -1752,6 +1752,9 @@
   "profile.title": "Profile",
   "profile.title": "Profile",
   "profile.uploadNewAvatar": "Upload New Image",
   "profile.uploadNewAvatar": "Upload New Image",
   "profile.viewPublicProfile": "View Public Profile",
   "profile.viewPublicProfile": "View Public Profile",
+  "search.filters": "Filters",
+  "search.results": "Search Results",
+  "search.sortBy": "Sort By",
   "tags.clearSelection": "Clear Selection",
   "tags.clearSelection": "Clear Selection",
   "tags.currentSelection": "Current Selection",
   "tags.currentSelection": "Current Selection",
   "tags.locale": "Locale",
   "tags.locale": "Locale",

+ 32 - 20
ux/src/components/HeaderNav.vue

@@ -31,37 +31,34 @@ q-header.bg-header.text-white.site-header(
       )
       )
       q-input(
       q-input(
         dark
         dark
-        v-model='state.search'
+        v-model='siteStore.search'
         standout='bg-white text-dark'
         standout='bg-white text-dark'
         dense
         dense
         rounded
         rounded
         ref='searchField'
         ref='searchField'
         style='width: 100%;'
         style='width: 100%;'
         label='Search...'
         label='Search...'
+        @keyup.enter='onSearchEnter'
+        @focus='state.searchKbdShortcutShown = false'
+        @blur='state.searchKbdShortcutShown = true'
         )
         )
         template(v-slot:prepend)
         template(v-slot:prepend)
           q-icon(name='las la-search')
           q-icon(name='las la-search')
         template(v-slot:append)
         template(v-slot:append)
-          q-icon.cursor-pointer(
-            name='las la-times'
-            @click='state.search=``'
-            v-if='state.search.length > 0'
-            :color='$q.dark.isActive ? `blue` : `grey-4`'
-            )
-          q-badge.q-ml-sm(
+          q-badge.q-mr-sm(
+            v-if='state.searchKbdShortcutShown'
             label='Ctrl+K'
             label='Ctrl+K'
             color='grey-7'
             color='grey-7'
             outline
             outline
             @click='searchField.focus()'
             @click='searchField.focus()'
             )
             )
-      //- q-btn.q-ml-md(
-      //-   flat
-      //-   round
-      //-   dense
-      //-   icon='las la-tags'
-      //-   color='grey'
-      //-   to='/_tags'
-      //-   )
+          q-icon.cursor-pointer(
+            name='las la-times'
+            size='20px'
+            @click='siteStore.search=``'
+            v-if='siteStore.search.length > 0'
+            color='grey-6'
+            )
     q-toolbar(
     q-toolbar(
       style='height: 64px;'
       style='height: 64px;'
       dark
       dark
@@ -124,17 +121,18 @@ q-header.bg-header.text-white.site-header(
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import AccountMenu from './AccountMenu.vue'
-import NewMenu from './PageNewMenu.vue'
-
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
 import { useQuasar } from 'quasar'
 import { useQuasar } from 'quasar'
 import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
 import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
 
 
 import { useCommonStore } from 'src/stores/common'
 import { useCommonStore } from 'src/stores/common'
 import { useSiteStore } from 'src/stores/site'
 import { useSiteStore } from 'src/stores/site'
 import { useUserStore } from 'src/stores/user'
 import { useUserStore } from 'src/stores/user'
 
 
+import AccountMenu from 'src/components/AccountMenu.vue'
+import NewMenu from 'src/components/PageNewMenu.vue'
+
 // QUASAR
 // QUASAR
 
 
 const $q = useQuasar()
 const $q = useQuasar()
@@ -145,6 +143,11 @@ const commonStore = useCommonStore()
 const siteStore = useSiteStore()
 const siteStore = useSiteStore()
 const userStore = useUserStore()
 const userStore = useUserStore()
 
 
+// ROUTER
+
+const router = useRouter()
+const route = useRoute()
+
 // I18N
 // I18N
 
 
 const { t } = useI18n()
 const { t } = useI18n()
@@ -152,7 +155,7 @@ const { t } = useI18n()
 // DATA
 // DATA
 
 
 const state = reactive({
 const state = reactive({
-  search: ''
+  searchKbdShortcutShown: true
 })
 })
 
 
 const searchField = ref(null)
 const searchField = ref(null)
@@ -172,12 +175,21 @@ function handleKeyPress (ev) {
   }
   }
 }
 }
 
 
+function onSearchEnter () {
+  if (!route.path.startsWith('/_search')) {
+    router.push({ path: '/_search', query: { q: siteStore.search } })
+  }
+}
+
 // MOUNTED
 // MOUNTED
 
 
 onMounted(() => {
 onMounted(() => {
   if (process.env.CLIENT) {
   if (process.env.CLIENT) {
     window.addEventListener('keydown', handleKeyPress)
     window.addEventListener('keydown', handleKeyPress)
   }
   }
+  if (route.path.startsWith('/_search')) {
+    searchField.value.focus()
+  }
 })
 })
 onBeforeUnmount(() => {
 onBeforeUnmount(() => {
   if (process.env.CLIENT) {
   if (process.env.CLIENT) {

+ 330 - 0
ux/src/pages/Search.vue

@@ -0,0 +1,330 @@
+<template lang="pug">
+q-layout(view='hHh Lpr lff')
+  header-nav
+  q-page-container.layout-search
+    .layout-search-card
+      .layout-search-sd
+        .text-header {{ t('search.sortBy') }}
+        q-list(dense, padding)
+          q-item(clickable, active)
+            q-item-section(side)
+              q-icon(name='las la-stream', color='primary')
+            q-item-section
+              q-item-label Relevance
+            q-item-section(side)
+              q-icon(name='mdi-chevron-double-down', size='sm', color='primary')
+          q-item(clickable)
+            q-item-section(side)
+              q-icon(name='las la-heading')
+            q-item-section Title
+          q-item(clickable)
+            q-item-section(side)
+              q-icon(name='las la-calendar')
+            q-item-section Last Updated
+        .text-header {{ t('search.filters') }}
+        .q-pa-sm
+          q-input(
+            outlined
+            dense
+            placeholder='Path starting with...'
+            prefix='/'
+            v-model='state.filterPath'
+            )
+            template(v-slot:prepend)
+              q-icon(name='las la-caret-square-right', size='xs')
+          q-input.q-mt-sm(
+            outlined
+            dense
+            placeholder='Tags'
+            )
+            template(v-slot:prepend)
+              q-icon(name='las la-hashtag', size='xs')
+          q-input.q-mt-sm(
+            outlined
+            dense
+            placeholder='Last updated...'
+            )
+            template(v-slot:prepend)
+              q-icon(name='las la-calendar', size='xs')
+          q-input.q-mt-sm(
+            outlined
+            dense
+            placeholder='Last edited by...'
+            )
+            template(v-slot:prepend)
+              q-icon(name='las la-user-edit', size='xs')
+          q-select.q-mt-sm(
+            outlined
+            v-model='state.filterLocale'
+            emit-value
+            map-options
+            dense
+            :aria-label='t(`admin.groups.ruleLocales`)'
+            :options='siteStore.locales.active'
+            option-value='code'
+            option-label='name'
+            multiple
+            :display-value='t(`admin.groups.selectedLocales`, { n: state.filterLocale.length > 0 ? state.filterLocale[0].toUpperCase() : state.filterLocale.length }, state.filterLocale.length)'
+            )
+            template(v-slot:prepend)
+              q-icon(name='las la-language', size='xs')
+          q-select.q-mt-sm(
+            outlined
+            v-model='state.filterEditor'
+            emit-value
+            map-options
+            dense
+            aria-label='Editor'
+            :options='editors'
+            )
+            template(v-slot:prepend)
+              q-icon(name='las la-pen-nib', size='xs')
+          q-select.q-mt-sm(
+            outlined
+            v-model='state.filterPublishState'
+            emit-value
+            map-options
+            dense
+            aria-label='Publish State'
+            :options='publishStates'
+            )
+            template(v-slot:prepend)
+              q-icon(name='las la-traffic-light', size='xs')
+      q-page(:style-fn='pageStyle')
+        .text-header.flex
+          span {{t('search.results')}}
+          q-space
+          span.text-caption #[strong 12] results
+        q-list(separator, padding)
+          q-item(v-for='item of 12', clickable)
+            q-item-section(avatar)
+              q-avatar(color='primary' text-color='white' rounded icon='las la-file-alt')
+            q-item-section
+              q-item-label Page ABC def {{ item }}
+              q-item-label(caption) Lorem ipsum beep boop foo bar
+              q-item-label(caption) ...Abc def #[span.text-highlight home] efg hig klm...
+            q-item-section(side)
+              .flex
+                q-chip(
+                  v-for='tag of 3'
+                  square
+                  :color='$q.dark.isActive ? `dark-2` : `grey-3`'
+                  :text-color='$q.dark.isActive ? `grey-4` : `grey-8`'
+                  icon='las la-hashtag'
+                  size='sm'
+                  ) tag {{ tag }}
+              .flex
+                .text-caption.q-mr-sm.text-grey /beep/boop/hello
+                .text-caption 2023-01-25
+
+      q-inner-loading(:showing='state.loading > 0')
+  main-overlay-dialog
+  footer-nav
+</template>
+
+<script setup>
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { computed, onMounted, reactive, watch } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+
+import { useFlagsStore } from 'src/stores/flags'
+import { useSiteStore } from 'src/stores/site'
+import { useUserStore } from 'src/stores/user'
+
+import HeaderNav from 'src/components/HeaderNav.vue'
+import FooterNav from 'src/components/FooterNav.vue'
+import MainOverlayDialog from 'src/components/MainOverlayDialog.vue'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const flagsStore = useFlagsStore()
+const siteStore = useSiteStore()
+const userStore = useUserStore()
+
+// ROUTER
+
+const router = useRouter()
+const route = useRoute()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  titleTemplate: title => `${title} - ${t('profile.title')} - Wiki.js`
+})
+
+// DATA
+
+const state = reactive({
+  loading: 0,
+  filterPath: '',
+  filterTags: [],
+  filterLocale: ['en'],
+  filterEditor: '',
+  filterPublishState: ''
+})
+
+const editors = computed(() => {
+  return [
+    { label: 'Any editor', value: '' },
+    { label: 'AsciiDoc', value: 'asciidoc' },
+    { label: 'Markdown', value: 'markdown' },
+    { label: 'Visual Editor', value: 'wysiwyg' }
+  ]
+})
+
+const publishStates = computed(() => {
+  return [
+    { label: 'Any publish state', value: '' },
+    { label: 'Draft', value: 'draft' },
+    { label: 'Published', value: 'published' },
+    { label: 'Scheduled', value: 'scheduled' }
+  ]
+})
+
+// WATCHERS
+
+watch(() => route.query, async (newQueryObj) => {
+  if (newQueryObj.q) {
+    siteStore.search = newQueryObj.q
+  }
+}, { immediate: true })
+
+// METHODS
+
+function pageStyle (offset, height) {
+  return {
+    'min-height': `${height - 100 - offset}px`
+  }
+}
+</script>
+
+<style lang="scss">
+.layout-search {
+  @at-root .body--light & {
+    background-color: $grey-3;
+  }
+  @at-root .body--dark & {
+    background-color: $dark-6;
+  }
+
+  &:before {
+    content: '';
+    height: 200px;
+    position: fixed;
+    top: 0;
+    width: 100%;
+    background: radial-gradient(ellipse at bottom, $dark-3, $dark-6);
+    border-bottom: 1px solid #FFF;
+
+    @at-root .body--dark & {
+      border-bottom-color: $dark-3;
+    }
+  }
+
+  &:after {
+    content: '';
+    height: 1px;
+    position: fixed;
+    top: 64px;
+    width: 100%;
+    background: linear-gradient(to right, transparent 0%, rgba(255,255,255,.1) 50%, transparent 100%);
+  }
+
+  &-card {
+    position: relative;
+    width: 90%;
+    max-width: 1400px;
+    margin: 50px auto;
+    box-shadow: $shadow-2;
+    border-radius: 7px;
+    display: flex;
+    align-items: stretch;
+    height: 100%;
+
+    @at-root .body--light & {
+      background-color: #FFF;
+    }
+    @at-root .body--dark & {
+      background-color: $dark-3;
+    }
+  }
+
+  &-sd {
+    flex: 0 0 300px;
+    border-radius: 8px 0 0 8px;
+    overflow: hidden;
+
+    @at-root .body--light & {
+      background-color: $grey-1;
+      border-right: 1px solid rgba($dark-3, .1);
+      box-shadow: inset -1px 0 0 #FFF;
+    }
+    @at-root .body--dark & {
+      background-color: $dark-4;
+      border-right: 1px solid rgba(#FFF, .12);
+      box-shadow: inset -1px 0 0 rgba($dark-6, .5);
+    }
+  }
+
+  .text-header {
+    padding: .75rem 1rem;
+    font-weight: 500;
+
+    @at-root .body--light & {
+      background-color: $grey-1;
+      border-bottom: 1px solid $grey-3;
+    }
+    @at-root .body--dark & {
+      background-color: $dark-3;
+      border-bottom: 1px solid $dark-2;
+    }
+  }
+
+  .text-highlight {
+    background-color: rgba($yellow-7, .5);
+    padding: 0 3px;
+    border-radius: 3px;
+  }
+
+  .q-page {
+    flex: 1 1;
+
+    .text-header:first-child {
+      border-top-right-radius: 7px;
+    }
+
+    @at-root .body--light & {
+      border-left: 1px solid #FFF;
+    }
+    @at-root .body--dark & {
+      border-left: 1px solid rgba($dark-6, .75);
+    }
+  }
+}
+
+body.body--dark {
+  background-color: $dark-6;
+}
+
+.q-footer {
+  .q-bar {
+    @at-root .body--light & {
+      background-color: $grey-3;
+      color: $grey-7;
+    }
+    @at-root .body--dark & {
+      background-color: $dark-4;
+      color: rgba(255,255,255,.3);
+    }
+  }
+}
+</style>

+ 4 - 0
ux/src/router/routes.js

@@ -32,6 +32,10 @@ const routes = [
       { path: 'groups', component: () => import('src/pages/ProfileGroups.vue') }
       { path: 'groups', component: () => import('src/pages/ProfileGroups.vue') }
     ]
     ]
   },
   },
+  {
+    path: '/_search',
+    component: () => import('src/pages/Search.vue')
+  },
   {
   {
     path: '/_admin',
     path: '/_admin',
     component: () => import('layouts/AdminLayout.vue'),
     component: () => import('layouts/AdminLayout.vue'),