Browse Source

Каркас будущего приложения

Book Pauk 2 years ago
parent
commit
78be5a9856

+ 0 - 4
build/webpack.base.config.js

@@ -21,10 +21,6 @@ module.exports = {
                 test: /\.vue$/,
                 loader: 'vue-loader',
             },
-            {
-                resourceQuery: /^\?vue/,
-                use: path.resolve(__dirname, 'includer.js')
-            },
             {
                 test: /\.js$/,
                 loader: 'babel-loader',

+ 2 - 1
build/webpack.dev.config.js

@@ -1,5 +1,6 @@
 const path = require('path');
 const webpack = require('webpack');
+const pckg = require('../package.json');
 
 const { merge } = require('webpack-merge');
 const baseWpConfig = require('./webpack.base.config');
@@ -8,7 +9,7 @@ baseWpConfig.entry.unshift('webpack-hot-middleware/client');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const CopyWebpackPlugin = require('copy-webpack-plugin');
 
-const publicDir = path.resolve(__dirname, '../server/public');
+const publicDir = path.resolve(__dirname, `../server/.${pckg.name}/public`);
 const clientDir = path.resolve(__dirname, '../client');
 
 module.exports = merge(baseWpConfig, {

+ 2 - 0
client/assets/robots.txt

@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /#

+ 141 - 0
client/components/App.vue

@@ -0,0 +1,141 @@
+<template>
+    <div class="fit row">
+        <Notify ref="notify" />
+        <StdDialog ref="stdDialog" />
+
+        <router-view v-slot="{ Component }">
+            <keep-alive>
+                <component :is="Component" class="col" />
+            </keep-alive>
+        </router-view>        
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from './vueComponent.js';
+
+//import * as utils from '../share/utils';
+import Notify from './share/Notify.vue';
+import StdDialog from './share/StdDialog.vue';
+
+import Search from './Search/Search.vue';
+
+const componentOptions = {
+    components: {
+        Notify,
+        StdDialog,
+
+        Search,
+    },
+    watch: {
+    },
+
+};
+class App {
+    _options = componentOptions;
+
+    created() {
+        //root route
+        let cachedRoute = '';
+        let cachedPath = '';
+        this.$root.getRootRoute = () => {
+            if (this.$route.path != cachedPath) {
+                cachedPath = this.$route.path;
+                const m = cachedPath.match(/^(\/[^/]*).*$/i);
+                cachedRoute = (m ? m[1] : this.$route.path);
+            }
+            return cachedRoute;
+        }
+
+        this.$root.isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
+
+        //global keyHooks
+        this.keyHooks = [];
+        this.keyHook = (event) => {
+            for (const hook of this.keyHooks)
+                hook(event);
+        }
+
+        this.$root.addKeyHook = (hook) => {
+            if (this.keyHooks.indexOf(hook) < 0)
+                this.keyHooks.push(hook);
+        }
+
+        this.$root.removeKeyHook = (hook) => {
+            const i = this.keyHooks.indexOf(hook);
+            if (i >= 0)
+                this.keyHooks.splice(i, 1);
+        }
+
+        document.addEventListener('keyup', (event) => {
+            this.keyHook(event);
+        });
+        document.addEventListener('keypress', (event) => {
+            this.keyHook(event);
+        });
+        document.addEventListener('keydown', (event) => {
+            this.keyHook(event);
+        });        
+    }
+
+    mounted() {
+        this.$root.notify = this.$refs.notify;
+        this.$root.stdDialog = this.$refs.stdDialog;
+
+        this.setAppTitle();
+    }
+
+    get rootRoute() {
+        return this.$root.getRootRoute();
+    }
+
+    setAppTitle(title) {
+        if (!title) {
+            document.title = 'inpx-web';
+        } else {
+            document.title = title;
+        }
+    }
+}
+
+export default vueComponent(App);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+</style>
+
+<style>
+body, html, #app {    
+    margin: 0;
+    padding: 0;
+    width: 100%;
+    height: 100%;
+    font: normal 12px GameDefault;
+}
+
+.dborder {
+    border: 2px solid yellow;
+}
+
+.icon-rotate {
+    vertical-align: middle;
+    animation: rotating 2s linear infinite;
+}
+
+@keyframes rotating { 
+    from { 
+        transform: rotate(0deg); 
+    } to { 
+        transform: rotate(360deg); 
+    }
+}
+
+@font-face {
+  font-family: 'GameDefault';
+  src: url('fonts/web-default.woff') format('woff'),
+       url('fonts/web-default.ttf') format('truetype');
+}
+
+</style>

+ 38 - 0
client/components/Search/Search.vue

@@ -0,0 +1,38 @@
+<template>
+    <div class="root row fit">
+        <div>Search</div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../vueComponent.js';
+
+//import _ from 'lodash';
+
+const componentOptions = {
+    components: {
+    },
+    watch: {
+    },
+};
+class Search {
+    _options = componentOptions;
+
+    created() {
+        this.commit = this.$store.commit;
+    }
+
+    mounted() {
+    }
+
+}
+
+export default vueComponent(Search);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.root {
+}
+</style>

BIN
client/components/fonts/web-default.ttf


BIN
client/components/fonts/web-default.woff


+ 80 - 0
client/components/share/Dialog.vue

@@ -0,0 +1,80 @@
+<template>
+    <q-dialog v-model="active" no-route-dismiss @show="onShow" @hide="onHide">
+        <div class="column bg-white no-wrap">
+            <div class="header row">
+                <div class="caption col row items-center q-ml-md">
+                    <slot name="header"></slot>
+                </div>
+                <div class="close-icon column justify-center items-center">
+                    <q-btn v-close-popup flat round dense>
+                        <q-icon name="la la-times" size="18px"></q-icon>
+                    </q-btn>
+                </div>
+            </div>
+
+            <div class="col q-mx-md">
+                <slot></slot>
+            </div>
+
+            <div class="row justify-end q-pa-md">
+                <slot name="footer"></slot>
+            </div>
+        </div>
+    </q-dialog>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../vueComponent.js';
+import * as utils from '../../share/utils';
+
+class Dialog {
+    _props = {
+        modelValue: Boolean,
+    };
+
+    shown = false;
+
+    get active() {
+        return this.modelValue;
+    }
+
+    set active(value) {
+        this.$emit('update:modelValue', value);
+    }
+
+    onShow() {
+        this.shown = true;
+    }
+
+    onHide() {
+        this.shown = false;
+    }
+
+    async waitShown() {
+        let i = 100;
+        while (!this.shown && i > 0) {
+            await utils.sleep(10);
+            i--;
+        }
+    }
+}
+
+export default vueComponent(Dialog);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.header {
+    height: 50px;
+}
+
+.caption {
+    font-size: 110%;
+    overflow: hidden;
+}
+
+.close-icon {
+    width: 50px;
+}
+</style>

+ 58 - 0
client/components/share/Notify.vue

@@ -0,0 +1,58 @@
+<template>
+    <div class="hidden"></div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../vueComponent.js';
+
+class Notify {
+    notify(opts) {
+        let {
+            caption = null,
+            captionColor = 'black',
+            color = 'positive',
+            icon = '',
+            iconColor = 'white',
+            message = '',
+            messageColor = 'black',
+            position = 'top-right',
+        } = opts;
+
+        caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : '');
+        return this.$q.notify({
+            position,
+            color,
+            textColor: iconColor,
+            icon,
+            actions: [{icon: 'la la-times notify-button-icon', color: 'black'}],
+            html: true,
+
+            message: 
+                `<div style="max-width: 350px;">
+                    ${caption}
+                    <div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div>
+                </div>`
+        });
+    }
+
+    success(message, caption, options) {
+        this.notify(Object.assign({color: 'positive', icon: 'la la-check-circle', message, caption}, options));
+    }
+
+    warning(message, caption, options) {
+        this.notify(Object.assign({color: 'warning', icon: 'la la-exclamation-circle', message, caption}, options));
+    }
+
+    error(message, caption, options) {
+        this.notify(Object.assign({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption}, options));
+    }
+
+    info(message, caption, options) {
+        this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options));
+    }
+}
+
+export default vueComponent(Notify);
+//-----------------------------------------------------------------------------
+</script>

