Browse Source

Работа над расширенным поиском

Book Pauk 2 years ago
parent
commit
0c07f638f9

+ 4 - 1
client/components/Search/BaseList.js

@@ -41,6 +41,7 @@ export default class BaseList {
     _props = {
         list: Object,
         search: Object,
+        extSearch: Object,
         genreMap: Object,
     };
     
@@ -68,6 +69,7 @@ export default class BaseList {
     tableData = [];
 
     created() {
+        this.isExtendedSearch = false;
         this.commit = this.$store.commit;
         this.api = this.$root.api;
 
@@ -516,7 +518,8 @@ export default class BaseList {
     }
 
     getQuery() {
-        let newQuery = _.cloneDeep(this.search);
+        const search = (this.isExtendedSearch ? this.extSearch : this.search);
+        let newQuery = _.cloneDeep(search);
         newQuery = newQuery.setDefaults(newQuery);
         delete newQuery.setDefaults;
 

+ 128 - 0
client/components/Search/ExtendedList/ExtendedList.vue

@@ -0,0 +1,128 @@
+<template>
+    <div>
+        <a ref="download" style="display: none;"></a>
+
+        <LoadingMessage :message="loadingMessage" z-index="2" />
+        <LoadingMessage :message="loadingMessage2" z-index="1" />
+
+        <!-- Формирование списка ------------------------------------------------------------------------>
+        <div v-for="item in tableData" :key="item.key" class="column" :class="{'odd-item': item.num % 2}" style="font-size: 120%">
+            <BookView
+                class="q-ml-md"
+                :book="item.book" mode="title" :genre-map="genreMap" :show-read-link="showReadLink" @book-event="bookEvent"
+            />
+        </div>
+        <!-- Формирование списка конец ------------------------------------------------------------------>
+
+        <div v-if="!refreshing && !tableData.length" class="row items-center q-ml-md" style="font-size: 120%">
+            <q-icon class="la la-meh q-mr-xs" size="28px" />
+            Поиск не дал результатов
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../vueComponent.js';
+import { reactive } from 'vue';
+
+import BaseList from '../BaseList';
+
+import * as utils from '../../../share/utils';
+
+import _ from 'lodash';
+
+class ExtendedList extends BaseList {
+    created() {
+        super.created();
+        this.isExtendedSearch = true;
+    }
+
+    get foundCountMessage() {
+        return `${this.list.totalFound} ссыл${utils.wordEnding(this.list.totalFound, 5)} на файл(ы)`;
+    }
+
+    async updateTableData() {
+        let result = [];
+
+        const books = this.searchResult.found;
+        if (!books)
+            return;
+
+        let num = 0;
+        for (const book of books) {
+            const item = reactive({
+                num: num++,
+                book,
+            });
+
+            result.push(item);
+        }
+
+        this.tableData = result;
+    }
+
+    async refresh() {
+        //параметры запроса
+        const newQuery = this.getQuery();
+        if (_.isEqual(newQuery, this.prevQuery))
+            return;
+        this.prevQuery = newQuery;
+
+        this.queryExecute = newQuery;
+
+        if (this.refreshing)
+            return;
+
+        this.refreshing = true;
+
+        (async() => {
+            await utils.sleep(500);
+            if (this.refreshing)
+                this.loadingMessage = 'Поиск книг...';
+        })();
+
+        try {
+            while (this.queryExecute) {
+                const query = this.queryExecute;
+                this.queryExecute = null;
+
+                try {
+                    const response = await this.api.bookSearch(query);
+
+                    this.list.queryFound = response.found.length;
+                    this.list.totalFound = response.totalFound;
+                    this.list.inpxHash = response.inpxHash;
+
+                    this.searchResult = response;
+
+                    await utils.sleep(1);
+                    if (!this.queryExecute) {
+                        await this.updateTableData();
+                        this.scrollToTop();
+                        this.highlightPageScroller(query);
+                    }
+                } catch (e) {
+                    this.$root.stdDialog.alert(e.message, 'Ошибка');
+                }
+            }
+        } finally {
+            this.refreshing = false;
+            this.loadingMessage = '';
+        }
+    }
+}
+
+export default vueComponent(ExtendedList);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.clickable2 {
+    cursor: pointer;
+}
+
+.odd-item {
+    background-color: #e8e8e8;
+}
+</style>

+ 120 - 28
client/components/Search/Search.vue

@@ -55,7 +55,7 @@
                         </template>
                     </DivBtn>
                 </div>
-                <div class="row q-mx-md q-mb-xs items-center">
+                <div v-show="!isExtendedSearch" class="row q-mx-md q-mb-xs items-center">
                     <DivBtn
                         class="text-grey-5 bg-yellow-1 q-mt-xs" :size="34" :icon-size="24" round
                         :icon="(extendedParams ? 'la la-angle-double-up' : 'la la-angle-double-down')"
@@ -121,7 +121,7 @@
                         </template>
                     </DivBtn>
                 </div>
-                <div v-show="extendedParams" class="row q-mx-md q-mb-xs items-center">
+                <div v-show="!isExtendedSearch && extendedParams" class="row q-mx-md q-mb-xs items-center">
                     <div style="width: 34px" />
                     <div class="q-mx-xs" />
                     <q-input
@@ -184,9 +184,39 @@
                         </q-tooltip>
                     </q-input>
                 </div>
-                <div v-show="!extendedParams && extendedParamsMessage" class="row q-mx-md items-center clickable" @click.stop.prevent="extendedParams = true">
+                <div v-show="!isExtendedSearch && !extendedParams && extendedParamsMessage" class="row q-mx-md items-center clickable" @click.stop.prevent="extendedParams = true">
                     +{{ extendedParamsMessage }}
                 </div>
+
+                <div v-show="isExtendedSearch" class="row q-mx-md q-mb-xs items-center">
+                    <q-input
+                        v-model="extSearchNames"
+                        class="col q-mt-xs" :bg-color="inputBgColor('extended')" input-style="cursor: pointer"
+                        style="min-width: 200px; max-width: 756px;" label="Расширенный поиск" stack-label outlined dense clearable readonly
+                        @click.stop.prevent="selectExtSearch"
+                    >
+                        <template v-if="extSearchNames" #append>
+                            <q-icon name="la la-times-circle" class="q-field__focusable-action" @click.stop.prevent="clearExtSearch" />
+                        </template>
+
+                        <q-tooltip v-if="extSearchNames && showTooltips" :delay="500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
+                            {{ extSearchNames }}
+                        </q-tooltip>
+                    </q-input>
+
+                    <div class="q-mx-xs" />
+                    <DivBtn
+                        class="text-grey-8 bg-yellow-1 q-mt-xs" :size="34" :icon-size="24" round
+                        icon="la la-level-up-alt"
+                        @click.stop.prevent="cloneSearch"
+                    >
+                        <template #tooltip>
+                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
+                                Клонировать поиск
+                            </q-tooltip>
+                        </template>
+                    </DivBtn>
+                </div>
             </div>
 
             <div class="row items-center q-ml-lg q-mt-sm">
@@ -202,7 +232,7 @@
             <!-- Формирование списка ------------------------------------------------------------------------>
             <div v-if="selectedListComponent">
                 <div class="separator" />
-                <component :is="selectedListComponent" ref="list" :list="list" :search="search" :genre-map="genreMap" @list-event="listEvent" />
+                <component :is="selectedListComponent" ref="list" :list="list" :search="search" :ext-search="extSearch" :genre-map="genreMap" @list-event="listEvent" />
                 <div class="separator" />
             </div>
             <!-- Формирование списка конец ------------------------------------------------------------------>
@@ -224,6 +254,7 @@
         <SelectLibRateDialog v-model="selectLibRateDialogVisible" v-model:librate="search.librate" />
         <SelectDateDialog v-model="selectDateDialogVisible" v-model:date="search.date" />
         <BookInfoDialog v-model="bookInfoDialogVisible" :book-info="bookInfo" />
+        <SelectExtSearchDialog v-model="selectExtSearchDialogVisible" v-model:ext-search="extSearch" />        
     </div>
 </template>
 
@@ -234,6 +265,7 @@ import vueComponent from '../vueComponent.js';
 import AuthorList from './AuthorList/AuthorList.vue';
 import SeriesList from './SeriesList/SeriesList.vue';
 import TitleList from './TitleList/TitleList.vue';
+import ExtendedList from './ExtendedList/ExtendedList.vue';
 
 import PageScroller from './PageScroller/PageScroller.vue';
 import SettingsDialog from './SettingsDialog/SettingsDialog.vue';
@@ -242,6 +274,7 @@ import SelectLangDialog from './SelectLangDialog/SelectLangDialog.vue';
 import SelectLibRateDialog from './SelectLibRateDialog/SelectLibRateDialog.vue';
 import SelectDateDialog from './SelectDateDialog/SelectDateDialog.vue';
 import BookInfoDialog from './BookInfoDialog/BookInfoDialog.vue';
+import SelectExtSearchDialog from './SelectExtSearchDialog/SelectExtSearchDialog.vue';
 
 import authorBooksStorage from './authorBooksStorage';
 import DivBtn from '../share/DivBtn.vue';
@@ -252,11 +285,13 @@ import diffUtils from '../../share/diffUtils';
 
 import _ from 'lodash';
 
+const maxLimit = 1000;
+
 const route2component = {
     'author': {component: 'AuthorList', label: 'Авторы'},
     'series': {component: 'SeriesList', label: 'Серии'},
     'title': {component: 'TitleList', label: 'Книги'},
-    'extended': {component: 'TitleList', label: 'Расширенный поиск'},
+    'extended': {component: 'ExtendedList', label: 'Расширенный поиск'},
 };
 
 const componentOptions = {
@@ -264,6 +299,7 @@ const componentOptions = {
         AuthorList,
         SeriesList,
         TitleList,
+        ExtendedList,
         PageScroller,
         SettingsDialog,
         SelectGenreDialog,
@@ -271,6 +307,7 @@ const componentOptions = {
         SelectLibRateDialog,
         SelectDateDialog,
         BookInfoDialog,
+        SelectExtSearchDialog,
         Dialog,
         DivBtn
     },
@@ -293,6 +330,12 @@ const componentOptions = {
                 this.makeTitle();
                 this.updateRouteQueryFromSearch();
                 this.updateSearchDate(true);
+
+                //extSearch
+                if (this.isExtendedSearch) {
+                    this.extSearch.page = newValue.page;
+                    this.extSearch.limit = newValue.limit;
+                }
             },
             deep: true,
         },
@@ -363,6 +406,7 @@ class Search {
     selectLibRateDialogVisible = false;
     selectDateDialogVisible = false;
     bookInfoDialogVisible = false;
+    selectExtSearchDialogVisible = false;
 
     pageCount = 1;    
 
@@ -381,11 +425,21 @@ class Search {
                 lang: search.lang || '',
                 date: search.date || '',
                 librate: search.librate || '',
+
                 page: search.page || 1,
                 limit: search.limit || 50,
             });
         },
-    };
+    };    
+
+    extSearch = {
+        setDefaults(search) {
+            return Object.assign({}, search, {
+                page: search.page || 1,
+                limit: search.limit || 50,
+            });
+        },
+    };    
 
     searchDate = '';
     prevManualDate = '';
@@ -429,6 +483,7 @@ class Search {
         this.api = this.$root.api;
 
         this.search = this.search.setDefaults(this.search);
+        this.extSearch = this.extSearch.setDefaults(this.extSearch);
         this.search.lang = this.langDefault;
 
         this.loadSettings();
@@ -550,6 +605,14 @@ class Search {
         return result.filter(s => s).join(', ');
     }
 
+    get isExtendedSearch() {
+        return this.selectedList === 'extended';
+    }
+
+    get extSearchNames() {
+        return '';
+    }
+
     inputBgColor(inp) {
         if (inp === this.selectedList)
             return 'white';
@@ -750,6 +813,14 @@ class Search {
         this.hideTooltip();
         this.selectLibRateDialogVisible = true;
     }
+
+    selectExtSearch() {
+        this.hideTooltip();
+        this.selectExtSearchDialogVisible = true;
+    }
+
+    clearExtSearch() {
+    }
     
     onScroll() {
         const curScrollTop = this.$refs.scroller.scrollTop;
@@ -862,22 +933,35 @@ class Search {
 
         const query = to.query;
 
-        this.search = this.search.setDefaults(
-            Object.assign({}, this.search, {
-                author: query.author,
-                series: query.series,
-                title: query.title,
-                genre: query.genre,
-                lang: (typeof(query.lang) == 'string' ? query.lang : this.langDefault),
-                date: query.date,
-                librate: query.librate,
-                page: parseInt(query.page, 10),
-                limit: parseInt(query.limit, 10) || this.search.limit,
-            })
-        );
-
-        if (this.search.limit > 1000)
-            this.search.limit = 1000;
+        if (!this.isExtendedSearch) {
+            this.search = this.search.setDefaults(
+                Object.assign({}, this.search, {
+                    author: query.author,
+                    series: query.series,
+                    title: query.title,
+                    genre: query.genre,
+                    lang: (typeof(query.lang) == 'string' ? query.lang : this.langDefault),
+                    date: query.date,
+                    librate: query.librate,
+
+                    page: parseInt(query.page, 10),
+                    limit: parseInt(query.limit, 10) || this.search.limit,
+                })
+            );
+
+            if (this.search.limit > maxLimit)
+                this.search.limit = maxLimit;
+        } else {
+            this.extSearch = this.extSearch.setDefaults(
+                Object.assign({}, this.extSearch, {
+                    page: parseInt(query.page, 10),
+                    limit: parseInt(query.limit, 10) || this.search.limit,
+                })
+            );
+
+            if (this.extSearch.limit > maxLimit)
+                this.extSearch.limit = maxLimit;
+        }
     }
 
     updateRouteQueryFromSearch() {
@@ -887,16 +971,24 @@ class Search {
         this.routeUpdating = true;
         try {
             const oldQuery = this.$route.query;
-            const cloned = _.cloneDeep(this.search);
+            let query = {};
+
+            if (!this.isExtendedSearch) {                
+                const cloned = _.cloneDeep(this.search);
 
-            delete cloned.setDefaults;
+                delete cloned.setDefaults;
 
-            const query = _.pickBy(cloned);
+                query = _.pickBy(cloned);
 
-            if (this.search.lang == this.langDefault) {
-                delete query.lang;
+                if (this.search.lang == this.langDefault) {
+                    delete query.lang;
+                } else {
+                    query.lang = this.search.lang;
+                }
             } else {
-                query.lang = this.search.lang;
+                const cloned = _.cloneDeep(this.extSearch);
+                delete cloned.setDefaults;
+                query = _.pickBy(cloned);
             }
 
             const diff = diffUtils.getObjDiff(oldQuery, query);

+ 67 - 0
client/components/Search/SelectExtSearchDialog/SelectExtSearchDialog.vue

@@ -0,0 +1,67 @@
+<template>
+    <Dialog ref="dialog" v-model="dialogVisible">
+        <template #header>
+            <div class="row items-center">
+                <div style="font-size: 110%">
+                    Расширенный поиск
+                </div>
+            </div>
+        </template>
+
+        <div ref="box" class="column q-mt-xs overflow-auto no-wrap" style="width: 200px; padding: 0px 10px 10px 10px;">
+        </div>
+
+        <template #footer>
+            <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
+                OK
+            </q-btn>
+        </template>
+    </Dialog>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../vueComponent.js';
+
+import Dialog from '../../share/Dialog.vue';
+
+const componentOptions = {
+    components: {
+        Dialog
+    },
+    watch: {
+        modelValue(newValue) {
+            this.dialogVisible = newValue;
+        },
+        dialogVisible(newValue) {
+            this.$emit('update:modelValue', newValue);
+        },
+    }
+};
+class SelectExtSearchDialog {
+    _options = componentOptions;
+    _props = {
+        modelValue: Boolean,
+        extSearch: Object,
+    };
+
+    dialogVisible = false;
+
+    created() {
+        this.commit = this.$store.commit;
+    }
+
+    mounted() {
+    }
+
+    okClick() {
+        this.dialogVisible = false;
+    }
+}
+
+export default vueComponent(SelectExtSearchDialog);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+</style>

+ 8 - 8
client/share/utils.js

@@ -36,14 +36,14 @@ export function keyEventToCode(event) {
 
 export function wordEnding(num, type = 0) {
     const endings = [
-        ['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
-        ['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й'],
-        ['о', '', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о'],
-        ['ий', 'ие', 'ия', 'ия', 'ия', 'ий', 'ий', 'ий', 'ий', 'ий'],
-        ['о', 'а', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о'],
-        ['ок', 'ка', 'ки', 'ки', 'ки', 'ок', 'ок', 'ок', 'ок', 'ок'],
-        ['ых', 'ое', 'ых', 'ых', 'ых', 'ых', 'ых', 'ых', 'ых', 'ых'],
-        ['о', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о'],
+        ['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],//0
+        ['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й'],//1
+        ['о', '', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о'],//2
+        ['ий', 'ие', 'ия', 'ия', 'ия', 'ий', 'ий', 'ий', 'ий', 'ий'],//3
+        ['о', 'а', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о'],//4
+        ['ок', 'ка', 'ки', 'ки', 'ки', 'ок', 'ок', 'ок', 'ок', 'ок'],//5
+        ['ых', 'ое', 'ых', 'ых', 'ых', 'ых', 'ых', 'ых', 'ых', 'ых'],//6
+        ['о', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о'],//7
     ];
     const deci = num % 100;
     if (deci > 10 && deci < 20) {

+ 2 - 0
server/core/DbSearcher.js

@@ -571,6 +571,8 @@ class DbSearcher {
                         checks.push(filterBySearch(f.field, searchValue));
                     } if (f.type === 'N') {
                         searchValue = parseInt(searchValue, 10);
+                        if (isNaN(searchValue))
+                            throw new Error(`Wrong query param, ${f.field}=${searchValue}`);
                         checks.push(`row.${f.field} === ${searchValue}`);
                     }
                 }