Каркас будущего приложения
This commit is contained in:
@@ -21,10 +21,6 @@ module.exports = {
|
|||||||
test: /\.vue$/,
|
test: /\.vue$/,
|
||||||
loader: 'vue-loader',
|
loader: 'vue-loader',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
resourceQuery: /^\?vue/,
|
|
||||||
use: path.resolve(__dirname, 'includer.js')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
const pckg = require('../package.json');
|
||||||
|
|
||||||
const { merge } = require('webpack-merge');
|
const { merge } = require('webpack-merge');
|
||||||
const baseWpConfig = require('./webpack.base.config');
|
const baseWpConfig = require('./webpack.base.config');
|
||||||
@@ -8,7 +9,7 @@ baseWpConfig.entry.unshift('webpack-hot-middleware/client');
|
|||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const CopyWebpackPlugin = require('copy-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');
|
const clientDir = path.resolve(__dirname, '../client');
|
||||||
|
|
||||||
module.exports = merge(baseWpConfig, {
|
module.exports = merge(baseWpConfig, {
|
||||||
|
|||||||
2
client/assets/robots.txt
Normal file
2
client/assets/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /#
|
||||||
141
client/components/App.vue
Normal file
141
client/components/App.vue
Normal file
@@ -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
client/components/Search/Search.vue
Normal file
38
client/components/Search/Search.vue
Normal file
@@ -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
Normal file
BIN
client/components/fonts/web-default.ttf
Normal file
Binary file not shown.
BIN
client/components/fonts/web-default.woff
Normal file
BIN
client/components/fonts/web-default.woff
Normal file
Binary file not shown.
80
client/components/share/Dialog.vue
Normal file
80
client/components/share/Dialog.vue
Normal file
@@ -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
client/components/share/Notify.vue
Normal file
58
client/components/share/Notify.vue
Normal file
@@ -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
client/components/share/StdDialog.vue
Normal file
361
client/components/share/StdDialog.vue
Normal file
@@ -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
client/components/vueComponent.js
Normal file
52
client/components/vueComponent.js
Normal file
@@ -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
client/index.html.template
Normal file
11
client/index.html.template
Normal file
@@ -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
client/main.js
Normal file
16
client/main.js
Normal file
@@ -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
client/quasar.js
Normal file
97
client/quasar.js
Normal file
@@ -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
client/router.js
Normal file
38
client/router.js
Normal file
@@ -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
client/share/utils.js
Normal file
30
client/share/utils.js
Normal file
@@ -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
client/store/index.js
Normal file
15
client/store/index.js
Normal file
@@ -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
client/store/root.js
Normal file
25
client/store/root.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@ const path = require('path');
|
|||||||
const pckg = require('../../package.json');
|
const pckg = require('../../package.json');
|
||||||
|
|
||||||
const execDir = path.resolve(__dirname, '..');
|
const execDir = path.resolve(__dirname, '..');
|
||||||
const dataDir = `${execDir}/.${pckg.name}/data`;
|
const dataDir = `${execDir}/.${pckg.name}`;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
branch: 'unknown',
|
branch: 'unknown',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const pckg = require('../../package.json');
|
|||||||
const base = require('./base');
|
const base = require('./base');
|
||||||
|
|
||||||
const execDir = path.dirname(process.execPath);
|
const execDir = path.dirname(process.execPath);
|
||||||
const dataDir = `${execDir}/.${pckg.name}/data`;
|
const dataDir = `${execDir}/.${pckg.name}`;
|
||||||
|
|
||||||
module.exports = Object.assign({}, base, {
|
module.exports = Object.assign({}, base, {
|
||||||
branch: 'production',
|
branch: 'production',
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function webpackDevMiddleware(app) {
|
|||||||
function logQueries(app) {
|
function logQueries(app) {
|
||||||
app.use(function(req, res, next) {
|
app.use(function(req, res, next) {
|
||||||
const start = Date.now();
|
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)}`)
|
//log(`${JSON.stringify(req.headers, null, 2)}`)
|
||||||
res.once('finish', () => {
|
res.once('finish', () => {
|
||||||
log(`${Date.now() - start}ms`);
|
log(`${Date.now() - start}ms`);
|
||||||
|
|||||||
@@ -105,6 +105,17 @@ async function main() {
|
|||||||
function initStatic(app, config) {// eslint-disable-line
|
function initStatic(app, config) {// eslint-disable-line
|
||||||
//загрузка файлов в /files
|
//загрузка файлов в /files
|
||||||
//TODO
|
//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() => {
|
(async() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user