+ 361 - 0
client/components/share/StdDialog.vue

@@ -0,0 +1,361 @@
+<template>
+    <q-dialog ref="dialog" v-model="active" no-route-dismiss @show="onShow" @hide="onHide">
+        <slot></slot>
+
+        <!--------------------------------------------------->
+        <div v-show="type == 'alert'" class="bg-white no-wrap">
+            <div class="header row">
+                <div class="caption col row items-center q-ml-md">
+                    <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
+                    <div v-html="caption"></div>
+                </div>
+                <div class="close-icon column justify-center items-center">
+                    <q-btn v-close-popup flat round dense>
+                        <q-icon name="la la-times" size="18px"></q-icon>
+                    </q-btn>
+                </div>
+            </div>
+
+            <div class="q-mx-md">
+                <div v-html="message"></div>
+            </div>
+
+            <div class="buttons row justify-end q-pa-md">
+                <q-btn class="q-px-md" dense no-caps @click="okClick">
+                    OK
+                </q-btn>
+            </div>
+        </div>
+
+        <!--------------------------------------------------->
+        <div v-show="type == 'confirm'" class="bg-white no-wrap">
+            <div class="header row">
+                <div class="caption col row items-center q-ml-md">
+                    <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
+                    <div v-html="caption"></div>
+                </div>
+                <div class="close-icon column justify-center items-center">
+                    <q-btn v-close-popup flat round dense>
+                        <q-icon name="la la-times" size="18px"></q-icon>
+                    </q-btn>
+                </div>
+            </div>
+
+            <div class="q-mx-md">
+                <div v-html="message"></div>
+            </div>
+
+            <div class="buttons row justify-end q-pa-md">
+                <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
+                    Отмена
+                </q-btn>
+                <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
+                    OK
+                </q-btn>
+            </div>
+        </div>
+
+        <!--------------------------------------------------->
+        <div v-show="type == 'prompt'" class="bg-white no-wrap">
+            <div class="header row">
+                <div class="caption col row items-center q-ml-md">
+                    <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
+                    <div v-html="caption"></div>
+                </div>
+                <div class="close-icon column justify-center items-center">
+                    <q-btn v-close-popup flat round dense>
+                        <q-icon name="la la-times" size="18px"></q-icon>
+                    </q-btn>
+                </div>
+            </div>
+
+            <div class="q-mx-md">
+                <div v-html="message"></div>
+                <q-input ref="input" v-model="inputValue" class="q-mt-xs" outlined dense />
+                <div class="error">
+                    <span v-show="error != ''">{{ error }}</span>
+                </div>
+            </div>
+
+            <div class="buttons row justify-end q-pa-md">
+                <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
+                    Отмена
+                </q-btn>
+                <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
+                    OK
+                </q-btn>
+            </div>
+        </div>
+
+        <!--------------------------------------------------->
+        <div v-show="type == 'hotKey'" class="bg-white no-wrap">
+            <div class="header row">
+                <div class="caption col row items-center q-ml-md">
+                    <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
+                    <div v-html="caption"></div>
+                </div>
+                <div class="close-icon column justify-center items-center">
+                    <q-btn v-close-popup flat round dense>
+                        <q-icon name="la la-times" size="18px"></q-icon>
+                    </q-btn>
+                </div>
+            </div>
+
+            <div class="q-mx-md">
+                <div v-html="message"></div>
+                <div class="q-my-md text-center">
+                    <div v-show="hotKeyCode == ''" class="text-grey-5">
+                        Нет
+                    </div>
+                    <div>{{ hotKeyCode }}</div>
+                </div>
+            </div>
+
+            <div class="buttons row justify-end q-pa-md">
+                <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
+                    Отмена
+                </q-btn>
+                <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps :disabled="hotKeyCode == ''" @click="okClick">
+                    OK
+                </q-btn>
+            </div>
+        </div>
+    </q-dialog>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../vueComponent.js';
+import * as utils from '../../share/utils';
+
+const componentOptions = {
+    watch: {
+        inputValue: function(newValue) {
+            this.validate(newValue);
+        },
+    }
+};
+class StdDialog {
+    _options = componentOptions;
+    caption = '';
+    message = '';
+    active = false;
+    type = '';
+    inputValue = '';
+    error = '';
+    iconColor = '';
+    iconName = '';
+    hotKeyCode = '';
+
+    created() {
+        if (this.$root.addEventHook) {
+            this.$root.addEventHook('key', this.keyHook);
+        }
+    }
+
+    init(message, caption, opts) {
+        this.caption = caption;
+        this.message = message;
+
+        this.ok = false;        
+        this.type = '';
+        this.inputValidator = null;
+        this.inputValue = '';
+        this.error = '';
+        this.showed = false;
+
+        this.iconColor = 'text-warning';
+        if (opts && opts.color) {
+            this.iconColor = `text-${opts.color}`;
+        }
+
+        this.iconName = 'las la-exclamation-circle';
+        if (opts && opts.iconName) {
+            this.iconName = opts.iconName;
+        }
+
+        this.hotKeyCode = '';
+        if (opts && opts.hotKeyCode) {
+            this.hotKeyCode = opts.hotKeyCode;
+        }
+    }
+
+    onHide() {
+        if (this.hideTrigger) {
+            this.hideTrigger();
+            this.hideTrigger = null;
+        }
+        this.showed = false;
+    }
+
+    onShow() {
+        if (this.type == 'prompt') {
+            this.enableValidator = true;
+            if (this.inputValue)
+                this.validate(this.inputValue);
+            this.$refs.input.focus();
+        }
+        this.showed = true;
+    }
+
+    validate(value) {
+        if (!this.enableValidator)
+            return false;
+
+        if (this.inputValidator) {
+            const result = this.inputValidator(value);
+            if (result !== true) {
+                this.error = result;
+                return false;
+            }
+        }
+        this.error = '';
+        return true;
+    }
+
+    okClick() {
+        if (this.type == 'prompt' && !this.validate(this.inputValue)) {
+            this.$refs.dialog.shake();
+            return;
+        }
+
+        if (this.type == 'hotKey' && this.hotKeyCode == '') {
+            this.$refs.dialog.shake();
+            return;
+        }
+
+        this.ok = true;
+        this.$refs.dialog.hide();
+    }
+
+    alert(message, caption, opts) {
+        return new Promise((resolve) => {
+            this.init(message, caption, opts);
+
+            this.hideTrigger = () => {
+                if (this.ok) {
+                    resolve(true);
+                } else {
+                    resolve(false);
+                }
+            };
+
+            this.type = 'alert';
+            this.active = true;
+        });
+    }
+
+    confirm(message, caption, opts) {
+        return new Promise((resolve) => {
+            this.init(message, caption, opts);
+
+            this.hideTrigger = () => {
+                if (this.ok) {
+                    resolve(true);
+                } else {
+                    resolve(false);
+                }
+            };
+
+            this.type = 'confirm';
+            this.active = true;
+        });
+    }
+
+    prompt(message, caption, opts) {
+        return new Promise((resolve) => {
+            this.enableValidator = false;
+            this.init(message, caption, opts);
+
+            this.hideTrigger = () => {
+                if (this.ok) {
+                    resolve({value: this.inputValue});
+                } else {
+                    resolve(false);
+                }
+            };
+
+            this.type = 'prompt';
+            if (opts) {
+                this.inputValidator = opts.inputValidator || null;
+                this.inputValue = opts.inputValue || '';
+            }
+            this.active = true;
+        });
+    }
+
+    getHotKey(message, caption, opts) {
+        return new Promise((resolve) => {
+            this.init(message, caption, opts);
+
+            this.hideTrigger = () => {
+                if (this.ok) {
+                    resolve(this.hotKeyCode);
+                } else {
+                    resolve(false);
+                }
+            };
+
+            this.type = 'hotKey';
+            this.active = true;
+        });
+    }
+
+    keyHook(event) {
+        if (this.active && this.showed) {
+            let handled = false;
+            if (this.type == 'hotKey') {
+                if (event.type == 'keydown') {
+                    this.hotKeyCode = utils.keyEventToCode(event);
+                    handled = true;
+                }
+            } else {
+                if (event.key == 'Enter') {
+                    this.okClick();
+                    handled = true;
+                }
+
+                if (event.key == 'Escape') {
+                    this.$nextTick(() => {
+                        this.$refs.dialog.hide();
+                    });
+                    handled = true;
+                }
+            }
+
+            if (handled) {
+                event.stopPropagation();
+                event.preventDefault();
+            }
+        }
+    }
+}
+
+export default vueComponent(StdDialog);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.header {
+    height: 50px;
+}
+
+.caption {
+    font-size: 110%;
+    overflow: hidden;
+}
+
+.close-icon {
+    width: 50px;
+}
+
+.buttons {
+    height: 60px;
+}
+
+.error {
+    height: 20px;
+    font-size: 80%;
+    color: red;
+}
+</style>

