Browse Source

Рефакторинг + добавлено формирование и парсинг параметров в URL

Book Pauk 2 years ago
parent
commit
9d3a1e45da

+ 106 - 62
client/components/Search/Search.vue

@@ -42,17 +42,17 @@
                 </div>
                 <div class="row q-mx-md q-mb-sm items-center">
                     <q-input
-                        ref="authorInput" v-model="author" :maxlength="5000" :debounce="inputDebounce"
+                        ref="authorInput" v-model="search.author" :maxlength="5000" :debounce="inputDebounce"
                         class="bg-white q-mt-xs" style="width: 300px;" label="Автор" stack-label outlined dense clearable
                     />
                     <div class="q-mx-xs" />
                     <q-input
-                        v-model="series" :maxlength="inputMaxLength" :debounce="inputDebounce"
+                        v-model="search.series" :maxlength="inputMaxLength" :debounce="inputDebounce"
                         class="bg-white q-mt-xs" style="width: 200px;" label="Серия" stack-label outlined dense clearable
                     />
                     <div class="q-mx-xs" />
                     <q-input
-                        v-model="title" :maxlength="inputMaxLength" :debounce="inputDebounce"
+                        v-model="search.title" :maxlength="inputMaxLength" :debounce="inputDebounce"
                         class="bg-white q-mt-xs" style="width: 200px;" label="Название" stack-label outlined dense clearable
                     />
                     <div class="q-mx-xs" />
@@ -67,12 +67,12 @@
                     </q-input>
                     <div class="q-mx-xs" />
                     <q-input
-                        v-model="lang" :maxlength="inputMaxLength" :debounce="inputDebounce"
+                        v-model="search.lang" :maxlength="inputMaxLength" :debounce="inputDebounce"
                         class="bg-white q-mt-xs" input-style="cursor: pointer" style="width: 80px;" label="Язык" stack-label outlined dense clearable readonly
                         @click="selectLang"
                     >
-                        <q-tooltip v-if="lang" :delay="500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
-                            {{ lang }}
+                        <q-tooltip v-if="search.lang" :delay="500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
+                            {{ search.lang }}
                         </q-tooltip>
                     </q-input>
 
@@ -101,10 +101,10 @@
                 </div>
             </div>
 
-            <div v-if="pageCount > 1" class="row justify-center">
-                <PageScroller v-model="page" :page-count="pageCount" />
+            <div v-show="pageCount > 1" class="row justify-center">
+                <PageScroller v-model="search.page" :page-count="pageCount" />
             </div>
-            <div v-else class="q-my-sm" />
+            <div v-show="pageCount <= 1" class="q-my-sm" />
 
             <!-- Формирование списка ------------------------------------------------------------------------>
             <div v-for="item in tableData" :key="item.key" class="column" :class="{'odd-author': item.num % 2}" style="font-size: 120%">
@@ -150,10 +150,10 @@
             </div>
             <!-- Формирование списка конец ------------------------------------------------------------------>
 
-            <div v-if="pageCount > 1" class="row justify-center">
-                <PageScroller v-model="page" :page-count="pageCount" />
+            <div v-show="pageCount > 1" class="row justify-center">
+                <PageScroller v-model="search.page" :page-count="pageCount" />
             </div>
-            <div v-else class="q-my-sm" />
+            <div v-show="pageCount <= 1" class="q-my-sm" />
         </div>
 
         <Dialog v-model="settingsDialogVisible">
@@ -188,8 +188,8 @@
             </template>
         </Dialog>
 
-        <SelectGenreDialog v-model="selectGenreDialogVisible" v-model:genre="genre" :genre-tree="genreTree" />
-        <SelectLangDialog v-model="selectLangDialogVisible" v-model:lang="lang" :lang-list="langList" :lang-default="langDefault" />        
+        <SelectGenreDialog v-model="selectGenreDialogVisible" v-model:genre="search.genre" :genre-tree="genreTree" />
+        <SelectLangDialog v-model="selectLangDialogVisible" v-model:lang="search.lang" :lang-list="langList" :lang-default="langDefault" />        
     </div>
 </template>
 