+ 52 - 0
client/components/vueComponent.js

@@ -0,0 +1,52 @@
+import { defineComponent } from 'vue';
+import _ from 'lodash';
+
+export default function(componentClass) {    
+    const comp = {};
+    const obj = new componentClass();
+    
+    //data, options, props
+    const data = {};
+    for (const prop of Object.getOwnPropertyNames(obj)) {
+        if (['_options', '_props'].includes(prop)) {//meta props
+            if (prop === '_options') {
+                const options = obj[prop];
+                for (const optName of ['components', 'watch', 'emits']) {
+                    if (options[optName]) {
+                        comp[optName] = options[optName];
+                    }
+                }
+            } else if (prop === '_props') {
+                comp['props'] = obj[prop];
+            }
+        } else {//usual prop
+            data[prop] = obj[prop];
+        }
+    }
+    comp.data = () => _.cloneDeep(data);
+    
+    //methods
+    const classProto = Object.getPrototypeOf(obj);
+    const classMethods = Object.getOwnPropertyNames(classProto);
+    const methods = {};
+    const computed = {};
+    for (const method of classMethods) {
+        const desc = Object.getOwnPropertyDescriptor(classProto, method);
+        if (desc.get) {//has getter, computed
+            computed[method] = {get: desc.get};
+            if (desc.set)
+                computed[method].set = desc.set;
+        } else if ( ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated',//life cycle hooks
+                    'deactivated', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered',//life cycle hooks
+                    'setup'].includes(method) ) {
+            comp[method] = obj[method];
+        } else if (method !== 'constructor') {//usual
+            methods[method] = obj[method];
+        }
+    }
+    comp.methods = methods;
+    comp.computed = computed;
+
+    //console.log(comp);
+    return defineComponent(comp);
+}

+ 11 - 0
client/index.html.template

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title></title>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+  </head>
+  <body>
+    <div id="app"></div>
+  </body>
+</html>

+ 16 - 0
client/main.js

@@ -0,0 +1,16 @@
+import { createApp } from 'vue';
+
+import router from './router';
+import store from './store';
+import q from './quasar';
+
+import App from './components/App.vue';
+
+const app = createApp(App);
+
+app.use(router);
+app.use(store);
+app.use(q.quasar, q.options);
+q.init();
+
+app.mount('#app');

+ 97 - 0
client/quasar.js

@@ -0,0 +1,97 @@
+import 'quasar/dist/quasar.css';
+
+import Quasar from 'quasar/src/vue-plugin.js';
+//config
+const config = {};
+
+//components
+//import {QLayout} from 'quasar/src/components/layout';
+//import {QPageContainer, QPage} from 'quasar/src/components/page';
+//import {QDrawer} from 'quasar/src/components/drawer';
+
+//import {QCircularProgress} from 'quasar/src/components/circular-progress';
+import {QInput} from 'quasar/src/components/input';
+import {QBtn} from 'quasar/src/components/btn';
+//import {QBtnGroup} from 'quasar/src/components/btn-group';
+//import {QBtnToggle} from 'quasar/src/components/btn-toggle';
+import {QIcon} from 'quasar/src/components/icon';
+//import {QSlider} from 'quasar/src/components/slider';
+//import {QTabs, QTab} from 'quasar/src/components/tabs';
+//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
+//import {QSeparator} from 'quasar/src/components/separator';
+//import {QList} from 'quasar/src/components/item';
+//import {QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
+//import {QTooltip} from 'quasar/src/components/tooltip';
+//import {QSpinner} from 'quasar/src/components/spinner';
+//import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
+//import {QCheckbox} from 'quasar/src/components/checkbox';
+//import {QSelect} from 'quasar/src/components/select';
+//import {QColor} from 'quasar/src/components/color';
+//import {QPopupProxy} from 'quasar/src/components/popup-proxy';
+import {QDialog} from 'quasar/src/components/dialog';
+//import {QChip} from 'quasar/src/components/chip';
+//import {QTree} from 'quasar/src/components/tree';
+//import {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
+
+//import {QExpansionItem} from 'quasar/src/components/expansion-item';
+
+const components = {
+    //QLayout,
+    //QPageContainer, QPage,
+    //QDrawer,
+
+    //QCircularProgress,
+    QInput,
+    QBtn,
+    //QBtnGroup,
+    //QBtnToggle,
+    QIcon,
+    //QSlider,
+    //QTabs, QTab,
+    //QTabPanels, QTabPanel,
+    //QSeparator,
+    //QList,
+    //QItem, QItemSection, QItemLabel,
+    //QTooltip,
+    //QSpinner,
+    //QTable, QTh, QTr, QTd,
+    //QCheckbox,
+    //QSelect,
+    //QColor,
+    //QPopupProxy,
+    QDialog,
+    //QChip,
+    //QTree,
+    //QExpansionItem,
+    //QVirtualScroll,
+};
+
+//directives 
+//import Ripple from 'quasar/src/directives/Ripple';
+import ClosePopup from 'quasar/src/directives/ClosePopup';
+
+const directives = {/*Ripple, */ClosePopup};
+
+//plugins
+//import AppFullscreen from 'quasar/src/plugins/AppFullscreen';
+//import Notify from 'quasar/src/plugins/Notify';
+
+const plugins = {
+    //AppFullscreen,
+    //Notify,
+};
+
+//icons
+//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
+//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
+
+import '@quasar/extras/line-awesome/line-awesome.css';
+import lineAwesome from 'quasar/icon-set/line-awesome.js'
+
+export default {
+    quasar: Quasar,
+    options: { config, components, directives, plugins }, 
+    init: () => {
+        Quasar.iconSet.set(lineAwesome);
+}
+};