@@ -207,6 +207,7 @@ import DivBtn from '../share/DivBtn.vue';
 import Dialog from '../share/Dialog.vue';
 
 import * as utils from '../../share/utils';
+import diffUtils from '../../share/diffUtils';
 
 import _ from 'lodash';
 
@@ -225,29 +226,17 @@ const componentOptions = {
         settings() {
             this.loadSettings();
         },
-        author() {
-            this.refresh();
-        },
-        series() {
-            this.refresh();
-        },
-        title() {
-            this.refresh();
-        },
-        genre() {
-            this.refresh();
-        },
-        lang() {
-            this.refresh();
-        },
-        page() {
-            this.refresh();
+        search: {
+            handler(newValue) {
+                this.limit = newValue.limit;
+                this.refresh();
+            },
+            deep: true,
         },
         limit(newValue) {
             this.setSetting('limit', newValue);
 
             this.updatePageCount();
-            this.refresh();
         },
         showCounts(newValue) {
             this.setSetting('showCounts', newValue);
@@ -261,6 +250,12 @@ const componentOptions = {
         totalFound() {
             this.updatePageCount();
         },
+        $route(to) {
+            this.updateQueryFromRoute(to);
+        },
+        langDefault() {
+            this.updateQueryFromRoute(this.$route);
+        },
     },
 };
 class Search {
@@ -274,7 +269,6 @@ class Search {
     selectGenreDialogVisible = false;
     selectLangDialogVisible = false;
 
-    page = 1;
     pageCount = 1;
 
     //input field consts 
@@ -282,12 +276,15 @@ class Search {
     inputDebounce = 200;
 
     //search fields
-    author = '';
-    series = '';
-    title = '';
-    genre = '';
-    lang = '';
-    limit = 50;//settings
+    search = {
+        author: '',
+        series: '',
+        title: '',
+        genre: '',
+        lang: '',
+        page: 1,
+        limit: 50,
+    };
 
     //settings
     expanded = [];
@@ -295,6 +292,7 @@ class Search {
     showDeleted = false;
     abCacheEnabled = true;
     langDefault = '';
+    limit = 50;
 
     //stuff
     queryFound = -1;
@@ -334,8 +332,10 @@ class Search {
                 this.$refs.authorInput.focus();
 
             this.setDefaults();
+            this.updateQueryFromRoute(this.$route);
 
-            //query from url parse
+            //чтоб не вызывался лишний refresh
+            await utils.sleep(100);
 
             this.ready = true;
             this.refresh();//no await
@@ -345,7 +345,7 @@ class Search {
     loadSettings() {
         const settings = this.settings;
 
-        this.limit = settings.limit;
+        this.search.limit = settings.limit;
         this.expanded = _.cloneDeep(settings.expanded);
         this.showCounts = settings.showCounts;
         this.showDeleted = settings.showDeleted;
@@ -363,7 +363,7 @@ class Search {
 
     get genreNames() {
         let result = [];
-        const genre = new Set(this.genre.split(','));
+        const genre = new Set(this.search.genre.split(','));
 
         for (const section of this.genreTree) {
             for (const g of section.value)
@@ -444,19 +444,28 @@ class Search {
     }
 
     updatePageCount() {
+        const prevPageCount = this.pageCount;
+
         this.pageCount = Math.ceil(this.totalFound/this.limit);
         this.pageCount = (this.pageCount < 1 ? 1 : this.pageCount);
-        if (this.page > this.pageCount)
-            this.page = 1;
+
+        if (this.prevPage && prevPageCount == 1 && this.pageCount > 1 && this.prevPage <= this.pageCount) {
+            this.search.page = this.prevPage;
+        }
+
+        if (this.search.page > this.pageCount) {
+            this.prevPage = this.search.page;
+            this.search.page = 1;
+        }
     }
 
     selectAuthor(author) {
-        this.author = `=${author}`;
+        this.search.author = `=${author}`;
         this.scrollToTop();
     }
 
     selectTitle(title) {
-        this.title = `=${title}`;
+        this.search.title = `=${title}`;
     }
 
     isExpanded(item) {
@@ -636,28 +645,63 @@ class Search {
     }
 
     setDefaults() {
-        this.author = '';
-        this.series = '';
-        this.title = '';
-        this.genre = '';
-        this.lang = this.langDefault;
+        this.search = Object.assign({}, this.search, {
+            author: '',
+            series: '',
+            title: '',
+            genre: '',
+            lang: this.langDefault,
+        });
+    }
+
+    async updateQueryFromRoute(to) {
+        if (this.routeUpdating)
+            return;
+
+        const query = to.query;
+
+        this.search = Object.assign({}, this.search, {
+            author: query.author || '',
+            series: query.series || '',
+            title: query.title || '',
+            genre: query.genre || '',
+            lang: (query.lang == 'default' ? this.langDefault : query.lang || ''),
+            page: parseInt(query.page, 10) || 1,
+        });
+    }
+
+    updateRouteQuery() {
+        this.routeUpdating = true;
+        try {
+            const oldQuery = this.$route.query;
+            const query = _.pickBy(this.search);
+            delete query.limit;
+            if (this.search.lang == this.langDefault)
+                query.lang = 'default'
+
+            const diff = diffUtils.getObjDiff(oldQuery, query);
+            if (!diffUtils.isEmptyObjDiff(diff)) {
+                this.$router.replace({query});
+            }
+        } finally {
+            (async() => {
+                await utils.sleep(100);
+                this.routeUpdating = false;
+            })();
+        }
     }
 
     async refresh() {
         if (!this.ready)
             return;
 
-        const offset = (this.page - 1)*this.limit;
-
-        const newQuery = {
-            author: this.author,
-            series: this.series,
-            title: this.title,
-            genre: this.genre,
-            lang:  this.lang,
-            limit: this.limit,
-            offset,
-        };
+        this.updateRouteQuery();
+
+        const offset = (this.search.page - 1)*this.limit;
+
+        const newQuery = _.cloneDeep(this.search);
+        newQuery.limit = this.limit;
+        newQuery.offset = offset;
 
         this.queryExecute = newQuery;
 

+ 1 - 0
client/components/Search/SelectGenreDialog/SelectGenreDialog.vue

@@ -86,6 +86,7 @@ class GenreSelectDialog {
     }
 
     mounted() {
+        this.updateTicked();
     }
 
     async init() {

+ 1 - 0
client/components/Search/SelectLangDialog/SelectLangDialog.vue

@@ -96,6 +96,7 @@ class SelectLangDialog {
     }
 
     mounted() {
+        this.updateTicked();
     }
 
     async init() {

+ 19 - 8
client/components/share/NumInput.vue

@@ -49,17 +49,18 @@ import * as utils from '../../share/utils';
 
 const componentOptions = {
     watch: {
-        filteredValue: function(newValue) {
-            if (this.validate(newValue)) {
-                this.error = false;
-                this.$emit('update:modelValue', this.string2number(newValue));
-            } else {
-                this.error = true;
-            }
+        filteredValue() {
+            this.checkErrorAndEmit(true);
         },
-        modelValue: function(newValue) {
+        modelValue(newValue) {
             this.filteredValue = newValue;
         },
+        min() {
+            this.checkErrorAndEmit();
+        },
+        max() {
+            this.checkErrorAndEmit();
+        }
     }
 };
 class NumInput {
@@ -97,6 +98,16 @@ class NumInput {
         return true;
     }
 
+    checkErrorAndEmit(emit = false) {
+        if (this.validate(this.filteredValue)) {
+            this.error = false;
+            if (emit)
+                this.$emit('update:modelValue', this.string2number(this.filteredValue));
+        } else {
+            this.error = true;
+        }
+    }
+
     plus() {
         const newValue = this.modelValue + this.step;
         if (this.validate(newValue))

+ 143 - 0
client/share/diffUtils.js

@@ -0,0 +1,143 @@
+const _ = require('lodash');
+
+function getObjDiff(oldObj, newObj, opts = {}) {
+    const {
+        exclude = [],
+        excludeAdd = [],
+        excludeDel = [],
+    } = opts;
+
+    const ex = new Set(exclude);
+    const exAdd = new Set(excludeAdd);
+    const exDel = new Set(excludeDel);
+
+    const makeObjDiff = (oldObj, newObj, keyPath) => {
+        const result = {__isDiff: true, change: {}, add: {}, del: []};
+
+        keyPath = `${keyPath}${keyPath ? '/' : ''}`;
+
+        for (const key of Object.keys(oldObj)) {
+            const kp = `${keyPath}${key}`;
+
+            if (newObj.hasOwnProperty(key)) {
+                if (ex.has(kp))
+                    continue;
+
+                if (!_.isEqual(oldObj[key], newObj[key])) {
+                    if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
+                        result.change[key] = makeObjDiff(oldObj[key], newObj[key], kp);
+                    } else {
+                        result.change[key] = _.cloneDeep(newObj[key]);
+                    }
+                }
+            } else {
+                if (exDel.has(kp))
+                    continue;
+                result.del.push(key);
+            }
+        }
+
+        for (const key of Object.keys(newObj)) {
+            const kp = `${keyPath}${key}`;
+            if (exAdd.has(kp))
+                continue;
+
+            if (!oldObj.hasOwnProperty(key)) {
+                result.add[key] = _.cloneDeep(newObj[key]);
+            }
+        }
+
+        return result;
+    }
+
+    return makeObjDiff(oldObj, newObj, '');
+}
+
+function isObjDiff(diff) {
+    return (_.isObject(diff) && diff.__isDiff && diff.change && diff.add && diff.del);
+}
+
+function isEmptyObjDiff(diff) {
+    return (!isObjDiff(diff) ||
+        !(Object.keys(diff.change).length ||
+          Object.keys(diff.add).length ||
+          diff.del.length
+        )
+    );
+}
+
+function isEmptyObjDiffDeep(diff, opts = {}) {
+    if (!isObjDiff(diff))
+        return true;
+
+    const {
+        isApplyChange = true,
+        isApplyAdd = true,
+        isApplyDel = true,
+    } = opts;
+
+    let notEmptyDeep = false;
+    const change = diff.change;
+    for (const key of Object.keys(change)) {
+        if (_.isObject(change[key]))
+            notEmptyDeep |= !isEmptyObjDiffDeep(change[key], opts);
+        else if (isApplyChange)
+            notEmptyDeep = true;
+    }
+
+    return !(
+        notEmptyDeep ||
+        (isApplyAdd && Object.keys(diff.add).length) ||
+        (isApplyDel && diff.del.length)
+    );
+}
+
+function applyObjDiff(obj, diff, opts = {}) {
+    const {
+        isAddChanged = false,
+        isApplyChange = true,
+        isApplyAdd = true,
+        isApplyDel = true,
+    } = opts;
+
+    let result = _.cloneDeep(obj);
+    if (!diff.__isDiff)
+        return result;
+
+    const change = diff.change;
+    for (const key of Object.keys(change)) {
+        if (result.hasOwnProperty(key)) {
+            if (_.isObject(change[key])) {
+                result[key] = applyObjDiff(result[key], change[key], opts);
+            } else {
+                if (isApplyChange)
+                    result[key] = _.cloneDeep(change[key]);
+            }
+        } else if (isAddChanged) {
+            result[key] = _.cloneDeep(change[key]);
+        }
+    }
+
+    if (isApplyAdd) {
+        for (const key of Object.keys(diff.add)) {
+            result[key] = _.cloneDeep(diff.add[key]);
+        }
+    }
+
+    if (isApplyDel && diff.del.length) {
+        for (const key of diff.del) {
+            delete result[key];
+        }
+        if (_.isArray(result))
+            result = result.filter(v => v);
+    }
+
+    return result;
+}
+
+module.exports = {
+    getObjDiff,
+    isObjDiff,
+    isEmptyObjDiff,
+    applyObjDiff,
+}