+ 38 - 0
client/router.js

@@ -0,0 +1,38 @@
+import { createRouter, createWebHashHistory } from 'vue-router';
+import _ from 'lodash';
+
+const Search = () => import('./components/Search/Search.vue');
+
+const myRoutes = [
+    ['/', Search],
+    ['/:pathMatch(.*)*', null, null, '/'],
+];
+
+let routes = {};
+
+for (let route of myRoutes) {
+    const [path, component, name, redirect] = route;
+    let cleanRoute = _.pickBy({path, component, name, redirect}, _.identity);
+    
+    let parts = cleanRoute.path.split('~');
+    let f = routes;
+    for (let part of parts) {
+        const curRoute = _.assign({}, cleanRoute, { path: part });
+
+        if (!f.children)
+            f.children = [];
+        let r = f.children;
+
+        f = _.find(r, {path: part});
+        if (!f) {
+            r.push(curRoute);
+            f = curRoute;
+        }
+    }
+}
+routes = routes.children;
+
+export default createRouter({
+    history: createWebHashHistory(),
+    routes
+});

+ 30 - 0
client/share/utils.js

@@ -0,0 +1,30 @@
+//import _ from 'lodash';
+
+export function sleep(ms) {
+    return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+export function keyEventToCode(event) {
+    let result = [];
+    let code = event.code;
+
+    const modCode = code.substring(0, 3);
+    if (event.metaKey && modCode != 'Met')
+        result.push('Meta');
+    if (event.ctrlKey && modCode != 'Con')
+        result.push('Ctrl');
+    if (event.shiftKey && modCode != 'Shi')
+        result.push('Shift');
+    if (event.altKey && modCode != 'Alt')
+        result.push('Alt');
+    
+    if (modCode == 'Dig') {
+        code = code.substring(5, 6);
+    } else if (modCode == 'Key') {
+        code = code.substring(3, 4);
+    }
+    result.push(code);
+
+    return result.join('+');
+}
+

+ 15 - 0
client/store/index.js

@@ -0,0 +1,15 @@
+import { createStore } from 'vuex';
+import VuexPersistence from 'vuex-persist';
+
+import root from './root.js';
+
+const debug = process.env.NODE_ENV !== 'production';
+
+const vuexLocal = new VuexPersistence();
+
+export default createStore(Object.assign({}, root, {
+    modules: {
+    },
+    strict: debug,
+    plugins: [vuexLocal.plugin]
+}));

+ 25 - 0
client/store/root.js

@@ -0,0 +1,25 @@
+// initial state
+const state = {
+    apiError: null,
+};
+
+// getters
+const getters = {};
+
+// actions
+const actions = {};
+
+// mutations
+const mutations = {
+    setApiError(state, value) {
+        state.apiError = value;
+    },
+};
+
+export default {
+    namespaced: true,
+    state,
+    getters,
+    actions,
+    mutations
+};

+ 1 - 1
server/config/base.js

@@ -2,7 +2,7 @@ const path = require('path');
 const pckg = require('../../package.json');
 
 const execDir = path.resolve(__dirname, '..');
-const dataDir = `${execDir}/.${pckg.name}/data`;
+const dataDir = `${execDir}/.${pckg.name}`;
 
 module.exports = {
     branch: 'unknown',

+ 1 - 1
server/config/production.js

@@ -3,7 +3,7 @@ const pckg = require('../../package.json');
 const base = require('./base');
 
 const execDir = path.dirname(process.execPath);
-const dataDir = `${execDir}/.${pckg.name}/data`;
+const dataDir = `${execDir}/.${pckg.name}`;
 
 module.exports = Object.assign({}, base, {
     branch: 'production',

+ 1 - 1
server/dev.js

@@ -20,7 +20,7 @@ function webpackDevMiddleware(app) {
 function logQueries(app) {
     app.use(function(req, res, next) {
         const start = Date.now();
-        log(`${req.method} ${req.originalUrl} ${JSON.stringify(req.body).substr(0, 4000)}`);
+        log(`${req.method} ${req.originalUrl} ${JSON.stringify(req.body ? req.body : '').substr(0, 4000)}`);
         //log(`${JSON.stringify(req.headers, null, 2)}`)
         res.once('finish', () => {
             log(`${Date.now() - start}ms`);

+ 11 - 0
server/index.js

@@ -105,6 +105,17 @@ async function main() {
 function initStatic(app, config) {// eslint-disable-line
     //загрузка файлов в /files
     //TODO
+
+    app.use(express.static(config.publicDir, {
+        maxAge: '30d',
+
+        /*setHeaders: (res, filePath) => {
+            if (path.dirname(filePath) == filesDir) {
+                res.set('Content-Type', 'application/xml');
+                res.set('Content-Encoding', 'gzip');
+            }
+        },*/
+    }));
 }
 
 (async() => {