Base Login
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
9
Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM node:18-alpine
|
||||
RUN apk update
|
||||
WORKDIR /src
|
||||
COPY . /src
|
||||
RUN npm i ipx
|
||||
RUN rm -rf node_modules
|
||||
RUN npm install --legacy-peer-deps
|
||||
RUN npm run build
|
||||
RUN npm install pm2 -g
|
||||
69
README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# store
|
||||
|
||||
## Build Setup
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
$ npm install
|
||||
|
||||
# serve with hot reload at localhost:3000
|
||||
$ npm run dev
|
||||
|
||||
# build for production and launch server
|
||||
$ npm run build
|
||||
$ npm run start
|
||||
|
||||
# generate static project
|
||||
$ npm run generate
|
||||
```
|
||||
|
||||
For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
|
||||
|
||||
## Special Directories
|
||||
|
||||
You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
|
||||
|
||||
### `assets`
|
||||
|
||||
The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
|
||||
|
||||
### `components`
|
||||
|
||||
The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
|
||||
|
||||
### `layouts`
|
||||
|
||||
Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
|
||||
|
||||
|
||||
### `pages`
|
||||
|
||||
This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
|
||||
|
||||
### `plugins`
|
||||
|
||||
The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
|
||||
|
||||
### `static`
|
||||
|
||||
This directory contains your static files. Each file inside this directory is mapped to `/`.
|
||||
|
||||
Example: `/static/robots.txt` is mapped as `/robots.txt`.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
|
||||
|
||||
### `store`
|
||||
|
||||
This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).
|
||||
20
assets/images/fb.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW X6 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14.2222in" height="14.2222in" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 14222 14222"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil0 {fill:#1977F3;fill-rule:nonzero}
|
||||
.fil1 {fill:#FEFEFE;fill-rule:nonzero}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<path class="fil0" d="M14222 7111c0,-3927 -3184,-7111 -7111,-7111 -3927,0 -7111,3184 -7111,7111 0,3549 2600,6491 6000,7025l0 -4969 -1806 0 0 -2056 1806 0 0 -1567c0,-1782 1062,-2767 2686,-2767 778,0 1592,139 1592,139l0 1750 -897 0c-883,0 -1159,548 -1159,1111l0 1334 1972 0 -315 2056 -1657 0 0 4969c3400,-533 6000,-3475 6000,-7025z"/>
|
||||
<path class="fil1" d="M9879 9167l315 -2056 -1972 0 0 -1334c0,-562 275,-1111 1159,-1111l897 0 0 -1750c0,0 -814,-139 -1592,-139 -1624,0 -2686,984 -2686,2767l0 1567 -1806 0 0 2056 1806 0 0 4969c362,57 733,86 1111,86 378,0 749,-30 1111,-86l0 -4969 1657 0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/images/google.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
48
assets/styles/main.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
// Import Bulma's core
|
||||
@import "~bulma/sass/utilities/_all";
|
||||
|
||||
// Set your colors
|
||||
$primary: #107FFB; // #4285F4; // #0F9D58; // #009047;
|
||||
$primary-invert: findColorInvert($primary);
|
||||
$twitter: #3392ec;
|
||||
$twitter-invert: findColorInvert($twitter);
|
||||
$findata: #ff8829; //#F4F7F8;
|
||||
$findata-invert: findColorInvert($findata);
|
||||
$sidebar-width: 35%;
|
||||
//$family-primary: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
$family-primary: Arial, sans-serif;
|
||||
|
||||
// Setup $colors to use as bulma classes (e.g. 'is-twitter')s
|
||||
$colors: (
|
||||
"white": ($white, $black),
|
||||
"black": ($black, $white),
|
||||
"light": ($light, $light-invert),
|
||||
"dark": ($dark, $dark-invert),
|
||||
"primary": ($primary, $primary-invert),
|
||||
"info": ($info, $info-invert),
|
||||
"link": ($link, $link-invert),
|
||||
"success": ($success, $success-invert),
|
||||
"warning": ($warning, $warning-invert),
|
||||
"danger": ($danger, $danger-invert),
|
||||
"twitter": ($twitter, $twitter-invert),
|
||||
"findata": ($findata, $findata-invert)
|
||||
);
|
||||
|
||||
// Links
|
||||
$link: $primary;
|
||||
$link-invert: $primary-invert;
|
||||
$link-focus-border: $primary;
|
||||
$body-family: $family-primary;
|
||||
$body-size: 15px !default;
|
||||
|
||||
$site-color: hsl(0, 0%, 14%);
|
||||
$body-color: $site-color;
|
||||
|
||||
$tabborder: #107FFB; //#4285F4;
|
||||
$tabs-boxed-link-active-border-color: $tabborder;
|
||||
$tabs-border-bottom-color: $tabborder;
|
||||
$tabs-link-active-color: $tabborder;
|
||||
|
||||
// Import Bulma and Buefy styles
|
||||
@import "~bulma";
|
||||
@import "~buefy/src/scss/buefy"
|
||||
330
assets/styles/style.scss
Normal file
@@ -0,0 +1,330 @@
|
||||
@import "~bulma/sass/utilities/_all";
|
||||
$color:(
|
||||
primary: #4285F4,
|
||||
findata: #ff8829,
|
||||
white: #FFFFFF,
|
||||
dark: #686868
|
||||
);
|
||||
$size: (
|
||||
one: 14.5px,
|
||||
two: 17px,
|
||||
three: 30px,
|
||||
four: 40px,
|
||||
five: 50px,
|
||||
six: 60px
|
||||
);
|
||||
@mixin cbox($width, $height, $font, $background) {
|
||||
display:flex;
|
||||
width: $width;
|
||||
height: $height;
|
||||
border: 1.5px solid #ff8829;
|
||||
font-size: $font;
|
||||
-webkit-border-radius: 50%;
|
||||
-moz-border-radius: 50%;
|
||||
-ms-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
color: #ff8829;
|
||||
font-weight: bold;
|
||||
background-color: $background;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
@each $name, $hex in $color {
|
||||
@each $n, $v in $size {
|
||||
.cbox-#{$name}-#{$n}{
|
||||
@include cbox($v*2, $v*2, $v*1.1, white);
|
||||
border-color: $hex ;
|
||||
color: $hex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@each $name, $hex in $color {
|
||||
@each $n, $v in $size {
|
||||
.cbox-fill-#{$name}-#{$n}{
|
||||
@include cbox($v*2, $v*2, $v, $hex);
|
||||
border-color: $hex ;
|
||||
color: findColorInvert($hex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@for $i from 10 through 40 {
|
||||
.fs-#{$i} {
|
||||
font-size: $i + px;
|
||||
}
|
||||
}
|
||||
@for $i from 10 through 40 {
|
||||
.fsb-#{$i} {
|
||||
font-size: $i + px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.number-circle {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 1.5px solid #ff8829;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
.number-circle-1 {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: auto;
|
||||
border: 1.5px solid #E8E8E8;;
|
||||
-webkit-border-radius: 50%;
|
||||
-moz-border-radius: 50%;
|
||||
-ms-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
display: table;
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
background-color: #E8E8E8;
|
||||
}
|
||||
|
||||
.close-button{
|
||||
float:right;
|
||||
cursor: pointer;
|
||||
line-height: 20px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.text-image {
|
||||
position: absolute; /* Position the background text */
|
||||
bottom: 0; /* At the bottom. Use top:0 to append it to the top */
|
||||
background: rgb(0, 0, 0); /* Fallback color */
|
||||
background: rgba(0, 0, 0, 0.5); /* Black background with 0.5 opacity */
|
||||
color: #f1f1f1; /* Grey text */
|
||||
width: 100%; /* Full width */
|
||||
padding: 10px; /* Some padding */
|
||||
}
|
||||
.btn-circle {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
line-height: 36px;
|
||||
font-size: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
color: red;
|
||||
text-align: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.tagborder {
|
||||
border: 1px solid hsl(0, 0%, 88%);
|
||||
font-size: 13px !important;
|
||||
color: hsl(0, 0%, 21%) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid hsl(0, 0%, 88%);
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-logo{
|
||||
background: url('/symbol.png') no-repeat center center;
|
||||
background-size: 68px;
|
||||
width: 60px
|
||||
}
|
||||
|
||||
.header-logo-main{
|
||||
background: url('/logo1.png') no-repeat center center;
|
||||
background-size: 140px;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.header-logo-main1{
|
||||
background: url('/logo1.png') no-repeat center center;
|
||||
background-size: 120px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.hyperlink {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.hyperlink:hover{
|
||||
color: #4285F4 !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.has-background-ceiling {background-color: #d602e3;}
|
||||
.has-background-floor {background-color: #08cfda;}
|
||||
.has-background-up {background-color: #09b007;}
|
||||
.has-background-down {background-color: #df0325; }
|
||||
.has-background-ref {background-color: #ff8829;}
|
||||
.has-text-ceiling {color: #ff25ff !important;}
|
||||
.has-text-floor {color: #1eeeee !important;}
|
||||
.has-text-up {color: #0f0 !important;}
|
||||
.has-text-down {color: #ff3737 !important;}
|
||||
.has-text-ref {color:#ffd900 !important;}
|
||||
|
||||
/* scrollbar */
|
||||
:root {
|
||||
--code-color: darkred;
|
||||
--code-bg-color: #696969;
|
||||
--code-font-size: 16px;
|
||||
--code-line-height: 1.4;
|
||||
--scroll-bar-color: #4285F4; //#696969;
|
||||
--scroll-bar-bg-color: #f6f6f6;
|
||||
}
|
||||
|
||||
pre {
|
||||
color: var(--code-color);
|
||||
font-size: var(--code-font-size);
|
||||
line-height: var(--code-line-height);
|
||||
background-color: var(--code-bg-color);
|
||||
}
|
||||
|
||||
.code-block {
|
||||
max-height: 100px;
|
||||
overflow: auto;
|
||||
padding: 8px 7px 5px 15px;
|
||||
margin: 0px 0px 0px 0px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner { background: rgba(0,0,0,0.5); }
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scroll-bar-color) var(--scroll-bar-bg-color);
|
||||
}
|
||||
|
||||
/* Works on Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: var(--scroll-bar-bg-color);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scroll-bar-color);
|
||||
border-radius: 20px;
|
||||
border: 3px solid var(--scroll-bar-bg-color);
|
||||
}
|
||||
|
||||
@mixin pulsating($color) {
|
||||
position: absolute;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 300%;
|
||||
height: 300%;
|
||||
box-sizing: border-box;
|
||||
margin-left: -100%;
|
||||
margin-top: -100%;
|
||||
border-radius: 45px;
|
||||
background-color: $color;
|
||||
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $color;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 0 8px rgba(0,0,0,.3);
|
||||
animation: pulse-dot 1.25s cubic-bezier(0.455, 0.03, 0.515, 0.955) -.4s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.pulsating-red {
|
||||
@include pulsating(#FF0000)
|
||||
}
|
||||
|
||||
.pulsating-yellow {
|
||||
@include pulsating(#E96F1D)
|
||||
}
|
||||
|
||||
.pulsating-blue {
|
||||
@include pulsating(#09B412)
|
||||
}
|
||||
|
||||
.pulsating-circle {
|
||||
position: absolute;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 300%;
|
||||
height: 300%;
|
||||
box-sizing: border-box;
|
||||
margin-left: -100%;
|
||||
margin-top: -100%;
|
||||
border-radius: 45px;
|
||||
background-color: #FF0000;
|
||||
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #FF0000;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 0 8px rgba(0,0,0,.3);
|
||||
animation: pulse-dot 1.25s cubic-bezier(0.455, 0.03, 0.515, 0.955) -.4s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: scale(.33);
|
||||
}
|
||||
80%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0% {
|
||||
transform: scale(.8);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(.8);
|
||||
}
|
||||
}
|
||||
47
components/AccessExtend.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="py-5 has-background-danger has-text-white mt-3 px-4" style="border-radius: 10px">
|
||||
<!-- <p class="fsb-20">
|
||||
Gói dịch vụ của quý khách đã hết hạn sử dụng. Để tiếp tục truy cập findata.vn với một số tính năng bị khóa vui
|
||||
lòng click:
|
||||
</p>
|
||||
<p>
|
||||
<button class="button is-medium is-primary px-2 mt-4" @click="redirect()">
|
||||
<span class="icon-text fsb-20">
|
||||
<span class="material-symbols-outlined">highlight_mouse_cursor</span>
|
||||
<span class="ml-2">Findata.vn</span>
|
||||
</span>
|
||||
</button>
|
||||
</p> -->
|
||||
<p>Nội dung đang được cập nhật</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Bowser from 'bowser';
|
||||
export default {
|
||||
methods: {
|
||||
async createToken() {
|
||||
let data = this.$copy(this.$store.state.login);
|
||||
const browser = Bowser.getParser(window.navigator.userAgent);
|
||||
let obj = {
|
||||
browser: browser.getBrowserName(),
|
||||
browser_version: browser.getBrowserVersion(),
|
||||
platform: browser.getPlatform().type,
|
||||
os: browser.getOSName(),
|
||||
user: data.id,
|
||||
token: this.$id(),
|
||||
};
|
||||
let ele = this.$copy(data);
|
||||
ele.token = obj.token;
|
||||
await this.$insertapi('authtoken', obj);
|
||||
this.$redirectWeb(ele);
|
||||
},
|
||||
async redirect() {
|
||||
let f = { user: this.$store.state.login.id };
|
||||
let row = await this.$getdata('accessextend', f, undefined, true);
|
||||
if (row) return await this.createToken();
|
||||
await this.$insertapi('accessextend', f);
|
||||
await this.createToken();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
23
components/Caption.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<span :class="`icon-text fsb-${size||17} ${type || 'has-text-findata'}`">
|
||||
<b>{{ title }}</b>
|
||||
<span class="material-symbols-outlined">
|
||||
chevron_right
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="control" v-if="note">
|
||||
<span class="icon-text px-2 fs-14 has-text-grey-dark" v-for="v in note">
|
||||
<span class="mdi mdi-circle" :style="`color:${v.color}`"></span>
|
||||
<span class="ml-2">{{ v.text }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['type', 'size', 'title', 'note', 'convert']
|
||||
}
|
||||
</script>
|
||||
126
components/DataView.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div>
|
||||
<DataTable @open="open" @insert="insert" @edit="edit" @delete="remove" @opensetting="openSetting" v-bind="{pagename: vpagename}"
|
||||
@dataevent="dataEvent" @company="openCompany" v-if="pagedata" />
|
||||
<Modal @close="comp=undefined"
|
||||
v-bind="{component: comp, width: width || '40%', height: height || '300px', vbind: vbind1}" v-if="comp"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['setting', 'api', 'filter', 'component', 'pagename', 'width', 'height', 'params', 'data', 'vbind'],
|
||||
data() {
|
||||
return {
|
||||
comp: undefined,
|
||||
current: undefined,
|
||||
chart: undefined,
|
||||
vbind1: {},
|
||||
vpagename: this.pagename? this.pagename : this.$findpage()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pagedata = this.$getpage()
|
||||
setTimeout(()=>this.getApi(), 10)
|
||||
},
|
||||
beforeDestroy() {
|
||||
if(!this.pagename && this.vpagename) this.$clearpage(this.vpagename)
|
||||
},
|
||||
watch: {
|
||||
dialog: function(newVal) {
|
||||
if(newVal? newVal.confirm : false) this.delete()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pagedata: {
|
||||
get: function() {return this.$store.state[this.vpagename]},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: this.vpagename, data: val})}
|
||||
},
|
||||
currentsetting: {
|
||||
get: function() {return this.$store.state.currentsetting},
|
||||
set: function(val) {this.$store.commit("updateCurrentSetting", {currentsetting: val})}
|
||||
},
|
||||
dialog: {
|
||||
get: function() {return this.$store.state['dialog']},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: 'dialog', data: val})}
|
||||
},
|
||||
settings: {
|
||||
get: function() {return this.$store.state['settings']},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: 'settings', data: val})}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getApi() {
|
||||
let connlist = []
|
||||
let row = this.setting.id? this.$copy(this.setting) : undefined
|
||||
if(!row) {
|
||||
let found = this.$find(this.settings, this.setting>0? {id: this.setting} : {name: this.setting})
|
||||
if(found) row = this.$copy(found)
|
||||
}
|
||||
if(!row) {
|
||||
let conn = this.$findapi('usersetting')
|
||||
conn.params.filter = this.setting>0? {id: this.setting} : {name: this.setting}
|
||||
connlist.push(conn)
|
||||
}
|
||||
let data = this.data? this.$copy(this.data) : undefined
|
||||
if(!data) {
|
||||
let conn1 = this.$findapi(this.api)
|
||||
if(this.filter) conn1.params.filter = this.filter
|
||||
if(this.params) conn1.params = this.params
|
||||
connlist.push(conn1)
|
||||
}
|
||||
let ele = undefined
|
||||
if(connlist.length>0) {
|
||||
let rs = await this.$getapi(connlist)
|
||||
ele = this.$find(rs, {name: 'usersetting'})
|
||||
if(ele) {
|
||||
row = this.$copy(ele.data.rows[0])
|
||||
let copy = this.$copy(this.settings)
|
||||
copy.push(row)
|
||||
this.settings = copy
|
||||
}
|
||||
let obj = this.$find(rs, {name: this.api})
|
||||
if(obj) data = this.$copy(obj.data.rows)
|
||||
}
|
||||
this.$setpage(this.vpagename, row, ele)
|
||||
data = this.$formatArray(data, this.pagedata.fields)
|
||||
this.$store.commit('updateState', {name: this.vpagename, key: 'update', data: {data: data}})
|
||||
},
|
||||
remove(v) {
|
||||
this,this.current = v
|
||||
this.$dialog({width: '500px', title: 'Xác nhận',
|
||||
content: `Bạn có chắc muốn xoá bản ghi ${v.id}`, type: 'is-primary', progress:true, duration: 10, askconfirm: true})
|
||||
},
|
||||
async delete() {
|
||||
await this.$deleterow(this.api, this.current.id)
|
||||
this.$emit('close')
|
||||
},
|
||||
dataEvent(row) {
|
||||
this.$emit('dataevent', row)
|
||||
},
|
||||
openSetting(v) {
|
||||
this.$emit('opensetting', v)
|
||||
},
|
||||
open(v) {
|
||||
this.$emit('open', v)
|
||||
this.$emit('modalevent', {name: 'open', data: v})
|
||||
},
|
||||
openCompany(v) {
|
||||
this.$emit('company', v)
|
||||
this.$emit('modalevent', {name: 'company', data: v})
|
||||
},
|
||||
insert() {
|
||||
this.vbind1 = {vpagename: this.vpagename, api: this.api}
|
||||
this.comp = this.$copy(this.component)
|
||||
},
|
||||
edit(v) {
|
||||
this.vbind1 = {vpagename: this.vpagename, api: this.api, row: v}
|
||||
if(this.vbind) {
|
||||
for (const [key, value] of Object.entries(this.vbind)) {
|
||||
this.vbind1[key] = value.indexOf('$')===0? v[value.replace('$', '')] : value
|
||||
}
|
||||
}
|
||||
this.comp = this.$copy(this.component)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
26
components/Logo.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="logo">
|
||||
<figure class="image is-clickable link" @click="$router.push('/signin')">
|
||||
<img v-if="isLogoMain" src="/logo-main.png" />
|
||||
<img v-else src="/logo.png" />
|
||||
</figure>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
isLogoMain: Boolean,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.logo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
48
components/Modal.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="modal is-active" @click="doClick">
|
||||
<div class="modal-background" :style="`opacity:${count===0? 0.7 : 0.3} !important;'`"></div>
|
||||
<div class="modal-card" :id="docid" :style="$store.state.ismobile? '' : `width:${width? width : '60%'}`">
|
||||
<header class="modal-card-head my-0 pt-4 pb-3" v-if="title">
|
||||
<p class="modal-card-title fsb-20 py-0 my-0" v-html="title"></p>
|
||||
</header>
|
||||
<section class="modal-card-body" :style="`min-height:${height? height : '700px'}`">
|
||||
<component :is="compobj" v-bind="vbind" @modalevent="modalEvent" @close="$emit('close')"></component>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['component', 'width', 'height', 'vbind', 'title'],
|
||||
data() {
|
||||
return {
|
||||
docid: this.$id(),
|
||||
count: 0
|
||||
}
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('keydown', (e) => {if(e.key==='Escape') this.$emit('close')})
|
||||
const collection = document.getElementsByClassName("modal-background")
|
||||
this.count = collection.length
|
||||
},
|
||||
computed: {
|
||||
compobj() {
|
||||
return () => import(`@/components/${this.component}`)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
modalEvent(ev) {
|
||||
this.$emit(ev.name, ev.data)
|
||||
},
|
||||
doClick(e) {
|
||||
//e.stopPropagation()
|
||||
if(!e.srcElement.offsetParent) return
|
||||
if(document.getElementById(this.docid)) {
|
||||
if(!document.getElementById(this.docid).contains(e.target)) {
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
41
components/Redirect.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="pt-3 fs-17">
|
||||
<button class="button is-primary px-2 mx-2" @click="redirect()">
|
||||
<span class="icon-text fs-18">
|
||||
<span class="material-symbols-outlined">highlight_mouse_cursor</span>
|
||||
<span class="ml-2">Click</span>
|
||||
</span>
|
||||
</button>
|
||||
để đi tới trang chủ <a class="ml-2 fsb-17" @click="redirect()">{{ company.name }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Bowser from 'bowser';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
company: this.$companyInfo(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async redirect() {
|
||||
let data = this.$copy(this.$store.state.login);
|
||||
const browser = Bowser.getParser(window.navigator.userAgent);
|
||||
let obj = {
|
||||
browser: browser.getBrowserName(),
|
||||
browser_version: browser.getBrowserVersion(),
|
||||
platform: browser.getPlatform().type,
|
||||
os: browser.getOSName(),
|
||||
user: data.id,
|
||||
token: this.$id(),
|
||||
};
|
||||
let ele = this.$copy(data);
|
||||
ele.token = obj.token;
|
||||
await this.$insertapi('authtoken', obj);
|
||||
this.$redirectWeb(ele);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
67
components/SearchBox.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-autocomplete
|
||||
placeholder=""
|
||||
icon="magnify"
|
||||
expanded
|
||||
:data="data"
|
||||
:open-on-focus="first? true : false"
|
||||
keep-first
|
||||
:field="field"
|
||||
v-model="value"
|
||||
@typing="beginSearch"
|
||||
@select="option => doSelect(option)">
|
||||
<template slot-scope="props">
|
||||
<p>{{ props.option[field] }}</p>
|
||||
</template>
|
||||
<template slot="empty">Không có giá trị thỏa mãn</template>
|
||||
</b-autocomplete>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['api', 'field', 'column', 'first', 'optionid', 'filter', 'storeval'],
|
||||
data() {
|
||||
return {
|
||||
search: undefined,
|
||||
data: [],
|
||||
timer: undefined,
|
||||
value: undefined,
|
||||
selected: undefined,
|
||||
params: this.$findapi(this.api)['params']
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
if(this.first) {
|
||||
this.data = await this.$getdata(this.api, this.filter)
|
||||
if(this.optionid) this.selected = this.$find(this.data, {id: this.optionid})
|
||||
} else if(this.optionid) {
|
||||
this.selected = await this.$getdata(this.api, {id: this.optionid.id}, undefined, true)
|
||||
}
|
||||
if(this.selected) this.doSelect(this.selected)
|
||||
},
|
||||
methods: {
|
||||
doSelect(option) {
|
||||
if(this.$empty(option)) return
|
||||
this.$emit('option', option)
|
||||
this.selected = option
|
||||
this.value = this.selected[this.field]
|
||||
},
|
||||
async getApi(val) {
|
||||
let text = val? val.toLowerCase() : ''
|
||||
let f = {}
|
||||
this.column.map(v=>{
|
||||
f[`${v}__icontains`] = text
|
||||
})
|
||||
this.params.filter_or = f
|
||||
if(this.filter) this.params.filter = this.$copy(this.filter)
|
||||
this.data = await this.$getdata(this.api, undefined, this.params)
|
||||
},
|
||||
beginSearch(val) {
|
||||
this.search = val
|
||||
if (this.timer) clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => this.getApi(val), 150)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
117
components/ServicePack.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="py-4 has-background-light" style="border: 1px solid #D3D3D3; border-radius: 10px; margin-top: 30px;">
|
||||
<div class="columns mx-0 is-mobile border-bottom pb-3 mb-5">
|
||||
<div class="column is-6" v-if="viewport>1">
|
||||
<p class="fsb-20">
|
||||
<span :class="`tag is-large is-dark`">TÍNH NĂNG</span>
|
||||
</p>
|
||||
</div>
|
||||
<div :class="`column is-${viewport>1? 3 : 6}`" v-for="v in packages">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control pr-4">
|
||||
<span :class="`tag is-large is-${v.code==='vip'? 'findata' : 'primary'}`"><b>{{ v.name }}</b></span>
|
||||
</div>
|
||||
<!--
|
||||
<div class="control">
|
||||
<p class="fsb-17 has-text-danger">
|
||||
<span>{{$numtoString(v.price, 'vi-VN')}} đ / tháng</span>
|
||||
<span class="ml-3" v-if="viewport>=4 && (v.origin_price>v.price)">(-{{$formatUnit((v.origin_price-v.price)/v.origin_price, 0.01, 0, true)}})</span>
|
||||
</p>
|
||||
<p class="fsb-17 has-text-danger" v-if="viewport<=3">
|
||||
<span v-if="v.origin_price>v.price">(-{{$formatUnit((v.origin_price-v.price)/v.origin_price, 0.01, 0, true)}})</span>
|
||||
</p>
|
||||
<p style="text-decoration: line-through;">
|
||||
<span class="fsb-17 has-text-grey">{{$numtoString(v.origin_price, 'vi-VN')}} đ / tháng</span>
|
||||
</p>
|
||||
</div>-->
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-multiline mt-4">
|
||||
<!--<div class="control pr-4" v-if="packinfo? packinfo.trialInfo==='no' : true">
|
||||
<button class="button is-dark" @click="trial(v)">Dùng thử</button>
|
||||
</div>-->
|
||||
<div class="control">
|
||||
<button :class="`button is-danger`" @click="buy(v)">Mua ngay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns mx-0 is-mobile" v-for="v in feature">
|
||||
<div class="column is-6 px-5">
|
||||
<Caption v-bind="{title: v.name, type: 'has-text-primary'}"></Caption>
|
||||
<template v-if="typeof v.detail==='string'">
|
||||
<p>{{ v.detail }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p v-for="x in v.detail">
|
||||
- {{ x }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<div class="column is-3 px-5" v-for="x in packages">
|
||||
<span :class="`material-symbols-outlined has-text-${v[x.code]? 'primary' : 'grey'}`">
|
||||
{{v[x.code]? 'select_check_box' : 'block'}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['packinfo'],
|
||||
data() {
|
||||
return {
|
||||
packages: undefined,
|
||||
feature: [],
|
||||
packageFeature: []
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.packages = await this.$getdata('servicepack', undefined, {sort: 'id'})
|
||||
this.packages = this.$filter(this.packages, {code: 'basic'})
|
||||
let data = await this.$getdata('feature')
|
||||
this.packageFeature = await this.$getdata('packagefeature')
|
||||
data.map(v=>{
|
||||
this.packages.map(x=>{
|
||||
let found = this.$find(this.packageFeature, {feature: v.id, package: x.id})
|
||||
if(found) v[x.code] = true
|
||||
})
|
||||
this.feature = data
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
login: {
|
||||
get: function() {return this.$store.state.login},
|
||||
set: function(val) {this.$store.commit('updateLogin', {login: val})}
|
||||
},
|
||||
viewport: {
|
||||
get: function() {return this.$store.state.viewport},
|
||||
set: function(val) {this.$store.commit("updateViewPort", {viewport: val})}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async trial(v) {
|
||||
if(!this.login) {
|
||||
this.$dialog('<span class="fs-16">BigDataTechCloud cần xác minh <b>gói dịch vụ</b> quý khách đang sử dụng. Vui lòng đăng nhập để tiếp tục. Xin cảm ơn quý khách.</span>', 'Đăng nhập', undefined, 8)
|
||||
return this.$router.push({path: '/signin', query: {href: '/service/package'}})
|
||||
}
|
||||
if(this.login.type!==1) return this.$router.push({path: '/welcome'})
|
||||
let trialpack = await this.$getdata('userpack', {user: this.login.id, status__code: 'trial'}, undefined, true)
|
||||
if(trialpack) {
|
||||
let diff = this.$dayjs(trialpack.to_date).diff(this.$dayjs(), 'day')
|
||||
if(trialpack.expiry || diff<0) this.$router.push({path: '/welcome'})
|
||||
else this.$router.push({path: '/service/information'})
|
||||
} else this.$router.push({path: '/service/trial', query: {code: v.code}})
|
||||
},
|
||||
async buy(v) {
|
||||
if(!this.login) {
|
||||
this.$dialog('<span class="fs-16">BigDataTechCloud cần xác minh <b>gói dịch vụ</b> quý khách đang sử dụng. Vui lòng đăng nhập để tiếp tục. Xin cảm ơn quý khách.</span>', 'Đăng nhập', undefined, 8)
|
||||
return this.$router.push({path: '/signin', query: {href: '/service/package'}})
|
||||
}
|
||||
if(this.login.type!==1) return this.$router.push({path: '/welcome'})
|
||||
let order = await this.$getdata('order', {user: this.login.id, payment_status__code: 'unpaid'}, undefined, true)
|
||||
if(order) this.$router.push({path: '/service/order-info', query: {id: order.id}})
|
||||
else this.$router.push({path: '/service/order', query: {code: v.code}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
22
components/TopMenu.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<html class="has-navbar-fixed-top">
|
||||
<nav class="navbar is-fixed-top is-spaced has-shadow py-0" role="navigation">
|
||||
<div class="navbar-brand">
|
||||
<nuxt-link class="navbar-item header-logo-main" to="/"></nuxt-link>
|
||||
<a role="button" class="navbar-burger" data-target="nav-menu" ref="burger" @click="$responsiveMenu()">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<div class="navbar-item">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</html>
|
||||
</template>
|
||||
<script>
|
||||
</script>
|
||||
69
components/UserPack.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="columns is-multiline mx-0" v-if="userpack? userpack.length>0 : false">
|
||||
<div class="column is-4 my-0 py-0" v-for="v in userpack">
|
||||
<div :class="`px-5 py-4 ${v.expiry? 'has-background-light' : 'has-background-light'}`" style="border: 1px solid #D3D3D3; border-radius: 10px; margin-top: 25px;">
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<Caption v-bind="{title: 'Gói dịch vụ', type: `has-text-${v.status__code==='buy'? 'primary' : 'findata'}`}"></Caption>
|
||||
</div>
|
||||
<div class="control">
|
||||
<span class="tag is-medium is-danger" v-if="v.expiry"><span class="fs-18">Hết hạn</span></span>
|
||||
<span class="tag is-medium is-primary is-clickable" v-else-if="v.package__code==='basic'" @click="upgrade(v)">
|
||||
<span class="fs-18">Nâng cấp</span>
|
||||
</span>
|
||||
<span class="tag is-medium is-success ml-4" v-if="!v.expiry">
|
||||
<span class="icon-text">
|
||||
<span class="material-symbols-outlined fs-18">check</span>
|
||||
<span class="fs-18 ml-1">Active</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="fsb-16">
|
||||
<span>{{ v.package__name }}</span>
|
||||
</p>
|
||||
<Caption class="mt-5" v-bind="{title: 'Thời hạn sử dụng', type: `has-text-${v.status__code==='buy'? 'primary' : 'findata'}`}"></Caption>
|
||||
<p class="fsb-16">
|
||||
<span>{{ $dayjs(v.from_date).format('DD/MM/YYYY') }}</span>
|
||||
<span class="px-2">-</span>
|
||||
<span>{{ $dayjs(v.to_date).format('DD/MM/YYYY') }}</span>
|
||||
</p>
|
||||
<Caption class="mt-5" v-bind="{title: 'Trạng thái', type: `has-text-${v.status__code==='buy'? 'primary' : 'findata'}`}"></Caption>
|
||||
<p class="fsb-16">
|
||||
<span>{{ v.status__name }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userpack: undefined
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
let arr = await this.$getdata('userpack', {user: this.$store.state.login.id}, undefined)
|
||||
arr.map(v=>{
|
||||
let diff = this.$dayjs(v.to_date).diff(this.$dayjs(), 'day')
|
||||
if(!v.expiry && diff<0) v.expiry = true
|
||||
})
|
||||
this.userpack = arr
|
||||
let trial = this.$find(arr, {status__code: 'trial'})
|
||||
let buy = arr.find(v=>v.status__code!=='trial' && v.expiry===false)
|
||||
let data = {userpack: arr, trialInfo: trial? (trial.expiry? 'expiry' : 'active') : 'no', trial: trial, buy: buy}
|
||||
this.$emit('info', data)
|
||||
},
|
||||
methods: {
|
||||
upgrade(v) {
|
||||
if(v.status__code==='trial') this.$router.push({path: '/service/trial-upgrade', query: {id: v.id}})
|
||||
else {
|
||||
this.$dialog(`Quý khách đang sử dụng gói dịch vụ <b>${v.package__name}</b>.
|
||||
Để nâng cấp lên <b>gói cao hơn</b> vui lòng liên hệ với nhân viên của chúng tôi để được hỗ trợ. Xin cảm ơn.`,
|
||||
'Liên hệ')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
84
components/affiliate/AffiliateInfo.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="has-background-light px-5 pt-4 pb-2">
|
||||
<Caption v-bind="{ title: 'Thông tin cộng tác viên' }"></Caption>
|
||||
<div class="columns is-multiline mx-0">
|
||||
<div class="column is-2">
|
||||
<div class="field">
|
||||
<label class="label">Mã số</label>
|
||||
<div class="control fs-16">
|
||||
{{ `CT${reginfo.user}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="field">
|
||||
<label class="label">Link</label>
|
||||
<div class="control fs-16">
|
||||
<a @click="openLink()">{{ reginfo.link }}</a>
|
||||
<p class="mt-2 hyperlink has-text-primary">
|
||||
<span class="material-symbols-outlined" @click="copyContent()">content_copy</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<label class="label">QR code</label>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<img style="width: 80px" :src="`${$path()}static/files/${reginfo.qrcode}`" />
|
||||
</div>
|
||||
<div class="control">
|
||||
<p class="hyperlink has-text-primary">
|
||||
<span class="material-symbols-outlined" @click="download()">download</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<div class="field">
|
||||
<label class="label">Số dư</label>
|
||||
<div class="control fs-16">
|
||||
<span class="has-text-danger">{{ $numtoString(reginfo.balance) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5" v-if="reginfo.status__code === 'wait'">
|
||||
<p class="fs-16 has-text-primary">
|
||||
Bạn đã đăng ký thành công. Vui lòng chờ phản hồi từ {{ company.name }}. Xin cảm ơn.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
company: this.$companyInfo(),
|
||||
};
|
||||
},
|
||||
props: ['reginfo'],
|
||||
|
||||
methods: {
|
||||
copyContent() {
|
||||
this.$copyToClipboard(this.reginfo.link);
|
||||
},
|
||||
openLink() {
|
||||
window.location.href = this.reginfo.link;
|
||||
},
|
||||
async download() {
|
||||
let ulr = `${this.$path()}download?type=file&name=${this.reginfo.qrcode}`;
|
||||
let response = await fetch(ulr);
|
||||
const blob = await response.blob();
|
||||
const urlDownload = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = urlDownload;
|
||||
link.setAttribute('download', this.reginfo.qrcode);
|
||||
link.click();
|
||||
link.remove();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
339
components/affiliate/RegisterForm.vue
Normal file
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<div v-if="record">
|
||||
<Caption class="pt-1 pb-4" v-bind="{title: 'From đăng ký cộng tác viên', size: 20, type: 'has-text-black'}"></Caption>
|
||||
<div class="has-background-light px-5 pt-4 pb-5">
|
||||
<Caption v-bind="{title: 'Thông tin cá nhân'}"></Caption>
|
||||
<div class="columns is-multiline mx-0 mt-3">
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Họ tên<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.fullname">
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.fullname">{{errors.fullname}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Điện thoại<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.phone">
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.phone">{{ errors.phone }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Ngày sinh<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<b-datepicker
|
||||
locale="en-GB"
|
||||
v-model="record._dob">
|
||||
</b-datepicker>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.dob">{{ errors.dob }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Giới tính<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox v-bind="{api:'sex', field:'name', column:['name'], first:true, optionid:record.sex}"
|
||||
@option="selected('_sex', $event)"></SearchBox>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.sex">{{ errors.sex }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Tỉnh / thành phố<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<b-autocomplete
|
||||
icon-right="magnify"
|
||||
v-model="province"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="provinces"
|
||||
field="province_name"
|
||||
@select="option => changeProvince(option)">
|
||||
</b-autocomplete>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.province">{{errors.province}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Quận / huyện<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<b-autocomplete
|
||||
icon-right="magnify"
|
||||
v-model="district"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="districts"
|
||||
field="district_name"
|
||||
@select="option => changeDistrict(option)">
|
||||
</b-autocomplete>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.province">{{errors.district}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Phường / xã<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<b-autocomplete
|
||||
icon-right="magnify"
|
||||
v-model="commune"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="communes"
|
||||
field="commune_name"
|
||||
@select="option => selectCommune = option">
|
||||
</b-autocomplete>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.commune">{{errors.commune}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-8">
|
||||
<div class="field">
|
||||
<label class="label">Địa chỉ<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.address">
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.address">{{errors.address}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">Giới thiệu về bạn</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" v-model="note" placeholder="Hãy cho chúng tôi biết đôi nét về nghề nghiệp, kinh nghiệm tham gia thị trường, nơi làm việc của bạn. Thông tin này giúp chúng tôi hiểu về bạn từ đó hõ trợ được tốt hơn" rows="2"></textarea>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.address">{{errors.address}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="has-background-light mt-5 px-5 pt-4 pb-5">
|
||||
<Caption v-bind="{title: 'Giấy tờ tùy thân'}"></Caption>
|
||||
<div class="columns is-multiline mx-0 mt-3">
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Số CMT / CC công dân<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.legal_id">
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.legal_id">{{errors.legal_id}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-8">
|
||||
<div class="field">
|
||||
<label class="label">Nơi cấp<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.issue_place">
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.issue_place">{{ errors.issue_place }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12 pt-0">
|
||||
<p class="mb-3 fs-13">Vui lòng tải lên mặt trước và sau của CMT / CCCD (hình ảnh hoặc file scan). Thông tin này dùng để trả thưởng cho bạn</p>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control is-expanded">
|
||||
<b-upload v-model="file">
|
||||
<a class="button is-findata is-rounded is-small" :class="loading? 'is-loading' : null">
|
||||
<b-icon icon="mdi mdi-plus mr-1"></b-icon>
|
||||
<span class="fs-14">Tải lên từ máy tính</span>
|
||||
</a>
|
||||
</b-upload>
|
||||
</div>
|
||||
<div class="control" v-for="(v,i) in files">
|
||||
<div class="tags has-addons">
|
||||
<a class="tag is-link">{{ v }}</a>
|
||||
<a class="tag is-delete" @click="remove(i)"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.files">{{ errors.files }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="has-background-light mt-5 px-5 pt-4 pb-5">
|
||||
<Caption v-bind="{title: 'Tài khoản ngân hàng'}"></Caption>
|
||||
<div class="columns is-multiline mx-0 mt-3">
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Số tài khoản<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.bank_account">
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.bank_account">{{errors.bank_account}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Tên tài khoản<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.account_name">
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.account_name">{{ errors.account_name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="field">
|
||||
<label class="label">Ngân hàng<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox v-bind="{api:'bank', field:'name', column:['name'], first:true, optionid:record.bank}"
|
||||
@option="selected('_bank', $event)"></SearchBox>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.bank">{{ errors.bank }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 pt-2 pb-5">
|
||||
<button class="button is-primary" @click="register()">Đăng ký</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
errors: {},
|
||||
record: undefined,
|
||||
provinces: this.$store.state.provinces || [],
|
||||
districts: [],
|
||||
communes: [],
|
||||
selectProvince: undefined,
|
||||
selectDistrict: undefined,
|
||||
selectCommune: undefined,
|
||||
selectLegal: undefined,
|
||||
showmodal: undefined,
|
||||
province: undefined,
|
||||
district: undefined,
|
||||
commune: undefined,
|
||||
file: undefined,
|
||||
datafile: undefined,
|
||||
loading: undefined,
|
||||
files: [],
|
||||
note: undefined,
|
||||
reginfo: undefined
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.reginfo = await this.$getdata('affiliate', {user: this.login.id}, undefined, true)
|
||||
if(this.reginfo) this.note = this.reginfo.note
|
||||
this.record = await this.$getdata('user', {id: this.login.id}, undefined, true)
|
||||
if(this.record.files) this.files = this.record.files
|
||||
if(this.record.dob) this.$set(this.record, '_dob', new Date(this.record.dob))
|
||||
if(!this.$store.state.provinces) {
|
||||
this.provinces = await this.$getdata('province')
|
||||
this.$store.commit('updateStore', {name: 'provinces', data: this.provinces})
|
||||
}
|
||||
if(this.record.location__province_code) {
|
||||
this.selectProvince = this.$find(this.provinces, {province_code: this.record.location__province_code})
|
||||
this.province = this.selectProvince.province_name
|
||||
await this.getDistrict()
|
||||
this.selectDistrict = this.$find(this.districts, {district_code: this.record.location__district_code})
|
||||
this.district = this.selectDistrict.district_name
|
||||
await this.getCommune()
|
||||
this.selectCommune = this.$find(this.communes, {commune_code: this.record.location__commune_code})
|
||||
this.commune = this.selectCommune.commune_name
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
login: {
|
||||
get: function() {return this.$store.state.login},
|
||||
set: function(val) {this.$store.commit('updateLogin', {login: val})}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
file: function(newVal) {
|
||||
if(!newVal) return
|
||||
var file = this.$upload(newVal, 'file', this.login.id)
|
||||
if(file.error) {
|
||||
let info = {duration: 4000, type: 'is-danger', hasIcon: false, message: file.text}
|
||||
this.$buefy.notification.open(info)
|
||||
return
|
||||
}
|
||||
this.datafile = file
|
||||
this.uploadImage(file)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async uploadImage(file) {
|
||||
this.loading = true
|
||||
let rs = await this.$insertapi('upload', file.form)
|
||||
this.loading = false
|
||||
if(rs!=='error') this.files.push(rs.rows[0].file)
|
||||
},
|
||||
checkError() {
|
||||
this.errors = {}
|
||||
if(this.$empty(this.record.fullname)) this.errors.fullname = 'Họ tên không được bỏ trống'
|
||||
if(this.$empty(this.record.phone)) this.errors.phone = 'Điện thoại không được bỏ trống'
|
||||
if(this.$empty(this.record._sex)) this.errors.sex = 'Chưa chọn giới tính'
|
||||
if(this.$empty(this.selectProvince)) this.errors.province = 'Chưa chọn Tỉnh / Thành phố'
|
||||
if(this.$empty(this.selectDistrict)) this.errors.district = 'Chưa chọn Quận / Huyện'
|
||||
if(this.$empty(this.selectCommune)) this.errors.commune = 'Chưa chọn Quận / Huyện'
|
||||
if(this.$empty(this.record._dob)) this.errors.dob = 'Chưa nhập ngày sinh'
|
||||
if(this.$empty(this.record.address)) this.errors.address = 'Địa chỉ không được bỏ trống'
|
||||
if(this.$empty(this.record.legal_id)) this.errors.legal_id = 'Số CMT / CC công dân không được bỏ trống'
|
||||
if(this.$empty(this.record.issue_place)) this.errors.issue_place = 'Nơi cấp không được bỏ trống'
|
||||
if(this.files.length===0) this.errors.files = 'Bạn chưa tải lên chứng minh thư / căn cước công dân'
|
||||
if(this.$empty(this.record.bank_account)) this.errors.bank_account = 'Số tài khoản không được bỏ trống'
|
||||
if(this.$empty(this.record.account_name)) this.errors.account_name = 'Tên tài khoản không được bỏ trống'
|
||||
if(this.$empty(this.record._bank)) this.errors.bank = 'Chưa chọn ngân hàng'
|
||||
return Object.keys(this.errors).length>0
|
||||
},
|
||||
async register() {
|
||||
if(this.checkError()) return
|
||||
if(this.record._dob) this.record.dob = this.$dayjs(this.record._dob).format('YYYY-MM-DD')
|
||||
if(this.record._sex) this.record.sex = this.record._sex.id
|
||||
this.record.files = this.files
|
||||
if(this.selectCommune) {
|
||||
let found = await this.$getdata('location', {province_code: this.selectProvince.code, district_code: this.selectDistrict.district_code, commune_code: this.selectCommune.commune_code}, undefined, true)
|
||||
if(found) this.record.location = found.id
|
||||
}
|
||||
let rs = await this.$updateapi('user', this.record)
|
||||
let obj = {user: this.login.id, note: this.note, status: 1, bank_account: this.record.bank_account, account_name: this.record.account_name, bank: this.record._bank.id}
|
||||
let found = this.$findapi('affiliate')
|
||||
let rs1 = await this.$insertapi('affiliate', obj, found.params.values)
|
||||
this.$emit('affiliate', rs1)
|
||||
},
|
||||
changeProvince(option) {
|
||||
this.selectProvince = option
|
||||
this.getDistrict()
|
||||
this.selectDistrict = undefined
|
||||
this.district = undefined
|
||||
this.communes = []
|
||||
this.selectCommune = undefined
|
||||
this.commune = undefined
|
||||
},
|
||||
changeDistrict(option) {
|
||||
this.selectDistrict = option
|
||||
this.getCommune()
|
||||
},
|
||||
selected(attr, obj) {
|
||||
this.record[attr] = obj
|
||||
},
|
||||
async getDistrict() {
|
||||
if(!this.selectProvince) return
|
||||
this.districts = await this.$getdata('district', {province_code: this.selectProvince.province_code})
|
||||
},
|
||||
async getCommune() {
|
||||
if(!this.selectProvince) return
|
||||
this.communes = await this.$getdata('commune', {province_code: this.selectProvince.province_code, district_code: this.selectDistrict.district_code})
|
||||
},
|
||||
openImage() {
|
||||
this.showmodal = {component: 'common/Imagebox', title: 'Hình ảnh', width: '90%'}
|
||||
},
|
||||
remove(i) {
|
||||
this.$delete(this.files, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
46
components/affiliate/RegisterInfo.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="has-background-light px-5 pt-4 pb-5">
|
||||
<Caption v-bind="{ title: 'Đăng ký cộng tác viên' }"></Caption>
|
||||
<div class="columns is-multiline mx-0 mt-3">
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Họ tên<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
{{ reginfo.user__fullname }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Ngày đăng ký<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
{{ $dayjs(reginfo.create_time).format('DD/MM/YYYY') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Trạng thái<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<span class="tag is-success is-medium">{{ reginfo.status__name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5" v-if="reginfo.status__code === 'wait'">
|
||||
<p class="fs-16 has-text-primary">Bạn đã đăng ký thành công. Vui lòng chờ phản hồi từ {{ company.name }}. Xin cảm ơn.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return{
|
||||
company: this.$company(),
|
||||
}
|
||||
},
|
||||
props: ['reginfo']
|
||||
}
|
||||
</script>
|
||||
39
components/datatable/ChartField.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<p><label class="label fs-14">Chọn cột hiển thị</label></p>
|
||||
<div class="field is-grouped is-grouped-multiline mt-2">
|
||||
<div class="control" v-for="(v,i) in args" :key="i">
|
||||
<div class="tags has-addons">
|
||||
<a class="tag is-primary">{{$stripHtml(v.label)}}</a>
|
||||
<a class="tag is-delete" @click="remove(args, i)"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['pagename'],
|
||||
data() {
|
||||
return {
|
||||
args: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.args = this.$copy(this.pagedata.fields.filter(v=>v.format==='number' && v.show))
|
||||
},
|
||||
computed: {
|
||||
pagedata: {
|
||||
get: function() {return this.$store.state[this.pagename]},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
remove(args, i) {
|
||||
this.$delete(args, i)
|
||||
this.$emit('changefields', args)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
946
components/datatable/ContextMenu.vue
Normal file
@@ -0,0 +1,946 @@
|
||||
<template>
|
||||
<div class="fs-14 has-text-weight-normal">
|
||||
<div>
|
||||
<b-tooltip label="Sắp xếp tăng dần" type="is-dark" position="is-right">
|
||||
<a @click="checkFilter()? false : $emit('modalevent', {name: 'dosort', data: 'az'})">
|
||||
<span class="icon is-medium fs-22 mr-2" :class="checkFilter()? 'has-text-grey-light' : ''">
|
||||
<i class="mdi mdi-sort-alphabetical-ascending" />
|
||||
</span>
|
||||
</a>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Sắp xếp giảm dần" type="is-dark" position="is-right">
|
||||
<a @click="checkFilter()? false : $emit('modalevent', {name: 'dosort', data: 'za'})">
|
||||
<span class="icon is-medium fs-22 mr-2" :class="checkFilter()? 'has-text-grey-light' : '' " >
|
||||
<i class="mdi mdi-sort-alphabetical-descending" />
|
||||
</span>
|
||||
</a>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Chuyển cột sang trái" type="is-dark" position="is-right">
|
||||
<a>
|
||||
<span class="icon is-medium fs-22 mr-2" @click="moveLeft()">
|
||||
<i class="mdi mdi-arrow-left" />
|
||||
</span>
|
||||
</a>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Chuyển cột sang phải" type="is-dark" position="is-right">
|
||||
<a>
|
||||
<span class="icon is-medium fs-22 mr-2" @click="moveRight()">
|
||||
<i class="mdi mdi-arrow-right"/>
|
||||
</span>
|
||||
</a>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Ẩn cột" type="is-dark" position="is-right">
|
||||
<a @click="hideField()">
|
||||
<span class="icon is-medium fs-22 mr-2">
|
||||
<i class="mdi mdi-eye-off-outline"/>
|
||||
</span>
|
||||
</a>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Xóa cột" type="is-dark" position="is-right">
|
||||
<a>
|
||||
<span class="icon is-medium fs-22 mr-2" @click="currentField.mandatory? false : doRemove()">
|
||||
<i :class="`mdi mdi-delete-outline ${currentField.mandatory? 'has-text-grey-light' : ''}`"/>
|
||||
</span>
|
||||
</a>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Sao chép cột" type="is-dark" position="is-right">
|
||||
<a>
|
||||
<span class="icon is-medium fs-20 mr-2" @click="$emit('modalevent', {name: 'copyfield', data:currentField})">
|
||||
<i class="mdi mdi-content-copy"/>
|
||||
</span>
|
||||
</a>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Tăng độ rộng cột" type="is-dark" position="is-right">
|
||||
<a>
|
||||
<span class="icon is-medium fs-22 mr-2" @click="resizeWidth()">
|
||||
<i class="mdi mdi-arrow-expand-horizontal"/>
|
||||
</span>
|
||||
</a>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Giảm độ rộng cột" type="is-dark" position="is-right">
|
||||
<a>
|
||||
<span class="icon is-medium fs-22 mr-2" @click="resizeWidth(true)">
|
||||
<i class="mdi mdi-arrow-collapse-horizontal"/>
|
||||
</span>
|
||||
</a>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Danh sách cột" type="is-dark" position="is-right">
|
||||
<a>
|
||||
<span class="icon is-medium mr-2 fs-22" @click="$emit('modalevent', {name: 'showsidebar', data: {field: currentField, name: 'option'}})">
|
||||
<i class="mdi mdi-format-list-bulleted"/>
|
||||
</span>
|
||||
</a>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
<div class="field mt-2 mb-2" v-if="currentField.disable? currentField.disable.indexOf('search') <0 : true">
|
||||
<div :class="loading? 'control is-loading' : 'control'">
|
||||
<input class="input is-rounded fs-13" type="text" v-model="search" @keyup="startSearch" @keypress.enter="pressEnter"
|
||||
:placeholder="'Tìm kiếm: ' + (currentField.label.indexOf('<')>=0? currentField.name : currentField.label)"
|
||||
@focus="doFocus()" :ref="currentField.name">
|
||||
</div>
|
||||
</div>
|
||||
<p class="panel-tabs mb-2">
|
||||
<a v-for="(v,i) in getMenu().filter(x=>currentField.format==='number'? (currentField.formula? true : x.code!=='formula')
|
||||
: !['filter','formula'].find(y=>y===x.code))" :key="i"
|
||||
:class="selectTab.code===v.code? 'is-active' : ''" @click="selectTab=v"> {{v.name}}
|
||||
</a>
|
||||
</p>
|
||||
<template v-if="selectTab.code==='value'">
|
||||
<ScrollBox v-bind="{data: filterData, name: currentField.name, maxheight: '380px', perpage: 20, selects: checkSelected()}"
|
||||
@selected="doSelect" />
|
||||
</template>
|
||||
<template v-else-if="selectTab.code==='display'">
|
||||
<div class="field is-horizontal border-bottom pb-0 mb-1">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Màu nền </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice" :key="i" v-model="radioBGcolor"
|
||||
:native-value="v" @input="changeBGColor()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioBGcolor? radioBGcolor.code==='option' : false">
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" v-model="bgcolor" @change="changeBGColor()">
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioBGcolor? radioBGcolor.code==='condition' : false">
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<a class="button is-small is-primary is-outlined is-rounded" @click="doAdvance('bgcolor')"> Nâng cao </a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal border-bottom pb-0 mb-1">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Màu chữ </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice" :key="i" v-model="radioColor"
|
||||
:native-value="v" @input="changeColor()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioColor? radioColor.code==='option' : false">
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" v-model="color" @change="changeColor()">
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioColor? radioColor.code==='condition' : false">
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<a class="button is-small is-primary is-outlined is-rounded" @click="doAdvance('color')"> Nâng cao </a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal border-bottom pb-0 mb-1">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Cỡ chữ </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice" :key="i" v-model="radioSize"
|
||||
:native-value="v" @input="changeSize()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioSize? radioSize.code==='option' : false">
|
||||
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" placeholder="Nhập số" v-model="textsize" @change="changeSize()">
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioSize? radioSize.code==='condition' : false">
|
||||
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<a class="button is-small is-primary is-outlined is-rounded" @click="doAdvance('textsize')"> Nâng cao </a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal pb-0 mb-1 border-bottom">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Vị trí text</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioAlign"
|
||||
:native-value="v" @input="changeAlign()">{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field is-narrow" v-if="radioAlign? radioAlign.code==='option' : false">
|
||||
<label class="label fs-14">Chọn vị trí <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<b-autocomplete
|
||||
size="is-small"
|
||||
icon-right="magnify"
|
||||
:value="selectAlign? selectAlign.name : ''"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="textalign"
|
||||
field="name"
|
||||
@select="option => { selectAlign = option; changeAlign()}">
|
||||
</b-autocomplete>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal border-bottom pb-0 mb-1">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Độ rộng nhỏ nhất</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioWidth"
|
||||
:native-value="v" @input="changeWidth()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioWidth? radioWidth.code==='option' : false">
|
||||
<label class="label fs-14"> Kích thước</label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" placeholder="Nhập số" v-model="minwidth" @change="changeWidth()">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal border-bottom pb-0 mb-1">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Độ rộng lớn nhất</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioMaxWidth"
|
||||
:native-value="v" @input="changeMaxWidth()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioMaxWidth? radioMaxWidth.code==='option' : false">
|
||||
<label class="label fs-14"> Kích thước</label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" placeholder="Nhập số" v-model="maxwidth" @change="changeMaxWidth()">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="selectTab.code==='filter'">
|
||||
<p class="mt-5 has-text-grey" v-if="arr===undefined">Không thể áp dụng đồng thời chức năng lọc cùng với sắp xếp</p>
|
||||
<div :class="`field is-horizontal mt-4`" v-for="(v,i) in arr" :key="i">
|
||||
<div class="field-body">
|
||||
<div class="field" style="width: 10%;">
|
||||
<label class="label" v-if="i===0">Điều kiện<span class="has-text-danger"> * </span></label>
|
||||
<p class="control">
|
||||
<b-autocomplete
|
||||
icon-right="magnify"
|
||||
v-model="v.condition"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="arr2"
|
||||
field="code"
|
||||
@select="option => doOption()">
|
||||
</b-autocomplete>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" style="width:30%">
|
||||
<label class="label" v-if="i===0">Giá trị<span class="has-text-danger"> *</span></label>
|
||||
<p class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="v.value" @change="checkValid()">
|
||||
</p>
|
||||
<p class="is-help mt-2 has-text-danger" v-if="v.error">{{v.error}}</p>
|
||||
</div>
|
||||
<div class="field" style="width: 10%;" v-if="arr.length>=1 && i===arr.length-2">
|
||||
<label class="label" v-if="i===0">Kết hợp<span class="has-text-danger"> * </span></label>
|
||||
<p class="control">
|
||||
<b-autocomplete
|
||||
icon-right="magnify"
|
||||
v-model="v.operator"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="arr1"
|
||||
field="code"
|
||||
@select="option => doOption()">
|
||||
</b-autocomplete>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<div :class="i===0? 'mt-5 pt-2' : ''">
|
||||
<span class="icon has-text-primary is-clickable" @click="addCondition()" v-if="arr.length<2">
|
||||
<i class="mdi mdi-plus-thick fs-22"></i>
|
||||
</span>
|
||||
<span class="icon has-text-danger is-clickable" @click="removeCondition(i)" v-if="arr.length>1">
|
||||
<i class="mdi mdi-minus-thick fs-22"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="selectTab.code==='detail'">
|
||||
<p class="fs-14 mt-3"> <strong> Tên trường: </strong> {{currentField.name}}
|
||||
<a @click="copyContent(currentField.name)">
|
||||
<b-tooltip label="Copy tên trường" type="is-dark">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-content-copy"/>
|
||||
</span>
|
||||
</b-tooltip>
|
||||
</a>
|
||||
</p>
|
||||
<div class="field mt-3">
|
||||
<label class="label fs-14">Mô tả<span class="has-text-danger"> *</span></label>
|
||||
<div class="control has-icons-right">
|
||||
<input
|
||||
class="input fs-14"
|
||||
type="text"
|
||||
@change="changeLabel($event.target.value)"
|
||||
v-model="label"
|
||||
/>
|
||||
<a class="button is-clickable icon is-right fs-14" @click="editLabel()">
|
||||
<i class="mdi mdi-pen has-text-primary fs-18"></i>
|
||||
</a>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'label')">
|
||||
{{ errors.find((v) => v.name === "label").msg }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-3">
|
||||
<label class="label fs-14">Kiểu dữ liệu<span class="has-text-danger"> * </span></label>
|
||||
<div class="control fs-14 has-text-primary">
|
||||
<b-radio v-for="(v,i) in datatype" :key="i" v-model="radioType" :native-value="v" disabled>
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal" v-if="field.format==='number'">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Đơn vị <span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<b-autocomplete
|
||||
size="is-small"
|
||||
icon-right="magnify"
|
||||
:value="selectUnit? selectUnit.name : ''"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="moneyunit"
|
||||
field="name"
|
||||
@select="option => {selectUnit = option; changeUnit()}">
|
||||
</b-autocomplete>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='unit')"> {{errors.find(v=>v.name==='unit').msg}} </p>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<label class="label fs-14">Phần thập phân</label>
|
||||
<div class="control">
|
||||
<input class="input is-small" type="text" placeholder="" v-model="decimal" @input="changeDecimal($event.target.value)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-3">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Định dạng nâng cao</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioTemplate"
|
||||
:native-value="v" @input="changeTemplate(undefined)">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3" v-if="radioTemplate? radioTemplate.code==='option' : false">
|
||||
<button class="button is-primary is-outlined is-rounded is-small" @click="$emit('modalevent', {name: 'showsidebar', data: {name: 'template', field: currentField}})">
|
||||
<span class="fs-14">{{`${currentField.template? 'Sửa' : 'Tạo'} định dạng`}}</span>
|
||||
</button>
|
||||
</p>
|
||||
</template>
|
||||
<template v-if="selectTab.code==='tooltip'">
|
||||
<p class="mt-5 fs-15 has-text-dark" v-if="currentField.template">
|
||||
Không thể sử dụng đồng thời template và tooltip
|
||||
</p>
|
||||
<template v-else>
|
||||
<div class="field mt-3">
|
||||
<label class="label fs-14">Sử dụng tooltip</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioTooltip"
|
||||
:native-value="v" @input="changeTooltip()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioTooltip? radioTooltip.code==='option' : false">
|
||||
<label class="label fs-14"> Chọn trường <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<b-autocomplete
|
||||
size="is-small"
|
||||
icon-right="magnify"
|
||||
:value="selectField? selectField.label : ''"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="pagedata.fields"
|
||||
field="label"
|
||||
@select="option => {selectField = option; changeTooltip()}">
|
||||
</b-autocomplete>
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tooltip')"> {{errors.find(v=>v.name==='tooltip').msg}} </p>
|
||||
</div>
|
||||
<div class="field mt-3" v-if="radioTooltip? radioTooltip.code==='option' : false">
|
||||
<label class="label fs-14"> Vị trí hiển thị <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control">
|
||||
<b-autocomplete
|
||||
size="is-small"
|
||||
icon-right="magnify"
|
||||
:value="selectPlacement? selectPlacement.name : ''"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="placement"
|
||||
field="name"
|
||||
@select="option => {selectPlacement = option; changeTooltip()}">
|
||||
</b-autocomplete>
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='placement')"> {{errors.find(v=>v.name==='placement').msg}} </p>
|
||||
</div>
|
||||
<div class="field mt-3" v-if="radioTooltip? radioTooltip.code==='option' : false">
|
||||
<label class="label fs-14"> Bảng màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<b-autocomplete
|
||||
size="is-small"
|
||||
icon-right="magnify"
|
||||
:value="selectScheme? selectScheme.name : ''"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="colorscheme"
|
||||
field="name"
|
||||
@select="option => {selectScheme = option; changeTooltip()}">
|
||||
</b-autocomplete>
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tooltip')"> {{errors.find(v=>v.name==='tooltip').msg}} </p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="selectTab.code==='formula'">
|
||||
<div class="field mt-3 px-0 mx-0">
|
||||
<label class="label fs-14"> Trường để tạo công thức <span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<b-taginput
|
||||
size="is-small"
|
||||
v-model="tags"
|
||||
:data="fields.filter(v=>v.format==='number')"
|
||||
type="is-dark is-light"
|
||||
autocomplete
|
||||
:open-on-focus="true"
|
||||
field="caption"
|
||||
icon="plus"
|
||||
placeholder="Chọn trường"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<span class="mr-3 has-text-danger">{{props.option.name}}</span>
|
||||
<span :class="tags.find(v=>v.id===props.option.id)? 'has-text-dark' : ''">{{$stripHtml(props.option.label,50)}}</span>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
Không có trường thỏa mãn
|
||||
</template>
|
||||
</b-taginput>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tags')"> {{errors.find(v=>v.name==='tags').message}} </p>
|
||||
</div>
|
||||
<div class="field mt-3" v-if="tags.length>0">
|
||||
<p class="help is-primary">Click đúp vào để thêm vào công thức tính.</p>
|
||||
<div class="tags mb-2">
|
||||
<span @dblclick="formula = formula? (formula + ' ' + y.name) : y.name" class="tag is-dark is-rounded is-clickable"
|
||||
v-for="y in tags"><b-tooltip type="is-primary" :label="$stripHtml(y.label)">{{y.name}}</b-tooltip></span>
|
||||
</div>
|
||||
<div class="tags">
|
||||
<b-tooltip type="is-dark" :label="v.name" v-for="(v,i) in operator" :key="i">
|
||||
<span @dblclick="addOperator(v)" class="tag is-primary is-rounded is-clickable mr-4">
|
||||
<span class="fs-16">{{v.code}}</span>
|
||||
</span>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-3 px-0 mx-0">
|
||||
<label class="label fs-14">Công thức tính <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control">
|
||||
<textarea class="textarea" rows="4" type="text" placeholder="Tạo công thức tại đây" v-model="formula"></textarea>
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='formula')"> {{errors.find(v=>v.name==='formula').message}} </p>
|
||||
</div>
|
||||
<div class="mt-5"><button class="button is-primary is-rounded" @click="changeFormula()">Cập nhật</button></div>
|
||||
</template>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal" @label="changeLabel"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
ScrollBox: ()=> import('@/components/datatable/ScrollBox')
|
||||
},
|
||||
props: ['pagename', 'field', 'filters', 'filterData', 'width'],
|
||||
data() {
|
||||
return {
|
||||
search: undefined,
|
||||
loading: false,
|
||||
fields: [],
|
||||
current: 1,
|
||||
currentPage: 1,
|
||||
timer: undefined,
|
||||
selectTab: undefined,
|
||||
radioBGcolor: undefined,
|
||||
radioColor: undefined,
|
||||
radioSize: undefined,
|
||||
selectAlign: undefined,
|
||||
radioAlign: undefined,
|
||||
radioWidth: undefined,
|
||||
minwidth: undefined,
|
||||
color: undefined,
|
||||
bgcolor: undefined,
|
||||
textsize: undefined,
|
||||
showPage: 1,
|
||||
perPage: 30,
|
||||
value1: undefined,
|
||||
value2: undefined,
|
||||
errors: [],
|
||||
label: undefined,
|
||||
radioType: undefined,
|
||||
selectUnit: undefined,
|
||||
radioTemplate: undefined,
|
||||
currentField: this.$copy(this.field),
|
||||
selectPlacement: undefined,
|
||||
radioTooltip: undefined,
|
||||
selectScheme: undefined,
|
||||
selectField: undefined,
|
||||
tags: [],
|
||||
formula: undefined,
|
||||
radioMaxWidth: undefined,
|
||||
maxwidth: undefined,
|
||||
bgcolorFilter: [{id: this.$id()}],
|
||||
colorFilter: [{id: this.$id()}],
|
||||
sizeFilter: [{id: this.$id()}],
|
||||
tabs: [{code: 'expression', name: 'Biểu thức'}, {code: 'script', name: 'Mã lệnh'}],
|
||||
tab: {},
|
||||
decimal: undefined,
|
||||
arr: [{id:this.$id(), operator: 'and'}],
|
||||
arr1: [{code: 'and', name: '&&'}, {code: 'or', name: 'or'}],
|
||||
arr2: [{code: '>', name: 'gt'}, {code: '>=', name: 'gte'}, {code: '=', name: 'e'},
|
||||
{code: '<=', name: 'lte'}, {code: '<', name: 'lt'}, {code: '<>', name: 'oth'}],
|
||||
showmodal: undefined,
|
||||
operator: [{code: '+', name: 'Cộng'}, {code: '-', name: 'Trừ'}, {code: '*', name: 'Nhân'}, {code: '/', name: 'Chia'}, {code: '>', name: 'Lớn hơn'},
|
||||
{code: '>=', name: 'Lớn hơn hoặc bằng'}, {code: '<', name: 'Nhỏ hơn'}, {code: '<=', name: 'Nhỏ hơn hoặc bằng'}, {code: '==', name: 'Bằng'},
|
||||
{code: '&&', name: 'Và'}, {code: '||', name: 'Hoặc'}, {code: 'iif', name: 'Điều kiện rẽ nhánh'}]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.label = this.$copy(this.field.label)
|
||||
this.getFields()
|
||||
this.selectTab = this.getMenu()[0]
|
||||
this.tab = this.tabs.find(v=>v.code==='expression')
|
||||
let found = this.filters.find(v=>v.name===this.field.name)
|
||||
if(found) this.arr = this.$copy(found.filter)
|
||||
this.getDisplay(this.field)
|
||||
},
|
||||
watch: {
|
||||
selectTab: function(newVal) {
|
||||
if(newVal? newVal.code==='value' : false) {
|
||||
if(this.$refs[this.field.name]) this.$refs[this.field.name].focus()
|
||||
}
|
||||
},
|
||||
field: function(newVal) {
|
||||
this.currentField = this.$copy(newVal)
|
||||
this.getDisplay(newVal)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pagedata: {
|
||||
get: function() {return this.$store.state[this.pagename]},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
|
||||
},
|
||||
tablesetting: {
|
||||
get: function() {return this.$store.state.tablesetting},
|
||||
set: function(val) {this.$store.commit("updateTableSetting", {tablesetting: val})}
|
||||
},
|
||||
colorchoice: {
|
||||
get: function() {return this.$store.state.colorchoice},
|
||||
set: function(val) {this.$store.commit("updateColorChoice", {colorchoice: val})}
|
||||
},
|
||||
textalign: {
|
||||
get: function() {return this.$store.state.textalign},
|
||||
set: function(val) {this.$store.commit("updateTextAlign", {textalign: val})}
|
||||
},
|
||||
filterchoice: {
|
||||
get: function () {return this.$store.state.filterchoice},
|
||||
set: function (val) {this.$store.commit("updateFilterChoice", { filterchoice: val })}
|
||||
},
|
||||
datatype: {
|
||||
get: function() {return this.$store.state.datatype},
|
||||
set: function(val) {this.$store.commit("updateDataType", {datatype: val})}
|
||||
},
|
||||
moneyunit: {
|
||||
get: function() {return this.$store.state.moneyunit},
|
||||
set: function(val) {this.$store.commit("updateMoneyUnit", {moneyunit: val})}
|
||||
},
|
||||
menuaction: {
|
||||
get: function() {return this.$store.state.menuaction},
|
||||
set: function(val) {this.$store.commit("updateMenuAction", {menuaction: val})}
|
||||
},
|
||||
placement: {
|
||||
get: function() {return this.$store.state.placement},
|
||||
set: function(val) {this.$store.commit("updatePlacement", {placement: val})}
|
||||
},
|
||||
colorscheme: {
|
||||
get: function() {return this.$store.state.colorscheme},
|
||||
set: function(val) {this.$store.commit("updateColorScheme", {colorscheme: val})}
|
||||
},
|
||||
menuchoice: {
|
||||
get: function() {return this.$store.state.menuchoice},
|
||||
set: function(val) {this.$store.commit("updateMenuChoice", {menuchoice: val})}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addOperator(v) {
|
||||
let text = v.code==='iif'? 'a>b? c : d' : v.code
|
||||
this.formula = `${this.formula || ''} ${text}`
|
||||
},
|
||||
doSelect(row) {
|
||||
this.$emit('modalevent', {name: 'doselect', data: row[this.field.name]})
|
||||
},
|
||||
editLabel() {
|
||||
this.showmodal = {component: 'datatable/EditLabel', width: '500px', height: '300px', vbind: {label: this.label}}
|
||||
},
|
||||
addCondition() {
|
||||
this.arr.push({})
|
||||
},
|
||||
removeCondition(i) {
|
||||
this.$delete(this.arr, i)
|
||||
this.setFilter(this.field)
|
||||
},
|
||||
getMenu() {
|
||||
let field = this.getfield()
|
||||
let arr = field.disable? field.disable.split(',') : undefined
|
||||
return arr? this.menuchoice.filter(v=>arr.findIndex(x=>x===v.code) <0) : this.menuchoice
|
||||
},
|
||||
getFields() {
|
||||
this.fields = this.pagedata? this.$copy(this.pagedata.fields) : []
|
||||
this.fields.map(v=>v.caption = (v.label? v.label.indexOf('<')>=0 : false)? v.name : v.label)
|
||||
},
|
||||
getDisplay(field) {
|
||||
this.current = 1
|
||||
this.value1 = undefined
|
||||
this.value2 = undefined
|
||||
this.radioType = this.datatype.find(v=>v.code===field.format)
|
||||
if(field.format==='number') this.selectUnit = this.moneyunit.find(v=>v.detail===field.unit)
|
||||
this.bgcolor = undefined
|
||||
this.radioBGcolor = this.colorchoice.find(v=>v.code==='none')
|
||||
this.color = undefined
|
||||
this.radioColor = this.colorchoice.find(v=>v.code==='none')
|
||||
this.textsize = undefined
|
||||
this.radioSize = this.colorchoice.find(v=>v.code==='none')
|
||||
this.minwidth = undefined
|
||||
this.radioWidth = this.colorchoice.find(v=>v.code==='none')
|
||||
this.radioMaxWidth = this.colorchoice.find(v=>v.code==='none')
|
||||
this.maxwidth = undefined
|
||||
this.selectAlign = undefined
|
||||
this.radioAlign = this.colorchoice.find(v=>v.code==='none')
|
||||
this.radioTemplate = this.colorchoice.find(v=>v.code=== (field.template? 'option' : 'none'))
|
||||
this.selectPlacement = this.placement.find(v=>v.code==='is-right')
|
||||
this.selectScheme = this.colorscheme.find(v=>v.code==='is-primary')
|
||||
this.radioTooltip = this.colorchoice.find(v=>v.code==='none')
|
||||
this.selectField = undefined
|
||||
this.tags = field.tags? field.tags.map(v=>this.fields.find(x=>x.name===v)) : []
|
||||
this.formula = field.formula? field.formula : undefined
|
||||
this.decimal = field.decimal
|
||||
let shortmenu = this.menuchoice.filter(x=>field.format==='number'? (field.formula? true : x.code!=='formula')
|
||||
: !['filter','formula'].find(y=>y===x.code))
|
||||
this.selectTab = shortmenu.find(v=>this.selectTab.code===v.code)? this.selectTab : this.menuchoice.find(v=>v.code==='value')
|
||||
this.search = undefined
|
||||
if(this.selectTab.code==='value') {
|
||||
let self = this
|
||||
setTimeout(function() {self.$refs[field.name]? self.$refs[field.name].focus() : false}, 50)
|
||||
}
|
||||
|
||||
this.bgcolorFilter = [{id: this.$id()}]
|
||||
if(field.bgcolor) {
|
||||
if(Array.isArray(field.bgcolor)) {
|
||||
this.radioBGcolor = this.colorchoice.find(v=>v.code==='condition')
|
||||
this.bgcolorFilter = this.$copy(field.bgcolor)
|
||||
} else {
|
||||
this.radioBGcolor = this.colorchoice.find(v=>v.code==='option')
|
||||
this.bgcolor = field.bgcolor
|
||||
}
|
||||
}
|
||||
|
||||
this.colorFilter = [{id: this.$id()}]
|
||||
if(field.color) {
|
||||
if(Array.isArray(field.color)) {
|
||||
this.radioColor = this.colorchoice.find(v=>v.code==='condition')
|
||||
this.colorFilter = this.$copy(field.color)
|
||||
} else {
|
||||
this.radioColor = this.colorchoice.find(v=>v.code==='option')
|
||||
this.color = field.color
|
||||
}
|
||||
}
|
||||
this.sizeFilter = [{id: this.$id()}]
|
||||
if(field.textsize) {
|
||||
if(Array.isArray(field.textsize)) {
|
||||
this.radioSize = this.colorchoice.find(v=>v.code==='condition')
|
||||
this.sizeFilter = field.textsize
|
||||
} else {
|
||||
this.radioSize = this.colorchoice.find(v=>v.code==='option')
|
||||
this.textsize = field.textsize
|
||||
}
|
||||
}
|
||||
|
||||
if(field.textalign) {
|
||||
this.radioAlign = this.colorchoice.find(v=>v.code==='option')
|
||||
this.selectAlign = this.textalign.find(v=>v.code===field.textalign)
|
||||
}
|
||||
|
||||
if(field.minwidth) {
|
||||
this.radioWidth = this.colorchoice.find(v=>v.code==='option')
|
||||
this.minwidth = field.minwidth
|
||||
}
|
||||
|
||||
if(field.maxwidth) {
|
||||
this.radioMaxWidth = this.colorchoice.find(v=>v.code==='option')
|
||||
this.maxwidth = field.maxwidth
|
||||
}
|
||||
|
||||
if(field.tooltip) {
|
||||
this.radioTooltip = this.colorchoice.find(v=>v.code==='option')
|
||||
this.selectPlacement = this.placement.find(v=>v.code===field.tooltip.placement)
|
||||
this.selectField = this.pagedata.fields.find(v=>v.name===field.tooltip.field)
|
||||
this.selectScheme = this.colorscheme.find(v=>v.code===field.tooltip.type)
|
||||
}
|
||||
},
|
||||
moveLeft() {
|
||||
let i = this.pagedata.fields.findIndex(v=>v.name===this.field.name)
|
||||
let copy = this.$copy(this.pagedata.fields)
|
||||
let idx = i-1>=0? i - 1 : copy.length - 1
|
||||
copy = this.$arrayMove(copy, i, idx)
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: copy}})
|
||||
this.$emit('changepos')
|
||||
},
|
||||
moveRight() {
|
||||
let i = this.pagedata.fields.findIndex(v=>v.name===this.field.name)
|
||||
let copy = this.$copy(this.pagedata.fields)
|
||||
let idx = copy.length-1>i? i + 1 : 0
|
||||
copy = this.$arrayMove(copy, i, idx)
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: copy}})
|
||||
this.$emit('changepos')
|
||||
},
|
||||
doRemove() {
|
||||
let field = this.getfield()
|
||||
let copy = this.$copy(this.pagedata.fields)
|
||||
let idx = copy.findIndex(v=>v.name===field.name)
|
||||
this.$delete(copy, idx)
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: copy}})
|
||||
this.$emit('close')
|
||||
},
|
||||
startSearch(event) {
|
||||
if (this.timer) clearTimeout(this.timer)
|
||||
let self = this
|
||||
this.timer = setTimeout(() => { self.$emit('modalevent', {name: 'dosearch', data: event.srcElement.value})}, 100)
|
||||
},
|
||||
pressEnter(event) {
|
||||
if(!this.$empty(event.srcElement.value) && this.filterData.length>0) this.$emit('modalevent', {name: 'doselect', data: this.filterData[0][this.field.name]})
|
||||
},
|
||||
getfield() {
|
||||
return this.currentField
|
||||
},
|
||||
changeColor() {
|
||||
let copy = this.getfield()
|
||||
copy.color = this.radioColor.code==='none'? undefined : this.color
|
||||
this.updateFields(copy)
|
||||
},
|
||||
changeBGColor() {
|
||||
let copy = this.getfield()
|
||||
copy.bgcolor = this.radioBGcolor.code==='none'? undefined : this.bgcolor
|
||||
this.updateFields(copy)
|
||||
},
|
||||
checkSelected() {
|
||||
let found = this.filters.find(v=>v.name===this.field.name)
|
||||
return found? found.select : undefined
|
||||
},
|
||||
doAdvance(name) {
|
||||
let field = this.getfield()
|
||||
let script = (field[name]? Array.isArray(field[name]) : false)? JSON.stringify(field[name]) : undefined
|
||||
this.$emit('modalevent', {name: 'showsidebar', data: {field: field, name: name, script: script,
|
||||
radio: name==='bgcolor'? this.radioBGcolor : (name==='color'? this.radioColor : this.radioSize) }})
|
||||
},
|
||||
changeSize() {
|
||||
let copy = this.getfield()
|
||||
if(this.radioSize.code==='option' && !this.$isNumber(this.textsize)) return
|
||||
copy.textsize = this.radioSize.code==='none'? undefined : this.textsize
|
||||
this.updateFields(copy)
|
||||
},
|
||||
changeAlign() {
|
||||
let copy = this.getfield()
|
||||
copy.textalign = this.radioAlign.code==='none'? undefined : (this.selectAlign? this.selectAlign.code : undefined)
|
||||
this.updateFields(copy)
|
||||
},
|
||||
changeWidth() {
|
||||
let copy = this.getfield()
|
||||
if(!this.$isNumber(this.minwidth)) return
|
||||
copy.minwidth = this.radioWidth.code==='none'? undefined : this.minwidth
|
||||
this.updateFields(copy)
|
||||
},
|
||||
changeMaxWidth() {
|
||||
let copy = this.getfield()
|
||||
if(!this.$isNumber(this.maxwidth)) return
|
||||
copy.maxwidth = this.radioMaxWidth.code==='none'? undefined : this.maxwidth
|
||||
this.updateFields(copy)
|
||||
},
|
||||
setFilter(field) {
|
||||
let arr = this.arr.map(v=>{return {
|
||||
condition: v.condition, value: v.value, operator: v.operator
|
||||
}})
|
||||
let text = ''
|
||||
arr.map((y,k)=>{
|
||||
text += `${k>0? (arr[k-1].operator==='and'? ' &' : ' ||') : ''} ${y.condition} ${this.$numtoString(y.value)}`
|
||||
})
|
||||
let filter = {name: field.name, label: field.label, filter: arr, condition: text}
|
||||
this.$emit('modalevent', {name: 'setfilter', data: filter})
|
||||
},
|
||||
copyContent(value) {
|
||||
this.$copyToClipboard(value)
|
||||
},
|
||||
changeLabel(value) {
|
||||
if(this.$empty(value)) return
|
||||
if(this.label!==value) this.label = value
|
||||
let copy = this.getfield()
|
||||
copy.label = value
|
||||
this.updateFields(copy)
|
||||
},
|
||||
changeUnit() {
|
||||
if(this.$empty(this.selectUnit)) return
|
||||
let copy = this.getfield()
|
||||
copy.unit = this.selectUnit.detail
|
||||
this.updateFields(copy)
|
||||
setTimeout(()=> this.menuaction = {pagename: this.pagename, name: 'reload-data', time: new Date()}, 1000)
|
||||
},
|
||||
changeTemplate(value) {
|
||||
if(this.radioTemplate.code==='none') value = undefined
|
||||
else if(this.$empty(value)) return
|
||||
let copy = this.getfield()
|
||||
copy.template = value
|
||||
this.updateFields(copy)
|
||||
},
|
||||
changeDecimal(evt) {
|
||||
if(!this.$isNumber(evt)) return
|
||||
let copy = this.getfield()
|
||||
copy.decimal = evt
|
||||
this.updateFields(copy)
|
||||
setTimeout(()=>this.menuaction = {pagename: this.pagename, name: 'reload-data', time: new Date()}, 1000)
|
||||
},
|
||||
checkFilter() {
|
||||
if(!this.pagedata) return
|
||||
let field = this.getfield()
|
||||
let found = this.pagedata.filters? this.pagedata.filters.find(v=>v.name===field.name) : undefined
|
||||
return found? (found.select || found.filter) : false
|
||||
},
|
||||
changeTooltip() {
|
||||
let copy = this.getfield()
|
||||
if(this.radioTooltip? this.radioTooltip.code==='none' : false) copy.tooltip = undefined
|
||||
else if(!(this.selectField && this.selectPlacement && this.selectScheme)) return
|
||||
else {copy.tooltip = {field: this.selectField.name, placement: this.selectPlacement.code, type: this.selectScheme.code}}
|
||||
this.updateFields(copy)
|
||||
},
|
||||
changeFormula() {
|
||||
let field = this.getfield()
|
||||
//check formula
|
||||
this.errors = []
|
||||
let val = this.$copy(this.formula)
|
||||
this.tags.forEach(v => {
|
||||
let myRegExp = new RegExp(v.name, 'g')
|
||||
val = val.replace(myRegExp, Math.random())
|
||||
})
|
||||
try {
|
||||
let value = this.$calc(val)
|
||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
||||
this.errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
this.errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
|
||||
}
|
||||
if(this.errors.length>0) return
|
||||
let copyField = this.$copy(field)
|
||||
copyField.formula = this.formula.trim()
|
||||
copyField.tags = this.tags.map(v=>v.name)
|
||||
copyField.level = Math.max(...this.tags.map(v=>v.level? v.level : 0)) + 1
|
||||
this.updateFields(copyField, 'fields')
|
||||
this.$emit('close')
|
||||
},
|
||||
updateFields(field, type) {
|
||||
let copy = this.$copy(this.pagedata.fields)
|
||||
let idx = copy.findIndex(v=>v.name===field.name)
|
||||
copy[idx] = this.$copy(field)
|
||||
if(type==='fields') this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: copy, data: this.pagedata.data}})
|
||||
else {
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'fields', data: copy})
|
||||
if(this.field.inputitem) this.$updateinput(this.pagename)
|
||||
setTimeout(()=>this.$store.commit("updateState", {name: this.pagename, key: "update", data: {columns: copy}}), 100)
|
||||
}
|
||||
},
|
||||
doFocus() {
|
||||
if(this.selectTab.code!=='value') this.selectTab = this.menuchoice.find(v=>v.code==='value')
|
||||
},
|
||||
hideField() {
|
||||
let field = this.getfield()
|
||||
let copy = this.$copy(this.pagedata.fields)
|
||||
let found = copy.find(v=>v.name===field.name)
|
||||
found.show = false
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'update', data: {fields: copy}})
|
||||
this.$emit('close')
|
||||
},
|
||||
checkValid() {
|
||||
let error
|
||||
this.arr.map((v,i) => {
|
||||
if(this.$empty(v.condition)) error = 'Chưa chọn điều kiện'
|
||||
if(this.arr.length>=2 && i===0 && this.$empty(v.operator)) error = 'Chưa chọn kết hợp'
|
||||
if(!this.$isNumber(v.value)) error = 'Giá trị phải là số'
|
||||
if(error) v.error = error
|
||||
else {
|
||||
v.error = undefined
|
||||
v.value = this.$numtoString(v.value)
|
||||
}
|
||||
})
|
||||
this.arr = this.$copy(this.arr)
|
||||
if(!error) this.setFilter(this.field)
|
||||
},
|
||||
doOption() {
|
||||
setTimeout(()=> this.checkValid(), 100)
|
||||
},
|
||||
resizeWidth(minus) {
|
||||
let val = this.maxwidth || this.minwidth || this.width
|
||||
val = minus? parseInt(val - 0.1* val) : parseInt(val + 0.1* val)
|
||||
if(val>1000) return this.$buefy.toast.open('Độ rộng cột lớn hơn giới hạn cho phép')
|
||||
else if(val<20) return this.$buefy.toast.open('Độ rộng cột nhỏ hơn giới hạn cho phép')
|
||||
this.radioMaxWidth = this.colorchoice.find(v=>v.code==='option')
|
||||
this.radioWidth = this.colorchoice.find(v=>v.code==='option')
|
||||
this.maxwidth = val
|
||||
this.currentField.maxwidth = val
|
||||
this.minwidth = val
|
||||
this.currentField.minwidth = val
|
||||
this.updateFields(this.currentField)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
240
components/datatable/CreateField.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="panel-tabs">
|
||||
<a v-for="(v,i) in fieldType" :key="i"
|
||||
:class="selectType.code===v.code? 'is-active' : ''"
|
||||
@click="selectType = v"
|
||||
>
|
||||
{{v.name}}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<template v-if="selectType.code==='formula'">
|
||||
<div class="field mt-1 px-0 mx-0">
|
||||
<label class="label fs-14"> Chọn trường để tạo công thức <span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<b-taginput
|
||||
size="is-small"
|
||||
v-model="tags"
|
||||
:data="pageData? pageData.fields.filter(v=>v.format==='number') : []"
|
||||
type="is-primary is-light"
|
||||
autocomplete
|
||||
:open-on-focus="true"
|
||||
field="label"
|
||||
icon="plus"
|
||||
placeholder="Chọn trường"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<span class="mr-3 has-text-danger has-text-weight-bold"> {{props.option.name}}</span>
|
||||
<span :class="tags.find(v=>v.id===props.option.id)? 'has-text-primary' : ''"> {{props.option.label}} </span>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
Không có trường thỏa mãn
|
||||
</template>
|
||||
</b-taginput>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tags')"> {{errors.find(v=>v.name==='tags').message}} </p>
|
||||
</div>
|
||||
|
||||
<div class="field mt-3" v-if="tags.length>0">
|
||||
<p class="help is-primary"> Click đúp vào để thêm vào công thức tính.</p>
|
||||
<div class="tags">
|
||||
<a @dblclick="formula = formula? (formula + ' ' + v.name) : v.name" class="tag is-rounded" v-for="(v,i) in tags" :key="i">{{v.name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-3 px-0 mx-0">
|
||||
<label class="label fs-14">Công thức tính <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control">
|
||||
<textarea class="textarea is-small" rows="3" type="text" v-model="formula"> </textarea>
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='formula')"> {{errors.find(v=>v.name==='formula').message}} </p>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-3 px-0 mx-0">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Hiển thị theo <span class="has-text-danger">*</span> </label>
|
||||
<div class="control">
|
||||
<b-autocomplete
|
||||
size="is-small"
|
||||
icon-right="magnify"
|
||||
:value="selectUnit? selectUnit.name : ''"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="moneyunit"
|
||||
field="name"
|
||||
@select="option => selectUnit = option">
|
||||
</b-autocomplete>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14">Phần thập phân</label>
|
||||
<div class="control">
|
||||
<input class="input is-small" type="text" placeholder="" v-model="decimal">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="field mt-3 px-0 mx-0">
|
||||
<label class="label fs-14">Tên trường <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control">
|
||||
<input class="input is-small" type="text" placeholder="Tên trường phải là duy nhất" v-model="name"
|
||||
:readonly="selectType? selectType.code==='formula': false">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='name')"> {{errors.find(v=>v.name==='name').message}} </p>
|
||||
<p class="help has-text-primary" v-else> Tên trường do hệ thống tự sinh.</p>
|
||||
</div>
|
||||
|
||||
<div class="field mt-3">
|
||||
<label class="label fs-14"> Mô tả <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control is-expanded">
|
||||
<input class="input is-small" type="text" placeholder="" v-model="label">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='label')"> {{errors.find(v=>v.name==='label').message}} </p>
|
||||
</div>
|
||||
|
||||
<div class="field mt-3" v-if="selectType.code==='empty'">
|
||||
<label class="label fs-14"
|
||||
>Kiểu dữ liệu
|
||||
<span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<div class="control fs-14">
|
||||
<b-radio v-for="(v,i) in datatype.filter(x=>x.code!=='date')" :key="i" v-model="radioType" :native-value="v">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field mt-4">
|
||||
<p class="control">
|
||||
<a class="button is-primary is-small is-outlined is-rounded"
|
||||
@click="selectType.code==='formula'? createField() : createEmptyField()"> Tạo trường</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['pagename'],
|
||||
data() {
|
||||
return {
|
||||
selectUnit: undefined,
|
||||
data: [],
|
||||
current: 1,
|
||||
filterData: [],
|
||||
loading: false,
|
||||
fieldType: [{code: 'formula', name: 'Công thức'}, {code: 'empty', name: 'Trường rỗng'}],
|
||||
tags: [],
|
||||
formula: undefined,
|
||||
name: 'f' + this.$dayjs(new Date()).format("hhmmss"),
|
||||
label: undefined,
|
||||
errors: [],
|
||||
selectType: undefined,
|
||||
radioType: undefined,
|
||||
decimal: undefined
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.radioType = this.datatype.find(v=>v.code==='string')
|
||||
this.selectType = this.fieldType.find(v=>v.code==='formula')
|
||||
this.selectUnit = this.moneyunit.find(v=>v.code==='one')
|
||||
},
|
||||
computed: {
|
||||
moneyunit: {
|
||||
get: function() {return this.$store.state.moneyunit},
|
||||
set: function(val) {this.$store.commit("updateMoneyUnit", {moneyunit: val})}
|
||||
},
|
||||
pageData: {
|
||||
get: function() {return this.$store.state[this.pagename]},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
|
||||
},
|
||||
tablesetting: {
|
||||
get: function() {return this.$store.state.tablesetting},
|
||||
set: function(val) {this.$store.commit("updateTableSetting", {tablesetting: val})}
|
||||
},
|
||||
datatype: {
|
||||
get: function() {return this.$store.state.datatype},
|
||||
set: function(val) {this.$store.commit("updateDataType", {datatype: val})}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkValid() {
|
||||
this.errors = []
|
||||
if(this.tags.length===0) {
|
||||
this.errors.push({name: 'tags', message: 'Chưa chọn trường xây dựng công thức.'})
|
||||
}
|
||||
|
||||
if(!this.$empty(this.formula)? this.$empty(this.formula.trim()) : true) {
|
||||
this.errors.push({name: 'formula', message: 'Công thức không được bỏ trống.'})
|
||||
}
|
||||
|
||||
if(!this.$empty(this.label)? this.$empty(this.label.trim()) : true )
|
||||
this.errors.push({name: 'label', message: 'Mô tả không được bỏ trống.'})
|
||||
else if(this.pageData.fields.find(v=>v.label.toLowerCase()===this.label.toLowerCase())) {
|
||||
this.errors.push({name: 'label', message: 'Mô tả bị trùng. Hãy đặt mô tả khác.'})
|
||||
}
|
||||
|
||||
if(this.errors.length>0) return false
|
||||
//check formula
|
||||
let val = this.$copy(this.formula)
|
||||
this.tags.forEach(v => {
|
||||
let myRegExp = new RegExp(v.name, 'g')
|
||||
val = val.replace(myRegExp, Math.random())
|
||||
})
|
||||
|
||||
try {
|
||||
let value = this.$calc(val)
|
||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
||||
this.errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
this.errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
|
||||
}
|
||||
return this.errors.length>0? false : true
|
||||
},
|
||||
createField() {
|
||||
if(!this.checkValid()) return
|
||||
let field = this.$createField(this.name.trim(), this.label.trim(), 'number', true)
|
||||
field.formula = this.formula.trim()
|
||||
field.tags = this.tags.map(v=>v.name)
|
||||
field.level = Math.max(...this.tags.map(v=>v.level? v.level : 0)) + 1
|
||||
field.unit = this.selectUnit.detail
|
||||
field.decimal = this.decimal
|
||||
field.disable = 'search,value'
|
||||
let copy = this.$copy(this.pageData.fields)
|
||||
copy.push(field)
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: copy}})
|
||||
this.$emit('newfield', field)
|
||||
this.tags = []
|
||||
this.formula = undefined
|
||||
this.label = undefined
|
||||
this.name = 'f' + this.$dayjs(new Date()).format("hhmmss")
|
||||
},
|
||||
createEmptyField() {
|
||||
this.errors = []
|
||||
if(!this.$empty(this.name)? this.$empty(this.name.trim()) : true )
|
||||
this.errors.push({name: 'name', message: 'Tên không được bỏ trống.'})
|
||||
else if(this.pageData.fields.find(v=>v.name.toLowerCase()===this.name.toLowerCase())) {
|
||||
this.errors.push({name: 'name', message: 'Tên trường bị trùng. Hãy đặt tên khác.'})
|
||||
}
|
||||
if(!this.$empty(this.label)? this.$empty(this.label.trim()) : true )
|
||||
this.errors.push({name: 'label', message: 'Mô tả không được bỏ trống.'})
|
||||
else if(this.pageData.fields.find(v=>v.label.toLowerCase()===this.label.toLowerCase())) {
|
||||
this.errors.push({name: 'label', message: 'Mô tả bị trùng. Hãy đặt mô tả khác.'})
|
||||
}
|
||||
if(this.errors.length>0) return
|
||||
let field = this.$createField(this.name.trim(), this.label.trim(), this.radioType.code, true)
|
||||
let copy = this.$copy(this.pageData.fields)
|
||||
copy.push(field)
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "fields", data: copy})
|
||||
this.$emit('newfield', field)
|
||||
this.label = undefined
|
||||
this.name = 'f' + this.$dayjs(new Date()).format("hhmmss")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
444
components/datatable/CreateTemplate.vue
Normal file
@@ -0,0 +1,444 @@
|
||||
<template>
|
||||
<div class="px-3">
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label">Đối tượng</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in types" :key="i" v-model="type"
|
||||
:native-value="v" @input="changeType(v)">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Kích cỡ</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in sizes.filter(v=>type? (type.code==='tag'? v.code!=='is-small' : 1>0) : true)" :key="i" v-model="size"
|
||||
:native-value="v" @input="changeType(v)">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" v-if="['tag'].find(v=>v===type.code)">Hình khối</label>
|
||||
<p class="control fs-14" v-if="['tag'].find(v=>v===type.code)">
|
||||
<b-radio v-for="(v,i) in shapes" :key="i" v-model="shape"
|
||||
:native-value="v" @input="changeType(v)">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal" v-if="['tag'].find(v=>v===type.code)">
|
||||
<div class="field-body">
|
||||
<div class="field" v-if="type.code!=='tag'">
|
||||
<label class="label">Outline</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in outlines" :key="i" v-model="outline"
|
||||
:native-value="v" @input="changeType(v)">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tags" v-if="type.code==='tag'">
|
||||
<a :class="getClass(v)" v-for="(v,i) in colorscheme" :key="i"
|
||||
@click="doSelect(v)" :ref="'tag' + i"> {{v.name}} </a>
|
||||
</div>
|
||||
|
||||
<div class="pt-2" v-else-if="type.code==='span'">
|
||||
<a class="mr-3" :class="getSpanClass(v)" v-for="(v,i) in colorscheme" :key="i"
|
||||
@click="doSelectSpan(v)" :ref="'span' + i"> {{v.name}} </a>
|
||||
</div>
|
||||
|
||||
<div :class="`tabs is-boxed mt-5 mb-5 ${tab.code==='template'? '' : 'pb-2'}`">
|
||||
<ul>
|
||||
<li :class="tab.code===v.code? 'is-active' : ''"
|
||||
v-for="(v,i) in tabs" :key="i" @click="tab=v"><a class="fs-15">{{v.name}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<template v-if="tab.code==='selected'">
|
||||
<a v-for="(v,i) in tags" :key="i" @click="selected=v">
|
||||
<div class="field is-grouped is-grouped-multiline mt-4">
|
||||
<p class="control">
|
||||
<a :class="v.class">
|
||||
{{v.name}}
|
||||
</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<input class="input is-small" type="text" v-model="v.name">
|
||||
</p>
|
||||
|
||||
<p class="control">
|
||||
<a @click="remove(i)">
|
||||
<span class="icon has-text-danger fs-26">
|
||||
<i class="mdi mdi-close"/>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<p class="control has-text-right ml-6" v-if="selected? selected.id===v.id : false">
|
||||
<span class="icon fs-26">
|
||||
<i class="mdi mdi-check"/>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab.code==='condition'">
|
||||
<div class="mb-5" v-if="selected">
|
||||
<b-radio v-for="(v,i) in conditions" :key="i" v-model="condition"
|
||||
:native-value="v" @input="changeCondition(v)">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</div>
|
||||
|
||||
<template v-if="condition? condition.code==='yes' : false">
|
||||
<div class="field mt-3">
|
||||
<label class="label fs-14">Chọn trường xây dựng biểu thức <span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<b-taginput
|
||||
size="is-small"
|
||||
v-model="tagsField"
|
||||
:data="pageData? pageData.fields.filter(v=>v.format==='number') : []"
|
||||
type="is-dark is-light"
|
||||
autocomplete
|
||||
:open-on-focus="true"
|
||||
field="name"
|
||||
icon="plus"
|
||||
placeholder="Chọn trường"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<span class="mr-3 has-text-danger has-text-weight-bold"> {{props.option.name}}</span>
|
||||
<span :class="tagsField.find(v=>v.id===props.option.id)? 'has-text-dark' : ''"> {{$stripHtml(props.option.label, 50)}} </span>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
Không có trường thỏa mãn
|
||||
</template>
|
||||
</b-taginput>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tagsField')"> {{errors.find(v=>v.name==='tagsField').message}} </p>
|
||||
</div>
|
||||
<div class="field mt-1" v-if="tagsField.length>0">
|
||||
<p class="help is-primary"> Click đúp vào để thêm vào biểu thức.</p>
|
||||
<div class="tagsField">
|
||||
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name"
|
||||
class="tag is-rounded" v-for="(v,i) in tagsField" :key="i">
|
||||
<b-tooltip :label="$stripHtml(v.label)" type="is-dark">{{v.name}}</b-tooltip></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14">Biểu thức có dạng Đúng / Sai <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control is-expanded">
|
||||
<input class="input" type="text" v-model="expression" placeholder="Tạo biểu thức tại đây">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='expression')"> {{errors.find(v=>v.name==='expression').message}} </p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab.code==='option' && selected">
|
||||
<div class="field is-horizontal border-bottom pb-2 mt-1">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Màu nền </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioBGcolor"
|
||||
:native-value="v" @input="changeStyle()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioBGcolor? radioBGcolor.code==='option' : false">
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" v-model="bgcolor" @change="changeStyle()">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal border-bottom pb-2">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Màu chữ </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioColor"
|
||||
:native-value="v" @input="changeStyle()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioColor? radioColor.code==='option' : false">
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" v-model="color" @change="changeStyle()">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal border-bottom pb-2">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Cỡ chữ </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioSize"
|
||||
:native-value="v" @input="changeStyle()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioSize? radioSize.code==='option' : false">
|
||||
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" placeholder="Nhập số" v-model="textsize" @change="changeStyle()">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="tab.code==='template'">
|
||||
<p class="mb-3">
|
||||
<a @click="copyContent()" class="mr-6">
|
||||
<span class="icon fs-20">
|
||||
<i class="mdi mdi-content-copy"/>
|
||||
</span>
|
||||
Copy
|
||||
</a>
|
||||
<a @click="paste()" class="mr-6">
|
||||
<span class="icon fs-20">
|
||||
<i class="mdi mdi-content-copy"/>
|
||||
</span>
|
||||
Paste
|
||||
</a>
|
||||
</p>
|
||||
<div>
|
||||
<textarea class="textarea fs-14" rows="8" v-model="text" @dblclick="doCheck"></textarea>
|
||||
</div>
|
||||
<p class="mt-5">
|
||||
<span class="icon-text fsb-18">
|
||||
Replace
|
||||
<span class="material-symbols-outlined">
|
||||
chevron_right
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<div class="field is-grouped mt-4">
|
||||
<div class="control">
|
||||
<p class="fsb-14 mb-1">Đoạn text</p>
|
||||
<input class="input" type="text" placeholder="" v-model="source">
|
||||
</div>
|
||||
<div class="control">
|
||||
<p class="fsb-14 mb-1">Thay bằng</p>
|
||||
<input class="input" type="text" placeholder="" v-model="target">
|
||||
</div>
|
||||
<div class="control pl-5">
|
||||
<button class="button is-primary is-rounded is-outlined mt-5" @click="replace()">Replace</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-6 has-text-centered">
|
||||
<button class="button is-primary is-medium is-rounded" @click="changeTemplate()">Áp dụng</button>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['pagename', 'field'],
|
||||
data() {
|
||||
return {
|
||||
type: undefined,
|
||||
size: undefined,
|
||||
types: [{code: 'span', name: 'span'}, {code: 'tag', name: 'tag'}],
|
||||
sizes: [{code: 'is-small', name: 'Nhỏ', value: 'is-size-6'}, {code: 'is-normal', name: 'Trung bình', value: 'is-size-5'},
|
||||
{code: 'is-medium', name: 'Lớn', value: 'is-size-4'}],
|
||||
shapes: [{code: 'default', name: 'Mặc định'}, {code: 'is-rounded', name: 'Tròn góc'}],
|
||||
shape: undefined,
|
||||
outlines: [{code: 'default', name: 'Mặc định'}, {code: 'is-outlined', name: 'Outline'}],
|
||||
outline: undefined,
|
||||
conditions: [{code: 'no', name: 'Không áp dụng'}, {code: 'yes', name: 'Có áp dụng'}],
|
||||
condition: undefined,
|
||||
tags: [],
|
||||
selected: undefined,
|
||||
tabs: [{code: 'selected', name: 'Bước 1: Tạo nội dung'}, {code: 'condition', name: 'Bước 2: Đặt điều kiện'}, {code: 'option', name: 'Bước 3: Chọn màu, cỡ chữ'},
|
||||
{code: 'template', name: 'Bước 4: Mã lệnh & áp dụng'}],
|
||||
tab: undefined,
|
||||
tagsField: [],
|
||||
errors: [],
|
||||
expression: '',
|
||||
text: undefined,
|
||||
radioBGcolor: undefined,
|
||||
radioColor: undefined,
|
||||
radioSize: undefined,
|
||||
bgcolor: undefined,
|
||||
color: undefined,
|
||||
textsize: undefined,
|
||||
source: undefined,
|
||||
target: this.$copy(this.field.name)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.type = this.types.find(v=>v.code==='tag')
|
||||
this.size = this.sizes.find(v=>v.code==='is-normal')
|
||||
this.shape = this.shapes.find(v=>v.code==='is-rounded')
|
||||
this.outline = this.shapes.find(v=>v.code==='default')
|
||||
if(this.$empty(this.field.template)) this.tab = this.tabs.find(v=>v.code==='selected')
|
||||
else {
|
||||
this.text = this.$copy(this.field.template)
|
||||
this.tab = this.tabs.find(v=>v.code==='template')
|
||||
}
|
||||
this.condition = this.conditions.find(v=>v.code==='no')
|
||||
},
|
||||
watch: {
|
||||
expression: function(newVal) {
|
||||
if(this.$empty(newVal)) return
|
||||
else this.checkExpression()
|
||||
},
|
||||
tab: function(newVal, oldVal) {
|
||||
if(oldVal===undefined) return
|
||||
if(newVal.code==='template') {
|
||||
let value = '<div>'
|
||||
this.tags.map((v,i)=>{
|
||||
value += '<span class="' + v.class + (this.tags.length>i+1? ' mr-2' : '') + '" '
|
||||
if(v.style) value += 'style="' + v.style + '" '
|
||||
value += (v.expression? ' v-if="' + v.expression + '"' : '') + '>' + v.name + '</span>'
|
||||
})
|
||||
value += '</div>'
|
||||
this.text = value
|
||||
} else if(newVal.code==='option') {
|
||||
if(!this.selected) return
|
||||
this.radioBGcolor = this.selected.bgcolor? this.colorchoice.find(v=>v.code==='option') : this.colorchoice.find(v=>v.code==='none')
|
||||
this.radioColor = this.selected.color? this.colorchoice.find(v=>v.code==='option') : this.colorchoice.find(v=>v.code==='none')
|
||||
this.radioSize = this.selected.textsize? this.colorchoice.find(v=>v.code==='option') : this.colorchoice.find(v=>v.code==='none')
|
||||
this.bgcolor = this.selected.bgcolor? this.selected.bgcolor : undefined
|
||||
this.color = this.selected.color? this.selected.color : undefined
|
||||
this.textsize = this.selected.textsize? this.selected.textsize : undefined
|
||||
} else if(newVal.code==='condition') {
|
||||
this.condition = this.conditions.find(v=>v.code==='no')
|
||||
this.tagsField = []
|
||||
this.expression = ''
|
||||
if(this.selected? this.selected.expression : false) {
|
||||
this.condition = this.conditions.find(v=>v.code==='yes')
|
||||
this.tagsField = this.$copy(this.selected.tags)
|
||||
this.expression = this.$copy(this.selected.formula)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
colorscheme: {
|
||||
get: function() {return this.$store.state.colorscheme},
|
||||
set: function(val) {this.$store.commit("updateColorScheme", {colorscheme: val})}
|
||||
},
|
||||
pageData: {
|
||||
get: function() {return this.$store.state[this.pagename]},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
|
||||
},
|
||||
colorchoice: {
|
||||
get: function() {return this.$store.state.colorchoice},
|
||||
set: function(val) {this.$store.commit("updateColorChoice", {colorchoice: val})}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async paste() {
|
||||
this.text = await navigator.clipboard.readText()
|
||||
},
|
||||
replace() {
|
||||
if(this.$empty(this.text)) return
|
||||
this.text = this.text.replaceAll(this.source, this.target)
|
||||
},
|
||||
doCheck() {
|
||||
let text = window.getSelection().toString()
|
||||
if(this.$empty(text)) return
|
||||
this.source = text
|
||||
},
|
||||
changeStyle() {
|
||||
this.selected.bgcolor = this.selected.color = this.selected.textsize = this.selected.style = undefined
|
||||
let style = ''
|
||||
if(this.radioBGcolor.code==='option'? !this.$empty(this.bgcolor) : false) {
|
||||
this.selected.bgcolor = this.bgcolor
|
||||
style += 'background-color: ' + this.bgcolor + ' !important; '
|
||||
}
|
||||
if(this.radioColor.code==='option'? !this.$empty(this.color) : false) {
|
||||
this.selected.color = this.color
|
||||
style += 'color: ' + this.color + ' !important; '
|
||||
}
|
||||
if(this.radioSize.code==='option'? this.$isNumber(this.textsize) : false) {
|
||||
this.selected.textsize = this.textsize
|
||||
style += 'font-size: ' + this.textsize + 'px !important; '
|
||||
}
|
||||
this.$empty(style)? false : this.selected.style = style
|
||||
},
|
||||
changeCondition(v) {
|
||||
if(v.code==='no') this.selected.expression = undefined
|
||||
},
|
||||
copyContent() {
|
||||
this.$copyToClipboard(this.text)
|
||||
},
|
||||
changeTemplate() {
|
||||
let copy = this.$copy(this.pageData.fields)
|
||||
let found = copy.find(v=>v.name===this.field.name)
|
||||
found.template = this.text
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: copy}})
|
||||
},
|
||||
checkExpression() {
|
||||
this.errors = []
|
||||
let val = this.$copy(this.expression)
|
||||
let exp = this.$copy(this.expression)
|
||||
this.tagsField.forEach(v => {
|
||||
let myRegExp = new RegExp(v.name, 'g')
|
||||
val = val.replace(myRegExp, Math.random())
|
||||
exp = exp.replace(myRegExp, "formatNumber(row['" + v.name + "'])")
|
||||
})
|
||||
try {
|
||||
let value = this.$calc(val)
|
||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
} else if(!(eval(value)===true || eval(value)===false)) {
|
||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
} else if(this.selected) {
|
||||
this.selected.expression = exp
|
||||
this.selected.formula = this.expression
|
||||
this.selected.tags = this.$copy(this.tagsField)
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
}
|
||||
return this.errors.length>0? false : true
|
||||
},
|
||||
changeType(v) {
|
||||
},
|
||||
doSelect(v) {
|
||||
this.tags.push({id: this.$id(), name: v.name, class: this.getClass(v)})
|
||||
this.tab = this.tabs.find(v=>v.code==='selected')
|
||||
this.selected = this.tags[this.tags.length-1]
|
||||
},
|
||||
doSelectSpan(v) {
|
||||
this.tags.push({id: this.$id(), name: v.name, class: this.getSpanClass(v)})
|
||||
this.tab = this.tabs.find(v=>v.code==='selected')
|
||||
this.selected = this.tags[this.tags.length-1]
|
||||
},
|
||||
remove(i) {
|
||||
this.$delete(this.tags, i)
|
||||
},
|
||||
getClass(v) {
|
||||
let value = this.type.code + ' ' + v.code + ' ' + this.size.code + (this.shape.code==='default'? '' : ' ' + this.shape.code)
|
||||
value += (this.outline.code==='default'? '' : ' ' + this.outline.code)
|
||||
return value
|
||||
},
|
||||
getSpanClass(v) {
|
||||
let value = 'has-text-' + v.name.toLowerCase() + ' ' + this.size.value
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
612
components/datatable/DataTable.vue
Normal file
@@ -0,0 +1,612 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="pb-1" v-if="pagedata.showFilter && filters.length>0">
|
||||
<div class="field is-grouped is-grouped-multiline pl-2" v-if="filters? filters.length>0 : false">
|
||||
<div class="control mr-5">
|
||||
<a class="button is-primary is-outlined is-rounded fs-10" @click="clearFilter()">
|
||||
<span class="fs-13">Xóa lọc</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="control pr-2 mr-5">
|
||||
<span class="icon has-text-primary fsb-18"> <i class="mdi mdi-sigma"> </i>
|
||||
{{totalRows}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="control" v-for="(v,i) in filters" :key="i">
|
||||
<div class="tags has-addons is-marginless">
|
||||
<a class="tag is-primary is-marginless" @click="showCondition(v)">{{v.label.indexOf('>')>=0? $stripHtml(v.label,30) : v.label}}</a>
|
||||
<a class="tag is-delete is-marginless has-text-black-bis" @click="removeFilter(i)"></a>
|
||||
</div>
|
||||
<span class="help has-text-black-bis">
|
||||
{{v.sort? v.sort : (v.select? ('[' + (v.select.length>0? $stripHtml(v.select[0],20) : '') + '...Σ' + v.select.length + ']') :
|
||||
(v.condition))}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container" ref="container">
|
||||
<table class="table is-bordered is-narrow is-hoverable" ref="table" :style="getSettingStyle('table')">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(field,i) in displayFields" :key="i" :ref="`th${field.name}`"
|
||||
:style="getSettingStyle('header', field)">
|
||||
<div class="hyperlink" @click="showField(field)" :style="getSettingStyle('dropdown', field)">
|
||||
<template v-if="field.label.indexOf('<')<0">{{field.label}}</template>
|
||||
<template v-else>
|
||||
<component v-bind="{row: field, tick: tickall}" :is="compiledComponent(field.label)" @clickevent="doAction($event, field)" />
|
||||
</template>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(v,i) in displayData" :key="i">
|
||||
<td v-for="(field, j) in fields.filter(v=>v.show)" :key="j" :ref="`${v.stock_code}${field.name}`" :id="`${v.stock_code}${field.name}`"
|
||||
:style="v[`${field.name}color`]" @dblclick="doubleClick(field, v)">
|
||||
<component v-bind="{row: v, tick: tick, pagename: pagename, field: field, highlight: highlight}" v-if="field.template"
|
||||
:is="compiledComponent(field.template)" @clickevent="doAction($event, v, field)"
|
||||
/>
|
||||
<template v-else-if="field.tooltip">
|
||||
<b-tooltip :label="v[field.tooltip.field]"
|
||||
:position="field.tooltip.placement"
|
||||
:type="field.tooltip.type">
|
||||
{{v[field.name]}}
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<template v-else> {{v[field.name]}} </template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-3 px-3" v-if="showPaging">
|
||||
<b-pagination
|
||||
:total="totalRows"
|
||||
:current.sync="currentPage"
|
||||
:order="'is-centered'"
|
||||
:rounded="true"
|
||||
:per-page="perPage"
|
||||
@change="changePage()"
|
||||
>
|
||||
</b-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"
|
||||
@dosearch="doSearch(currentField, $event)" @doselect="doSelect(currentField, $event)" @dosort="doSort(currentField, $event)"
|
||||
@setfilter="setFilter(currentField, $event)" @showsidebar="showSidebar($event)" @copyfield="copyField">
|
||||
</Modal>
|
||||
<Modal @close="showmodal1=undefined" v-bind="showmodal1" v-if="showmodal1" @updatefields="updateFields"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
export default {
|
||||
props: ['pagename'],
|
||||
data() {
|
||||
return {
|
||||
data: [],
|
||||
fields: [],
|
||||
search: '',
|
||||
filters: [],
|
||||
currentPage: 1,
|
||||
filterData: [],
|
||||
timer: undefined,
|
||||
perPage: 30,
|
||||
currentField: undefined,
|
||||
flagSearch: false,
|
||||
scrollbar: undefined,
|
||||
tablesetting: undefined,
|
||||
tick: {},
|
||||
tickall: false,
|
||||
displayData: [],
|
||||
displayFields: [],
|
||||
showmodal: undefined,
|
||||
showmodal1: undefined,
|
||||
totalRows: 0,
|
||||
showPaging: false,
|
||||
highlight: undefined
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if(this.pagedata.data) this.data = this.$copy(this.pagedata.data)
|
||||
if(this.pagedata.fields) this.fields = this.$copy(this.pagedata.fields)
|
||||
if(this.pagedata.filters) this.filters = this.$copy(this.pagedata.filters)
|
||||
this.tablesetting = this.$copy(this.pagedata.tablesetting || this.gridsetting)
|
||||
this.perPage = this.pagedata.perPage? this.pagedata.perPage : this.$formatNumber(this.tablesetting.find(v=>v.code==='per-page').detail)
|
||||
if(this.data.length>0 && this.fields.length>0) this.updateShow()
|
||||
else this.showPagination()
|
||||
},
|
||||
watch: {
|
||||
'pagedata.update': function(newVal) {
|
||||
if(newVal) this.updateData(newVal)
|
||||
},
|
||||
menuaction: function(newVal) {
|
||||
if(this.$empty(newVal)) return
|
||||
if(newVal.name==='export-excel' && (newVal.pagename? newVal.pagename===this.pagename : true)) {
|
||||
this.$exportExcel(this.data, this.menuaction.file, this.fields.filter(v=>v.show))
|
||||
} else if(newVal.name==='opensidebar' && (newVal.pagename? newVal.pagename===this.pagename : true)) {
|
||||
this.showmodal = {component: 'datatable/TableOption', vbind: {pagename: this.pagename}, width: '850px', height: '600px', title: 'Danh sách cột'}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pagedata: {
|
||||
get: function() {return this.$store.state[this.pagename]},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
|
||||
},
|
||||
gridsetting: {
|
||||
get: function() {return this.$store.state.tablesetting},
|
||||
set: function(val) {this.$store.commit("updateTableSetting", {tablesetting: val})}
|
||||
},
|
||||
menuaction: {
|
||||
get: function() {return this.$store.state.menuaction},
|
||||
set: function(val) {this.$store.commit("updateMenuAction", {menuaction: val})}
|
||||
},
|
||||
currentsetting: {
|
||||
get: function() {return this.$store.state.currentsetting},
|
||||
set: function(val) {this.$store.commit("updateCurrentSetting", {currentsetting: val})}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showPagination() {
|
||||
this.showPaging = this.pagedata.pagination===false? false : true
|
||||
this.totalRows = this.data.length
|
||||
if(this.showPaging && this.pagedata.api) {
|
||||
if(this.pagedata.api.full_data===false) this.totalRows = this.pagedata.api.total_rows
|
||||
this.showPaging = this.totalRows > this.perPage
|
||||
}
|
||||
},
|
||||
clearFilter() {
|
||||
this.updateData({filters: []})
|
||||
},
|
||||
async updateData(newVal) {
|
||||
if(newVal.columns) { //change attribute
|
||||
this.fields = this.$copy(newVal.columns)
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'fields', data: this.fields})
|
||||
this.$emit('changefield', this.fields)
|
||||
let fields = this.fields.filter(v=>v.show)
|
||||
this.data.map(v=>{
|
||||
fields.map(x=>v[`${x.name}color`] = this.getStyle(x, v))
|
||||
})
|
||||
return this.updateShow()
|
||||
}
|
||||
if(newVal.tablesetting) {
|
||||
this.tablesetting = newVal.tablesetting
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'tablesetting', data: this.tablesetting})
|
||||
this.perPage = this.$formatNumber(this.tablesetting.find(v=>v.code=="per-page").detail)
|
||||
this.currentPage = 1
|
||||
}
|
||||
this.tablesetting = this.$copy(this.pagedata.tablesetting || this.gridsetting)
|
||||
if(this.tablesetting) {
|
||||
this.perPage = this.pagedata.perPage? this.pagedata.perPage : Number(this.tablesetting.find(v=>v.code==='per-page').detail)
|
||||
}
|
||||
if(newVal.fields) {
|
||||
this.fields = this.$copy(newVal.fields)
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'fields', data: this.$copy(this.fields)})
|
||||
this.$emit('changefield', this.fields)
|
||||
} else this.fields = this.$copy(this.pagedata.fields)
|
||||
if(newVal.data || newVal.fields) {
|
||||
let copy = this.$copy(newVal.data || this.data)
|
||||
this.data = this.$calculateData(copy, this.fields)
|
||||
let fields = this.fields.filter(v=>v.show)
|
||||
this.data.map(v=>{
|
||||
fields.map(x=>v[`${x.name}color`] = this.getStyle(x, v))
|
||||
})
|
||||
if(newVal.data) {
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'data', data: this.$copy(this.data)})
|
||||
this.$emit('changedata', this.data)
|
||||
}
|
||||
}
|
||||
if(newVal.filters) this.filters = this.$copy(newVal.filters)
|
||||
else if(this.pagedata.filters) this.filters = this.$copy(this.pagedata.filters)
|
||||
if(newVal.data || newVal.fields || newVal.filters) {
|
||||
let copy = this.$copy(this.filters)
|
||||
this.filters.map((v,i)=>{
|
||||
let idx = this.$findIndex(this.fields, {name: v.name})
|
||||
let index = this.$findIndex(copy, {name: v.name})
|
||||
if(idx<0 && index>=0) this.$delete(copy, index)
|
||||
else if(idx>=0 && index>=0) copy[index].label = this.fields[idx].label
|
||||
})
|
||||
this.filters = copy
|
||||
this.doFilter(this.filters)
|
||||
}
|
||||
if(newVal.data || newVal.fields || newVal.filters || newVal.tablesetting) this.updateShow()
|
||||
if(newVal.data || newVal.fields) setTimeout(()=> this.scrollbarVisible(), 100)
|
||||
if(newVal.highlight) {
|
||||
this.highlight = this.$copy(newVal.highlight)
|
||||
setTimeout(()=>this.highlight = undefined, 500)
|
||||
}
|
||||
},
|
||||
updateShow(full_data) {
|
||||
this.showPagination()
|
||||
this.displayFields = this.fields.filter(v=>v.show)
|
||||
if(full_data===false) this.displayData = this.data
|
||||
else this.displayData = this.data.filter((ele,index) => (index>=(this.currentPage-1)*this.perPage && index<this.currentPage*this.perPage))
|
||||
},
|
||||
async changePage() {
|
||||
if(this.pagedata.api? this.pagedata.api.full_data===false : false) await this.backendFilter(this.filters)
|
||||
else this.updateShow()
|
||||
this.$emit('changepage', this.currentPage)
|
||||
},
|
||||
doubleClick(field, v) {
|
||||
this.doSelect(field, v[field.name])
|
||||
},
|
||||
showField(field) {
|
||||
if(this.pagedata.contextMenu===false || field.menu==='no') return
|
||||
this.showContextMenu(field)
|
||||
let doc = this.$refs[`th${field.name}`]
|
||||
let width = (doc? doc.length>0 : false)? doc[0].getBoundingClientRect().width : 100
|
||||
if(this.pagedata.setting) this.currentsetting = this.$copy(this.pagedata.setting)
|
||||
this.showmodal = {vbind: {pagename: this.pagename, field: this.currentField, filters: this.filters, filterData: this.filterData, width: width},
|
||||
component: 'datatable/ContextMenu', title: this.$stripHtml(field.label), width: '600px', height: '500px'}
|
||||
},
|
||||
showCondition(v) {
|
||||
this.$emit('contextmenu', 'open')
|
||||
this.currentField = this.$find(this.pagedata.fields, {'name': v.name})
|
||||
this.showField(this.currentField)
|
||||
},
|
||||
compiledComponent(value) {
|
||||
return {
|
||||
template: `${value}`,
|
||||
props: ['row', 'tick', 'pagename', 'field', 'highlight'],
|
||||
methods: {
|
||||
formatNumber(val) {
|
||||
return this.$formatNumber(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
showContextMenu(field) {
|
||||
this.currentField = field
|
||||
this.filterData = this.$unique(this.data, [field.name])
|
||||
this.menuaction = {name: 'display', key: this.$id(), field: field}
|
||||
this.$emit('contextmenu', 'open')
|
||||
},
|
||||
showSidebar(event) {
|
||||
let title = 'Danh sách cột'
|
||||
if(event.name==="bgcolor") title = `Đổi màu nền: ${event.field.name} / ${this.$stripHtml(event.field.label, 30)}`
|
||||
else if(event.name==="color") title = `Đổi màu chữ: ${event.field.name} / ${this.$stripHtml(event.field.label, 30)}`
|
||||
else if(event.name==='template') title = `Định dạng nâng cao: ${this.$stripHtml(event.field.label, 30)}`
|
||||
this.showmodal1 = {component: 'datatable/FormatOption',
|
||||
vbind: {event: event, currentField: this.currentField, pagename: this.pagename}, width: '850px', height: '700px', title: title}
|
||||
},
|
||||
getStyle(field, record) {
|
||||
var stop = false
|
||||
let val = this.tablesetting.find(v=>v.code==='td-border')? this.tablesetting.find(v=>v.code==='td-border').detail : 'border: solid 1px rgb(44, 44, 44); '
|
||||
val = val.indexOf(';')>=0? val : val + ';'
|
||||
if(field.bgcolor? !Array.isArray(field.bgcolor) : false) {
|
||||
val += ` background-color:${field.bgcolor}; `
|
||||
} else if(field.bgcolor? Array.isArray(field.bgcolor) : false) {
|
||||
field.bgcolor.map(v=>{
|
||||
if(v.type==='search') {
|
||||
if(record[field.name] && !stop? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase())>=0 : false) {
|
||||
val += ` background-color:${v.color}; `
|
||||
stop = true
|
||||
}
|
||||
} else {
|
||||
let res = this.$calculate(record, v.tags, v.expression)
|
||||
if(res.success && res.value && !stop) {
|
||||
val += ` background-color:${v.color}; `
|
||||
stop = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
stop = false
|
||||
if(field.color? !Array.isArray(field.color) : false) {
|
||||
val += ` color:${field.color}; `
|
||||
} else if(field.color? Array.isArray(field.color) : false) {
|
||||
field.color.map(v=>{
|
||||
if(v.type==='search') {
|
||||
if(record[field.name] && !stop? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase())>=0 : false) {
|
||||
val += ` color:${v.color}; `
|
||||
stop = true
|
||||
}
|
||||
} else {
|
||||
let res = this.$calculate(record, v.tags, v.expression)
|
||||
if(res.success && res.value && !stop) {
|
||||
val += ` color:${v.color}; `
|
||||
stop = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
stop = false
|
||||
if(field.textsize? !Array.isArray(field.textsize) : false) {
|
||||
val += ` font-size:${field.textsize}px; `
|
||||
} else if(field.textsize? Array.isArray(field.textsize) : false) {
|
||||
field.textsize.map(v=>{
|
||||
if(v.type==='search') {
|
||||
if(record[field.name] && !stop? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase())>=0 : false) {
|
||||
val += ` font-size:${v.size}px; `
|
||||
stop = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
let res = this.$calculate(record, v.tags, v.expression)
|
||||
if(res.success && res.value && !stop) {
|
||||
val += ` font-size:${v.size}px; `
|
||||
stop = true
|
||||
}
|
||||
}
|
||||
})
|
||||
} else val += ` font-size:${this.tablesetting.find(v=>v.code==='table-font-size').detail}px;`
|
||||
if(field.textalign) val += ` text-align:${field.textalign}; `
|
||||
if(field.minwidth) val += ` min-width:${field.minwidth}px; `
|
||||
if(field.maxwidth) val += ` max-width:${field.maxwidth}px; `
|
||||
return val
|
||||
},
|
||||
getSettingStyle(name, field) {
|
||||
let value = ''
|
||||
if(name==='container') {
|
||||
value = 'min-height:' + this.tablesetting.find(v=>v.code==='container-height').detail + 'rem; '
|
||||
} else if(name==='table') {
|
||||
value += 'background-color:' + this.tablesetting.find(v=>v.code==='table-background').detail + '; '
|
||||
value += 'font-size:' + this.tablesetting.find(v=>v.code==='table-font-size').detail + 'px;'
|
||||
value += 'color:' + this.tablesetting.find(v=>v.code==='table-font-color').detail + '; '
|
||||
} else if(name==='header') {
|
||||
value += 'background-color:' + this.tablesetting.find(v=>v.code==='header-background').detail + '; '
|
||||
if(field.minwidth) value += ' min-width: ' + field.minwidth + 'px; '
|
||||
if(field.maxwidth) value += ' max-width: ' + field.maxwidth + 'px; '
|
||||
} else if(name==='menu') {
|
||||
let arg = this.tablesetting.find(v=>v.code==='menu-width').detail
|
||||
arg = field? (field.menuwidth? field.menuwidth : arg) : arg
|
||||
value += 'width:' + arg + 'rem; '
|
||||
value += 'min-height:' + this.tablesetting.find(v=>v.code==='menu-min-height').detail + 'rem; '
|
||||
value += 'max-height:' + this.tablesetting.find(v=>v.code==='menu-max-height').detail + 'rem; '
|
||||
value += "overflow:auto; "
|
||||
} else if(name==='dropdown') {
|
||||
value += 'font-size:' + this.tablesetting.find(v=>v.code==='header-font-size').detail + 'px; '
|
||||
let found = this.filters.find(v=>v.name===field.name)
|
||||
found? value += 'color:' + this.tablesetting.find(v=>v.code==='header-filter-color').detail + '; '
|
||||
:value += 'color:' + this.tablesetting.find(v=>v.code==='header-font-color').detail + '; '
|
||||
}
|
||||
return value
|
||||
},
|
||||
removeFilter(i) {
|
||||
Vue.delete(this.filters, i)
|
||||
this.doFilter(this.filters)
|
||||
this.updateShow()
|
||||
},
|
||||
updateFields(field) {
|
||||
let copy = this.$copy(this.pagedata.fields)
|
||||
let idx = copy.findIndex(v=>v.name===field.name)
|
||||
copy[idx] = field
|
||||
this.updateData({columns: copy})
|
||||
this.currentField = this.$copy(field)
|
||||
if(this.showmodal) {
|
||||
this.showmodal.vbind.field = this.$copy(field)
|
||||
this.showmodal = this.$copy(this.showmodal)
|
||||
}
|
||||
},
|
||||
doAction(event, row, field) {
|
||||
let name = typeof event === "string"? event : event.name
|
||||
let data = typeof event === "string"? event : event.data
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'action', data: {event: name, row: row, field: field, data: data, time: new Date()}})
|
||||
if(name==='remove') this.$deleterow(this.pagedata.api.name, row.id, this.pagename, true)
|
||||
if(name==='batchdelete') this.batchDelete()
|
||||
this.$emit(name, row, field, data)
|
||||
if(name==='tickall') {
|
||||
this.tickall = data
|
||||
if(data===false) this.tick = {}
|
||||
else {
|
||||
this.data.map(v=>this.tick[v.id] = true)
|
||||
this.tick = this.$copy(this.tick)
|
||||
}
|
||||
}
|
||||
},
|
||||
batchDelete() {
|
||||
let arr = []
|
||||
Object.entries(this.tick).forEach(([key, value]) => {
|
||||
if(value) arr.push({id: Number(key)})
|
||||
})
|
||||
if(arr.length===0) this.$buefy.toast.open({message: 'Bạn chưa chọn bản ghi để xoá', type: 'is-warning'})
|
||||
else this.$deleterow(this.pagedata.api.name, arr, this.pagename, true)
|
||||
},
|
||||
doSort(field, type) {
|
||||
let filter = {name: field.name, label: field.label, sort: type, format: field.format}
|
||||
let idx = this.filters.findIndex(v=>v.name===field.name)
|
||||
if(idx>=0) Vue.set(this.filters, idx, filter)
|
||||
else this.filters.push(filter)
|
||||
this.doFilter(this.filters)
|
||||
this.updateShow()
|
||||
},
|
||||
doSearch(field, search) {
|
||||
let copy = this.$copy(this.filters)
|
||||
let idx = copy.findIndex(v=>v.name===field.name)
|
||||
if(idx>=0) Vue.delete(copy, idx)
|
||||
if(this.pagedata.origin_api.full_data) {
|
||||
let data = this.frontendFilter(copy)
|
||||
let rows = this.$empty(search)? data
|
||||
: data.filter(v=>v[field.name]? v[field.name].toString().toLowerCase().indexOf(search.toLowerCase())>=0 : false)
|
||||
this.filterData = this.$unique(rows, [field.name])
|
||||
if(this.showmodal) this.showmodal.vbind.filterData = this.filterData
|
||||
} else {
|
||||
copy.push({name: field.name, label: field.label, search: search.toLowerCase(), format: field.format})
|
||||
this.flagSearch = true
|
||||
this.backendFilter(copy)
|
||||
}
|
||||
},
|
||||
doSelect(field, value) {
|
||||
let found = this.filters.find(v=>v.name===field.name)
|
||||
if(found) {
|
||||
!found.select? found.select = [] : false
|
||||
let idx = found.select.findIndex(x=>x===value)
|
||||
idx>=0? Vue.delete(found.select, idx) : found.select.push(value)
|
||||
if(found.select.length===0) {
|
||||
idx = this.filters.findIndex(v=>v.name===field.name)
|
||||
if(idx>=0) Vue.delete(this.filters, idx)
|
||||
}
|
||||
} else {
|
||||
this.filters.push({name: field.name, label: field.label, select: [value], format: field.format})
|
||||
}
|
||||
this.doFilter(this.filters)
|
||||
this.updateShow()
|
||||
},
|
||||
setFilter(field, filter) {
|
||||
let idx = this.filters.findIndex(v=>v.name===field.name)
|
||||
if(idx<0) this.filters.push(filter)
|
||||
else Vue.set(this.filters, idx, filter)
|
||||
this.doFilter(this.filters)
|
||||
this.updateShow()
|
||||
},
|
||||
doFilter(newVal, nonset) {
|
||||
if(this.currentPage>1 && nonset!==true) this.currentPage = 1
|
||||
if(this.pagedata.origin_api.full_data) {
|
||||
this.data = this.frontendFilter(newVal)
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "dataFilter", data: this.$copy(this.data)})
|
||||
this.$emit('changedata', newVal)
|
||||
}
|
||||
else {
|
||||
if(this.timer) clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => this.backendFilter(newVal), 200)
|
||||
}
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "filters", data: this.$copy(newVal)})
|
||||
this.$emit('changefilter', newVal? newVal.length>0 : false)
|
||||
},
|
||||
frontendFilter(newVal) {
|
||||
let self = this
|
||||
let checkValid = function(name, x, filter) {
|
||||
if(self.$empty(x[name])) return false
|
||||
else {
|
||||
let text = ''
|
||||
filter.map((y,k)=>{
|
||||
text += `${k>0? (filter[k-1].operator==='and'? ' &&' : ' ||') : ''} ${self.$formatNumber(x[name])}
|
||||
${y.condition==='='? '==' : (y.condition==='<>'? '!==' : y.condition)} ${self.$formatNumber(y.value)}`
|
||||
})
|
||||
return self.$calc(text)
|
||||
}
|
||||
}
|
||||
newVal = this.$copy(newVal)
|
||||
var data = this.$copy(this.pagedata.data)
|
||||
newVal.filter(m=>m.select || m.filter).map(v => {
|
||||
if(v.select) {
|
||||
data = data.filter(x => v.select.findIndex(y => this.$empty(y)? this.$empty(x[v.name]) : (y===x[v.name])) >-1)
|
||||
} else if(v.filter) {
|
||||
data = data.filter(x => checkValid(v.name, x, v.filter))
|
||||
}
|
||||
})
|
||||
let sort = {}
|
||||
let format = {}
|
||||
let list = this.filters.filter(x=>x.sort)
|
||||
list.map(v=>{
|
||||
sort[v.name] = v.sort === "az" ? "asc" : "desc"
|
||||
format[v.name] = v.format;
|
||||
})
|
||||
return list.length>0? this.$multiSort(data, sort, format) : data
|
||||
},
|
||||
// Sử dụng backend filter
|
||||
async backendFilter(newVal) {
|
||||
let arr = [{code: '>', name: 'gt'}, {code: '>=', name: 'gte'}, {code: '<', name: 'lt'}, {code: '<=', name: 'lte'}, {code: '=', name: 'e'},
|
||||
{code: '<>', name: 'e'}]
|
||||
let params = this.pagedata.origin_api.params? this.$copy(this.pagedata.origin_api.params) : {}
|
||||
params.page = this.currentPage
|
||||
var where = params.filter? params.filter : {}
|
||||
var exclude = {}
|
||||
var sort = params.sort? params.sort.split(',') : []
|
||||
var filter = newVal.filter(v=>!v.formula)
|
||||
if (this.pagedata.origin_api.url.indexOf("data/") >= 0) {
|
||||
filter.forEach(v => {
|
||||
if(v.search) where[v.name + "__icontains"] = v.search
|
||||
else if (v.select) where[v.name + "__in"] = v.select
|
||||
else if (v.filter) {
|
||||
v.filter.map(x=>{
|
||||
let obj = this.$find(arr, {code: x.condition})
|
||||
if(obj) {
|
||||
if(x.condition==='<>') exclude[v.name + (obj.name==='e'? '' : '__' + obj.name)] = this.$formatNumber(x.value)
|
||||
else where[v.name + (obj.name==='e'? '' : '__' + obj.name)] = this.$formatNumber(x.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
else if (v.sort) sort.push(v.sort==="az" ? v.name : "-" + v.name)
|
||||
})
|
||||
params.filter = Object.keys(where).length>0? where : undefined
|
||||
params.exclude = Object.keys(exclude).length>0? exclude : undefined
|
||||
params.sort = sort.length === 0 ? undefined : sort.toString()
|
||||
}
|
||||
// Tải lại dữ liệu
|
||||
let found = this.$copy(this.pagedata.api)
|
||||
found.params = params
|
||||
await this.loadData(found)
|
||||
},
|
||||
async loadData(found) {
|
||||
let result = await this.$getapi([found])
|
||||
if(result==='error') return
|
||||
if(this.flagSearch) {
|
||||
this.flagSearch = false
|
||||
var rows = result[0].data.rows
|
||||
if(this.currentField) this.filterData = this.$unique(rows, [this.currentField.name])
|
||||
} else {
|
||||
let copy = this.$copy(result[0])
|
||||
copy.total_rows = copy.data.total_rows
|
||||
copy.full_data = copy.data.full_data
|
||||
delete copy.data
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "data", data: this.$copy(result[0].data.rows)})
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "api", data: this.$copy(copy)})
|
||||
this.data = this.$copy(result[0].data.rows)
|
||||
this.updateShow(copy.full_data)
|
||||
}
|
||||
},
|
||||
scrollbarVisible() {
|
||||
let element = this.$refs['container']
|
||||
if(!element) return
|
||||
let result = element.scrollWidth > element.clientWidth? true : false
|
||||
if(this.scrollbar) {
|
||||
element.parentNode.removeChild(this.scrollbar)
|
||||
this.scrollbar = undefined
|
||||
}
|
||||
if(result) this.doubleScroll(element)
|
||||
},
|
||||
doubleScroll(element) {
|
||||
var scrollbar= document.createElement('div');
|
||||
scrollbar.appendChild(document.createElement('div'));
|
||||
scrollbar.style.overflow= 'auto';
|
||||
scrollbar.style.overflowY= 'hidden';
|
||||
scrollbar.firstChild.style.width= element.scrollWidth+'px';
|
||||
scrollbar.firstChild.style.height = '1px'
|
||||
scrollbar.firstChild.appendChild(document.createTextNode('\xA0'));
|
||||
var running = false;
|
||||
scrollbar.onscroll= function() {
|
||||
if(running) {
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
element.scrollLeft= scrollbar.scrollLeft;
|
||||
};
|
||||
element.onscroll= function() {
|
||||
if(running) {
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
scrollbar.scrollLeft= element.scrollLeft;
|
||||
}
|
||||
element.parentNode.insertBefore(scrollbar, element)
|
||||
scrollbar.scrollLeft= element.scrollLeft
|
||||
this.scrollbar = scrollbar
|
||||
},
|
||||
copyField(field) {
|
||||
let newField = this.$copy(field)
|
||||
newField.name = `f${this.$id()}`
|
||||
newField.label = field.label + '-copy'
|
||||
if(field.format==='number') {
|
||||
newField.formula = field.name
|
||||
newField.tags = [field.name]
|
||||
newField.unit = field.unit==='0.01'? field.unit : '1'
|
||||
} else {
|
||||
newField.copy = field.name
|
||||
if(newField.mandatory==='yes') delete newField['mandatory']
|
||||
}
|
||||
let copy = this.$copy(this.pagedata.fields)
|
||||
let idx = copy.findIndex(v=>v.name===field.name)
|
||||
copy.splice(idx+1, 0, newField)
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: copy, data: this.pagedata.data}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.table tbody tr:hover td, .table tbody tr:hover th {
|
||||
background-color: hsl(0, 0%, 29%);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
38
components/datatable/Date.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="has-text-right mb-2">
|
||||
<span class="tag is-rounded is-danger is-clickable" @click="remove()">Xóa ngày</span>
|
||||
</div>
|
||||
<b-field>
|
||||
<b-datepicker
|
||||
inline
|
||||
v-model="date"
|
||||
:unselectable-days-of-week="[0, 6]"
|
||||
@input="changeDate()">
|
||||
</b-datepicker>
|
||||
</b-field>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['trandate'],
|
||||
data() {
|
||||
return {
|
||||
date: undefined
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.date = new Date(this.trandate)
|
||||
},
|
||||
methods: {
|
||||
changeDate() {
|
||||
this.$emit('modalevent', {name: 'changedate', data: this.date})
|
||||
this.$emit('close')
|
||||
},
|
||||
remove() {
|
||||
this.date = null
|
||||
this.changeDate()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
84
components/datatable/EditLabel.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="fsb-20 mb-5">Điều chỉnh tiêu đề</p>
|
||||
<div v-for="(v, i) in arr" :key="i" :class="(i>0? 'mt-4' : null)">
|
||||
<p class="fsb-14">Dòng thứ {{(i+1)}}<span class="has-text-danger"> *</span></p>
|
||||
<div class="field has-addons mt-1">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="text" v-model="v.label">
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button is-primary" @click="add()">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-plus"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="control" @click="remove(i)" v-if="(i>0)">
|
||||
<a class="button is-primary">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-minus"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="v.error"> {{v.error}} </p>
|
||||
</div>
|
||||
<div class="buttons mt-5">
|
||||
<button class="button is-primary" @click="update()">Cập nhật</button>
|
||||
<button class="button is-dark" @click="$emit('close')">Hủy bỏ</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['label'],
|
||||
data() {
|
||||
return {
|
||||
arr: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
let arr1 = this.label.replace('<div>', '').replace('</div>', '').split("</p>")
|
||||
arr1.map(v=>{
|
||||
if(!this.$empty(v)) {
|
||||
let label = v + '</p>'
|
||||
label = this.$stripHtml(label)
|
||||
this.arr.push({label: label})
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.arr.push({label: undefined})
|
||||
},
|
||||
remove(i) {
|
||||
this.$delete(this.arr, i)
|
||||
},
|
||||
checkError() {
|
||||
let error = false
|
||||
this.arr.map(v=>{
|
||||
if(this.$empty(v.label)) {
|
||||
v.error = 'Nội dung không được bỏ trống'
|
||||
error = true
|
||||
}
|
||||
})
|
||||
if(error) this.arr = this.$copy(this.arr)
|
||||
return error
|
||||
},
|
||||
update() {
|
||||
if(this.checkError()) return
|
||||
let label = ''
|
||||
if(this.arr.length>1) {
|
||||
this.arr.map((v,i)=>{
|
||||
label += `<p${i<this.arr.length-1? ' style="border-bottom: 1px solid white;"' : ''}>${v.label.trim()}</p>`
|
||||
})
|
||||
label = `<div>${label}</div>`
|
||||
} else label = this.arr[0].label.trim()
|
||||
this.$emit('modalevent', {name: 'label', data: label})
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
88
components/datatable/FieldAttribute.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="keys.length>0">
|
||||
<div class="field is-horizontal" v-for="(v,i) in keys" :key="i">
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<input class="input fs-14" type="text" placeholder="" v-model="keys[i]">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input fs-14" type="text" placeholder="" v-model="values[i]">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<p class="control">
|
||||
<a>
|
||||
<span class="icon fs-22" @click="addAttr()"><i class="mdi mdi-plus-thick" /></span>
|
||||
</a>
|
||||
<a class="ml-2">
|
||||
<span class="icon has-text-danger fs-22" @click="remove(i)"><i class="mdi mdi-minus-thick" /></span>
|
||||
</a>
|
||||
<a class="ml-2">
|
||||
<span class="icon fs-22" @click="jsonData(v, i)"><i class="mdi mdi-dots-grid" /></span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="mb-6" v-else>
|
||||
<button class="button is-primary" @click="addAttr()">Thêm thuộc tính</button>
|
||||
</div>
|
||||
<div class="buttons mt-5">
|
||||
<a class="button is-primary fs-14" @click="update()">Cập nhật</a>
|
||||
</div>
|
||||
<Modal @close="comp=undefined" @update="doUpdate"
|
||||
v-bind="{component: comp, width: '40%', height: '300px', vbind: vbind}" v-if="comp"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['field', 'close'],
|
||||
data() {
|
||||
return {
|
||||
keys: [],
|
||||
values: [],
|
||||
comp: undefined,
|
||||
vbind: undefined,
|
||||
current: undefined
|
||||
}
|
||||
},
|
||||
created() {
|
||||
Object.keys(this.field).map(v=>{
|
||||
this.keys.push(v)
|
||||
this.values.push(this.field[v])
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
doUpdate(v) {
|
||||
this.values[this.current.i] = v
|
||||
},
|
||||
jsonData(v, i) {
|
||||
this.current = {v: v, i: i}
|
||||
this.vbind = {field: this.$empty(this.values[i]) || typeof this.values[i] === 'string'? {} : this.values[i], close: true}
|
||||
this.comp = 'datatable/FieldAttribute'
|
||||
},
|
||||
addAttr() {
|
||||
this.keys.push(undefined)
|
||||
this.values.push(undefined)
|
||||
},
|
||||
remove(i) {
|
||||
this.$delete(this.keys, i)
|
||||
this.$delete(this.values, i)
|
||||
},
|
||||
update() {
|
||||
let obj = {}
|
||||
this.keys.map((v,i)=>{
|
||||
if(!this.$empty(v)) obj[v] = this.values[i]
|
||||
})
|
||||
this.$emit('update', obj)
|
||||
this.$emit('modalevent', {name: 'update', data: obj})
|
||||
if(this.close) this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
180
components/datatable/FilterOption.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="field mt-3 mb-1" v-if="field.format==='number'">
|
||||
<label class="label fs-14">Chọn trường<span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<b-taginput
|
||||
size="is-small"
|
||||
v-model="tagsField"
|
||||
:data="pageData? pageData.fields.filter(v=>v.format==='number') : []"
|
||||
type="is-dark is-light"
|
||||
autocomplete
|
||||
:open-on-focus="true"
|
||||
field="name"
|
||||
icon="plus"
|
||||
placeholder="Chọn trường"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<span class="mr-3 has-text-danger has-text-weight-bold"> {{props.option.name}}</span>
|
||||
<span :class="tagsField.find(v=>v.id===props.option.id)? 'has-text-dark' : ''"> {{$stripHtml(props.option.label, 60)}} </span>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
Không có trường thỏa mãn
|
||||
</template>
|
||||
</b-taginput>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tagsField')"> {{errors.find(v=>v.name==='tagsField').message}} </p>
|
||||
</div>
|
||||
<div class="mt-2" v-if="tagsField.length>0">
|
||||
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name"
|
||||
class="tag is-rounded" v-for="(v,i) in tagsField" :key="i">
|
||||
<b-tooltip :label="$stripHtml(v.label)" type="is-dark">{{v.name}}</b-tooltip></a>
|
||||
</div>
|
||||
<div class="tags mt-2 mb-0">
|
||||
<b-tooltip type="is-dark" :label="v.name" v-for="(v,i) in operator" :key="i">
|
||||
<span @dblclick="addOperator(v)" class="tag is-success is-rounded is-clickable mr-4">
|
||||
<span class="fs-16">{{v.code}}</span>
|
||||
</span>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-1">
|
||||
<div class="field-body">
|
||||
<div class="field" v-if="field.format==='number'">
|
||||
<label class="label fs-14">Biểu thức có dạng Đúng / Sai <span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<p class="control is-expanded">
|
||||
<input class="input is-small" type="text" v-model="expression">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='expression')"> {{errors.find(v=>v.name==='expression').message}} </p>
|
||||
</div>
|
||||
|
||||
<div class="field" v-else>
|
||||
<label class="label"> Chuỗi kí tự <span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<p class="control">
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="searchText"
|
||||
@change="changeStyle()"
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'searchText')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "searchText").msg }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field is-narrow" v-if="filterType==='color'">
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" v-model="color" @change="changeStyle()">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='color')"> {{errors.find(v=>v.name==='color').message}} </p>
|
||||
</div>
|
||||
<div class="field is-narrow" v-else-if="filterType==='size'">
|
||||
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" placeholder="Nhập số" v-model="size" @change="changeStyle()">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='size')"> {{errors.find(v=>v.name==='size').message}} </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['filterObj', 'filterType', 'pagename', 'field'],
|
||||
data() {
|
||||
return {
|
||||
tagsField: [],
|
||||
expression: undefined,
|
||||
form: undefined,
|
||||
color: undefined,
|
||||
size: undefined,
|
||||
errors: [],
|
||||
searchText: undefined,
|
||||
operator: [{code: '+', name: 'Cộng'}, {code: '-', name: 'Trừ'}, {code: '*', name: 'Nhân'}, {code: '/', name: 'Chia'}, {code: '>', name: 'Lớn hơn'},
|
||||
{code: '>=', name: 'Lớn hơn hoặc bằng'}, {code: '<', name: 'Nhỏ hơn'}, {code: '<=', name: 'Nhỏ hơn hoặc bằng'}, {code: '==', name: 'Bằng'},
|
||||
{code: '&&', name: 'Và'}, {code: '||', name: 'Hoặc'}, {code: 'iif', name: 'Điều kiện rẽ nhánh'}]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.color = this.filterObj.color
|
||||
this.size = this.filterObj.size
|
||||
this.expression = this.filterObj.expression? this.filterObj.expression : this.field.name
|
||||
if(this.filterObj.tags) {
|
||||
this.filterObj.tags.map(v=>{
|
||||
this.tagsField.push(this.pageData.fields.find(x=>x.name===v))
|
||||
})
|
||||
} else if(this.field.format==='number') this.tagsField.push(this.pageData.fields.find(v=>v.name===this.field.name))
|
||||
},
|
||||
watch: {
|
||||
expression: function(newVal) {
|
||||
if(this.$empty(newVal)) return
|
||||
else this.changeStyle()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
colorscheme: {
|
||||
get: function() {return this.$store.state.colorscheme},
|
||||
set: function(val) {this.$store.commit("updateColorScheme", {colorscheme: val})}
|
||||
},
|
||||
pageData: {
|
||||
get: function() {return this.$store.state[this.pagename]},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
|
||||
},
|
||||
colorchoice: {
|
||||
get: function() {return this.$store.state.colorchoice},
|
||||
set: function(val) {this.$store.commit("updateColorChoice", {colorchoice: val})}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addOperator(v) {
|
||||
this.expression = `${this.expression} ${v.code}`
|
||||
},
|
||||
changeStyle() {
|
||||
let check = this.field.format==='number'? this.checkExpression() : this.checkCondition()
|
||||
if(!check) return
|
||||
var row = this.field.format==='number'? {expression: this.expression, tags: this.tagsField.map(v=>v.name)}
|
||||
: {keyword: this.searchText, type: 'search'}
|
||||
this.filterType==='color'? row.color = this.color : row.size = this.size
|
||||
this.$emit('databack', row)
|
||||
},
|
||||
checkCondition() {
|
||||
this.errors = []
|
||||
if(this.filterType==='color' && this.$empty(this.color)) this.errors.push({name: 'color', message: 'Chọn màu'})
|
||||
if(this.filterType==='size' && this.$empty(this.size)) this.errors.push({name: 'size', message: 'Nhập cỡ chữ'})
|
||||
if(this.$empty(this.searchText)) this.errors.push({name: 'searchText', message: 'Chưa nhập chuỗi kí tự'})
|
||||
return this.errors.length>0? false : true
|
||||
},
|
||||
checkExpression() {
|
||||
this.errors = []
|
||||
if(this.filterType==='color' && this.$empty(this.color)) this.errors.push({name: 'color', message: 'Chọn màu'})
|
||||
if(this.filterType==='size' && this.$empty(this.size)) this.errors.push({name: 'size', message: 'Nhập cỡ chữ'})
|
||||
let val = this.$copy(this.expression)
|
||||
let exp = this.$copy(this.expression)
|
||||
this.tagsField.forEach(v => {
|
||||
let myRegExp = new RegExp(v.name, 'g')
|
||||
val = val.replace(myRegExp, Math.random())
|
||||
exp = exp.replace(myRegExp, "field.formatNumber(row['" + v.name + "'])")
|
||||
})
|
||||
try {
|
||||
let value = this.$calc(val)
|
||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
} else if(!(eval(value)===true || eval(value)===false)) {
|
||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
}
|
||||
return this.errors.length>0? false : true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
195
components/datatable/FormatOption.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="['bgcolor', 'color', 'textsize'].find(x=>x===sideBar)">
|
||||
<p class="has-text-right has-text-grey is-italic fs-13">Màu sắc sẽ hiển thị theo điều kiện Đúng / Sai, mã lệnh do hệ thống tự sinh</p>
|
||||
<div class="tabs is-boxed">
|
||||
<ul> <li v-for="(v,i) in tabs" :key="i" :class="tab.code===v.code? 'is-active' : ''" @click="tab=v">
|
||||
<a>{{v.name}}</a></li> </ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="tab.code==='expression' && ['bgcolor', 'color', 'textsize'].find(x=>x===sideBar)">
|
||||
<div style="max-height:530px; overflow-y:auto">
|
||||
<template v-if="radio? (radio.code==='condition' && sideBar==='bgcolor') : false">
|
||||
<div v-for="(v,i) in bgcolorFilter" :key="v.id" class="px-4 mb-3" style="border: 1px solid #DCDCDC; border-radius: 10px;">
|
||||
<FilterOption v-bind="{filterObj: v, filterType: 'color', pagename: pagename, field: openField}" :ref="v.id"
|
||||
@databack="doConditionFilter($event, 'bgcolor', v.id)" />
|
||||
<p class="fs-14 mt-1 has-text-centered" :class="currentField.format==='string'? 'mb-1' : 'mb-2'">
|
||||
<a class="has-text-primary mr-5" @click="addCondition(bgcolorFilter)" v-if="bgcolorFilter.length<=30"> Thêm </a>
|
||||
<a class="has-text-danger" @click="removeCondition(bgcolorFilter, i)" v-if="bgcolorFilter.length>1"> Bớt </a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="radio? (radio.code==='condition' && sideBar==='color') : false">
|
||||
<div v-for="(v,i) in colorFilter" :key="v.id" class="px-4 mb-3" style="border: 1px solid #DCDCDC; border-radius: 10px;">
|
||||
<FilterOption v-bind="{filterObj: v, filterType: 'color', pagename: pagename, field: openField}" :ref="v.id"
|
||||
@databack="doConditionFilter($event, 'color', v.id)"/>
|
||||
<p class="fs-14 mt-1 has-text-centered" :class="currentField.format==='string'? 'mb-1' : 'mb-2'">
|
||||
<a class="has-text-primary mr-5" @click="addCondition(colorFilter)" v-if="colorFilter.length<=30"> Thêm </a>
|
||||
<a class="has-text-danger" @click="removeCondition(colorFilter,i)" v-if="colorFilter.length>1"> Bớt </a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="radio? (radio.code==='condition' && sideBar==='textsize') : false">
|
||||
<div v-for="(v, i) in sizeFilter" :key="v.id" class="px-4 mb-3" style="border: 1px solid #DCDCDC; border-radius: 10px;">
|
||||
<FilterOption v-bind="{filterObj: v, filterType: 'size', pagename: pagename, field: openField}" :ref="v.id"
|
||||
@databack="doConditionFilter($event, 'textsize', v.id)"/>
|
||||
<p class="fs-14 mt-1 has-text-centered" :class="currentField.format==='string'? 'mb-1' : 'mb-2'">
|
||||
<a class="has-text-primary mr-5" @click="addCondition(sizeFilter)" v-if="sizeFilter.length<=30"> Thêm </a>
|
||||
<a class="has-text-danger" @click="removeCondition(sizeFilter, i)" v-if="sizeFilter.length>1"> Bớt </a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!--<p class="mx-4 mt-4"><button class="button is-primary is-rounded" @click="update()">Cập nhật</button></p>-->
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab.code==='script' && ['bgcolor', 'color', 'textsize'].find(x=>x===sideBar)">
|
||||
<p class="my-4 mx-4">
|
||||
<a @click="copyContent(script? script : '')" class="mr-6">
|
||||
<span class="icon fs-20">
|
||||
<i class="mdi mdi-content-copy"/>
|
||||
</span>
|
||||
Copy
|
||||
</a>
|
||||
<a @click="paste()" class="mr-6">
|
||||
<span class="icon fs-20">
|
||||
<i class="mdi mdi-content-copy"/>
|
||||
</span>
|
||||
Paste
|
||||
</a>
|
||||
</p>
|
||||
<div class="mx-4">
|
||||
<textarea class="textarea fs-14" rows="8" v-model="script" @change="checkScript()" @dblclick="doCheck"></textarea>
|
||||
</div>
|
||||
<p class="mt-5 mx-4">
|
||||
<span class="icon-text fsb-18">
|
||||
Replace
|
||||
<span class="material-symbols-outlined">
|
||||
chevron_right
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<div class="field is-grouped mx-4 mt-4">
|
||||
<div class="control">
|
||||
<p class="fsb-14 mb-1">Đoạn text</p>
|
||||
<input class="input" type="text" placeholder="" v-model="source">
|
||||
</div>
|
||||
<div class="control">
|
||||
<p class="fsb-14 mb-1">Thay bằng</p>
|
||||
<input class="input" type="text" placeholder="" v-model="target">
|
||||
</div>
|
||||
<div class="control pl-5">
|
||||
<button class="button is-primary is-rounded is-outlined mt-5" @click="replace()">Replace</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-5 pt-2 mx-4">
|
||||
<span class="icon-text fsb-18">
|
||||
Thay đổi màu
|
||||
<span class="material-symbols-outlined">
|
||||
chevron_right
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<p class="mx-4 mt-4"><button class="button is-primary is-rounded" @click="changeScript()">Cập nhật</button></p>
|
||||
</template>
|
||||
<TableOption v-bind="{pagename: pagename}" v-else-if="sideBar==='option'"> </TableOption>
|
||||
<CreateTemplate v-else-if="sideBar==='template'" v-bind="{pagename: pagename, field: openField}"> </CreateTemplate>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
FilterOption: () => import("@/components/datatable/FilterOption"),
|
||||
TableOption: () => import("@/components/datatable/TableOption"),
|
||||
CreateTemplate: () => import("@/components/datatable/CreateTemplate")
|
||||
},
|
||||
props: ['event', 'currentField', 'pagename'],
|
||||
data() {
|
||||
return {
|
||||
openField: {},
|
||||
bgcolorFilter: [],
|
||||
colorFilter: [],
|
||||
sizeFilter: [],
|
||||
sideBar: undefined,
|
||||
script: undefined,
|
||||
radio: undefined,
|
||||
tabs: [{code: 'expression', name: 'Biểu thức'}, {code: 'script', name: 'Mã lệnh'}],
|
||||
tab: {code: 'expression', name: 'Biểu thức'},
|
||||
source: undefined,
|
||||
target: this.$copy(this.currentField.name),
|
||||
openField: this.$copy(this.currentField)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.openField = this.event.field
|
||||
this.sideBar = this.event.name
|
||||
this.script = this.event.script
|
||||
this.radio = this.event.radio
|
||||
let field = this.event.field
|
||||
this.bgcolorFilter = [{id: this.$id()}]
|
||||
if(field.bgcolor) {
|
||||
if(Array.isArray(field.bgcolor)) this.bgcolorFilter = this.$copy(field.bgcolor)
|
||||
}
|
||||
this.colorFilter = [{id: this.$id()}]
|
||||
if(field.color) {
|
||||
if(Array.isArray(field.color)) this.colorFilter = this.$copy(field.color)
|
||||
}
|
||||
this.sizeFilter = [{id: this.$id()}]
|
||||
if(field.textsize) {
|
||||
if(Array.isArray(field.textsize)) this.sizeFilter = field.textsize
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
doCheck() {
|
||||
let text = window.getSelection().toString()
|
||||
if(this.$empty(text)) return
|
||||
this.source = text
|
||||
},
|
||||
replace() {
|
||||
if(this.$empty(this.script)) return
|
||||
this.script = this.script.replaceAll(this.source, this.target)
|
||||
},
|
||||
async paste() {
|
||||
this.script = await navigator.clipboard.readText()
|
||||
},
|
||||
addCondition(arr) {
|
||||
arr.push({id: this.$id()})
|
||||
},
|
||||
removeCondition(arr, i) {
|
||||
this.$delete(arr, i)
|
||||
},
|
||||
copyContent(value) {
|
||||
this.$copyToClipboard(value)
|
||||
},
|
||||
checkScript() {
|
||||
if(this.$empty(this.script)) return
|
||||
try {
|
||||
JSON.parse(this.script)
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
changeScript() {
|
||||
if(!this.checkScript()) return
|
||||
let copy = this.$copy(this.openField)
|
||||
copy[this.sideBar] = JSON.parse(this.script)
|
||||
this.$emit('modalevent', {name: 'updatefields', data: copy})
|
||||
this.$buefy.toast.open('Màu / cỡ chữ đã được cập nhật')
|
||||
this.$emit('close')
|
||||
},
|
||||
doConditionFilter(v, type, id) {
|
||||
v.id = id
|
||||
let copy = this.openField
|
||||
if(copy[type]? Array.isArray(copy[type]) : false) {
|
||||
let idx = copy[type].findIndex(x=>x.id===id)
|
||||
idx>=0? copy[type][idx] = v : copy[type].push(v)
|
||||
} else copy[type] = [v]
|
||||
this.$emit('modalevent', {name: 'updatefields', data: this.openField})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
56
components/datatable/InputDate.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="hyperlink" @click="open()" v-html="note" v-if="note"></div>
|
||||
<div class="inputshow" style="min-height: 20px;" v-else>
|
||||
<span class="material-symbols-outlined fs-12 is-clickable inputhide" @click="open()">edit</span>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" @changedate="changeDate" v-if="showmodal" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['pagename', 'row', 'field'],
|
||||
data() {
|
||||
return {
|
||||
showmodal: undefined,
|
||||
note: undefined
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if(this.row[this.field.name]) this.note = this.row[this.field.name]
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
let title = this.$stripHtml(this.field.label)
|
||||
title = title.toLowerCase().indexOf('nhập')>=0? title : `Nhập ${title}`
|
||||
let date = this.note? `${this.note.substring(6,10)}-${this.note.substring(3,5)}-${this.note.substring(0,2)}` : new Date()
|
||||
this.showmodal = {component: 'datatable/Date', vbind: {trandate: date}, title: title, width: '460px', height: '300px'}
|
||||
},
|
||||
changeDate(date) {
|
||||
this.note = date? this.$dayjs(date).format('DD/MM/YYYY') : null
|
||||
let copy = this.$copy(this.$store.state[this.pagename])
|
||||
let idx = this.$findIndex(copy.data, {stock_code: this.row.stock_code})
|
||||
let copyRow = this.$copy(this.row)
|
||||
copyRow[this.field.name] = this.note
|
||||
copy.data[idx] = copyRow
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {data: copy.data}})
|
||||
let datainput = copy.setting.input || {}
|
||||
let f = datainput[this.row.stock_code] || {}
|
||||
this.note? f[this.field.name] = this.note : delete f[this.field.name]
|
||||
Object.keys(f).length>0? datainput[this.row.stock_code] = f : delete datainput[this.row.stock_code]
|
||||
copy.setting.input = Object.keys(datainput).length>0? datainput : null
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "setting", data: copy.setting})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.inputhide {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.inputshow:hover .inputhide {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
56
components/datatable/InputNote.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="hyperlink" @click="open()" v-if="note">{{ note }}</div>
|
||||
<div class="inputshow" style="min-height: 20px;" v-else>
|
||||
<span class="material-symbols-outlined fs-12 is-clickable inputhide" @click="open()">edit</span>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" @changenote="changeNote" v-if="showmodal" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['pagename', 'row', 'field'],
|
||||
data() {
|
||||
return {
|
||||
showmodal: undefined,
|
||||
note: undefined
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if(this.row[this.field.name]) this.note = this.row[this.field.name]
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
let title = this.$stripHtml(this.field.label)
|
||||
title = title.toLowerCase().indexOf('nhập')>=0? title : `Nhập ${title}`
|
||||
this.showmodal = {component: 'datatable/Note', vbind: {note: this.note}, title: title, width: '500px', height: '200px'}
|
||||
},
|
||||
changeNote(data) {
|
||||
this.note = data.note
|
||||
let copy = this.$copy(this.$store.state[this.pagename])
|
||||
let idx = this.$findIndex(copy.data, {stock_code: this.row.stock_code})
|
||||
let copyRow = this.$copy(this.row)
|
||||
copyRow[this.field.name] = this.note
|
||||
copy.data[idx] = copyRow
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {data: copy.data}})
|
||||
this.showmodal = undefined
|
||||
let datainput = copy.setting.input || {}
|
||||
let f = datainput[this.row.stock_code] || {}
|
||||
this.note? f[this.field.name] = this.note : delete f[this.field.name]
|
||||
Object.keys(f).length>0? datainput[this.row.stock_code] = f : delete datainput[this.row.stock_code]
|
||||
copy.setting.input = Object.keys(datainput).length>0? datainput : null
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "setting", data: copy.setting})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.inputhide {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.inputshow:hover .inputhide {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
57
components/datatable/InputNumber.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="hyperlink" @click="open()" v-html="note" v-if="note"></div>
|
||||
<div class="inputshow" style="min-height: 20px;" v-else>
|
||||
<span class="material-symbols-outlined fs-12 is-clickable inputhide" @click="open()">edit</span>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" @changenote="changeNote" v-if="showmodal" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['pagename', 'row', 'field'],
|
||||
data() {
|
||||
return {
|
||||
showmodal: undefined,
|
||||
note: undefined
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if(this.row[this.field.name]) this.note = this.row[this.field.name]
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
let title = this.$stripHtml(this.field.label)
|
||||
title = title.toLowerCase().indexOf('nhập')>=0? title : `Nhập ${title}`
|
||||
let value = this.note? this.$formatUnit(this.note, 1/this.field.unit, this.field.decimal, true) : undefined
|
||||
this.showmodal = {component: 'datatable/Number', vbind: {note: value}, title: title, width: '300px', height: '150px'}
|
||||
},
|
||||
changeNote(data) {
|
||||
this.note = data.note
|
||||
let copy = this.$copy(this.$store.state[this.pagename])
|
||||
let idx = this.$findIndex(copy.data, {stock_code: this.row.stock_code})
|
||||
let copyRow = this.$copy(this.row)
|
||||
copyRow[this.field.name] = this.$formatUnit(data.note, this.field.unit, this.field.decimal, true, this.field.decimal)
|
||||
copy.data[idx] = copyRow
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {data: copy.data}})
|
||||
this.showmodal = undefined
|
||||
let datainput = copy.setting.input || {}
|
||||
let f = datainput[this.row.stock_code] || {}
|
||||
data.note? f[this.field.name] = data.note : delete f[this.field.name]
|
||||
Object.keys(f).length>0? datainput[this.row.stock_code] = f : delete datainput[this.row.stock_code]
|
||||
copy.setting.input = Object.keys(datainput).length>0? datainput : null
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "setting", data: copy.setting})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.inputhide {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.inputshow:hover .inputhide {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
32
components/datatable/Note.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div>
|
||||
<textarea class="textarea" rows="6" maxlength="300" placeholder="Nhập tối đa 300 kí tự" v-model="text" id="refinput"></textarea>
|
||||
<p class="mt-5 pt-3">
|
||||
<button class="button is-primary" @click="changeNote()">Cập nhật</button>
|
||||
<button class="button is-danger ml-5" @click="remove()">Xóa</button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['note'],
|
||||
data() {
|
||||
return {
|
||||
text: this.note? this.$copy(this.note) : ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.getElementById('refinput').focus()
|
||||
},
|
||||
methods: {
|
||||
changeNote() {
|
||||
let stext = this.$empty(this.text)? null : this.text.trimLeft().trimRight()
|
||||
this.$emit('modalevent', {name: 'changenote', data: {note: this.$empty(stext)? null : stext}})
|
||||
},
|
||||
remove() {
|
||||
this.text = null
|
||||
this.changeNote()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
54
components/datatable/Number.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="has-text-left">
|
||||
<input class="input" type="text" placeholder="" @keyup="change" v-model="text" @keyup.enter="doClick()" id="refinput">
|
||||
<p class="help has-text-danger mt-1" v-if="error">{{ error }}</p>
|
||||
<div class="mt-5 pt-3">
|
||||
<button class="button is-primary" @click="doClick()">Cập nhật</button>
|
||||
<button class="button is-danger ml-5" @click="remove()">Xóa</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['note'],
|
||||
data() {
|
||||
return {
|
||||
text: this.note? this.$copy(this.note) : '',
|
||||
error: false,
|
||||
timer: undefined
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.getElementById('refinput').focus()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if(this.timer) clearTimeout(this.timer)
|
||||
this.timer = undefined
|
||||
},
|
||||
methods: {
|
||||
changeNote() {
|
||||
this.$emit('modalevent', {name: 'changenumber', data: {note: this.$empty(this.text)? null : this.text}})
|
||||
},
|
||||
change(e) {
|
||||
if(this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(()=>this.checkNumber(e.target.value), 1000)
|
||||
} else this.timer = setTimeout(()=>this.checkNumber(e.target.value), 1000)
|
||||
},
|
||||
checkNumber(text) {
|
||||
this.error = undefined
|
||||
if(!this.$empty(text) && !this.$isNumber(text)) return this.error = 'Số không hợp lệ'
|
||||
this.text = this.$numtoString(text)
|
||||
},
|
||||
doClick() {
|
||||
this.checkNumber(this.text)
|
||||
if(this.error) return
|
||||
this.$emit('modalevent', {name: 'changenote', data: {note: this.$empty(this.text)? null : this.text}})
|
||||
},
|
||||
remove() {
|
||||
this.text = null
|
||||
this.doClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
103
components/datatable/ScrollBox.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="px-2" :style="`max-height: ${maxheight}; overflow-y: auto;`" @scroll="handleScroll">
|
||||
<div class="field is-grouped py-1 border-bottom my-0" v-for="(v, i) in rows" :key="i">
|
||||
<p class="control is-expanded py-0 fs-14 hyperlink" @click="doClick(v,i)">
|
||||
{{v[name]? $stripHtml(v[name], 75) : 'n/a'}}
|
||||
<span class="icon has-text-primary" v-if="checked[i]"><i class="mdi mdi-check"></i></span>
|
||||
</p>
|
||||
<p class="control py-0" v-if="show">
|
||||
<span class="icon-text has-text-grey mr-2 fs-13" v-if="show.author">
|
||||
<span class="icon"><i class="mdi mdi-account-outline"></i></span>
|
||||
<span>{{ v[show.author] }}</span>
|
||||
</span>
|
||||
<span class="icon-text has-text-grey mr-2 fs-13" v-if="show.view">
|
||||
<span class="icon"><i class="mdi mdi-eye-outline"></i></span>
|
||||
<span>{{ v[show.view] }}</span>
|
||||
</span>
|
||||
<span class="fs-13 has-text-grey" v-if="show.time">{{$dayjs(v['create_time']).fromNow(true)}}</span>
|
||||
<b-tooltip label="Mở trong tab mới" type="is-dark" position="is-left">
|
||||
<span class="icon hyperlink ml-1" v-if="show.link" @click="doClick(v,i, 'newtab')"><i class="mdi mdi-open-in-new"></i></span>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Đổi tên" type="is-dark" position="is-left" v-if="show.rename">
|
||||
<span class="icon hyperlink ml-1" @click="$emit('rename', v, i)"><i class="mdi mdi-pencil-outline"></i></span>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Xóa" type="is-dark" position="is-left" v-if="show.delete">
|
||||
<span class="icon hyperlink has-text-danger ml-1" @click="$emit('remove', v, i)"><i class="mdi mdi-delete-outline"></i></span>
|
||||
</b-tooltip>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['data', 'name', 'maxheight', 'perpage', 'sort', 'selects', 'keyval', 'show'],
|
||||
data() {
|
||||
return {
|
||||
currentPage: 1,
|
||||
total: this.data.length,
|
||||
rows: this.data.slice(0, this.perpage),
|
||||
selected: [],
|
||||
checked: {},
|
||||
time: undefined,
|
||||
array: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getdata()
|
||||
},
|
||||
watch: {
|
||||
data: function(newVal) {
|
||||
this.getdata()
|
||||
},
|
||||
selects: function(newVal) {
|
||||
this.getSelect()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getdata() {
|
||||
this.currentPage = 1
|
||||
this.array = this.$copy(this.data)
|
||||
if(this.sort!==false) {
|
||||
let f = {}
|
||||
let showtime = this.show? this.show.time : false
|
||||
showtime? f['create_time'] = 'desc' : f[this.name] = 'asc'
|
||||
this.$multiSort(this.array, f)
|
||||
}
|
||||
this.rows = this.array.slice(0, this.perpage)
|
||||
this.getSelect()
|
||||
},
|
||||
getSelect() {
|
||||
if(!this.selects) return
|
||||
this.selected = []
|
||||
this.checked = {}
|
||||
this.selects.map(v=>{
|
||||
let idx = this.rows.findIndex(x=>x[this.keyval? this.keyval : this.name]===v)
|
||||
if(idx>=0) {
|
||||
this.selected.push(this.rows[idx])
|
||||
this.checked[idx] = true
|
||||
}
|
||||
})
|
||||
},
|
||||
doClick(v, i, type) {
|
||||
this.checked[i] = this.checked[i]? false : true
|
||||
this.checked = this.$copy(this.checked)
|
||||
let idx = this.selected.findIndex(x=>x.id===v.id)
|
||||
idx>=0? this.$delete(this.selected) : this.selected.push(v)
|
||||
this.$emit('selected', v, type)
|
||||
},
|
||||
handleScroll(e) {
|
||||
const bottom = e.target.scrollHeight - e.target.scrollTop -5 < e.target.clientHeight
|
||||
if (bottom) {
|
||||
if(this.total? this.total>this.rows.length : true) {
|
||||
this.currentPage +=1
|
||||
let arr = this.array.filter((ele,index) => (index>=(this.currentPage-1)*this.perpage && index<this.currentPage*this.perpage))
|
||||
this.rows = this.rows.concat(arr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
177
components/datatable/TableOption.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div v-if="pagedata">
|
||||
<p class="has-text-right has-text-grey is-italic fs-13 mb-1">Chọn để ẩn hiện cột, kéo thả thay đổi vị trí</p>
|
||||
<b-table
|
||||
class="fs-13"
|
||||
:data="fields"
|
||||
draggable
|
||||
@dragstart="dragstart"
|
||||
@drop="drop"
|
||||
@dragover="dragover"
|
||||
@dragleave="dragleave"
|
||||
>
|
||||
<b-table-column field="index" label="Tt cột" numeric v-slot="props">
|
||||
{{ `C${props.index}` }}
|
||||
</b-table-column>
|
||||
<b-table-column field="label" label="Tên trường" v-slot="props">
|
||||
<span class="hyperlink" @click="openField(props.row)" v-if="1>0">{{ props.row.name }}</span>
|
||||
<span v-else>{{ props.row.name }}</span>
|
||||
<a @click="copyContent(props.row.name)">
|
||||
<span class="icon"><i class="mdi mdi-content-copy"/></span>
|
||||
</a>
|
||||
</b-table-column>
|
||||
<b-table-column>
|
||||
<template v-slot:header="{}">
|
||||
Tên cột
|
||||
<a class="ml-2" @click="edit=!edit">{{edit? 'Cập nhật' : 'Sửa tên'}}</a>
|
||||
</template>
|
||||
<template v-slot="props" v-if="edit">
|
||||
<input class="input is-small" type="text" placeholder="" v-model="props.row.label" @change="checkChange(props.row)">
|
||||
</template>
|
||||
<template v-slot="props" v-else>
|
||||
{{$stripHtml(props.row.label, 80)}}
|
||||
</template>
|
||||
</b-table-column>
|
||||
<b-table-column field="check" width="80px">
|
||||
<template v-slot:header="{}">
|
||||
<span :class="`material-symbols-outlined fs-18 ${checkAll? 'has-text-primary' : 'has-text-grey'} is-clickable`" @click="multiCheck()">
|
||||
{{ checkAll? 'visibility' : 'visibility_off' }}
|
||||
</span>
|
||||
|
||||
<span class="is-clickable" @click="deleteConfirm()">
|
||||
<span class="material-symbols-outlined fs-18 ml-3">delete</span>
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot="props">
|
||||
<span class="is-clickable" v-if="!(props.row.required || props.row.mandatory)" @click="checkChange(props.row)">
|
||||
<span :class="`material-symbols-outlined fs-18 ${props.row.show? 'has-text-primary' : 'has-text-grey'}`">
|
||||
{{ props.row.show? 'visibility' : 'visibility_off' }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="is-clickable ml-3" v-if="!(props.row.required || props.row.mandatory)" @click="deleteRow(props.index)">
|
||||
<span class="material-symbols-outlined fs-18">delete</span>
|
||||
</span>
|
||||
</template>
|
||||
</b-table-column>
|
||||
</b-table>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal" @update="updateField"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['pagename', 'detail'],
|
||||
data() {
|
||||
return {
|
||||
fields: [],
|
||||
draggingRow: undefined,
|
||||
draggingRowIndex: undefined,
|
||||
checkAll: true,
|
||||
change: false,
|
||||
edit: false,
|
||||
showmodal: undefined
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.pagedata) this.fields = this.$copy(this.pagedata.fields)
|
||||
},
|
||||
watch: {
|
||||
'pagedata.fields': function(newVal) {
|
||||
if(this.change) this.change = false
|
||||
else this.fields = this.$copy(this.pagedata.fields)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pagedata: {
|
||||
get: function() {return this.$store.state[this.pagename]},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
|
||||
},
|
||||
settingclass: {
|
||||
get: function() {return this.$store.state.settingclass},
|
||||
set: function(val) {this.$store.commit("updateSettingClass", { settingclass: val })}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
multiCheck() {
|
||||
this.checkAll = !this.checkAll
|
||||
let newVal = this.checkAll
|
||||
this.fields.filter((v) => !v.mandatory).map((x) => (x.show = newVal));
|
||||
this.fields = this.$copy(this.fields);
|
||||
this.change = true
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: this.$copy(this.fields)}})
|
||||
},
|
||||
openField(v) {
|
||||
this.showmodal = {title: `${v.name} / ${this.$stripHtml(v.label,50)}`, component: "datatable/FieldAttribute", vbind: {field: v}, width: '40%', height: '600px'}
|
||||
},
|
||||
deleteConfirm() {
|
||||
this.$buefy.dialog.confirm({
|
||||
message: 'Bạn có chắc chắc muốn xóa tất cả các trường không?.',
|
||||
confirmText: 'Có',
|
||||
cancelText: 'Không',
|
||||
onConfirm: () => {this.deleteFields()}
|
||||
})
|
||||
},
|
||||
copyContent(value) {
|
||||
this.$copyToClipboard(value)
|
||||
},
|
||||
checkChange(row) {
|
||||
row.show = !row.show
|
||||
this.change = true
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: this.$copy(this.fields)}})
|
||||
},
|
||||
deleteRow(idx) {
|
||||
this.change = true
|
||||
this.$delete(this.fields, idx)
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: this.$copy(this.fields)}})
|
||||
},
|
||||
deleteFields() {
|
||||
let filter = this.fields.filter(
|
||||
(v) => !v.mandatory && !v.required
|
||||
);
|
||||
if (filter.length === 0) return;
|
||||
filter.map((v) => {
|
||||
let idx = this.fields.findIndex((x) => x.name === v.name);
|
||||
if (idx >= 0) this.$delete(this.fields, idx);
|
||||
});
|
||||
this.change = true
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: this.$copy(this.fields)}})
|
||||
},
|
||||
dragstart(payload) {
|
||||
this.draggingRow = payload.row;
|
||||
this.draggingRowIndex = payload.index;
|
||||
payload.event.dataTransfer.effectAllowed = "copy";
|
||||
},
|
||||
dragover(payload) {
|
||||
payload.event.dataTransfer.dropEffect = "copy";
|
||||
payload.event.target.closest("tr").classList.add("is-selected");
|
||||
payload.event.preventDefault();
|
||||
},
|
||||
dragleave(payload) {
|
||||
payload.event.target.closest("tr").classList.remove("is-selected");
|
||||
payload.event.preventDefault();
|
||||
},
|
||||
drop(payload) {
|
||||
payload.event.target.closest("tr").classList.remove("is-selected")
|
||||
this.$arrayMove(this.fields, this.draggingRowIndex, payload.index)
|
||||
this.change = true
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: this.$copy(this.fields)}})
|
||||
},
|
||||
exportData() {
|
||||
this.$exportExcel(this.pagedata.dataFilter || this.pagedata.data, 'data-export', this.pagedata.fields.filter(v=>v.show))
|
||||
},
|
||||
updateField(field) {
|
||||
let copy = this.$copy(this.pagedata.fields)
|
||||
let idx = copy.findIndex(v=>v.name===field.name)
|
||||
copy[idx] = field
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: copy}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
* >>> .table tbody tr td {
|
||||
height: 14px !important;
|
||||
padding-top: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
</style>
|
||||
156
components/datatable/TableSetting.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="field is-horizontal mt-3">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Cỡ chữ của bảng <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="number" :value="tablesetting.find(v=>v.code==='table-font-size').detail"
|
||||
@change="changeSetting($event.target.value, 'table-font-size')">
|
||||
</p>
|
||||
<p class="help is-danger mt-1" v-if="errors['table-font-size']">{{errors['table-font-size']}}</p>
|
||||
</div>
|
||||
<div class="field" >
|
||||
<label class="label fs-14"> Cỡ chữ tiêu đề <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="number" :value="tablesetting.find(v=>v.code==='header-font-size').detail"
|
||||
@change="changeSetting($event.target.value, 'header-font-size')">
|
||||
</p>
|
||||
<p class="help is-danger mt-1" v-if="errors['header-font-size']">{{errors['header-font-size']}}</p>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Số dòng trên 1 trang <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="number" :value="tablesetting.find(v=>v.code==='per-page').detail"
|
||||
@change="changeSetting($event.target.value, 'per-page')">
|
||||
</p>
|
||||
<p class="help is-danger mt-1" v-if="errors['per-page']">{{errors['per-page']}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-5">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu nền bảng <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" :value="tablesetting.find(v=>v.code==='table-background').detail"
|
||||
@change="changeSetting($event.target.value, 'table-background')">
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" :value="tablesetting.find(v=>v.code==='table-font-color').detail"
|
||||
@change="changeSetting($event.target.value, 'table-font-color')">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-5">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu chữ tiêu đề <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" :value="tablesetting.find(v=>v.code==='header-font-color').detail"
|
||||
@change="changeSetting($event.target.value, 'header-font-color')">
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu nền tiêu đề <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" :value="tablesetting.find(v=>v.code==='header-background').detail"
|
||||
@change="changeSetting($event.target.value, 'header-background')">
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu chữ khi filter<span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" :value="tablesetting.find(v=>v.code==='header-filter-color').detail"
|
||||
@change="changeSetting($event.target.value, 'header-filter-color')">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-5">
|
||||
<div class="field-body">
|
||||
<div class="field" >
|
||||
<label class="label fs-14"> Đường viền <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text"
|
||||
:value="tablesetting.find(v=>v.code==='td-border')? tablesetting.find(v=>v.code==='td-border').detail : undefined"
|
||||
@change="changeSetting($event.target.value, 'td-border')">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['pagename'],
|
||||
data() {
|
||||
return {
|
||||
errors: [],
|
||||
radioNote: 'no',
|
||||
tablesetting: undefined,
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.tablesetting = this.$copy(this.pagedata.tablesetting || this.setting)
|
||||
this.readSetting()
|
||||
},
|
||||
watch: {
|
||||
radioNote: function(newVal) {
|
||||
if(newVal==='no') this.changeSetting('@', 'note')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
setting: {
|
||||
get: function() {return this.$store.state.tablesetting},
|
||||
set: function(val) {this.$store.commit("updateTableSetting", {tablesetting: val})}
|
||||
},
|
||||
pagedata: {
|
||||
get: function() {return this.$store.state[this.pagename]},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
readSetting() {
|
||||
let found = this.tablesetting.find(v=>v.code==='note')
|
||||
if(found? found.detail!=='@' : false) this.radioNote = 'yes'
|
||||
},
|
||||
checkError(value, code) {
|
||||
this.errors = {}
|
||||
let fvalue = this.$formatNumber(value)
|
||||
if(code==='table-font-size' || code==='header-font-size') {
|
||||
if(fvalue <8) this.errors[code] = 'Giá trị phải từ 8 trở lên'
|
||||
else if(fvalue >100) this.errors[code] = 'Giá trị lớn nhất là 100'
|
||||
}
|
||||
if(this.pagedata.setting? this.pagedata.setting.classify__code==='priceboard' : false) {
|
||||
if(code==='per-page') {
|
||||
if(fvalue>50) this.errors[code] = 'Giá trị lớn nhất là 50'
|
||||
else if(fvalue<1) this.errors[code] = 'Giá trị nhỏ nhất là 1'
|
||||
}
|
||||
}
|
||||
return Object.keys(this.errors).length>0? true : false
|
||||
},
|
||||
changeSetting(value, code) {
|
||||
if(this.checkError(value, code)) return
|
||||
if(code==='note' && this.$empty(value)) return
|
||||
let copy = this.$copy(this.tablesetting)
|
||||
let found = copy.find(v=>v.code===code)
|
||||
if(found) found.detail = value
|
||||
else {
|
||||
found = this.$copy(this.tablesetting.find(v=>v.code===code))
|
||||
found.detail = value
|
||||
copy.push(found)
|
||||
}
|
||||
this.tablesetting = copy
|
||||
if(this.pagename) this.$store.commit("updateState", {name: this.pagename, key: "update", data: {tablesetting: copy}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
585
components/datatable/TableView.vue
Normal file
@@ -0,0 +1,585 @@
|
||||
<template>
|
||||
<div :style="`max-height: ${maxheight}; overflow-y: auto;`" @scroll="handleScroll">
|
||||
<div class="table-container" ref="container">
|
||||
<table class="table is-bordered is-narrow is-hoverable" ref="table" :style="getSettingStyle('table')">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(field,i) in displayFields" :key="i" :ref="`th${field.name}`"
|
||||
:style="getSettingStyle('header', field)">
|
||||
<div class="hyperlink" @click="showField(field)" :style="getSettingStyle('dropdown', field)">
|
||||
<template v-if="field.label.indexOf('<')<0">{{field.label}}</template>
|
||||
<template v-else>
|
||||
<component v-bind="{row: field, tick: tickall}" :is="compiledComponent(field.label)" @clickevent="doAction($event, field)" />
|
||||
</template>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(v,i) in displayData" :key="i">
|
||||
<td v-for="(field, j) in fields.filter(v=>v.show)" :key="j" :ref="`${v.stock_code}${field.name}`" :id="`${v.stock_code}${field.name}`"
|
||||
:style="v[`${field.name}color`]" @dblclick="doubleClick(field, v)">
|
||||
<component v-bind="{row: v, tick: tick, pagename: pagename, field: field, highlight: highlight}" v-if="field.template"
|
||||
:is="compiledComponent(field.template)" @clickevent="doAction($event, v, field)"
|
||||
/>
|
||||
<template v-else-if="field.tooltip">
|
||||
<b-tooltip :label="v[field.tooltip.field]"
|
||||
:position="field.tooltip.placement"
|
||||
:type="field.tooltip.type">
|
||||
{{v[field.name]}}
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<template v-else> {{v[field.name]}} </template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"
|
||||
@dosearch="doSearch(currentField, $event)" @doselect="doSelect(currentField, $event)" @dosort="doSort(currentField, $event)"
|
||||
@setfilter="setFilter(currentField, $event)" @showsidebar="showSidebar($event)" @copyfield="copyField">
|
||||
</Modal>
|
||||
<Modal @close="showmodal1=undefined" v-bind="showmodal1" v-if="showmodal1" @updatefields="updateFields"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
export default {
|
||||
props: ['pagename', 'vmaxheight'],
|
||||
data() {
|
||||
return {
|
||||
data: [],
|
||||
fields: [],
|
||||
search: '',
|
||||
filters: [],
|
||||
currentPage: 1,
|
||||
filterData: [],
|
||||
timer: undefined,
|
||||
perPage: 30,
|
||||
currentField: undefined,
|
||||
flagSearch: false,
|
||||
scrollbar: undefined,
|
||||
tablesetting: undefined,
|
||||
tick: {},
|
||||
tickall: false,
|
||||
displayData: [],
|
||||
displayFields: [],
|
||||
showmodal: undefined,
|
||||
showmodal1: undefined,
|
||||
totalRows: 0,
|
||||
showPaging: false,
|
||||
highlight: undefined,
|
||||
maxheight: this.vmaxheight || '500px',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "contextMenu", data: false})
|
||||
if(this.pagedata.data) this.data = this.$copy(this.pagedata.data)
|
||||
if(this.pagedata.fields) this.fields = this.$copy(this.pagedata.fields)
|
||||
if(this.pagedata.filters) this.filters = this.$copy(this.pagedata.filters)
|
||||
this.tablesetting = this.$copy(this.pagedata.tablesetting || this.gridsetting)
|
||||
this.perPage = this.pagedata.perPage? this.pagedata.perPage : this.$formatNumber(this.tablesetting.find(v=>v.code==='per-page').detail)
|
||||
if(this.data.length>0 && this.fields.length>0) this.updateShow()
|
||||
else this.showPagination()
|
||||
},
|
||||
watch: {
|
||||
'pagedata.update': function(newVal) {
|
||||
if(newVal) this.updateData(newVal)
|
||||
},
|
||||
menuaction: function(newVal) {
|
||||
if(this.$empty(newVal)) return
|
||||
if(newVal.name==='export-excel' && (newVal.pagename? newVal.pagename===this.pagename : true)) {
|
||||
this.$exportExcel(this.data, this.menuaction.file, this.fields.filter(v=>v.show))
|
||||
} else if(newVal.name==='opensidebar' && (newVal.pagename? newVal.pagename===this.pagename : true)) {
|
||||
this.showmodal = {component: 'datatable/TableOption', vbind: {pagename: this.pagename}, width: '850px', height: '600px', title: 'Danh sách cột'}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pagedata: {
|
||||
get: function() {return this.$store.state[this.pagename]},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
|
||||
},
|
||||
gridsetting: {
|
||||
get: function() {return this.$store.state.tablesetting},
|
||||
set: function(val) {this.$store.commit("updateTableSetting", {tablesetting: val})}
|
||||
},
|
||||
menuaction: {
|
||||
get: function() {return this.$store.state.menuaction},
|
||||
set: function(val) {this.$store.commit("updateMenuAction", {menuaction: val})}
|
||||
},
|
||||
currentsetting: {
|
||||
get: function() {return this.$store.state.currentsetting},
|
||||
set: function(val) {this.$store.commit("updateCurrentSetting", {currentsetting: val})}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showPagination() {
|
||||
this.showPaging = this.pagedata.pagination===false? false : true
|
||||
this.totalRows = this.data.length
|
||||
if(this.showPaging && this.pagedata.api) {
|
||||
if(this.pagedata.api.full_data===false) this.totalRows = this.pagedata.api.total_rows
|
||||
this.showPaging = this.totalRows > this.perPage
|
||||
}
|
||||
},
|
||||
async updateData(newVal) {
|
||||
if(newVal.columns) { //change attribute
|
||||
this.fields = this.$copy(newVal.columns)
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'fields', data: this.fields})
|
||||
this.$emit('changefield', this.fields)
|
||||
let fields = this.fields.filter(v=>v.show)
|
||||
this.data.map(v=>{
|
||||
fields.map(x=>v[`${x.name}color`] = this.getStyle(x, v))
|
||||
})
|
||||
return this.updateShow()
|
||||
}
|
||||
if(newVal.tablesetting) {
|
||||
this.tablesetting = newVal.tablesetting
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'tablesetting', data: this.tablesetting})
|
||||
this.perPage = this.$formatNumber(this.tablesetting.find(v=>v.code=="per-page").detail)
|
||||
this.currentPage = 1
|
||||
}
|
||||
this.tablesetting = this.$copy(this.pagedata.tablesetting || this.gridsetting)
|
||||
if(this.tablesetting) {
|
||||
this.perPage = this.pagedata.perPage? this.pagedata.perPage : Number(this.tablesetting.find(v=>v.code==='per-page').detail)
|
||||
}
|
||||
if(newVal.fields) {
|
||||
this.fields = this.$copy(newVal.fields)
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'fields', data: this.fields})
|
||||
this.$emit('changefield', this.fields)
|
||||
} else this.fields = this.$copy(this.pagedata.fields)
|
||||
if(newVal.data || newVal.fields) {
|
||||
let copy = this.$copy(newVal.data || this.data)
|
||||
this.data = this.$calculateData(copy, this.fields)
|
||||
let fields = this.fields.filter(v=>v.show)
|
||||
this.data.map(v=>{
|
||||
fields.map(x=>v[`${x.name}color`] = this.getStyle(x, v))
|
||||
})
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'data', data: this.data})
|
||||
this.$emit('changedata', this.data)
|
||||
}
|
||||
if(newVal.filters) this.filters = this.$copy(newVal.filters)
|
||||
else if(this.pagedata.filters) this.filters = this.$copy(this.pagedata.filters)
|
||||
if(newVal.data || newVal.fields || newVal.filters) {
|
||||
let copy = this.$copy(this.filters)
|
||||
this.filters.map((v,i)=>{
|
||||
let idx = this.$findIndex(this.fields, {name: v.name})
|
||||
let index = this.$findIndex(copy, {name: v.name})
|
||||
if(idx<0 && index>=0) this.$delete(copy, index)
|
||||
else if(idx>=0 && index>=0) copy[index].label = this.fields[idx].label
|
||||
})
|
||||
this.filters = copy
|
||||
this.doFilter(this.filters)
|
||||
}
|
||||
if(newVal.data || newVal.fields || newVal.filters || newVal.tablesetting) this.updateShow()
|
||||
if(newVal.data || newVal.fields) setTimeout(()=> this.scrollbarVisible(), 100)
|
||||
if(newVal.highlight) {
|
||||
this.highlight = this.$copy(newVal.highlight)
|
||||
setTimeout(()=>this.highlight = undefined, 500)
|
||||
}
|
||||
},
|
||||
updateShow(full_data) {
|
||||
this.displayFields = this.fields.filter(v=>v.show)
|
||||
if(full_data===false) this.displayData = this.data
|
||||
else {
|
||||
let data = this.data.filter((ele,index) => (index>=(this.currentPage-1)*this.perPage && index<this.currentPage*this.perPage))
|
||||
if(data.length>0) this.displayData = this.displayData.concat(data)
|
||||
}
|
||||
},
|
||||
async changePage() {
|
||||
if(this.pagedata.api? this.pagedata.api.full_data===false : false) await this.backendFilter(this.filters)
|
||||
else this.updateShow()
|
||||
this.$emit('changepage', this.currentPage)
|
||||
this.loading = false
|
||||
},
|
||||
doubleClick(field, v) {
|
||||
this.doSelect(field, v[field.name])
|
||||
},
|
||||
showField(field) {
|
||||
if(this.pagedata.contextMenu===false || field.menu==='no') return
|
||||
this.showContextMenu(field)
|
||||
let doc = this.$refs[`th${field.name}`]
|
||||
let width = (doc? doc.length>0 : false)? doc[0].getBoundingClientRect().width : 100
|
||||
if(this.pagedata.setting) this.currentsetting = this.$copy(this.pagedata.setting)
|
||||
this.showmodal = {vbind: {pagename: this.pagename, field: this.currentField, filters: this.filters, filterData: this.filterData, width: width},
|
||||
component: 'datatable/ContextMenu', title: this.$stripHtml(field.label), width: '600px', height: '500px'}
|
||||
},
|
||||
showCondition(v) {
|
||||
this.$emit('contextmenu', 'open')
|
||||
this.currentField = this.$find(this.pagedata.fields, {'name': v.name})
|
||||
this.showField(this.currentField)
|
||||
},
|
||||
compiledComponent(value) {
|
||||
return {
|
||||
template: `${value}`,
|
||||
props: ['row', 'tick', 'pagename', 'field', 'highlight'],
|
||||
methods: {
|
||||
formatNumber(val) {
|
||||
return this.$formatNumber(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
showContextMenu(field) {
|
||||
this.currentField = field
|
||||
this.filterData = this.$unique(this.data, [field.name])
|
||||
this.menuaction = {name: 'display', key: this.$id(), field: field}
|
||||
this.$emit('contextmenu', 'open')
|
||||
},
|
||||
showSidebar(event) {
|
||||
let title = 'Danh sách cột'
|
||||
if(event.name==="bgcolor") title = `Đổi màu nền: ${event.field.name} / ${this.$stripHtml(event.field.label, 30)}`
|
||||
else if(event.name==="color") title = `Đổi màu chữ: ${event.field.name} / ${this.$stripHtml(event.field.label, 30)}`
|
||||
else if(event.name==='template') title = `Định dạng nâng cao: ${this.$stripHtml(event.field.label, 30)}`
|
||||
this.showmodal1 = {component: 'datatable/FormatOption',
|
||||
vbind: {event: event, currentField: this.currentField, pagename: this.pagename}, width: '850px', height: '700px', title: title}
|
||||
},
|
||||
getStyle(field, record) {
|
||||
var stop = false
|
||||
let val = this.tablesetting.find(v=>v.code==='td-border')? this.tablesetting.find(v=>v.code==='td-border').detail : 'border: solid 1px rgb(44, 44, 44); '
|
||||
val = val.indexOf(';')>=0? val : val + ';'
|
||||
if(field.bgcolor? !Array.isArray(field.bgcolor) : false) {
|
||||
val += ` background-color:${field.bgcolor}; `
|
||||
} else if(field.bgcolor? Array.isArray(field.bgcolor) : false) {
|
||||
field.bgcolor.map(v=>{
|
||||
if(v.type==='search') {
|
||||
if(record[field.name] && !stop? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase())>=0 : false) {
|
||||
val += ` background-color:${v.color}; `
|
||||
stop = true
|
||||
}
|
||||
} else {
|
||||
let res = this.$calculate(record, v.tags, v.expression)
|
||||
if(res.success && res.value && !stop) {
|
||||
val += ` background-color:${v.color}; `
|
||||
stop = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
stop = false
|
||||
if(field.color? !Array.isArray(field.color) : false) {
|
||||
val += ` color:${field.color}; `
|
||||
} else if(field.color? Array.isArray(field.color) : false) {
|
||||
field.color.map(v=>{
|
||||
if(v.type==='search') {
|
||||
if(record[field.name] && !stop? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase())>=0 : false) {
|
||||
val += ` color:${v.color}; `
|
||||
stop = true
|
||||
}
|
||||
} else {
|
||||
let res = this.$calculate(record, v.tags, v.expression)
|
||||
if(res.success && res.value && !stop) {
|
||||
val += ` color:${v.color}; `
|
||||
stop = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
stop = false
|
||||
if(field.textsize? !Array.isArray(field.textsize) : false) {
|
||||
val += ` font-size:${field.textsize}px; `
|
||||
} else if(field.textsize? Array.isArray(field.textsize) : false) {
|
||||
field.textsize.map(v=>{
|
||||
if(v.type==='search') {
|
||||
if(record[field.name] && !stop? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase())>=0 : false) {
|
||||
val += ` font-size:${v.size}px; `
|
||||
stop = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
let res = this.$calculate(record, v.tags, v.expression)
|
||||
if(res.success && res.value && !stop) {
|
||||
val += ` font-size:${v.size}px; `
|
||||
stop = true
|
||||
}
|
||||
}
|
||||
})
|
||||
} else val += ` font-size:${this.tablesetting.find(v=>v.code==='table-font-size').detail}px;`
|
||||
if(field.textalign) val += ` text-align:${field.textalign}; `
|
||||
if(field.minwidth) val += ` min-width:${field.minwidth}px; `
|
||||
if(field.maxwidth) val += ` max-width:${field.maxwidth}px; `
|
||||
return val
|
||||
},
|
||||
getSettingStyle(name, field) {
|
||||
let value = ''
|
||||
if(name==='container') {
|
||||
value = 'min-height:' + this.tablesetting.find(v=>v.code==='container-height').detail + 'rem; '
|
||||
} else if(name==='table') {
|
||||
value += 'background-color:' + this.tablesetting.find(v=>v.code==='table-background').detail + '; '
|
||||
value += 'font-size:' + this.tablesetting.find(v=>v.code==='table-font-size').detail + 'px;'
|
||||
value += 'color:' + this.tablesetting.find(v=>v.code==='table-font-color').detail + '; '
|
||||
} else if(name==='header') {
|
||||
value += 'background-color:' + this.tablesetting.find(v=>v.code==='header-background').detail + '; '
|
||||
if(field.minwidth) value += ' min-width: ' + field.minwidth + 'px; '
|
||||
if(field.maxwidth) value += ' max-width: ' + field.maxwidth + 'px; '
|
||||
} else if(name==='menu') {
|
||||
let arg = this.tablesetting.find(v=>v.code==='menu-width').detail
|
||||
arg = field? (field.menuwidth? field.menuwidth : arg) : arg
|
||||
value += 'width:' + arg + 'rem; '
|
||||
value += 'min-height:' + this.tablesetting.find(v=>v.code==='menu-min-height').detail + 'rem; '
|
||||
value += 'max-height:' + this.tablesetting.find(v=>v.code==='menu-max-height').detail + 'rem; '
|
||||
value += "overflow:auto; "
|
||||
} else if(name==='dropdown') {
|
||||
value += 'font-size:' + this.tablesetting.find(v=>v.code==='header-font-size').detail + 'px; '
|
||||
let found = this.filters.find(v=>v.name===field.name)
|
||||
found? value += 'color:' + this.tablesetting.find(v=>v.code==='header-filter-color').detail + '; '
|
||||
:value += 'color:' + this.tablesetting.find(v=>v.code==='header-font-color').detail + '; '
|
||||
}
|
||||
return value
|
||||
},
|
||||
removeFilter(i) {
|
||||
Vue.delete(this.filters, i)
|
||||
this.doFilter(this.filters)
|
||||
this.updateShow()
|
||||
},
|
||||
updateFields(field) {
|
||||
let copy = this.$copy(this.pagedata.fields)
|
||||
let idx = copy.findIndex(v=>v.name===field.name)
|
||||
copy[idx] = field
|
||||
this.updateData({columns: copy})
|
||||
this.currentField = this.$copy(field)
|
||||
if(this.showmodal) {
|
||||
this.showmodal.vbind.field = this.$copy(field)
|
||||
this.showmodal = this.$copy(this.showmodal)
|
||||
}
|
||||
},
|
||||
doAction(event, row, field) {
|
||||
let name = typeof event === "string"? event : event.name
|
||||
let data = typeof event === "string"? event : event.data
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'action', data: {event: name, row: row, field: field, data: data, time: new Date()}})
|
||||
if(name==='remove') this.$deleterow(this.pagedata.api.name, row.id, this.pagename, true)
|
||||
if(name==='batchdelete') this.batchDelete()
|
||||
this.$emit(name, row, field, data)
|
||||
if(name==='tickall') {
|
||||
this.tickall = data
|
||||
if(data===false) this.tick = {}
|
||||
else {
|
||||
this.data.map(v=>this.tick[v.id] = true)
|
||||
this.tick = this.$copy(this.tick)
|
||||
}
|
||||
}
|
||||
},
|
||||
batchDelete() {
|
||||
let arr = []
|
||||
Object.entries(this.tick).forEach(([key, value]) => {
|
||||
if(value) arr.push({id: Number(key)})
|
||||
})
|
||||
if(arr.length===0) this.$buefy.toast.open({message: 'Bạn chưa chọn bản ghi để xoá', type: 'is-warning'})
|
||||
else this.$deleterow(this.pagedata.api.name, arr, this.pagename, true)
|
||||
},
|
||||
doSort(field, type) {
|
||||
let filter = {name: field.name, label: field.label, sort: type, format: field.format}
|
||||
let idx = this.filters.findIndex(v=>v.name===field.name)
|
||||
if(idx>=0) Vue.set(this.filters, idx, filter)
|
||||
else this.filters.push(filter)
|
||||
this.doFilter(this.filters)
|
||||
this.updateShow()
|
||||
},
|
||||
doSearch(field, search) {
|
||||
let copy = this.$copy(this.filters)
|
||||
let idx = copy.findIndex(v=>v.name===field.name)
|
||||
if(idx>=0) Vue.delete(copy, idx)
|
||||
if(this.pagedata.origin_api.full_data) {
|
||||
let data = this.frontendFilter(copy)
|
||||
let rows = this.$empty(search)? data
|
||||
: data.filter(v=>v[field.name]? v[field.name].toString().toLowerCase().indexOf(search.toLowerCase())>=0 : false)
|
||||
this.filterData = this.$unique(rows, [field.name])
|
||||
if(this.showmodal) this.showmodal.vbind.filterData = this.filterData
|
||||
} else {
|
||||
copy.push({name: field.name, label: field.label, search: search.toLowerCase(), format: field.format})
|
||||
this.flagSearch = true
|
||||
this.backendFilter(copy)
|
||||
}
|
||||
},
|
||||
doSelect(field, value) {
|
||||
let found = this.filters.find(v=>v.name===field.name)
|
||||
if(found) {
|
||||
!found.select? found.select = [] : false
|
||||
let idx = found.select.findIndex(x=>x===value)
|
||||
idx>=0? Vue.delete(found.select, idx) : found.select.push(value)
|
||||
if(found.select.length===0) {
|
||||
idx = this.filters.findIndex(v=>v.name===field.name)
|
||||
if(idx>=0) Vue.delete(this.filters, idx)
|
||||
}
|
||||
} else {
|
||||
this.filters.push({name: field.name, label: field.label, select: [value], format: field.format})
|
||||
}
|
||||
this.doFilter(this.filters)
|
||||
this.updateShow()
|
||||
},
|
||||
setFilter(field, filter) {
|
||||
let idx = this.filters.findIndex(v=>v.name===field.name)
|
||||
if(idx<0) this.filters.push(filter)
|
||||
else Vue.set(this.filters, idx, filter)
|
||||
this.doFilter(this.filters)
|
||||
this.updateShow()
|
||||
},
|
||||
doFilter(newVal, nonset) {
|
||||
if(this.currentPage>1 && nonset!==true) this.currentPage = 1
|
||||
if(this.pagedata.origin_api.full_data) {
|
||||
this.data = this.frontendFilter(newVal)
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "dataFilter", data: this.$copy(this.data)})
|
||||
this.$emit('changedata', newVal)
|
||||
}
|
||||
else {
|
||||
if(this.timer) clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => this.backendFilter(newVal), 200)
|
||||
}
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "filters", data: this.$copy(newVal)})
|
||||
this.$emit('changefilter', newVal? newVal.length>0 : false)
|
||||
},
|
||||
frontendFilter(newVal) {
|
||||
let self = this
|
||||
let checkValid = function(name, x, filter) {
|
||||
if(self.$empty(x[name])) return false
|
||||
else {
|
||||
let text = ''
|
||||
filter.map((y,k)=>{
|
||||
text += `${k>0? (filter[k-1].operator==='and'? ' &&' : ' ||') : ''} ${self.$formatNumber(x[name])}
|
||||
${y.condition==='='? '==' : (y.condition==='<>'? '!==' : y.condition)} ${self.$formatNumber(y.value)}`
|
||||
})
|
||||
return self.$calc(text)
|
||||
}
|
||||
}
|
||||
newVal = this.$copy(newVal)
|
||||
var data = this.$copy(this.pagedata.data)
|
||||
newVal.filter(m=>m.select || m.filter).map(v => {
|
||||
if(v.select) {
|
||||
data = data.filter(x => v.select.findIndex(y => this.$empty(y)? this.$empty(x[v.name]) : (y===x[v.name])) >-1)
|
||||
} else if(v.filter) {
|
||||
data = data.filter(x => checkValid(v.name, x, v.filter))
|
||||
}
|
||||
})
|
||||
let sort = {}
|
||||
let format = {}
|
||||
let list = this.filters.filter(x=>x.sort)
|
||||
list.map(v=>{
|
||||
sort[v.name] = v.sort === "az" ? "asc" : "desc"
|
||||
format[v.name] = v.format;
|
||||
})
|
||||
return list.length>0? this.$multiSort(data, sort, format) : data
|
||||
},
|
||||
// Sử dụng backend filter
|
||||
async backendFilter(newVal) {
|
||||
let arr = [{code: '>', name: 'gt'}, {code: '>=', name: 'gte'}, {code: '<', name: 'lt'}, {code: '<=', name: 'lte'}, {code: '=', name: 'e'},
|
||||
{code: '<>', name: 'e'}]
|
||||
let params = this.pagedata.origin_api.params? this.$copy(this.pagedata.origin_api.params) : {}
|
||||
params.page = this.currentPage
|
||||
var where = params.filter? params.filter : {}
|
||||
var exclude = {}
|
||||
var sort = params.sort? params.sort.split(',') : []
|
||||
var filter = newVal.filter(v=>!v.formula)
|
||||
if (this.pagedata.origin_api.url.indexOf("data/") >= 0) {
|
||||
filter.forEach(v => {
|
||||
if(v.search) where[v.name + "__icontains"] = v.search
|
||||
else if (v.select) where[v.name + "__in"] = v.select
|
||||
else if (v.filter) {
|
||||
v.filter.map(x=>{
|
||||
let obj = this.$find(arr, {code: x.condition})
|
||||
if(obj) {
|
||||
if(x.condition==='<>') exclude[v.name + (obj.name==='e'? '' : '__' + obj.name)] = this.$formatNumber(x.value)
|
||||
else where[v.name + (obj.name==='e'? '' : '__' + obj.name)] = this.$formatNumber(x.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
else if (v.sort) sort.push(v.sort==="az" ? v.name : "-" + v.name)
|
||||
})
|
||||
params.filter = Object.keys(where).length>0? where : undefined
|
||||
params.exclude = Object.keys(exclude).length>0? exclude : undefined
|
||||
params.sort = sort.length === 0 ? undefined : sort.toString()
|
||||
}
|
||||
// Tải lại dữ liệu
|
||||
let found = this.$copy(this.pagedata.api)
|
||||
found.params = params
|
||||
await this.loadData(found)
|
||||
},
|
||||
async loadData(found) {
|
||||
let result = await this.$getapi([found])
|
||||
if(result==='error') return
|
||||
if(this.flagSearch) {
|
||||
this.flagSearch = false
|
||||
var rows = result[0].data.rows
|
||||
if(this.currentField) this.filterData = this.$unique(rows, [this.currentField.name])
|
||||
} else {
|
||||
let copy = this.$copy(result[0])
|
||||
copy.total_rows = copy.data.total_rows
|
||||
copy.full_data = copy.data.full_data
|
||||
delete copy.data
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "data", data: this.$copy(result[0].data.rows)})
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "api", data: this.$copy(copy)})
|
||||
this.data = this.$copy(result[0].data.rows)
|
||||
this.data = this.$formatArray(this.data, this.pagedata.fields)
|
||||
if(this.data.length>0) this.displayData = this.displayData.concat(this.data)
|
||||
}
|
||||
},
|
||||
scrollbarVisible() {
|
||||
let element = this.$refs['container']
|
||||
if(!element) return
|
||||
let result = element.scrollWidth > element.clientWidth? true : false
|
||||
if(this.scrollbar) {
|
||||
element.parentNode.removeChild(this.scrollbar)
|
||||
this.scrollbar = undefined
|
||||
}
|
||||
if(result) this.doubleScroll(element)
|
||||
},
|
||||
doubleScroll(element) {
|
||||
var scrollbar= document.createElement('div');
|
||||
scrollbar.appendChild(document.createElement('div'));
|
||||
scrollbar.style.overflow= 'auto';
|
||||
scrollbar.style.overflowY= 'hidden';
|
||||
scrollbar.firstChild.style.width= element.scrollWidth+'px';
|
||||
scrollbar.firstChild.style.height = '1px'
|
||||
scrollbar.firstChild.appendChild(document.createTextNode('\xA0'));
|
||||
var running = false;
|
||||
scrollbar.onscroll= function() {
|
||||
if(running) {
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
element.scrollLeft= scrollbar.scrollLeft;
|
||||
};
|
||||
element.onscroll= function() {
|
||||
if(running) {
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
scrollbar.scrollLeft= element.scrollLeft;
|
||||
}
|
||||
element.parentNode.insertBefore(scrollbar, element)
|
||||
scrollbar.scrollLeft= element.scrollLeft
|
||||
this.scrollbar = scrollbar
|
||||
},
|
||||
copyField(field) {
|
||||
let newField = this.$copy(field)
|
||||
newField.formula = field.name
|
||||
newField.tags = [field.name]
|
||||
newField.name = 'f' + this.$dayjs(new Date()).format("hhmmss")
|
||||
newField.label = field.label + '-copy'
|
||||
newField.unit = field.unit==='0.01'? field.unit : '1'
|
||||
let copy = this.$copy(this.pagedata.fields)
|
||||
let idx = copy.findIndex(v=>v.name===field.name)
|
||||
copy.splice(idx+1, 0, newField)
|
||||
this.$store.commit("updateState", {name: this.pagename, key: "update", data: {fields: copy}})
|
||||
},
|
||||
handleScroll(e) {
|
||||
if(this.loading) return
|
||||
const bottom = e.target.scrollHeight - e.target.scrollTop -5 < e.target.clientHeight
|
||||
if (bottom) {
|
||||
this.currentPage += 1
|
||||
this.loading = true
|
||||
console.log('bottom', this.currentPage)
|
||||
this.changePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.table tbody tr:hover td, .table tbody tr:hover th {
|
||||
background-color: hsl(0, 0%, 29%);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
32
components/dialog/Confirm.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div>
|
||||
<p v-html="content"></p>
|
||||
<p class="border-bottom mt-3 mb-5"></p>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-primary" @click="confirm()">Đồng ý</button>
|
||||
<button class="button is-dark ml-5" @click="cancel()">Hủy bỏ</button>
|
||||
</div>
|
||||
<div class="control" v-if="duration">
|
||||
<CountDown v-bind="{duration: duration}"></CountDown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['content', 'duration'],
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$store.commit('updateStore', {name: 'showmodal', data: undefined})
|
||||
this.$emit('close')
|
||||
},
|
||||
confirm() {
|
||||
let data = {action: 'confirm', time: new Date()}
|
||||
this.$store.commit('updateStore', {name: 'action', data: data})
|
||||
this.$emit('modalevent', {name: 'confirm'})
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
76
components/dialog/CountDown.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div id="countdown">
|
||||
<div id="countdown-number"></div>
|
||||
<svg><circle r="18" cx="20" cy="20" color="red"></circle></svg>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['duration'],
|
||||
data() {
|
||||
return {
|
||||
timer: undefined,
|
||||
countdown: this.duration || 10
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
var countdownNumberEl = document.getElementById('countdown-number')
|
||||
countdownNumberEl.textContent = this.countdown;
|
||||
this.timer = setInterval(()=>this.startCount(), 1000)
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer)
|
||||
},
|
||||
methods: {
|
||||
startCount() {
|
||||
this.countdown -= 1
|
||||
var countdownNumberEl = document.getElementById('countdown-number')
|
||||
countdownNumberEl.textContent = this.countdown;
|
||||
if(this.countdown===0) {
|
||||
clearInterval(this.timer)
|
||||
this.$store.commit('updateStore', {name: 'showmodal', data: undefined})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#countdown {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
#countdown-number {
|
||||
color: black;
|
||||
display: inline-block;
|
||||
line-height: 40px;
|
||||
}
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
transform: rotateY(-180deg) rotateZ(-90deg);
|
||||
}
|
||||
svg circle {
|
||||
stroke-dasharray: 113px;
|
||||
stroke-dashoffset: 0px;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 2px;
|
||||
stroke: black;
|
||||
fill: none;
|
||||
animation: countdown 10s linear infinite forwards;
|
||||
}
|
||||
|
||||
@keyframes countdown {
|
||||
from {
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: 113px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
43
components/dialog/Delete.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<p v-html="content"></p>
|
||||
<p class="border-bottom mt-4 mb-5"></p>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-danger" @click="remove()">Đồng ý</button>
|
||||
<button class="button is-dark ml-5" @click="cancel()">Hủy bỏ</button>
|
||||
</div>
|
||||
<div class="control" v-if="duration">
|
||||
<CountDown v-bind="{duration: duration}"></CountDown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['content', 'duration', 'vbind'],
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$store.commit('updateStore', {name: 'showmodal', data: undefined})
|
||||
},
|
||||
async remove() {
|
||||
let pagename = this.vbind.pagename
|
||||
let pagedata = this.$store.state[pagename]
|
||||
let name = pagedata.origin_api.name
|
||||
console.log(pagedata)
|
||||
let id = this.vbind.row.id
|
||||
let result = await this.$deleteapi(name, id)
|
||||
if(result==='error') return
|
||||
this.$snackbar('Dữ liệu đã được xoá khỏi hệ thống', undefined, 'Success')
|
||||
let arr = Array.isArray(id)? id : [{id: id}]
|
||||
let copy = this.$copy(this.$store.state[pagename].data)
|
||||
arr.map(x=>{
|
||||
let index = copy.findIndex(v=>v.id===x.id)
|
||||
index>=0? this.$delete(copy,index) : false
|
||||
})
|
||||
this.$store.commit('updateState', {name: pagename, key: 'update', data: {data: copy}})
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
29
components/dialog/Error.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded pr-3" v-html="content"></div>
|
||||
<div class="control">
|
||||
<span class="material-symbols-outlined has-text-danger fs-36">error</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="border-bottom mt-3 mb-5"></p>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-dark" @click="cancel()">Đóng</button>
|
||||
</div>
|
||||
<div class="control" v-if="duration">
|
||||
<CountDown v-bind="{duration: duration}"></CountDown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['content', 'duration'],
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$store.commit('updateStore', {name: 'showmodal', data: undefined})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
24
components/dialog/Info.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<p v-html="content"></p>
|
||||
<p class="border-bottom mt-3 mb-5"></p>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-dark" @click="cancel()">Đóng</button>
|
||||
</div>
|
||||
<div class="control" v-if="duration">
|
||||
<CountDown v-bind="{duration: duration}"></CountDown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['content', 'duration'],
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$store.commit('updateStore', {name: 'showmodal', data: undefined})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
30
components/dialog/Success.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded pr-3" v-html="content"></div>
|
||||
<div class="control">
|
||||
<span class="material-symbols-outlined has-text-primary fs-36">check_circle</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p class="border-bottom mt-3 mb-5"></p>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-dark" @click="cancel()">Đóng</button>
|
||||
</div>
|
||||
<div class="control" v-if="duration">
|
||||
<CountDown v-bind="{duration: duration}"></CountDown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['content', 'duration'],
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$store.commit('updateStore', {name: 'showmodal', data: undefined})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
21
components/snackbar/Error.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<Caption v-bind="{title: 'Lỗi', type: 'has-text-findata'}"></Caption>
|
||||
<div class="field is-grouped mb-0">
|
||||
<div class="control is-expanded pr-3" v-html="content"></div>
|
||||
<div class="control">
|
||||
<span class="material-symbols-outlined has-text-findata fs-34">error</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['content', 'duration'],
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$store.commit('updateStore', {name: 'showmodal', data: undefined})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
15
components/snackbar/Info.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<p v-html="content"></p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['content', 'duration'],
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$store.commit('updateStore', {name: 'showmodal', data: undefined})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
77
components/snackbar/SnackBar.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="show" style="max-width: 500px;">
|
||||
<component :is="compobj" v-bind="vbind" @close="$emit('close')"></component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['component', 'width', 'height', 'vbind', 'title'],
|
||||
data() {
|
||||
return {
|
||||
timer: undefined
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(()=>this.snackbar = undefined, 2900)
|
||||
},
|
||||
computed: {
|
||||
snackbar: {
|
||||
get: function() {return this.$store.state['snackbar']},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: 'snackbar', data: val})}
|
||||
},
|
||||
compobj() {
|
||||
return () => import(`@/components/${this.component}`)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
var x = document.getElementById("snackbar");
|
||||
x.className = x.className.replace("show", "")
|
||||
setTimeout(()=>this.snackbar = undefined, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.show {
|
||||
min-width: 250px; /* Set a default minimum width */
|
||||
margin-left: -125px; /* Divide value of min-width by 2 */
|
||||
background-color: #303030; /* Black background color */
|
||||
color: white; /* White text color */
|
||||
text-align: left; /* Centered text */
|
||||
border-radius: 6px; /* Rounded borders */
|
||||
padding: 10px; /* Padding */
|
||||
position: fixed; /* Sit on top of the screen */
|
||||
z-index: 999; /* Add a z-index if needed */
|
||||
left: 50%; /* Center the snackbar */
|
||||
top: 50px; /* 50px from the top */
|
||||
visibility: visible; /* Show the snackbar */
|
||||
/* Add animation: Take 0.5 seconds to fade in and out the snackbar.
|
||||
However, delay the fade out process for 2.5 seconds */
|
||||
-webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
||||
animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
||||
}
|
||||
|
||||
/* Animations to fade the snackbar in and out */
|
||||
@-webkit-keyframes fadein {
|
||||
from {top: 0; opacity: 0;}
|
||||
to {top: 50px; opacity: 1;}
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {top: 0; opacity: 0;}
|
||||
to {top: 50px; opacity: 1;}
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeout {
|
||||
from {top: 50px; opacity: 1;}
|
||||
to {top: 0; opacity: 0;}
|
||||
}
|
||||
|
||||
@keyframes fadeout {
|
||||
from {top: 50px; opacity: 1;}
|
||||
to {top: 0; opacity: 0;}
|
||||
}
|
||||
</style>
|
||||
21
components/snackbar/Success.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<Caption v-bind="{title: 'Thành công', type: 'has-text-primary'}"></Caption>
|
||||
<div class="field is-grouped mb-0 pb-0">
|
||||
<div class="control is-expanded pr-3 mb-0" v-html="content"></div>
|
||||
<div class="control mb-0">
|
||||
<span class="material-symbols-outlined has-text-primary fs-34">check_circle</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['content', 'duration'],
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$store.commit('updateStore', {name: 'showmodal', data: undefined})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
27
constants/company.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export const COMPANY = {
|
||||
name: 'BigDataTechCloud',
|
||||
website: 'https://bigdatatech.cloud',
|
||||
|
||||
parent: {
|
||||
name: 'BigDataTech',
|
||||
website: 'https://bigdatatech.vn/',
|
||||
email: 'contact@bigdatatech.vn',
|
||||
phone: '(+84) 943 833 599',
|
||||
address: 'Tầng 4, 505 Đường Minh Khai, Phường Vĩnh Tuy, Hà Nội',
|
||||
slogan: 'BigDataTech - Giải pháp Big Data & Phát triển Phần mềm',
|
||||
sloganEn: 'BigDataTech - Big Data Solutions & Software Development',
|
||||
},
|
||||
|
||||
email: {
|
||||
contact: 'contact@bigdatatech.vn',
|
||||
support: 'support@bigdatatech.vn',
|
||||
},
|
||||
|
||||
phone: {
|
||||
main: '(+84) 943 833 599',
|
||||
hotline: '(+84) 943 833 599',
|
||||
support: '(+84) 943 833 599',
|
||||
},
|
||||
|
||||
address: 'Tầng 4, 505 Đường Minh Khai, Phường Vĩnh Tuy, Hà Nội',
|
||||
};
|
||||
11
ecosystem.config.cjs
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'cms',
|
||||
exec_mode: 'cluster',
|
||||
instances: 'max', // Or a number of instances
|
||||
script: './node_modules/nuxt/bin/nuxt.js',
|
||||
args: 'start'
|
||||
}
|
||||
]
|
||||
}
|
||||
12
jsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
"@/*": ["./*"],
|
||||
"~~/*": ["./*"],
|
||||
"@@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", ".nuxt", "dist"]
|
||||
}
|
||||
83
layouts/colud.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<Nuxt v-if="ready" />
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
ready: false,
|
||||
tasks: [],
|
||||
timerTask: undefined
|
||||
}
|
||||
},
|
||||
head() {
|
||||
return {title: 'Cloud'}
|
||||
},
|
||||
async created() {
|
||||
let connlist = this.$readyapi(['moneyunit', 'datatype', 'filterchoice', 'colorchoice', 'textalign', 'placement', 'sex', 'legaltype',
|
||||
'colorscheme', 'filtertype', 'sorttype', 'tablesetting', 'settingchoice', 'menuchoice', 'settingtype', 'settingclass', 'runtype',
|
||||
'usertype', 'dbtype'])
|
||||
let filter = connlist.filter(v=>!v.ready)
|
||||
if(filter.length>0) {
|
||||
let rs = await this.$getapi(filter)
|
||||
this.ready = true
|
||||
if(!this.$store.state.login) this.$router.push('/signin')
|
||||
} else {
|
||||
this.ready = true
|
||||
if(!this.$store.state.login) this.$router.push('/signin')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
if(this.$empty(this.viewport)) {
|
||||
if(width<=768) this.viewport = 1 //'mobile'
|
||||
else if(width>=769 && width<=1023) this.viewport = 2 //'tablet'
|
||||
else if(width>=1024 && width<=1215) this.viewport = 3 //'desktop'
|
||||
else if(width>=1216 && width<=1407) this.viewport = 4 //'widescreen'
|
||||
else if(width>=1408) this.viewport = 5 //'fullhd'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
login: {
|
||||
get: function() {return this.$store.state.login},
|
||||
set: function(val) {this.$store.commit("updateLogin", {login: val})}
|
||||
},
|
||||
language: {
|
||||
get: function() {return this.$store.state.language},
|
||||
set: function(val) {this.$store.commit("updateLanguage", {language: val})}
|
||||
},
|
||||
common: {
|
||||
get: function() {return this.$store.state.common},
|
||||
set: function(val) {this.$store.commit("updateCommon", {common: val})}
|
||||
},
|
||||
tablesetting: {
|
||||
get: function() {return this.$store.state.tablesetting},
|
||||
set: function(val) {this.$store.commit("updateTable", {tablesetting: val})}
|
||||
},
|
||||
dialog: {
|
||||
get: function() {return this.$store.state['dialog']},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: 'dialog', data: val})}
|
||||
},
|
||||
sshlist: {
|
||||
get: function() {return this.$store.state.sshlist},
|
||||
set: function(val) {this.$store.commit("updateSshlist", {sshlist: val})}
|
||||
},
|
||||
viewport: {
|
||||
get: function() {return this.$store.state.viewport},
|
||||
set: function(val) {this.$store.commit("updateViewPort", {viewport: val})}
|
||||
},
|
||||
taskstoday: {
|
||||
get: function() {return this.$store.state['taskstoday']},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: 'taskstoday', data: val})}
|
||||
},
|
||||
datatask: {
|
||||
get: function() {return this.$store.state['datatask']},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: 'datatask', data: val})}
|
||||
},
|
||||
showmodal: {
|
||||
get: function() {return this.$store.state['showmodal']},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: 'showmodal', data: val})}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
84
layouts/default.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<Nuxt v-if="ready" />
|
||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
<SnackBar @close="snackbar = undefined" v-bind="snackbar" v-if="snackbar" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
ready: false,
|
||||
company: this.$companyInfo(),
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: `${this.company.name}`,
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
let connlist = this.$readyapi(['registermethod', 'authmethod', 'usertype', 'authstatus', 'common']);
|
||||
let filter = connlist.filter((v) => !v.ready);
|
||||
let result = filter.length > 0 ? await this.$getapi(filter) : undefined;
|
||||
if (this.result === 'error') {
|
||||
this.$dialog({
|
||||
width: '500px',
|
||||
icon: 'mdi mdi-alert-circle',
|
||||
content: 'Đã có lỗi xảy ra khi kết nối dữ liệu. Vui lòng thử lại sau ít phút',
|
||||
type: 'is-danger',
|
||||
progress: true,
|
||||
duration: 6,
|
||||
});
|
||||
} else this.ready = true;
|
||||
},
|
||||
mounted() {
|
||||
var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
if (this.$empty(this.ismobile)) this.ismobile = width < 1024 ? true : false;
|
||||
if (this.$empty(this.viewport)) {
|
||||
if (width <= 768) this.viewport = 1; //'mobile'
|
||||
else if (width >= 769 && width <= 1023) this.viewport = 2; //'tablet'
|
||||
else if (width >= 1024 && width <= 1215) this.viewport = 3; //'desktop'
|
||||
else if (width >= 1216 && width <= 1407) this.viewport = 4; //'widescreen'
|
||||
else if (width >= 1408) this.viewport = 5; //'fullhd'
|
||||
}
|
||||
if (this.$route.query.link) this.$store.commit('updateLink', { link: this.$route.query.link });
|
||||
if (this.$route.query.iframe) this.$store.commit('updateStore', { name: 'iframe', data: this.$route.query.iframe });
|
||||
},
|
||||
computed: {
|
||||
ismobile: {
|
||||
get: function () {
|
||||
return this.$store.state.ismobile;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateIsMobile', { ismobile: val });
|
||||
},
|
||||
},
|
||||
showmodal: {
|
||||
get: function () {
|
||||
return this.$store.state['showmodal'];
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateStore', { name: 'showmodal', data: val });
|
||||
},
|
||||
},
|
||||
snackbar: {
|
||||
get: function () {
|
||||
return this.$store.state['snackbar'];
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateStore', { name: 'snackbar', data: val });
|
||||
},
|
||||
},
|
||||
viewport: {
|
||||
get: function () {
|
||||
return this.$store.state.viewport;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateViewPort', { viewport: val });
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
65
nuxt.config.js
Normal file
@@ -0,0 +1,65 @@
|
||||
export default {
|
||||
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||
server: {
|
||||
port: 3001, // default: 3000
|
||||
host: '0.0.0.0', // default: localhost
|
||||
},
|
||||
|
||||
head: {
|
||||
title: 'store',
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
},
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ hid: 'description', name: 'description', content: '' },
|
||||
{ name: 'format-detection', content: 'telephone=no' }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0' }
|
||||
]
|
||||
},
|
||||
|
||||
// Global CSS: https://go.nuxtjs.dev/config-css
|
||||
css: [
|
||||
'~/assets/styles/main.scss',
|
||||
'~/assets/styles/style.scss',
|
||||
'~/node_modules/@mdi/font/css/materialdesignicons.min.css'
|
||||
],
|
||||
|
||||
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
||||
plugins: [
|
||||
'~/plugins/buefy',
|
||||
'~/plugins/connection',
|
||||
'~/plugins/common',
|
||||
'~/plugins/datatable',
|
||||
'~/plugins/components'
|
||||
],
|
||||
|
||||
// Auto import components: https://go.nuxtjs.dev/config-components
|
||||
components: true,
|
||||
|
||||
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
|
||||
buildModules: [
|
||||
],
|
||||
|
||||
// Modules: https://go.nuxtjs.dev/config-modules
|
||||
modules: [
|
||||
// https://go.nuxtjs.dev/axios
|
||||
'@nuxtjs/axios',
|
||||
'@nuxtjs/dayjs'
|
||||
],
|
||||
|
||||
// Axios module configuration: https://go.nuxtjs.dev/config-axios
|
||||
axios: {},
|
||||
|
||||
// Build Configuration (https://go.nuxtjs.dev/config-build)
|
||||
build: {
|
||||
postcss: null,
|
||||
extend(config, { isDev, isClient }) {
|
||||
config.resolve.alias["vue"] = "vue/dist/vue.common";
|
||||
}
|
||||
}
|
||||
}
|
||||
18365
package-lock.json
generated
Normal file
25
package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "login",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nuxt dev",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^6.6.96",
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
"@nuxtjs/dayjs": "^1.4.1",
|
||||
"bowser": "^2.11.0",
|
||||
"buefy": "^0.9.27",
|
||||
"bulma": "^0.9.4",
|
||||
"core-js": "^3.35.1",
|
||||
"nuxt": "^2.18.1",
|
||||
"sass": "^1.79.5",
|
||||
"sass-loader": "^10.5.2",
|
||||
"vue-advanced-cropper": "^1.11.6",
|
||||
"vue-google-login": "^2.0.5"
|
||||
}
|
||||
}
|
||||
150
pages/account/auth.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="columns is-centered mt-6 mx-3">
|
||||
<div class="column is-5 mx-0">
|
||||
<Logo class="mb-5"></Logo>
|
||||
<template v-if="!userAuth">
|
||||
<article class="message is-primary">
|
||||
<div class="message-body has-background-white py-3">
|
||||
<strong> Mã xác thực tài khoản </strong> là chuỗi gồm <strong> 9 kí tự, </strong> chúng tôi đã gửi cho bạn
|
||||
qua email <b>{{ $route.query.email ? ' ' + $route.query.email : '' }}</b
|
||||
>. Hãy nhập dãy số đó vào ô dưới đây. Hoặc click vào đường link có trong email.
|
||||
</div>
|
||||
</article>
|
||||
<div class="field mt-4">
|
||||
<div class="control">
|
||||
<input class="input is-primary" type="text" placeholder="Nhập mã xác thực" v-model="code" id="inputcode" />
|
||||
</div>
|
||||
<p class="help is-danger mt5 fs13" v-if="errors.find((v) => v.name === 'code')">
|
||||
{{ errors.find((v) => v.name === 'code').text }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field mt-5">
|
||||
<p class="control">
|
||||
<a class="button is-primary" @click="checkCode()">Xác thực tài khoản</a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="mt-5 pt-4 has-text-danger" v-if="$route.query.id">
|
||||
<span>Không nhận được mã xác thực?</span>
|
||||
<button :class="`button is-dark is-light ${loading ? 'is-loading' : ''} ml-4`" @click="sendCode()">
|
||||
Gửi lại mã
|
||||
</button>
|
||||
</p>
|
||||
</template>
|
||||
<template v-else-if="userAuth">
|
||||
<article class="message" :class="success ? 'is-primary' : 'is-danger'" v-if="success !== undefined">
|
||||
<div class="message-body has-background-white py-3">
|
||||
{{ message }}
|
||||
</div>
|
||||
</article>
|
||||
<div class="field mt-5" v-if="action">
|
||||
<p class="control">
|
||||
<nuxt-link class="button is-primary" :to="action.to">
|
||||
{{ action.text }}
|
||||
</nuxt-link>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
code: undefined,
|
||||
errors: [],
|
||||
userAuth: undefined,
|
||||
message: undefined,
|
||||
success: undefined,
|
||||
action: undefined,
|
||||
code: undefined,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.$route.query.code) {
|
||||
this.code = this.$route.query.code;
|
||||
this.checkCode();
|
||||
} else if (this.$route.query.action === 'sendcode') this.sendCode();
|
||||
},
|
||||
mounted() {
|
||||
let doc = document.getElementById('inputcode');
|
||||
if (doc) doc.focus();
|
||||
window.addEventListener('keyup', (ev) => (ev.key === 'Enter' && !this.success ? this.checkCode() : false));
|
||||
},
|
||||
computed: {
|
||||
login: {
|
||||
get: function () {
|
||||
return this.$store.state.login;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateLogin', { login: val });
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async checkCode() {
|
||||
this.success = undefined;
|
||||
this.message = undefined;
|
||||
this.action = undefined;
|
||||
this.errors = [];
|
||||
if (this.$empty(this.code)) this.errors.push({ name: 'code', text: 'Mã xác thực không được bỏ trống' });
|
||||
else if (this.code.length !== 9) this.errors.push({ name: 'code', text: 'Mã xác thực phải là 9 kí tự' });
|
||||
if (this.errors.length > 0) return;
|
||||
let found = { name: 'userauth', url: 'data/User_Auth', params: { filter: { code: this.code } } };
|
||||
let result = await this.$getapi([found]);
|
||||
let data = result[0].data.rows;
|
||||
if (data.length > 0) {
|
||||
this.userAuth = data[0];
|
||||
if (this.userAuth.expiry) {
|
||||
this.message = 'Bạn đã xác thực tài khoản thành công.';
|
||||
this.success = true;
|
||||
this.action = { name: 'signin', to: { path: '/signin' }, text: 'Đi tới trang đăng nhập' };
|
||||
} else {
|
||||
let found = { name: 'user', url: 'data/User', params: { filter: { id: this.userAuth.user } } };
|
||||
result = await this.$getapi([found]);
|
||||
let user = result[0].data.rows[0];
|
||||
user.auth_status = 2;
|
||||
user.update_time = new Date();
|
||||
result = await this.$updateapi('user', user);
|
||||
this.processAuth(result);
|
||||
}
|
||||
} else this.errors.push({ name: 'code', text: 'mã xác thực không hợp lệ' });
|
||||
},
|
||||
async processAuth(newVal) {
|
||||
if (newVal !== 'error') {
|
||||
this.message = 'Xác thực tài khoản thành công.';
|
||||
this.success = true;
|
||||
this.action = { name: 'signin', to: { path: '/signin' }, text: 'Đi tới trang đăng nhập' };
|
||||
this.userAuth.expiry = true;
|
||||
this.userAuth.update_time = new Date();
|
||||
let result = await this.$updateapi('userauth', this.userAuth);
|
||||
} else if (newVal === false) {
|
||||
this.message = 'Có lỗi xẩy ra. Xác thực tài khoản thành công.';
|
||||
this.success = false;
|
||||
}
|
||||
},
|
||||
async sendCode() {
|
||||
let code = this.$id();
|
||||
let data = { user: this.$route.query.id, code: code };
|
||||
let result = await this.$insertapi('userauth', data);
|
||||
let query = this.$store.state.link ? { code: code, link: this.$store.state.link } : { code: code };
|
||||
let routeData = this.$router.resolve({ path: '/account/auth', query: query });
|
||||
let path = window.location.origin + routeData.href;
|
||||
let conn = this.$findapi('notiform');
|
||||
conn.params.filter = { code: 'account-auth' };
|
||||
result = await this.$getapi([conn]);
|
||||
let msg = result[0].data.rows[0].detail;
|
||||
msg = msg.replace(' [1]', '');
|
||||
msg = msg.replace('[2]', code);
|
||||
msg = msg.replace('[3]', path);
|
||||
data = { subject: 'Xác thực tài khoản BigDataTechCloud', content: msg, to: this.$route.query.email, sender: 2 };
|
||||
this.loading = true;
|
||||
result = await this.$insertapi('sendemailnow', data);
|
||||
let text = `Hãy mở email <b>${this.$route.query.email}</b> để nhận mã xác thực`;
|
||||
this.$dialog(text, 'Mã xác thực', undefined, 10);
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
194
pages/account/recovery.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div class="columns is-centered mt-6 mx-0">
|
||||
<div class="column is-5">
|
||||
<div class="mb-5">
|
||||
<Logo></Logo>
|
||||
</div>
|
||||
<section class="hero">
|
||||
<div class="hero-body px-3 pt-3">
|
||||
<template v-if="!action">
|
||||
<article class="message is-primary">
|
||||
<div class="message-body has-background-white py-1">
|
||||
<strong> Để lấy lại mật khẩu </strong> vui lòng nhập email và mã kiểm tra. Nếu thông tin là hợp lệ chúng
|
||||
tôi sẽ gửi cho bạn một email chứa đường link để thay đổi mật khẩu.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="field is-horizontal mt-2">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label has-text-dark">Nhập email</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input is-primary"
|
||||
type="text"
|
||||
placeholder="Email đã dùng để mở tài khoản"
|
||||
v-model="email"
|
||||
ref="inputcode"
|
||||
@change="checkInfo()"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'email')">
|
||||
{{ errors.find((v) => v.name === 'email').text }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<label class="label has-text-dark"> Mã kiểm tra : {{ refcode }} </label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input is-primary"
|
||||
type="text"
|
||||
:placeholder="'Nhập ' + refcode + ' vào đây'"
|
||||
v-model="code"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'code')">
|
||||
{{ errors.find((v) => v.name === 'code').text }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field mt-4" v-if="isPhone">
|
||||
<label class="label has-text-dark"> Vui lòng cung cấp email để nhận link </label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input is-primary"
|
||||
type="text"
|
||||
placeholder="Nhập email"
|
||||
v-model="email"
|
||||
@change="checkEmail()"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger mt5 fs13" v-if="errors.find((v) => v.name === 'email')">
|
||||
{{ errors.find((v) => v.name === 'email').text }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field mt-5">
|
||||
<p class="control">
|
||||
<a class="button is-primary" :class="loading ? 'is-loading' : ''" @click="getPassword()">
|
||||
Lấy lại mật khẩu</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<article class="message" :class="success ? 'is-primary' : 'is-danger'" v-if="success !== undefined">
|
||||
<div class="message-body fs18 has-background-white py-2" v-html="message"></div>
|
||||
</article>
|
||||
<div class="field mt-5">
|
||||
<p class="control">
|
||||
<nuxt-link class="button is-primary" :to="action.to">
|
||||
{{ action.text }}
|
||||
</nuxt-link>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
code: undefined,
|
||||
errors: [],
|
||||
user: undefined,
|
||||
message: undefined,
|
||||
success: undefined,
|
||||
action: undefined,
|
||||
code: undefined,
|
||||
refcode: undefined,
|
||||
username: undefined,
|
||||
isPhone: false,
|
||||
email: undefined,
|
||||
authcode: undefined,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.refcode = this.$id().substring(0, 4);
|
||||
if (this.$refs.inputcode) this.$refs.inputcode.focus();
|
||||
window.addEventListener('keyup', (ev) => (ev.key === 'Enter' && !this.success ? this.getPassword() : false));
|
||||
},
|
||||
methods: {
|
||||
checkInfo() {
|
||||
this.errors = [];
|
||||
if (!this.$empty(this.username)) this.username = this.username.trim().toLowerCase();
|
||||
let result = this.$errEmail(this.username);
|
||||
if (result) this.errors.push({ name: 'username', text: 'Email không hợp lệ' });
|
||||
else this.isPhone = this.username.indexOf('@') >= 0 ? false : true;
|
||||
},
|
||||
checkEmail() {
|
||||
this.errors = [];
|
||||
let result = this.$errEmail(this.email);
|
||||
if (result) this.errors.push({ name: 'email', text: 'Email không hợp lệ' });
|
||||
},
|
||||
async getPassword() {
|
||||
this.success = undefined;
|
||||
this.message = undefined;
|
||||
this.action = undefined;
|
||||
this.errors = [];
|
||||
let result = this.$errEmail(this.username);
|
||||
if (result) this.errors.push({ name: 'username', text: 'Email không hợp lệ' });
|
||||
if (this.$empty(this.code)) this.errors.push({ name: 'code', text: 'Chưa nhập mã kiểm tra' });
|
||||
else if (this.refcode !== this.code) this.errors.push({ name: 'code', text: 'Mã kiểm tra không đúng' });
|
||||
if (this.errors.length > 0) return;
|
||||
let found = {
|
||||
name: 'user',
|
||||
url: 'data/User/',
|
||||
params: {
|
||||
filter: {
|
||||
email: this.username,
|
||||
},
|
||||
},
|
||||
};
|
||||
result = await this.$getapi([found]);
|
||||
console.log('===>', result);
|
||||
|
||||
let data = result[0].data.rows;
|
||||
if (data.length > 0) {
|
||||
this.user = data[0];
|
||||
this.authcode = this.$id();
|
||||
let ele = { user: this.user.id, code: this.authcode };
|
||||
result = await this.$insertapi('accountrecovery', ele);
|
||||
|
||||
console.log('=====>', result);
|
||||
|
||||
this.sendEmail(result);
|
||||
} else {
|
||||
this.errors.push({ name: 'username', text: 'Tài khoản không tồn tại' });
|
||||
}
|
||||
},
|
||||
async sendEmail(data) {
|
||||
let query = { id: this.user.id, code: this.authcode };
|
||||
if (this.$store.state.link) query.link = this.$store.state.link;
|
||||
let routeData = this.$router.resolve({ path: '/get-password', query: query });
|
||||
let path = window.location.origin + routeData.href;
|
||||
let conn = this.$findapi('notiform');
|
||||
console.log('conn =====>', conn);
|
||||
|
||||
conn.params.filter = { code: 'get-password' };
|
||||
let result = await this.$getapi([conn]);
|
||||
let msg = result[0].data.rows[0].detail;
|
||||
msg = msg.replace('[1]', this.user.fullname);
|
||||
msg = msg.replace('[3]', path);
|
||||
data = {
|
||||
subject: 'Phục hồi tài khoản BigDataTech.vn',
|
||||
content: msg,
|
||||
to: this.email ? this.email : this.username,
|
||||
sender: 2,
|
||||
};
|
||||
this.loading = true;
|
||||
result = await this.$insertapi('sendemailnow', data);
|
||||
this.message = `<b>Thành công</b>. Hãy mở email <b>${this.username}</b> để lấy lại mật khẩu.`;
|
||||
this.success = true;
|
||||
this.action = { name: 'signin', to: { path: '/signin' }, text: 'Đi tới trang đăng nhập' };
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
8
pages/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template></template>
|
||||
<script>
|
||||
export default {
|
||||
created() {
|
||||
this.$router.push('/signin');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
22
pages/notice.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="columns is-centered mt-6 mx-3">
|
||||
<div class="column is-6">
|
||||
<div class="mb-5">
|
||||
<Logo></Logo>
|
||||
</div>
|
||||
<section class="hero">
|
||||
<div class="hero-body px-3 pt-3">
|
||||
<article class="message is-primary">
|
||||
<div class="message-body fs-16 has-background-white">
|
||||
<p><strong>Website của chúng tôi đang trong quá trình xây dựng. Vui lòng trở lại sau</strong></p>
|
||||
<p class="mt-4">Xin cảm ơn.</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
146
pages/policy.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="column user-policy is-4 p-0" style="margin: auto">
|
||||
<div class="user-part_head">
|
||||
<div class="user-part_head-btn" @click="getback()">
|
||||
<i class="mdi mdi-chevron-left"></i>
|
||||
</div>
|
||||
<div class="user-part_head-text">
|
||||
<p>Điều khoản và dịch vụ</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="policy_body px-3">
|
||||
<div class="policy_body-title1">
|
||||
<p>CÁC ĐIỀU KHOẢN DỊCH VỤ BÊN DƯỚI ĐÂY CÓ HIỆU LỰC CHO CẢ KHÁCH HÀNG CHÍNH THỨC VÀ KHÁCH HÀNG DÙNG THỬ</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Nội dung Điều khoản và Chính sách đang được cập nhật để đảm bảo tính đầy đủ và chính xác. Chúng tôi sẽ sớm hoàn
|
||||
thiện trong thời gian tới.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
getback() {
|
||||
this.$router.push('/signup');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.user-policy_body {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.user-policy_body-title {
|
||||
margin-top: 40px;
|
||||
padding-bottom: 10px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #09b412;
|
||||
position: relative;
|
||||
}
|
||||
.user-policy_body-title::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #eee;
|
||||
bottom: 0px;
|
||||
}
|
||||
.user-pocily_body-content {
|
||||
padding-top: 40px;
|
||||
}
|
||||
.user-pocily_body-content-1 {
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.user-pocily_body-content-2 {
|
||||
font-weight: 600;
|
||||
}
|
||||
.user-pocily_body-content-3 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.user-pocily_body-content-4 {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.policy {
|
||||
margin: auto;
|
||||
}
|
||||
.policy_header {
|
||||
font-weight: 700;
|
||||
font-size: 40px;
|
||||
color: #09b412;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.policy_body {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.policy_body-title1 {
|
||||
text-align: center;
|
||||
margin: 10px 10px 0;
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
}
|
||||
.policy_body-title2 {
|
||||
font-weight: 600;
|
||||
margin: 50px 0 20px;
|
||||
}
|
||||
.policy_body-sub span {
|
||||
font-weight: 600;
|
||||
}
|
||||
.policy_body-sub p {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* ____________________________user_______________________________ */
|
||||
.user-part_head {
|
||||
height: 70px;
|
||||
position: relative;
|
||||
background-color: rgba(9, 180, 18, 1);
|
||||
margin-bottom: 16px;
|
||||
border-bottom-right-radius: 20px;
|
||||
border-bottom-left-radius: 20px;
|
||||
}
|
||||
.user-part_head-btn {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 12px;
|
||||
left: 30px;
|
||||
color: #fdfdfd;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.user-part_head-btn:hover::after {
|
||||
transform: scale(1.2);
|
||||
transition-duration: 0.4s;
|
||||
}
|
||||
.user-part_head-btn::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0px;
|
||||
top: 0;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 50%;
|
||||
transform: scale(0);
|
||||
transition-duration: 0.4s;
|
||||
}
|
||||
.user-part_head-btn:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.user-part_head-text {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
color: #fdfdfd;
|
||||
}
|
||||
</style>
|
||||
625
pages/signin.vue
Normal file
@@ -0,0 +1,625 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="columns is-centered px-0 mx-0 my-0">
|
||||
<div :class="`column wrapper-login is-4-desktop is-6-tablet`">
|
||||
<div class="px-6 py-6 login-box">
|
||||
<div class="login-header">
|
||||
<Logo />
|
||||
<h2 class="title mt-4 has-text-centered">{{ isVietnamese ? 'Đăng nhập' : 'Login' }}</h2>
|
||||
</div>
|
||||
<div class="login-body">
|
||||
<div class="form-login">
|
||||
<div class="field mt-5">
|
||||
<label class="label">Email<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="email"
|
||||
placeholder="Nhập email"
|
||||
v-model="email"
|
||||
@blur="validateEmail(email)"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.find((v) => v.name === 'email')"
|
||||
v-html="errors.find((v) => v.name === 'email').text"
|
||||
></p>
|
||||
</div>
|
||||
<div class="field mt-5">
|
||||
<label class="label">Mật khẩu<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="field-body">
|
||||
<div class="field has-addons">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
:type="showpass ? 'text' : 'password'"
|
||||
placeholder="Nhập mật khẩu"
|
||||
v-model="password"
|
||||
@blur="validatePassword(password)"
|
||||
/>
|
||||
</p>
|
||||
<div class="control">
|
||||
<a class="button" @click="showpass = !showpass">
|
||||
<span class="icon">
|
||||
<i
|
||||
:class="
|
||||
showpass
|
||||
? 'mdi mdi-eye-outline has-text-dark fs22'
|
||||
: 'mdi mdi-eye-off-outline has-text-dark fs22'
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'password')">
|
||||
{{ errors.find((v) => v.name === 'password').text }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field mt-5 pt-1 group-action">
|
||||
<button class="button is-primary" @click="signin()">Đăng nhập</button>
|
||||
<a class="is-primary" @click="accountRecovery()">Quên mật khẩu?</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="social-login">
|
||||
<div class="social-header my-4">
|
||||
<span>Hoặc</span>
|
||||
</div>
|
||||
<div class="social-body">
|
||||
<div class="social-item">
|
||||
<GoogleLogin class="button" :onSuccess="onSuccess" :params="params">
|
||||
<img class="icon" src="/icons/google.svg" alt="Google" />
|
||||
<span>Đăng nhập với Google</span>
|
||||
</GoogleLogin>
|
||||
</div>
|
||||
<div class="social-item">
|
||||
<button class="button" @click="logInWithFacebook()">
|
||||
<img class="icon" src="/icons/facebook.svg" alt="Facebook" />
|
||||
<span> Đăng nhập với Facebook </span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-5 has-text-centered">
|
||||
Chưa có tài khoản?
|
||||
<nuxt-link class="ml-2" to="/signup">Tạo tài khoản</nuxt-link>
|
||||
</p>
|
||||
|
||||
<div class="info-support mt-4">
|
||||
<div class="support-header">
|
||||
<h5 class="title">Cần hỗ trợ</h5>
|
||||
</div>
|
||||
<div class="support-body">
|
||||
<p class="support-item">
|
||||
<a :href="`tel:${company.phone.support}`" target="_blank">
|
||||
<img src="/icons/phone-call.svg" alt="Support" width="24" height="24" />
|
||||
<span>{{ company.phone.support }}</span>
|
||||
</a>
|
||||
</p>
|
||||
<p class="support-item">
|
||||
<a :href="`mailto:${company.email.support}`">
|
||||
<img src="/icons/email.svg" alt="" width="24" height="24" />
|
||||
<span>{{ company.email.support }}</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
<img src="/icons/shield.svg" alt="" width="24" />
|
||||
|
||||
<span> {{ new Date().getFullYear() }} </span>
|
||||
<span v-if="isVietnamese">
|
||||
Bản quyền thuộc về
|
||||
<a
|
||||
:href="company.parent.website"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:title="company.parent.slogan"
|
||||
>{{ company.parent.name }}</a
|
||||
>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
Copyright by
|
||||
<a
|
||||
:href="company.parent.website"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:title="company.parent.sloganEn"
|
||||
>{{ company.parent.name }}</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Bowser from 'bowser';
|
||||
import GoogleLogin from 'vue-google-login';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GoogleLogin,
|
||||
},
|
||||
head() {
|
||||
return { title: `${this.isVietnamese ? 'Đăng nhập' : 'Login'} - ${this.company.name}` };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fullname: undefined,
|
||||
username: undefined,
|
||||
email: undefined,
|
||||
password: undefined,
|
||||
errors: [],
|
||||
showpass: false,
|
||||
registers: [],
|
||||
params: { client_id: '389310749372-mnfsd09ukgl0ahbdrm94dsepremlfgs9.apps.googleusercontent.com' },
|
||||
type: undefined,
|
||||
isLoaded: false,
|
||||
data: undefined,
|
||||
account: undefined,
|
||||
company: this.$companyInfo(),
|
||||
isVietnamese: true,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keyup', (ev) =>
|
||||
ev.key === 'Enter' && this.$route.name === 'signup' ? this.signin() : false,
|
||||
);
|
||||
},
|
||||
watch: {
|
||||
isLoaded: function (newVal) {
|
||||
if (this.isLoaded) {
|
||||
let self = this;
|
||||
window.FB.login(
|
||||
function (response) {
|
||||
if (response.authResponse) {
|
||||
let userId = response.authResponse.userID;
|
||||
self.getUserInfo(userId);
|
||||
}
|
||||
},
|
||||
{
|
||||
scope: 'public_profile,email',
|
||||
return_scopes: true,
|
||||
auth_type: 'rerequest',
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
login: {
|
||||
get: function () {
|
||||
return this.$store.state.login;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateLogin', { login: val });
|
||||
},
|
||||
},
|
||||
registermethod: {
|
||||
get: function () {
|
||||
return this.$store.state.registermethod;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateRegisterMethod', { registermethod: val });
|
||||
},
|
||||
},
|
||||
authmethod: {
|
||||
get: function () {
|
||||
return this.$store.state.authmethod;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateAuthMethod', { authmethod: val });
|
||||
},
|
||||
},
|
||||
authstatus: {
|
||||
get: function () {
|
||||
return this.$store.state.authstatus;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateAuthStatus', { authstatus: val });
|
||||
},
|
||||
},
|
||||
usertype: {
|
||||
get: function () {
|
||||
return this.$store.state.usertype;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateUserType', { usertype: val });
|
||||
},
|
||||
},
|
||||
viewport: {
|
||||
get: function () {
|
||||
return this.$store.state.viewport;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateViewPort', { viewport: val });
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
checkError() {
|
||||
this.errors = [];
|
||||
if (!this.$empty(this.email)) {
|
||||
this.email = this.email.trim().toLowerCase();
|
||||
}
|
||||
|
||||
this.validateEmail(this.email);
|
||||
this.validatePassword(this.password);
|
||||
|
||||
return this.errors.length > 0 ? true : false;
|
||||
},
|
||||
|
||||
validateEmail(email) {
|
||||
// Xóa lỗi cũ
|
||||
this.errors = this.errors.filter((err) => err.name !== 'email');
|
||||
|
||||
if (this.$empty(email)) {
|
||||
this.errors.push({
|
||||
name: 'email',
|
||||
text: 'Email không được để trống.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.$regexEmail(String(email).toLowerCase())) {
|
||||
this.errors.push({
|
||||
name: 'email',
|
||||
text: 'Email không đúng định dạng.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validatePassword(password) {
|
||||
// Xóa lỗi cũ
|
||||
this.errors = this.errors.filter((err) => err.name !== 'password');
|
||||
|
||||
if (this.$empty(password)) {
|
||||
this.errors.push({
|
||||
name: 'password',
|
||||
text: 'Mật khẩu không được để trống.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.$regexPassword(String(password).toLowerCase())) {
|
||||
this.errors.push({
|
||||
name: 'password',
|
||||
text: 'Mật khẩu không đúng định dạng.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
async signin() {
|
||||
if (this.checkError()) return;
|
||||
let conn = this.$findapi('login');
|
||||
conn.params.filter = { email: this.email, password: this.password };
|
||||
let result = await this.$getapi([conn]);
|
||||
let data = result?.find((v) => v.name === 'login').data.rows;
|
||||
this.account = data;
|
||||
this.fillData(data);
|
||||
},
|
||||
onSuccess(googleUser) {
|
||||
let info = googleUser.getBasicProfile();
|
||||
|
||||
let keys = Object.keys(info);
|
||||
this.email = info[keys[5]];
|
||||
this.fullname = info[keys[1]];
|
||||
this.username = info[keys[0]];
|
||||
this.type = 'google';
|
||||
if (!this.$empty(this.email)) this.checkAccount();
|
||||
},
|
||||
async checkAccount() {
|
||||
if (this.$empty(this.email)) return;
|
||||
let found = this.$findapi('user');
|
||||
found.params = { filter: { email: this.email } };
|
||||
let result = await this.$getapi([found]);
|
||||
let data = result.find((v) => v.name === 'user').data.rows;
|
||||
if (data.length > 0) this.fillData(data[0]);
|
||||
else {
|
||||
let rs = await this.$insertapi('gethash', { text: this.$id() });
|
||||
let hash = rs.rows[0];
|
||||
let registerMethod = this.registermethod.find((v) => v.code === 'create-account').id;
|
||||
if (this.type) registerMethod = this.registermethod.find((v) => v.code === this.type).id;
|
||||
data = {
|
||||
fullname: this.fullname,
|
||||
username: this.username ?? this.email,
|
||||
email: this.email,
|
||||
password: hash,
|
||||
type: this.usertype.find((v) => v.code === 'customer').id,
|
||||
register_method: registerMethod,
|
||||
auth_status: this.authstatus.find((v) => v.code === 'auth').id,
|
||||
auth_method:
|
||||
this.email.indexOf('@') >= 0
|
||||
? this.authmethod.find((v) => v.code === 'email').id
|
||||
: this.authmethod.find((v) => v.code === 'phone').id,
|
||||
display_name: this.fullname,
|
||||
};
|
||||
|
||||
let res = await this.$insertapi('user', data);
|
||||
if (res !== 'error') {
|
||||
this.login = res;
|
||||
this.redirectUrl();
|
||||
}
|
||||
}
|
||||
},
|
||||
invalidLogin(data) {
|
||||
console.log('login', data);
|
||||
|
||||
if (!data) {
|
||||
const text = 'Tài khoản hoặc mật khẩu không chính xác';
|
||||
this.errors.push({ name: 'email', text: text });
|
||||
this.errors.push({ name: 'password', text: text });
|
||||
} else if (data.blocked) {
|
||||
this.errors.push({ name: 'email', text: 'Tài khoản đang bị khóa. Đăng nhập không thành công' });
|
||||
} else if (data.auth_status === 1) {
|
||||
this.errors.push({
|
||||
name: 'email',
|
||||
text: `Tài khoản đang chờ xác thực. <a @click="$emit('resend')">Gửi lại mã</a>`,
|
||||
});
|
||||
}
|
||||
return this.errors.length > 0 ? true : false;
|
||||
},
|
||||
async fillData(data) {
|
||||
if (this.invalidLogin(data)) return;
|
||||
this.login = data; //store login
|
||||
if (data.type === 1 || data.type__code === 'customer') {
|
||||
this.redirectUrl();
|
||||
} else if (this.$store.state.link) {
|
||||
let ele = this.$copy(data);
|
||||
if (
|
||||
this.$store.state.link.indexOf('bigdatatech.vn') < 0 &&
|
||||
this.$store.state.link.indexOf('localhost:3000') < 0
|
||||
) {
|
||||
return this.$redirectWeb(ele);
|
||||
}
|
||||
//get token & redirect link
|
||||
const browser = Bowser.getParser(window.navigator.userAgent);
|
||||
let obj = {
|
||||
browser: browser.getBrowserName(),
|
||||
browser_version: browser.getBrowserVersion(),
|
||||
platform: browser.getPlatform().type,
|
||||
os: browser.getOSName(),
|
||||
user: data.id,
|
||||
token: this.$id(),
|
||||
};
|
||||
ele.token = obj.token;
|
||||
await this.$insertapi('authtoken', obj);
|
||||
//await this.sendNoti(obj)
|
||||
this.$redirectWeb(ele);
|
||||
} else this.redirectUrl();
|
||||
},
|
||||
async sendNoti(obj) {
|
||||
let found = this.$findapi('notiform');
|
||||
found.params.filter = { code: 'login-alert' };
|
||||
const result = await this.$getapi([found]);
|
||||
let data = result[0].data.rows[0];
|
||||
let content = data.detail;
|
||||
content = content.replace('[1]', this.email);
|
||||
content = content.replace('[2]', obj.browser);
|
||||
content = content.replace('[3]', obj.browser_version);
|
||||
content = content.replace('[4]', obj.platform);
|
||||
content = content.replace('[5]', obj.os);
|
||||
content = content.replace('[6]', this.$dayjs().format('DD/MM/YYYY HH:mm'));
|
||||
let ele = { title: data.name, content: content, user: [obj.user.toString()], type: 1 };
|
||||
await this.$insertapi('notification', ele);
|
||||
},
|
||||
async logInWithFacebook() {
|
||||
if (window.FB) {
|
||||
let self = this;
|
||||
window.FB.login(
|
||||
function (response) {
|
||||
if (response.authResponse) {
|
||||
let userId = response.authResponse.userID;
|
||||
self.getUserInfo(userId);
|
||||
}
|
||||
},
|
||||
{
|
||||
scope: 'public_profile,email',
|
||||
return_scopes: true,
|
||||
auth_type: 'rerequest',
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await this.loadFacebookSDK(document, 'script', 'facebook-jssdk');
|
||||
await this.initFacebook();
|
||||
}
|
||||
},
|
||||
async initFacebook() {
|
||||
let self = this;
|
||||
window.fbAsyncInit = function () {
|
||||
window.FB.init({
|
||||
appId: '314849628253331',
|
||||
cookie: true,
|
||||
xfbml: true,
|
||||
version: 'v8.0',
|
||||
});
|
||||
self.isLoaded = true;
|
||||
};
|
||||
},
|
||||
async loadFacebookSDK(d, s, id) {
|
||||
var js,
|
||||
fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
js = d.createElement(s);
|
||||
js.id = id;
|
||||
js.src = 'https://connect.facebook.net/en_US/sdk.js';
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
},
|
||||
getUserInfo(userId) {
|
||||
let self = this;
|
||||
window.FB.api('/me?fields=id,name,email', function (response) {
|
||||
self.email = response.email;
|
||||
self.fullname = response.name;
|
||||
self.type = 'facebook';
|
||||
self.checkAccount();
|
||||
});
|
||||
},
|
||||
accountRecovery() {
|
||||
this.$router.push({ path: '/account/recovery' });
|
||||
},
|
||||
// async resendAuthcode() {
|
||||
// let code = this.$id();
|
||||
// let result = await this.$insertapi('userauth', { user: this.account.id, code: code });
|
||||
// let query = this.$store.state.link ? { link: this.$store.state.link } : {};
|
||||
// query.id = this.account.id;
|
||||
// query.email = this.email;
|
||||
// query.action = 'sendcode';
|
||||
// this.$router.push({ path: '/account/auth', query: query });
|
||||
// },
|
||||
async redirectUrl() {
|
||||
this.$router.push(this.$route.query.href || '/welcome');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.login-page {
|
||||
background-color: #f0fdf4;
|
||||
min-height: 100vh;
|
||||
align-content: center;
|
||||
|
||||
button.is-primary {
|
||||
background-color: #53b147;
|
||||
box-shadow: 0 4px 12px rgba(83, 177, 71, 0.25);
|
||||
&:hover {
|
||||
background: #45963a;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-login {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #bbf7d0;
|
||||
background-color: #fff;
|
||||
padding: 0;
|
||||
// .login-header {
|
||||
// }
|
||||
.login-body {
|
||||
.form-login {
|
||||
.group-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
.social-login {
|
||||
.social-header {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #ddd;
|
||||
}
|
||||
span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: #fff;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
.social-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.social-item {
|
||||
flex: 1;
|
||||
button {
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
&:hover {
|
||||
background-color: #bbf7d0;
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-support {
|
||||
background-color: #fff;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.support-header {
|
||||
background-color: #f0fdf4;
|
||||
text-align: center;
|
||||
padding: 14px 0;
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.support-body {
|
||||
display: flex;
|
||||
padding: 16px 0;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
.support-item {
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: #374151;
|
||||
&:hover {
|
||||
color: #16a34a;
|
||||
img {
|
||||
filter: brightness(0) saturate(100%) invert(40%) sepia(80%) saturate(400%) hue-rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: 24px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
696
pages/signup.vue
Normal file
@@ -0,0 +1,696 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="columns is-centered px-0 mx-0 my-0">
|
||||
<div :class="`column wrapper-login is-${viewport >= 4 ? 4 : 6}`">
|
||||
<div class="px-6 py-6 login-box">
|
||||
<div class="login-header">
|
||||
<Logo />
|
||||
<h2 class="title mt-4 has-text-centered">
|
||||
{{ isVietnamese ? 'Đăng ký tài khoản' : 'Register an account' }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="login-body">
|
||||
<div class="form-login">
|
||||
<div class="field mt-5 pt-1">
|
||||
<label class="label">Họ và tên<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="fullname"
|
||||
@blur="validateFullName(fullname)"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'fullname')">
|
||||
{{ errors.find((v) => v.name === 'fullname').text }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field mt-4">
|
||||
<label class="label">Điện thoại</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="phone" @blur="validatePhone(phone)" />
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'phone')">
|
||||
{{ errors.find((v) => v.name === 'phone').text }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field mt-4">
|
||||
<label class="label">Email<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input class="input" type="email" placeholder="" v-model="email" @blur="validateEmail(email)" />
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'email')">
|
||||
{{ errors.find((v) => v.name === 'email').text }}
|
||||
</p>
|
||||
<p class="help is-primary" v-else-if="info">{{ info }}</p>
|
||||
</div>
|
||||
<div class="field mt-4">
|
||||
<label class="label">Mật khẩu<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="field-body">
|
||||
<div class="field has-addons">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
:type="showpass ? 'text' : 'password'"
|
||||
placeholder="Mật khẩu phải có ít nhất 8 ký tự, bao gồm chữ hoa, chữ thường, số và ký tự đặc biệt."
|
||||
v-model="password"
|
||||
@blur="validatePassword(password)"
|
||||
/>
|
||||
</p>
|
||||
<div class="control">
|
||||
<a class="button" @click="showpass = !showpass">
|
||||
<span class="icon">
|
||||
<i
|
||||
:class="
|
||||
showpass
|
||||
? 'mdi mdi-eye-outline has-text-dark fs22'
|
||||
: 'mdi mdi-eye-off-outline has-text-dark fs22'
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'password')">
|
||||
{{ errors.find((v) => v.name === 'password').text }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field mt-4">
|
||||
<label class="label">Nhập lại mật khẩu<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
:type="showpass ? 'text' : 'password'"
|
||||
placeholder=""
|
||||
v-model="retypePassword"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'retypePassword')">
|
||||
{{ errors.find((v) => v.name === 'retypePassword').text }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field mt-4">
|
||||
<div class="control">
|
||||
<b-checkbox v-model="checkbox"
|
||||
>Tôi đồng ý với <a @click="$router.push('/policy')">các điều khoản và điều kiện</a></b-checkbox
|
||||
>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'checkbox')">
|
||||
{{ errors.find((v) => v.name === 'checkbox').text }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field mt-5 pt-1 group-action">
|
||||
<button class="button is-primary" @click="createAccount()">Tạo tài khoản</button>
|
||||
<!-- <a class="ml-2" @click="$router.push('/signin')">Đăng nhập</a> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="social-login">
|
||||
<div class="social-header my-4">
|
||||
<span>Hoặc</span>
|
||||
</div>
|
||||
<div class="social-body">
|
||||
<div class="social-item">
|
||||
<GoogleLogin class="button" :onSuccess="onSuccess" :params="params">
|
||||
<img class="icon" src="/icons/google.svg" alt="Google" />
|
||||
<span>Đăng ký với Google</span>
|
||||
</GoogleLogin>
|
||||
</div>
|
||||
<div class="social-item">
|
||||
<button class="button" @click="logInWithFacebook()">
|
||||
<img class="icon" src="/icons/facebook.svg" alt="Facebook" />
|
||||
<span> Đăng ký với Facebook </span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-5 has-text-centered">
|
||||
Bạn đã có tài khoản?
|
||||
<nuxt-link class="ml-2" to="/signin">Đăng nhập</nuxt-link>
|
||||
</p>
|
||||
|
||||
<div class="info-support mt-4">
|
||||
<div class="support-header">
|
||||
<h5 class="title">Cần hỗ trợ</h5>
|
||||
</div>
|
||||
<div class="support-body">
|
||||
<p class="support-item">
|
||||
<a :href="`tel:${company.phone.support}`" target="_blank">
|
||||
<img src="/icons/phone-call.svg" alt="Support" width="24" height="24" />
|
||||
<span>{{ company.phone.support }}</span>
|
||||
</a>
|
||||
</p>
|
||||
<p class="support-item">
|
||||
<a :href="`mailto:${company.email.support}`">
|
||||
<img src="/icons/email.svg" alt="" width="24" height="24" />
|
||||
<span>{{ company.email.support }}</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
<img src="/icons/shield.svg" alt="" width="24" />
|
||||
|
||||
<span> {{ new Date().getFullYear() }} </span>
|
||||
<span v-if="isVietnamese">
|
||||
Bản quyền thuộc về
|
||||
<a
|
||||
:href="company.parent.website"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:title="company.parent.slogan"
|
||||
>{{ company.parent.name }}</a
|
||||
>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
Copyright by
|
||||
<a
|
||||
:href="company.parent.website"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:title="company.parent.sloganEn"
|
||||
>{{ company.parent.name }}</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GoogleLogin from 'vue-google-login';
|
||||
export default {
|
||||
components: { GoogleLogin },
|
||||
head() {
|
||||
return { title: `${this.isVietnamese ? 'Đăng ký tài khoản' : 'Register an account'} - ${this.company.name}` };
|
||||
},
|
||||
//
|
||||
data() {
|
||||
return {
|
||||
fullname: undefined,
|
||||
email: undefined,
|
||||
password: undefined,
|
||||
retypePassword: undefined,
|
||||
errors: [],
|
||||
info: 'Hệ thống gửi mã xác thực qua email. Sau khi tạo vui lòng kiểm tra email trong Inbox hoặc Spam',
|
||||
showpass: false,
|
||||
hash: undefined,
|
||||
checkbox: false,
|
||||
params: { client_id: '389310749372-mnfsd09ukgl0ahbdrm94dsepremlfgs9.apps.googleusercontent.com' },
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
user: undefined,
|
||||
code: undefined,
|
||||
isLoaded: false,
|
||||
show: true,
|
||||
discount: undefined,
|
||||
authEmail: undefined,
|
||||
phone: undefined,
|
||||
validTo: this.$dayjs().add(30, 'day').format('DD/MM/YYYY'),
|
||||
company: this.$companyInfo(),
|
||||
isVietnamese: true,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
window.addEventListener('keyup', (ev) =>
|
||||
ev.key === 'Enter' && this.$route.name === 'signup' ? this.createAccount() : false,
|
||||
);
|
||||
if (!this.$empty(this.$route.query.referer)) this.referer = this.$route.query.referer;
|
||||
if (this.referer) this.discount = await this.$getdata('commission', { code: 'customer' }, undefined, true);
|
||||
},
|
||||
watch: {
|
||||
isLoaded: function (newVal) {
|
||||
if (newVal) {
|
||||
let self = this;
|
||||
window.FB.login(function (response) {
|
||||
if (response.authResponse) {
|
||||
let userId = response.authResponse.userID;
|
||||
self.getUserInfo(userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
registermethod: {
|
||||
get: function () {
|
||||
return this.$store.state.registermethod;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateRegisterMethod', { registermethod: val });
|
||||
},
|
||||
},
|
||||
authmethod: {
|
||||
get: function () {
|
||||
return this.$store.state.authmethod;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateAuthMethod', { authmethod: val });
|
||||
},
|
||||
},
|
||||
authstatus: {
|
||||
get: function () {
|
||||
return this.$store.state.authstatus;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateAuthStatus', { authstatus: val });
|
||||
},
|
||||
},
|
||||
usertype: {
|
||||
get: function () {
|
||||
return this.$store.state.usertype;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateUserType', { usertype: val });
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
get: function () {
|
||||
return this.$store.state['dialog'];
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateStore', { name: 'dialog', data: val });
|
||||
},
|
||||
},
|
||||
viewport: {
|
||||
get: function () {
|
||||
return this.$store.state.viewport;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateViewPort', { viewport: val });
|
||||
},
|
||||
},
|
||||
referer: {
|
||||
get: function () {
|
||||
return this.$store.state['referer'];
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit('updateStore', { name: 'referer', data: val });
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
checkError() {
|
||||
this.errors = [];
|
||||
if (!this.$empty(this.fullname)) {
|
||||
this.fullname = this.fullname.trim();
|
||||
}
|
||||
if (!this.$empty(this.email)) {
|
||||
this.email = this.email.trim().toLowerCase();
|
||||
}
|
||||
// if (this.$empty(this.fullname)) {
|
||||
// this.errors.push({ name: 'fullname', text: 'Họ và tên không được bỏ trống.' });
|
||||
// } else if (this.fullname.length < 5) {
|
||||
// this.errors.push({ name: 'fullname', text: 'Họ và tên quá ngắn. Yêu cầu từ 5 kí tự trở nên.' });
|
||||
// }
|
||||
// if (!this.$empty(this.phone)) {
|
||||
// if (!this.$regexPhone(this.phone)) {
|
||||
// this.errors.push({ name: 'phone', text: 'Số điện thoại không hợp lệ.' });
|
||||
// }
|
||||
// }
|
||||
this.validateFullName(this.fullname);
|
||||
this.validatePhone(this.phone);
|
||||
this.validateEmail(this.email);
|
||||
this.validatePassword(this.password);
|
||||
|
||||
if (this.$empty(this.retypePassword)) {
|
||||
this.errors.push({ name: 'retypePassword', text: 'Nhắc lại mật khẩu không được bỏ trống.' });
|
||||
} else if (this.password !== this.retypePassword) {
|
||||
this.errors.push({ name: 'retypePassword', text: 'Nhắc lại mật khẩu phải giống với mật khẩu đã nhập.' });
|
||||
}
|
||||
if (!this.checkbox)
|
||||
this.errors.push({ name: 'checkbox', text: 'Bạn chưa chọn đồng ý với điều khoản và điều kiện của chúng tôi.' });
|
||||
return this.errors.length > 0 ? true : false;
|
||||
},
|
||||
|
||||
validateFullName(fullName) {
|
||||
// Xóa lỗi cũ
|
||||
this.errors = this.errors.filter((err) => err.name !== 'fullname');
|
||||
|
||||
if (this.$empty(fullName)) {
|
||||
this.errors.push({
|
||||
name: 'fullname',
|
||||
text: 'Họ và tên không được để trống.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fullName.length < 5) {
|
||||
this.errors.push({
|
||||
name: 'fullname',
|
||||
text: 'Họ và tên quá ngắn. Yêu cầu từ 5 kí tự trở nên.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validateEmail(email) {
|
||||
// Xóa lỗi cũ
|
||||
this.errors = this.errors.filter((err) => err.name !== 'email');
|
||||
|
||||
if (this.$empty(email)) {
|
||||
this.errors.push({
|
||||
name: 'email',
|
||||
text: 'Email không được để trống.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.$regexEmail(String(email).toLowerCase())) {
|
||||
this.errors.push({
|
||||
name: 'email',
|
||||
text: 'Email không đúng định dạng.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validatePassword(password) {
|
||||
// Xóa lỗi cũ
|
||||
this.errors = this.errors.filter((err) => err.name !== 'password');
|
||||
|
||||
if (this.$empty(password)) {
|
||||
this.errors.push({
|
||||
name: 'password',
|
||||
text: 'Mật khẩu không được để trống.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.$regexPassword(String(password).toLowerCase())) {
|
||||
this.errors.push({
|
||||
name: 'password',
|
||||
text: 'Mật khẩu không đúng định dạng.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validatePhone(phone) {
|
||||
// Xóa lỗi cũ
|
||||
this.errors = this.errors.filter((err) => err.name !== 'phone');
|
||||
if (!this.$empty(phone)) {
|
||||
if (!this.$regexPhone(phone)) {
|
||||
this.errors.push({
|
||||
name: 'phone',
|
||||
text: 'Số điện thoại không hợp lệ.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
async createAccount() {
|
||||
if (this.checkError()) return;
|
||||
const userRes = await this.$getdata('user', { email: this.email });
|
||||
const customerRes = await this.$getdata('customer', { email: this.email });
|
||||
|
||||
if (userRes.length > 0 || customerRes.length > 0) {
|
||||
return this.errors.push({ name: 'email', text: 'Tài khoản đã tồn tại trong hệ thống.' });
|
||||
}
|
||||
|
||||
let rs = await this.$insertapi('gethash', { text: this.password });
|
||||
this.hash = rs.rows[0];
|
||||
let ele = this.$findapi('user');
|
||||
ele.params = { filter: { email: this.email } };
|
||||
let result = await this.$getapi([ele]);
|
||||
let data = result.find((v) => v.name === 'user').data.rows;
|
||||
if (data.length > 0) {
|
||||
return this.errors.push({ name: 'email', text: 'Tài khoản đã tồn tại trong hệ thống.' });
|
||||
}
|
||||
|
||||
let registerMethod = this.registermethod.find((v) => v.code === 'create-account').id;
|
||||
if (this.type) {
|
||||
if (this.authEmail !== this.email) this.type = undefined;
|
||||
else registerMethod = this.registermethod.find((v) => v.code === this.type).id;
|
||||
}
|
||||
this.status = this.authstatus.find((v) => v.code === 'auth');
|
||||
if (!this.type && this.email.indexOf('@') >= 0) this.status = this.authstatus.find((v) => v.code === 'wait');
|
||||
data = {
|
||||
fullname: this.fullname,
|
||||
email: this.email,
|
||||
password: this.hash,
|
||||
type: this.usertype.find((v) => v.code === 'customer').id,
|
||||
register_method: registerMethod,
|
||||
auth_status: this.status.id,
|
||||
phone: this.phone,
|
||||
auth_method:
|
||||
this.email.indexOf('@') >= 0
|
||||
? this.authmethod.find((v) => v.code === 'email').id
|
||||
: this.authmethod.find((v) => v.code === 'phone').id,
|
||||
display_name: this.fullname,
|
||||
};
|
||||
this.$route.query.type === 'employee' ? (data.type = this.usertype.find((v) => v.code === 'employee').id) : false;
|
||||
if (this.referer) data.referer = this.referer;
|
||||
|
||||
this.user = await this.$insertapi('user', data);
|
||||
const customer = await this.$insertapi('customer', { ...data, type: null });
|
||||
if (this.user !== 'error' && customer != 'error') {
|
||||
let text = 'Bạn đã đăng ký tài khoản thành công. Hệ thống tự động chuyển tới trang đăng nhập.';
|
||||
if (this.status.code === 'wait')
|
||||
text = 'Bạn đã đăng ký tài khoản thành công. Hệ thống tự động chuyển tới trang xác thực tài khoản';
|
||||
this.$dialog(text, 'Xác thực tài khoản', undefined, 5);
|
||||
}
|
||||
if (this.status.code === 'wait') {
|
||||
this.code = this.$id();
|
||||
let data = { user: this.user.id, code: this.code };
|
||||
result = await this.$insertapi('userauth', data);
|
||||
this.processAuth();
|
||||
} else this.$router.push({ path: '/signin' });
|
||||
},
|
||||
async processAuth() {
|
||||
let query = this.$store.state.link ? { code: this.code, link: this.$store.state.link } : { code: this.code };
|
||||
let routeData = this.$router.resolve({ path: '/account/auth', query: query });
|
||||
let path = window.location.origin + routeData.href;
|
||||
let conn = this.$findapi('userauth');
|
||||
conn.params.filter = { user: this.user.id };
|
||||
let result = await this.$getapi([conn]);
|
||||
|
||||
let msg = result[0].data.rows[0].code;
|
||||
msg = msg.replace('[1]', this.fullname);
|
||||
msg = msg.replace('[2]', this.code);
|
||||
msg = msg.replace('[3]', path);
|
||||
|
||||
let data = { subject: 'Xác thực tài khoản BigDataTechCloud', content: msg, to: this.user.email };
|
||||
result = await this.$insertapi('sendemail', data);
|
||||
this.$router.push({ path: '/account/auth', query: { id: this.user.id, email: this.email } });
|
||||
},
|
||||
onSuccess(googleUser) {
|
||||
let info = googleUser.getBasicProfile();
|
||||
console.log('Google info', info);
|
||||
|
||||
let keys = Object.keys(info);
|
||||
this.email = info[keys[5]];
|
||||
this.fullname = info[keys[1]];
|
||||
this.type = 'google';
|
||||
this.authEmail = this.$copy(this.email);
|
||||
},
|
||||
async logInWithFacebook() {
|
||||
if (window.FB) {
|
||||
let self = this;
|
||||
window.FB.login(
|
||||
function (response) {
|
||||
if (response.authResponse) {
|
||||
let userId = response.authResponse.userID;
|
||||
self.getUserInfo(userId);
|
||||
}
|
||||
},
|
||||
{
|
||||
scope: 'public_profile,email',
|
||||
return_scopes: true,
|
||||
auth_type: 'rerequest',
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await this.loadFacebookSDK(document, 'script', 'facebook-jssdk');
|
||||
await this.initFacebook();
|
||||
}
|
||||
},
|
||||
async initFacebook() {
|
||||
let self = this;
|
||||
window.fbAsyncInit = function () {
|
||||
window.FB.init({
|
||||
appId: '434259677606053',
|
||||
cookie: true,
|
||||
version: 'v8.0',
|
||||
});
|
||||
self.isLoaded = true;
|
||||
};
|
||||
},
|
||||
async loadFacebookSDK(d, s, id) {
|
||||
var js,
|
||||
fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
js = d.createElement(s);
|
||||
js.id = id;
|
||||
js.src = 'https://connect.facebook.net/en_US/sdk.js';
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
},
|
||||
getUserInfo(userId) {
|
||||
let self = this;
|
||||
window.FB.api('/me?fields=id,name,email', function (response) {
|
||||
self.username = response.email;
|
||||
self.fullname = response.name;
|
||||
self.type = 'facebook';
|
||||
self.authEmail = self.$copy(self.username);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.login-page {
|
||||
background-color: #f0fdf4;
|
||||
min-height: 100vh;
|
||||
align-content: center;
|
||||
|
||||
button.is-primary {
|
||||
background-color: #53b147;
|
||||
box-shadow: 0 4px 12px rgba(83, 177, 71, 0.25);
|
||||
&:hover {
|
||||
background: #45963a;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-login {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #bbf7d0;
|
||||
background-color: #fff;
|
||||
padding: 0;
|
||||
// .login-header {
|
||||
// }
|
||||
.login-body {
|
||||
.form-login {
|
||||
.group-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
.social-login {
|
||||
.social-header {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #ddd;
|
||||
}
|
||||
span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: #fff;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
.social-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.social-item {
|
||||
flex: 1;
|
||||
button {
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
&:hover {
|
||||
background-color: #bbf7d0;
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-support {
|
||||
background-color: #fff;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.support-header {
|
||||
background-color: #f0fdf4;
|
||||
text-align: center;
|
||||
padding: 14px 0;
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.support-body {
|
||||
display: flex;
|
||||
padding: 16px 0;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
.support-item {
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: #374151;
|
||||
&:hover {
|
||||
color: #16a34a;
|
||||
img {
|
||||
filter: brightness(0) saturate(100%) invert(40%) sepia(80%) saturate(400%) hue-rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: 24px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
62
pages/welcome.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="columns is-centered mx-0 mt-2">
|
||||
<div class="column is-10">
|
||||
<Logo class="mb-5 pb-1"></Logo>
|
||||
<p class="fs-16" v-if="login">Xin chào <b>{{ login.fullname }}</b><span v-if="packinfo? (packinfo.trialInfo==='active' || packinfo.buy) : false">. Sau đây là gói dịch vụ quý khách <b>đang sử dụng</b></span>
|
||||
</p>
|
||||
<template v-if="login? login.type!==1 : false">
|
||||
<Redirect class="mt-4"></Redirect>
|
||||
</template>
|
||||
<template v-else>
|
||||
<AccessExtend v-if="extend"></AccessExtend>
|
||||
<UserPack v-if="login" @info="getinfo"></UserPack>
|
||||
<template v-if="packinfo">
|
||||
<Redirect v-if="packinfo.trialInfo==='active' || packinfo.buy"></Redirect>
|
||||
<template v-else>
|
||||
<Redirect class="mb-3" v-if="packinfo.trialInfo==='no'"></Redirect>
|
||||
<p class="mt-5 fs-16" v-if="packinfo.trialInfo==='expiry'">Quý khách vui lòng chọn <b>gói dịch vụ</b> để tiếp tục. Cảm ơn quý khách đã tin tưởng và lựa chọn dịch vụ của chúng tôi</p>
|
||||
<p class="mt-2 fs-16" v-else="packinfo.trialInfo==='expiry'">BigDataTechCloud cung cấp các gói dịch vụ như dưới dây, quý khách có thể lựa chọn <b>dùng thử </b>để trải nghiệm sản phẩm hoặc <b>mua ngay</b>. Cảm ơn quý khách đã tin tưởng và lựa chọn dịch vụ của chúng tôi</p>
|
||||
<ServicePack v-bind="{packinfo: packinfo}"></ServicePack>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
packinfo: undefined,
|
||||
expiry: undefined,
|
||||
extend: false
|
||||
}
|
||||
},
|
||||
// async created() {
|
||||
// if(!this.login) return this.$router.push('/signin')
|
||||
// let found = await this.$getdata('order', {user: this.login.id, payment_status__code: 'unpaid'}, undefined, true)
|
||||
// if(found) return this.$router.push(`/service/order-info?id=${found.id}`)
|
||||
// },
|
||||
computed: {
|
||||
login: {
|
||||
get: function() {return this.$store.state.login},
|
||||
set: function(val) {this.$store.commit('updateLogin', {login: val})}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getinfo(v) {
|
||||
if(this.login.type===1) {
|
||||
let found = this.$find(this.$store.state.common, {category: 'system', classify: 'mode', code: 'status'})
|
||||
if(found.detail==='dev') {
|
||||
if(!(v.trialInfo==='active' || v.buy)) this.$router.push('/notice')
|
||||
}
|
||||
}
|
||||
this.packinfo = v
|
||||
if(v.trialInfo==='expiry' && !v.buy) this.extend = true
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
4
plugins/buefy.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import Vue from 'vue'
|
||||
import Buefy from 'buefy'
|
||||
|
||||
Vue.use(Buefy)
|
||||
301
plugins/common.js
Normal file
@@ -0,0 +1,301 @@
|
||||
import Vue from 'vue';
|
||||
import { COMPANY } from '~/constants/company';
|
||||
|
||||
Vue.use({
|
||||
install(Vue) {
|
||||
Vue.prototype.$dialog = function (content, title, type, duration, width, height, vbind) {
|
||||
if (typeof content == 'string') {
|
||||
let vtitle = type === 'Success' ? `<span class="has-text-primary">${title}</span>` : title;
|
||||
if (type === 'Error') vtitle = `<span class="has-text-danger">${title}</span>`;
|
||||
let data = {
|
||||
id: this.$id(),
|
||||
component: `dialog/${type || 'Info'}`,
|
||||
vbind: { content: content, duration: duration, vbind: vbind },
|
||||
title: vtitle,
|
||||
width: width || '500px',
|
||||
height: height || '100px',
|
||||
};
|
||||
this.$store.commit('updateStore', { name: 'showmodal', data: data });
|
||||
} else this.$store.commit('updateStore', { name: 'showmodal', data: content });
|
||||
};
|
||||
|
||||
Vue.prototype.$snackbar = function (content, title, type, width, height) {
|
||||
if (typeof content == 'string') {
|
||||
let vtitle = type === 'Success' ? `<span class="has-text-primary">${title}</span>` : title;
|
||||
if (type === 'Error') vtitle = `<span class="has-text-danger">${title}</span>`;
|
||||
let data = {
|
||||
id: this.$id(),
|
||||
component: `snackbar/${type || 'Info'}`,
|
||||
vbind: { content: content },
|
||||
title: vtitle,
|
||||
width: width || '400px',
|
||||
height: height || '100px',
|
||||
};
|
||||
this.$store.commit('updateStore', { name: 'snackbar', data: data });
|
||||
} else this.$store.commit('updateStore', { name: 'snackbar', data: content });
|
||||
};
|
||||
|
||||
Vue.prototype.$pending = function () {
|
||||
this.$dialog({
|
||||
width: '500px',
|
||||
icon: ' mdi mdi-wrench-clock',
|
||||
content: '<p class="fs-16">Chức năng này đang được xây dựng, vui lòng trở lại sau</p>',
|
||||
type: 'is-dark',
|
||||
progress: true,
|
||||
duration: 5,
|
||||
});
|
||||
};
|
||||
|
||||
Vue.prototype.$getLink = function (val) {
|
||||
if (val === undefined || val === null || val === '' || val === '') return '';
|
||||
let json = val.indexOf('{') >= 0 ? JSON.parse(val) : { path: val };
|
||||
return json;
|
||||
};
|
||||
|
||||
Vue.prototype.$timeFormat = function (startDate, endDate) {
|
||||
let milliseconds = startDate - endDate;
|
||||
let secs = Math.floor(Math.abs(milliseconds) / 1000);
|
||||
let mins = Math.floor(secs / 60);
|
||||
let hours = Math.floor(mins / 60);
|
||||
let days = Math.floor(hours / 24);
|
||||
const millisecs = Math.floor(Math.abs(milliseconds)) % 1000;
|
||||
function pad2(n) {
|
||||
return (n < 10 ? '0' : '') + n;
|
||||
}
|
||||
let display = undefined;
|
||||
|
||||
if (days >= 1) {
|
||||
display =
|
||||
pad2(startDate.getHours()) +
|
||||
':' +
|
||||
pad2(startDate.getMinutes()) +
|
||||
' ' +
|
||||
pad2(startDate.getDate()) +
|
||||
'/' +
|
||||
pad2(startDate.getMonth());
|
||||
} else if (hours > 0) display = hours + 'h trước';
|
||||
else if (mins > 0) display = mins + "' trước";
|
||||
else if (secs > 0 || millisecs > 0) display = 'Vừa xong';
|
||||
|
||||
return {
|
||||
days: days,
|
||||
hours: hours % 24,
|
||||
minutes: mins % 60,
|
||||
seconds: secs % 60,
|
||||
milliSeconds: millisecs,
|
||||
display: display,
|
||||
};
|
||||
};
|
||||
|
||||
(Vue.prototype.$errPhone = function (phone) {
|
||||
var text = undefined;
|
||||
if (this.$empty(phone)) {
|
||||
text = 'Số điện thoại di động không được bỏ trống.';
|
||||
} else if (isNaN(phone)) {
|
||||
text = 'Số điện thoại di động không hợp lệ.';
|
||||
} else if (phone.length < 9 || phone.length > 11) {
|
||||
text = 'Số điện thoại di động phải có từ 9-11 số.';
|
||||
}
|
||||
return text;
|
||||
}),
|
||||
(Vue.prototype.$errEmail = function (email) {
|
||||
const re =
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
var text = undefined;
|
||||
if (this.$empty(email)) {
|
||||
text = 'Email không được bỏ trống.';
|
||||
} else if (!re.test(String(email).toLowerCase())) {
|
||||
text = 'Email không hợp lệ.';
|
||||
}
|
||||
return text;
|
||||
});
|
||||
|
||||
Vue.prototype.$errPhoneEmail = function (contact) {
|
||||
const re =
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
var text = undefined;
|
||||
|
||||
if (this.$empty(contact)) {
|
||||
text = 'Số điện thoại di động hoặc Email không được bỏ trống.';
|
||||
} else if (!(re.test(String(contact).toLowerCase()) || !isNaN(contact))) {
|
||||
text = 'Số điện thoại di động hoặc Email không hợp lệ.';
|
||||
} else if (!isNaN(contact) && (contact.length < 9 || contact.length > 11)) {
|
||||
text = 'Số điện thoại di động không hợp lệ.';
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
Vue.prototype.$dummy = function (data, count) {
|
||||
let list = this.$copy(data);
|
||||
for (let index = 0; index < count; index++) {
|
||||
if (data.length < index + 1) list.push({ dummy: true });
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
Vue.prototype.$upload = function (file, type, user) {
|
||||
var fileFormat = [
|
||||
{ type: 'image', format: ['.png', '.jpg', 'jpeg', '.bmp', '.gif', '.svg'] },
|
||||
{ type: 'video', format: ['.wmv', '.avi', '.mp4', '.flv', '.mov', '.mpg', '.amv', '.rm'] },
|
||||
];
|
||||
var valid = undefined;
|
||||
if (type === 'image' || type === 'video') {
|
||||
valid = false;
|
||||
let found = fileFormat.find((v) => v.type === type);
|
||||
found.format.map((x) => {
|
||||
if (file.name.toLowerCase().indexOf(x) >= 0) valid = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (valid === false) return { error: true, text: 'Định dạng file không hợp lệ' };
|
||||
if ((type === 'image' || type === 'file') && file.size > 500 * 1024 * 1024) {
|
||||
return { error: true, text: 'Kích thước ' + (type === 'image' ? 'hình ảnh' : 'tài liệu') + ' phải dưới 500MB' };
|
||||
} else if (type === 'video' && file.size > 1073741274) {
|
||||
return { error: true, text: 'Kích thước video phải dưới 1GB' };
|
||||
}
|
||||
|
||||
let data = new FormData();
|
||||
let fileName = this.$dayjs(new Date()).format('YYYYMMDDhhmmss') + '-' + file.name;
|
||||
data.append('name', fileName);
|
||||
data.append('file', file);
|
||||
data.append('type', type);
|
||||
data.append('size', file.size);
|
||||
data.append('user', user);
|
||||
return { form: data, name: fileName, type: type, size: file.size, file: file };
|
||||
};
|
||||
|
||||
Vue.prototype.$change = function (obj1, obj2, list) {
|
||||
var change = false;
|
||||
if (list) {
|
||||
list.map((v) => {
|
||||
if (obj1[v] !== obj2[v]) change = true;
|
||||
});
|
||||
} else {
|
||||
for (var k in obj1) {
|
||||
if (obj1[k] !== obj2[k]) change = true;
|
||||
}
|
||||
}
|
||||
return change;
|
||||
};
|
||||
|
||||
Vue.prototype.$resetNull = function (obj) {
|
||||
for (var key in obj) {
|
||||
if (obj[key] === '' || obj[key] === '') obj[key] = null;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
Vue.prototype.$responsiveMenu = function () {
|
||||
// Get all "navbar-burger" elements
|
||||
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
||||
// Check if there are any navbar burgers
|
||||
if ($navbarBurgers.length > 0) {
|
||||
// Add a click event on each of them
|
||||
$navbarBurgers.forEach((el) => {
|
||||
// Get the target from the "data-target" attribute
|
||||
const target = el.dataset.target;
|
||||
const $target = document.getElementById(target);
|
||||
|
||||
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
||||
el.classList.toggle('is-active');
|
||||
$target.classList.toggle('is-active');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Vue.prototype.$copyToClipboard = function (text) {
|
||||
if (window.clipboardData && window.clipboardData.setData) {
|
||||
// IE specific code path to prevent textarea being shown while dialog is visible.
|
||||
return clipboardData.setData('Text', text);
|
||||
} else if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.textContent = text;
|
||||
textarea.style.position = 'fixed'; // Prevent scrolling to bottom of page in MS Edge.
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
return document.execCommand('copy'); // Security exception may be thrown by some browsers.
|
||||
} catch (ex) {
|
||||
console.warn('Copy to clipboard failed.', ex);
|
||||
return false;
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Vue.prototype.$nonAccent = function (str) {
|
||||
if (this.$empty(str)) return null;
|
||||
str = str.replaceAll('/', '-').replaceAll('%', '-').replaceAll('?', '-');
|
||||
str = str.toLowerCase();
|
||||
str = str.replace(/à|á|ạ|ả|ã|â|ầ|ấ|ậ|ẩ|ẫ|ă|ằ|ắ|ặ|ẳ|ẵ/g, 'a');
|
||||
str = str.replace(/è|é|ẹ|ẻ|ẽ|ê|ề|ế|ệ|ể|ễ/g, 'e');
|
||||
str = str.replace(/ì|í|ị|ỉ|ĩ/g, 'i');
|
||||
str = str.replace(/ò|ó|ọ|ỏ|õ|ô|ồ|ố|ộ|ổ|ỗ|ơ|ờ|ớ|ợ|ở|ỡ/g, 'o');
|
||||
str = str.replace(/ù|ú|ụ|ủ|ũ|ư|ừ|ứ|ự|ử|ữ/g, 'u');
|
||||
str = str.replace(/ỳ|ý|ỵ|ỷ|ỹ/g, 'y');
|
||||
str = str.replace(/đ/g, 'd');
|
||||
// Some system encode vietnamese combining accent as individual utf-8 characters
|
||||
str = str.replace(/\u0300|\u0301|\u0303|\u0309|\u0323/g, ''); // Huyền sắc hỏi ngã nặng
|
||||
str = str.replace(/\u02C6|\u0306|\u031B/g, ''); // Â, Ê, Ă, Ơ, Ư
|
||||
str = str
|
||||
.split(' ')
|
||||
.filter((s) => s)
|
||||
.join('-');
|
||||
return str;
|
||||
};
|
||||
|
||||
Vue.prototype.$linkID = function (link) {
|
||||
link = link ? link : this.$route.params.slug;
|
||||
if (this.$empty(link)) return;
|
||||
let idx = link.lastIndexOf('-');
|
||||
let id = idx > -1 && idx < link.length - 1 ? link.substring(idx + 1, link.length) : undefined;
|
||||
return id;
|
||||
};
|
||||
|
||||
Vue.prototype.$redirectWeb = function (ele) {
|
||||
if (this.$store.state.iframe) {
|
||||
let info = { id: ele.id, email: ele.email, fullname: ele.fullname, avatar: ele.avatar, token: ele.token };
|
||||
if (window.parent) window.parent.postMessage(JSON.stringify(info), '*');
|
||||
return;
|
||||
}
|
||||
let link = this.$store.state.link || COMPANY.website;
|
||||
|
||||
window.location.href =
|
||||
link +
|
||||
'?email=' +
|
||||
ele.email +
|
||||
'&userid=' +
|
||||
ele.id +
|
||||
'&fullname=' +
|
||||
ele.fullname +
|
||||
(ele.avatar ? '&avatar=' + ele.avatar : '') +
|
||||
'&token=' +
|
||||
ele.token;
|
||||
};
|
||||
|
||||
Vue.prototype.$companyInfo = function () {
|
||||
return COMPANY;
|
||||
};
|
||||
|
||||
Vue.prototype.$regexEmail = function (email) {
|
||||
const regexEmail = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return regexEmail.test(email);
|
||||
};
|
||||
|
||||
Vue.prototype.$regexPassword = function (password, length = 8) {
|
||||
const regexPass = new RegExp(`^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z\\d]).{${length},}$`);
|
||||
return regexPass.test(password);
|
||||
};
|
||||
|
||||
Vue.prototype.$regexPhone = function (phone) {
|
||||
const regexPhone = /^(0|\+84)(3|5|7|8|9)[0-9]{8}$/;
|
||||
return regexPhone.test(phone);
|
||||
};
|
||||
|
||||
Vue.prototype.$regexFullName = function (fullName) {
|
||||
const regexFullName = /^[A-Za-zÀ-ỹ]+(\s[A-Za-zÀ-ỹ]+)+$/;
|
||||
return regexFullName.test(fullName);
|
||||
};
|
||||
},
|
||||
});
|
||||
12
plugins/components.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Vue from 'vue'
|
||||
import SnackBar from '@/components/snackbar/SnackBar'
|
||||
import CountDown from '@/components/dialog/CountDown'
|
||||
import Modal from '@/components/Modal'
|
||||
import SearchBox from '@/components/SearchBox'
|
||||
import DataTable from '@/components/datatable/DataTable'
|
||||
|
||||
const components = { SnackBar, Modal, CountDown, SearchBox, DataTable}
|
||||
|
||||
Object.entries(components).forEach(([name, component]) => {
|
||||
Vue.component(name, component)
|
||||
})
|
||||
424
plugins/connection.js
Normal file
@@ -0,0 +1,424 @@
|
||||
import Vue from 'vue';
|
||||
const mode = 'dev';
|
||||
var paths = [
|
||||
{ name: 'local', url: 'http://127.0.0.1:8000/' },
|
||||
{ name: 'dev', url: 'https://api.bigdatatech.cloud/' },
|
||||
{ name: 'prod', url: 'https://api.bigdatatech.cloud/' },
|
||||
];
|
||||
const path = paths.find((v) => v.name === mode).url;
|
||||
const apis = [
|
||||
{ name: 'upload', url: 'upload/', params: {} },
|
||||
{ name: 'image', url: 'data/Image/', url_detail: 'data-detail/Image/', params: {} },
|
||||
{ name: 'file', url: 'data/File/', url_detail: 'data-detail/File/', params: {} },
|
||||
|
||||
{
|
||||
name: 'user',
|
||||
url: 'data/User/',
|
||||
url_detail: 'data-detail/User/',
|
||||
params: {
|
||||
sort: '-id',
|
||||
values:
|
||||
'id,auth_method__code,blocked,auth_status__code,username,register_method__code,fullname,type,type__code,type__name,create_time,create_time__date,auth_method,auth_status,register_method,create_time,update_time',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'blockreason',
|
||||
commit: 'updateBlockReason',
|
||||
url: 'data/Block_Reason/',
|
||||
url_detail: 'data-detail/Block_Reason/',
|
||||
params: { page: -1 },
|
||||
},
|
||||
{
|
||||
name: 'authstatus',
|
||||
commit: 'updateAuthStatus',
|
||||
url: 'data/Auth_Status/',
|
||||
url_detail: 'data-detail/Auth_Status/',
|
||||
params: { page: -1 },
|
||||
},
|
||||
{
|
||||
name: 'authmethod',
|
||||
commit: 'updateAuthMethod',
|
||||
url: 'data/Auth_Method/',
|
||||
url_detail: 'data-detail/Auth_Method/',
|
||||
params: { page: -1 },
|
||||
},
|
||||
{
|
||||
name: 'usertype',
|
||||
commit: 'updateUserType',
|
||||
url: 'data/User_Type/',
|
||||
url_detail: 'data-detail/User_Type/',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
name: 'registermethod',
|
||||
commit: 'updateRegisterMethod',
|
||||
url: 'data/Register_Method/',
|
||||
url_detail: 'data-detail/Register_Method/',
|
||||
params: { page: -1 },
|
||||
},
|
||||
{
|
||||
name: 'langchoice',
|
||||
commit: 'updateLangChoice',
|
||||
url: 'data/Lang_Choice/',
|
||||
url_detail: 'data-detail/Lang_Choice/',
|
||||
params: {},
|
||||
},
|
||||
{ name: 'userauth', url: 'data/User_Auth/', url_detail: 'data-detail/User_Auth/', params: { sort: '-id' } },
|
||||
{ name: 'accountrecovery', url: 'data/Account_Recovery/', url_detail: 'data-detail/Account_Recovery/', params: {} },
|
||||
{
|
||||
name: 'login',
|
||||
url: 'login/',
|
||||
params: {
|
||||
values:
|
||||
'id,username,email,password,avatar,fullname,display_name,type,type__code,type__name,blocked,block_reason,block_reason__code,block_reason__name,blocked_by,last_login,auth_method,auth_method__code,auth_method__name,auth_status,auth_status__code,auth_status__name,register_method,register_method__code,register_method__name,create_time,update_time',
|
||||
},
|
||||
},
|
||||
{ name: 'authtoken', url: 'auth-token/', params: {} },
|
||||
|
||||
{ name: 'emailsetup', url: 'data/Email_Setup/', url_detail: 'data-detail/Email_Setup/', params: { sort: '-id' } },
|
||||
{
|
||||
name: 'emailsent',
|
||||
url: 'data/Email_Sent/',
|
||||
url_detail: 'data-detail/Email_Sent/',
|
||||
params: {
|
||||
values:
|
||||
'id,receiver,content,content__sender__email,content__subject,content__content,status__code,status,status__name,create_time',
|
||||
sort: '-id',
|
||||
},
|
||||
},
|
||||
{ name: 'sendemail', url: 'send-email/', params: {} },
|
||||
{ name: 'token', url: 'data/Token/', url_detail: 'data-detail/Token', params: { filter: { expiry: 0 } } },
|
||||
{
|
||||
name: 'common',
|
||||
commit: 'updateCommon',
|
||||
url: 'data/Common/',
|
||||
url_detail: 'data-detail/Common/',
|
||||
params: { sort: 'index' },
|
||||
},
|
||||
{ name: 'sex', url: 'data/Sex/', url_detail: 'data-detail/Sex/', params: {} },
|
||||
{ name: 'downloadfile', url: 'download-file/', params: {} },
|
||||
{ name: 'download', url: 'download/', params: {} },
|
||||
{ name: 'gethash', url: 'get-hash/', params: {} },
|
||||
{ name: 'userapps', url: 'data/User_Apps/', url_detail: 'data-detail/User_Apps/', params: {} },
|
||||
{ name: 'customer', url: 'data/Customer/', url_detail: 'data-detail/Customer/', params: {} },
|
||||
];
|
||||
|
||||
Vue.use({
|
||||
install(Vue) {
|
||||
Vue.prototype.$path = function (name) {
|
||||
return name ? paths.find((v) => v.name === name).url : path;
|
||||
};
|
||||
|
||||
Vue.prototype.$findapi = function (name) {
|
||||
const result = Array.isArray(name)
|
||||
? apis.filter((v) => name.findIndex((x) => v.name === x) >= 0)
|
||||
: apis.find((v) => v.name === name);
|
||||
return this.$copy(result);
|
||||
};
|
||||
|
||||
Vue.prototype.$readyapi = function (list) {
|
||||
var array = [];
|
||||
list.forEach((element) => {
|
||||
let found = apis.find((v) => v.name === element);
|
||||
if (found) {
|
||||
let ele = JSON.parse(JSON.stringify(found));
|
||||
ele.ready = this.$store.state[element] ? true : false;
|
||||
array.push(ele);
|
||||
}
|
||||
});
|
||||
return array;
|
||||
};
|
||||
|
||||
// get data
|
||||
Vue.prototype.$getapi = async function (list) {
|
||||
try {
|
||||
let arr = list.map((v) => {
|
||||
let found = apis.find((v) => v.name === v.name);
|
||||
let url = (v.path ? paths.find((x) => x.name === v.path).url : path) + (v.url ? v.url : found.url);
|
||||
let params = v.params ? v.params : found.params === undefined ? {} : found.params;
|
||||
params.login = this.$store.state.login ? this.$store.state.login.id : undefined;
|
||||
return { url: url, params: params };
|
||||
});
|
||||
let data = await Promise.all(arr.map((v) => this.$axios.get(v.url, { params: v.params })));
|
||||
data.map((v, i) => {
|
||||
list[i].data = v.data;
|
||||
if (list[i].commit) {
|
||||
let payload = {};
|
||||
payload[list[i].name] = v.data.rows ? v.data.rows : v.data;
|
||||
this.$store.commit(list[i].commit, payload);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return 'error';
|
||||
}
|
||||
};
|
||||
|
||||
// insert data
|
||||
Vue.prototype.$insertapi = async function (name, data, values) {
|
||||
try {
|
||||
let found = this.$findapi(name);
|
||||
let curpath = found.path ? paths.find((x) => x.name === found.path).url : path;
|
||||
var rs;
|
||||
if (!Array.isArray(data))
|
||||
rs = await this.$axios.post(`${curpath}${found.url}`, data, { params: { values: values } });
|
||||
else {
|
||||
let params = { action: 'import', values: values };
|
||||
rs = await this.$axios.post(`${curpath}import-data/${found.url.substring(5, found.url.length - 1)}/`, data, {
|
||||
params: params,
|
||||
});
|
||||
}
|
||||
|
||||
// update store
|
||||
if (found.commit) {
|
||||
if (this.$store.state[found.name]) {
|
||||
let copy = JSON.parse(JSON.stringify(this.$store.state[found.name]));
|
||||
let rows = Array.isArray(rs.data) ? rs.data : [rs.data];
|
||||
rows.map((v) => {
|
||||
if (v.id && !v.error) {
|
||||
let idx = copy.findIndex((x) => x.id === v.id);
|
||||
if (idx >= 0) copy[idx] = v;
|
||||
else copy.push(v);
|
||||
}
|
||||
});
|
||||
this.$store.commit('updateStore', { name: found.name, data: copy });
|
||||
}
|
||||
}
|
||||
return rs.data;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return 'error';
|
||||
}
|
||||
};
|
||||
|
||||
// update api
|
||||
Vue.prototype.$updateapi = async function (name, data, values) {
|
||||
try {
|
||||
let found = this.$findapi(name);
|
||||
let rs = await this.$axios.put(`${path}${found.url_detail}${data.id}/`, data, {
|
||||
params: { values: values ? values : found.params.values },
|
||||
});
|
||||
if (found.commit) {
|
||||
let index = this.$store.state[found.name]
|
||||
? this.$store.state[found.name].findIndex((v) => v.id === rs.data.id)
|
||||
: -1;
|
||||
if (index >= 0) {
|
||||
var copy = JSON.parse(JSON.stringify(this.$store.state[found.name]));
|
||||
if (Array.isArray(rs.data) === false) Vue.set(copy, index, rs.data);
|
||||
else {
|
||||
rs.data.forEach((v) => {
|
||||
let index = copy.findIndex((v) => v.id === v.id);
|
||||
if (index >= 0) Vue.set(copy, index, v);
|
||||
});
|
||||
}
|
||||
this.$store.commit('updateStore', { name: found.name, data: copy });
|
||||
}
|
||||
}
|
||||
return rs.data;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return 'error';
|
||||
}
|
||||
};
|
||||
|
||||
// delete data
|
||||
Vue.prototype.$deleteapi = async function (name, id) {
|
||||
try {
|
||||
let found = this.$findapi(name);
|
||||
var rs;
|
||||
if (!Array.isArray(id)) rs = await this.$axios.delete(`${path}${found.url_detail}${id}`);
|
||||
else {
|
||||
let params = { action: 'delete' };
|
||||
rs = await this.$axios.post(`${path}import-data/${found.url.substring(5, found.url.length - 1)}/`, id, {
|
||||
params: params,
|
||||
});
|
||||
}
|
||||
if (found.commit) {
|
||||
let copy = JSON.parse(JSON.stringify(this.$store.state[found.name]));
|
||||
if (!Array.isArray(id)) {
|
||||
let index = copy.findIndex((v) => v.id === id);
|
||||
if (index >= 0) this.$delete(copy, index);
|
||||
} else {
|
||||
rs.data.forEach((element) => {
|
||||
let index = copy.findIndex((v) => v.id === element.id);
|
||||
if (index >= 0) this.$delete(copy, index);
|
||||
});
|
||||
}
|
||||
this.$store.commit('updateStore', { name: found.name, data: copy });
|
||||
}
|
||||
return rs.data;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return 'error';
|
||||
}
|
||||
};
|
||||
|
||||
// insert row
|
||||
Vue.prototype.$insertrow = async function (name, data, values, pagename) {
|
||||
let result = await this.$insertapi(name, data, values);
|
||||
if (result === 'error') return;
|
||||
let arr = Array.isArray(result) ? result : [result];
|
||||
let copy = this.$copy(this.$store.state[pagename].data);
|
||||
arr.map((x) => {
|
||||
let index = copy.findIndex((v) => v.id === x.id);
|
||||
index >= 0 ? (copy[index] = x) : copy.unshift(x);
|
||||
});
|
||||
this.$store.commit('updateState', { name: pagename, key: 'data', data: copy });
|
||||
let pagedata = this.$store.state[pagename];
|
||||
if (pagedata.filters ? pagedata.filters.length > 0 : false) {
|
||||
this.$store.commit('updateState', { name: pagename, key: 'filterby', data: this.$copy(pagedata.filters) });
|
||||
}
|
||||
};
|
||||
|
||||
// update row
|
||||
Vue.prototype.$updaterow = async function (name, data, values, pagename) {
|
||||
let result = await this.$updateapi(name, data, values);
|
||||
if (result === 'error') return;
|
||||
let arr = Array.isArray(result) ? result : [result];
|
||||
let copy = this.$copy(this.$store.state[pagename].data);
|
||||
arr.map((x) => {
|
||||
let index = copy.findIndex((v) => v.id === x.id);
|
||||
index >= 0 ? (copy[index] = x) : copy.unshift(x);
|
||||
});
|
||||
this.$store.commit('updateState', { name: pagename, key: 'data', data: copy });
|
||||
let pagedata = this.$store.state[pagename];
|
||||
if (pagedata.filters ? pagedata.filters.length > 0 : false) {
|
||||
this.$store.commit('updateState', { name: pagename, key: 'filterby', data: this.$copy(pagedata.filters) });
|
||||
}
|
||||
};
|
||||
|
||||
// delete row
|
||||
Vue.prototype.$deleterow = async function (name, id, pagename, ask) {
|
||||
let self = this;
|
||||
var remove = async function () {
|
||||
let result = await self.$deleteapi(name, id);
|
||||
if (result === 'error') return;
|
||||
let arr = Array.isArray(id) ? id : [id];
|
||||
let copy = self.$copy(self.$store.state[pagename].data);
|
||||
arr.map((x) => {
|
||||
let index = copy.findIndex((v) => v.id === x);
|
||||
index >= 0 ? self.$delete(copy, index) : false;
|
||||
});
|
||||
self.$store.commit('updateState', { name: pagename, key: 'data', data: copy });
|
||||
let pagedata = this.$store.state[pagename];
|
||||
if (pagedata.filters ? pagedata.filters.length > 0 : false) {
|
||||
this.$store.commit('updateState', { name: pagename, key: 'filterby', data: this.$copy(pagedata.filters) });
|
||||
}
|
||||
};
|
||||
|
||||
// ask confirm
|
||||
if (ask) {
|
||||
this.$buefy.dialog.confirm({
|
||||
message: 'Bạn muốn xóa bản ghi: ' + id,
|
||||
onConfirm: () => remove(),
|
||||
});
|
||||
} else remove();
|
||||
};
|
||||
|
||||
// update page
|
||||
Vue.prototype.$updatepage = function (pagename, row, action) {
|
||||
let pagedata = this.$store.state[pagename];
|
||||
let copy = this.$copy(pagedata.data);
|
||||
let idx = copy.findIndex((v) => v.id === row.id);
|
||||
if (action === 'delete') this.$delete(copy, idx);
|
||||
else if (action === 'insert') copy.unshift(row);
|
||||
else copy[idx] = row;
|
||||
this.$store.commit('updateState', { name: pagename, key: 'data', data: copy });
|
||||
if (pagedata.filters ? pagedata.filters.length > 0 : false) {
|
||||
this.$store.commit('updateState', { name: pagename, key: 'filterby', data: this.$copy(pagedata.filters) });
|
||||
}
|
||||
};
|
||||
|
||||
Vue.prototype.$getdata = async function (name, filter, params, first) {
|
||||
let found = this.$findapi(name);
|
||||
if (params) found.params = params;
|
||||
else if (filter) found.params.filter = filter;
|
||||
let rs = await this.$getapi([found]);
|
||||
return first ? (rs[0].data.rows.length > 0 ? rs[0].data.rows[0] : undefined) : rs[0].data.rows;
|
||||
};
|
||||
|
||||
Vue.prototype.$getpage = function (showFilter) {
|
||||
return {
|
||||
data: [],
|
||||
fields: [],
|
||||
filters: [],
|
||||
update: undefined,
|
||||
action: undefined,
|
||||
filterby: undefined,
|
||||
api: { full_data: true },
|
||||
origin_api: { full_data: true },
|
||||
tablesetting: undefined,
|
||||
setting: undefined,
|
||||
tabfield: true,
|
||||
setpage: {},
|
||||
showFilter: this.$empty(showFilter) ? true : showFilter,
|
||||
};
|
||||
};
|
||||
|
||||
Vue.prototype.$setpage = function (pagename, row, api) {
|
||||
if (!this.$store.state[pagename]) return;
|
||||
let json = row.detail;
|
||||
let fields = this.$updateSeriesFields(json.fields);
|
||||
this.$store.commit('updateState', { name: pagename, key: 'fields', data: fields });
|
||||
this.$store.commit('updateState', { name: pagename, key: 'setting', data: this.$copy(row) });
|
||||
if (json.filters)
|
||||
this.$store.commit('updateState', { name: pagename, key: 'filters', data: this.$copy(json.filters) });
|
||||
if (json.tablesetting)
|
||||
this.$store.commit('updateState', { name: pagename, key: 'tablesetting', data: json.tablesetting });
|
||||
if (api) {
|
||||
let copy = this.$copy(api);
|
||||
delete copy.data;
|
||||
copy.full_data = api.data.full_data;
|
||||
copy.total_rows = api.data.total_rows;
|
||||
this.$store.commit('updateState', { name: pagename, key: 'api', data: copy });
|
||||
this.$store.commit('updateState', { name: pagename, key: 'origin_api', data: copy });
|
||||
}
|
||||
};
|
||||
|
||||
Vue.prototype.$findpage = function (arr) {
|
||||
var copy = this.$copy(this.$store.state.pagetrack);
|
||||
var doFind = function () {
|
||||
let found = undefined;
|
||||
for (let i = 1; i <= 30; i++) {
|
||||
let name = `pagedata${i}`;
|
||||
if (!copy[name]) {
|
||||
found = name;
|
||||
copy[name] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) console.log('pagename not found');
|
||||
return found;
|
||||
};
|
||||
let result;
|
||||
if (arr) {
|
||||
result = [];
|
||||
arr.map((v) => {
|
||||
result.push({ name: v, value: doFind() });
|
||||
});
|
||||
} else {
|
||||
result = doFind(copy);
|
||||
}
|
||||
this.$store.commit('updateStore', { name: 'pagetrack', data: copy });
|
||||
return result;
|
||||
};
|
||||
|
||||
Vue.prototype.$clearpage = function (pagename) {
|
||||
if (!pagename) return;
|
||||
if (pagename === 'reset') return this.$store.commit('updateStore', { name: 'pagetrack', data: {} });
|
||||
let copy = this.$copy(this.$store.state.pagetrack);
|
||||
let arr = Array.isArray(pagename) ? pagename : [pagename];
|
||||
arr.map((v) => {
|
||||
copy[v] = false;
|
||||
this.$store.commit('updateStore', { name: v, data: undefined });
|
||||
});
|
||||
this.$store.commit('updateStore', { name: 'pagetrack', data: copy });
|
||||
};
|
||||
|
||||
Vue.prototype.$updatepath = function (val) {
|
||||
path = `https://${val}/`;
|
||||
};
|
||||
},
|
||||
});
|
||||
358
plugins/datatable.js
Normal file
@@ -0,0 +1,358 @@
|
||||
import Vue from 'vue'
|
||||
Vue.use( {
|
||||
install(Vue) {
|
||||
//==========Find & filter=================
|
||||
Vue.prototype.$find = function(arr, obj, attr) {
|
||||
const keys = Object.keys(obj)
|
||||
let found = arr.find(v=>{
|
||||
let valid = true
|
||||
keys.map(key=>{
|
||||
let val = obj[key]
|
||||
if(valid===false) return false
|
||||
else if(Array.isArray(val)) {
|
||||
if(val.findIndex(x=>x===v[key]) <0) valid = false
|
||||
} else if(!(v[key]===val)) valid = false
|
||||
})
|
||||
return valid
|
||||
})
|
||||
return found? (attr? found[attr] : found) : undefined
|
||||
}
|
||||
|
||||
Vue.prototype.$findIndex = function(arr, obj) {
|
||||
const keys = Object.keys(obj)
|
||||
return arr.findIndex(v=>{
|
||||
let valid = true
|
||||
keys.map(key=>{
|
||||
let val = obj[key]
|
||||
if(valid===false) return false
|
||||
else if(Array.isArray(val)) {
|
||||
if(val.findIndex(x=>x===v[key]) <0) valid = false
|
||||
} else if(!(v[key]===val)) valid = false
|
||||
})
|
||||
return valid
|
||||
})
|
||||
}
|
||||
|
||||
Vue.prototype.$filter = function(arr, obj, attr) {
|
||||
const keys = Object.keys(obj)
|
||||
let rows = arr.filter(v=>{
|
||||
let valid = true
|
||||
keys.map(key=>{
|
||||
let val = obj[key]
|
||||
if(valid===false) return false
|
||||
else if(Array.isArray(val)) {
|
||||
if(val.findIndex(x=>x===v[key]) <0) valid = false
|
||||
} else if(!(v[key]===val)) valid = false
|
||||
})
|
||||
return valid
|
||||
})
|
||||
return attr? rows.map(v=>v[attr]) : rows
|
||||
}
|
||||
|
||||
//=========Empty & copy============
|
||||
Vue.prototype.$id = function() {
|
||||
return Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
|
||||
Vue.prototype.$empty = function(val) {
|
||||
if(val === undefined || val === null || val === '' || val==="") return true
|
||||
return false
|
||||
},
|
||||
|
||||
Vue.prototype.$copy = function(val) {
|
||||
if(val === undefined || val === null || val === '' || val==="") return val
|
||||
return JSON.parse(JSON.stringify(val))
|
||||
}
|
||||
|
||||
Vue.prototype.$clone = function(obj) {
|
||||
if (obj === null || typeof (obj) !== 'object' || 'isActiveClone' in obj)
|
||||
return obj;
|
||||
if (obj instanceof Date)
|
||||
var temp = new obj.constructor(); //or new Date(obj);
|
||||
else
|
||||
var temp = obj.constructor();
|
||||
for (var key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
obj['isActiveClone'] = null;
|
||||
temp[key] = this.$clone(obj[key]);
|
||||
delete obj['isActiveClone'];
|
||||
}
|
||||
}
|
||||
return temp
|
||||
}
|
||||
|
||||
Vue.prototype.$delete = function(arr, idx) {
|
||||
arr.splice(idx, 1)
|
||||
}
|
||||
|
||||
Vue.prototype.$stripHtml = function(html, length) {
|
||||
if(!html) return null
|
||||
else if(typeof html!=='string') return html
|
||||
if(html? html.indexOf('<')<0 : false) {
|
||||
return length? (html.length > length? html.substring(0, length) + '...' : html ) : html
|
||||
}
|
||||
var tmp = document.createElement("DIV")
|
||||
tmp.innerHTML = html
|
||||
var val = tmp.textContent || tmp.innerText || ""
|
||||
return length? (val.length > length? val.substring(0, length) + '...' : val ) : val
|
||||
}
|
||||
|
||||
//==========Convert=================
|
||||
Vue.prototype.$isNumber = function(val) {
|
||||
if(val === undefined || val === null || val === '' || val==="") return false
|
||||
val = val.toString().replace(/,/g, "")
|
||||
return !isNaN(val)
|
||||
}
|
||||
|
||||
Vue.prototype.$numtoString = function(val, type, decimal, mindecimal) {
|
||||
if(val === undefined || val === "" || val==='' || val === null) return
|
||||
val = val.toString().replace(/,/g, "")
|
||||
if(isNaN(val)) return
|
||||
let f = decimal? {maximumFractionDigits: decimal || 0} : {}
|
||||
if(mindecimal) f['minimumFractionDigits'] = mindecimal
|
||||
return decimal? Number(val).toLocaleString(type || 'en-EN', f) : Number(val).toLocaleString(type || 'en-EN')
|
||||
}
|
||||
|
||||
Vue.prototype.$formatNumber = function(val) {
|
||||
if(val === undefined || val === "" || val==='' || val === null) return
|
||||
val = val.toString().replace(/,/g, "")
|
||||
if (val.indexOf('%') >0) {
|
||||
val = val.replace(/%/g, "")
|
||||
return isNaN(val)? undefined : Number(val)/100
|
||||
}
|
||||
return isNaN(val)? undefined : Number(val)
|
||||
}
|
||||
|
||||
Vue.prototype.$formatUnit = function(val, unit, decimal, string, mindecimal) {
|
||||
val = this.$formatNumber(val)
|
||||
if(val===undefined) return
|
||||
let percentage = (unit===0.01 || unit==="0.01")? '%' : ''
|
||||
val = unit? val/Number(unit) : val
|
||||
let f = {maximumFractionDigits: decimal || 0}
|
||||
if(mindecimal) f['minimumFractionDigits'] = mindecimal
|
||||
return string? (val.toLocaleString('en-EN', f) + percentage) : val
|
||||
}
|
||||
|
||||
//==========Calculate=================
|
||||
Vue.prototype.$calc = function(fn) {
|
||||
return new Function('return ' + fn)()
|
||||
}
|
||||
|
||||
Vue.prototype.$calculate = function(row, tags, formula, decimal, unit) {
|
||||
let val = this.$copy(formula)
|
||||
let valid = 0
|
||||
tags.forEach(v => {
|
||||
let myRegExp = new RegExp(v, 'g')
|
||||
let res = this.$formatNumber(row[v])
|
||||
if(res) valid = 1
|
||||
val = val.replace(myRegExp, `(${res || 0})`)
|
||||
})
|
||||
if(valid===0) return {success: false, value : undefined} //all values is null
|
||||
//calculate
|
||||
try {
|
||||
let value = this.$calc(val)
|
||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
||||
var result = {success: false, value : value}
|
||||
} else {
|
||||
value = (value===true || value===false)? value : this.$formatUnit(value, unit, decimal, true, decimal)
|
||||
var result = {success: true, value: value}
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
var result = {success: false, value : undefined}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Vue.prototype.$calculateFunc = function(row, cols, func, decimal, unit) {
|
||||
let value
|
||||
let arr1 = cols.map(v=>this.$formatNumber(row[v]))
|
||||
let arr = arr1.filter(v=>v)
|
||||
if(arr.length===0) return {success: false, value : undefined}
|
||||
if(func==='max') value = Math.max(...arr)
|
||||
else if(func==='min') value = Math.min(...arr)
|
||||
else if(func==='sum') value = arr.reduce((a, b) => a + b, 0)
|
||||
else if(func==='avg') {
|
||||
let total = arr.reduce((a, b) => a + b, 0)
|
||||
value = total / cols.length
|
||||
}
|
||||
if(!value) return {success: false, value: undefined}
|
||||
value = this.$formatUnit(value, unit, decimal, true, decimal)
|
||||
return {success: true, value: value}
|
||||
}
|
||||
|
||||
Vue.prototype.$calculateData = function(data, fields) {
|
||||
let arr = this.$copy(fields.filter(h=>h.formula))
|
||||
if(arr.length===0) return data
|
||||
let arr1 = arr.filter(v=>v.func)
|
||||
arr1.map(v=>{
|
||||
if(v.vals.indexOf(':')>=0) {
|
||||
let arr2 = v.vals.toLowerCase().replaceAll('c', '').split(':')
|
||||
let cols = []
|
||||
for (let i = parseInt(arr2[0]); i <= parseInt(arr2[1]); i++) {
|
||||
let field = fields.length>i? fields[i] : undefined
|
||||
if(field? (field.format==='number' && field.name!==v.name) : false) cols.push(field.name)
|
||||
}
|
||||
v.cols = cols
|
||||
} else {
|
||||
let arr2 = v.vals.toLowerCase().replaceAll('c', '').split(',')
|
||||
let cols = []
|
||||
arr2.map(v=>{
|
||||
let i = parseInt(v)
|
||||
let field = fields.length>i? fields[i] : undefined
|
||||
if(field? (field.format==='number' && field.name!==v.name) : false) cols.push(field.name)
|
||||
})
|
||||
v.cols = cols
|
||||
}
|
||||
})
|
||||
arr = this.$multiSort(arr, {level: 'asc'})
|
||||
let copy = data
|
||||
copy.map(v=>{
|
||||
arr.map(x=>{
|
||||
if(x.func) {
|
||||
let res = this.$calculateFunc(v, x.cols, x.func, x.decimal, x.unit)
|
||||
if(res? res.success : false) v[x.name] = res.value
|
||||
} else {
|
||||
let res = this.$calculate(v, x.tags, x.formula, x.decimal, x.unit)
|
||||
if(res? res.success : false) v[x.name] = res.value
|
||||
}
|
||||
})
|
||||
})
|
||||
return copy
|
||||
}
|
||||
|
||||
Vue.prototype.$summary = function(arr, fields, type) {
|
||||
let obj = {}
|
||||
if(type==='total') {
|
||||
fields.map(x=> obj[x] = arr.map(v=>v[x]? v[x] : 0).reduce((a, b) => a + b, 0))
|
||||
} else if(type==='min') {
|
||||
fields.map(x=>obj[x] = Math.min(...arr.map(v=>v[x])))
|
||||
}
|
||||
else if(type==='max') {
|
||||
fields.map(x=>obj[x] = Math.max(...arr.map(v=>v[x])))
|
||||
}
|
||||
else if(type==='count') {
|
||||
fields.map(x=>obj[x] = arr.map(v=>!this.$empty(v[x])).length)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
//====================Array====================
|
||||
Vue.prototype.$formatArray = function(data, fields) {
|
||||
let args = fields.filter(v=>v.format==='number')
|
||||
data.map(v=>{
|
||||
args.map(x=>{
|
||||
v[x.name] = this.$empty(v[x.name])? undefined : this.$formatUnit(v[x.name], x.unit, x.decimal, true, x.decimal)
|
||||
})
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
Vue.prototype.$unique = function(arr, keyProps) {
|
||||
const kvArray = arr.map(entry => {
|
||||
const key = keyProps.map(k => entry[k]).join('|');
|
||||
return [key, entry];
|
||||
});
|
||||
const map = new Map(kvArray);
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
Vue.prototype.$arrayMove = function(arr, old_index, new_index) {
|
||||
if (new_index >= arr.length) {
|
||||
var k = new_index - arr.length + 1;
|
||||
while (k--) {
|
||||
arr.push(undefined);
|
||||
}
|
||||
}
|
||||
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
|
||||
return arr; // for testing
|
||||
}
|
||||
|
||||
Vue.prototype.$multiSort = function(array, sortObject = {}, format = {}) {
|
||||
const sortKeys = Object.keys(sortObject)
|
||||
|
||||
// Return array if no sort object is supplied.
|
||||
if (!sortKeys.length) return array
|
||||
|
||||
// Change the values of the sortObject keys to -1, 0, or 1.
|
||||
for (let key in sortObject)
|
||||
sortObject[key] = sortObject[key] === 'desc' || sortObject[key] === -1 ? -1 : (sortObject[key] === 'skip' || sortObject[key] === 0 ? 0 : 1)
|
||||
|
||||
const keySort = (a, b, direction) => {
|
||||
direction = direction !== null ? direction : 1
|
||||
if (a === b) return 0
|
||||
|
||||
// If b > a, multiply by -1 to get the reverse direction.
|
||||
return a > b ? direction : -1 * direction;
|
||||
}
|
||||
|
||||
return array.sort((a, b) => {
|
||||
let sorted = 0, index = 0
|
||||
|
||||
// Loop until sorted (-1 or 1) or until the sort keys have been processed.
|
||||
while (sorted === 0 && index < sortKeys.length) {
|
||||
const key = sortKeys[index]
|
||||
if (key) {
|
||||
const direction = sortObject[key]
|
||||
let val1 = format[key]==='number'? (this.$empty(a[key])? 0 : this.$formatNumber(a[key])) : a[key]
|
||||
let val2 = format[key]==='number'? (this.$empty(b[key])? 0 : this.$formatNumber(b[key])) : b[key]
|
||||
sorted = keySort(val1, val2, direction)
|
||||
index++
|
||||
}
|
||||
}
|
||||
return sorted
|
||||
})
|
||||
}
|
||||
|
||||
//======================Fields====================
|
||||
Vue.prototype.$createField = function(name,label,format,show, minwidth) {
|
||||
let field = {name: name, label: label, format: format, show: show, minwidth: minwidth}
|
||||
if(format==='number') {
|
||||
field.unit = '1'
|
||||
field.textalign = 'right'
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
Vue.prototype.$updateFields = function(pagename, field, action) {
|
||||
let pagedata = this.$store.state[pagename]
|
||||
let copy = this.$copy(pagedata.fields)
|
||||
let idx = this.$findIndex(copy, {name: field.name})
|
||||
if(action==='delete') {
|
||||
this.$delete(copy, idx)
|
||||
if(pagedata.filters? pagedata.filters.length>0 : false) {
|
||||
let index = this.$findIndex(pagedata.filters, {name: field.name})
|
||||
if(index>=0) {
|
||||
let copyFilter = this.$copy(this.pagedata.filters)
|
||||
this.$delete(copyFilter, index)
|
||||
this.$store.commit('updateState', {name: pagename, key: 'filterby', data: copyFilter})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
idx>=0? copy[idx] = field : copy.push(field)
|
||||
}
|
||||
this.$store.commit("updateState", {name: pagename, key: "fields", data: copy})
|
||||
},
|
||||
|
||||
Vue.prototype.$updateSeriesFields = function(fields) {
|
||||
fields.filter(v=>v.series).map(field=>{
|
||||
let obj = field.api==='finitem'? this.$findPeriod(field.series, 'period') : this.$findPeriod(field.series)
|
||||
field.period = field.api==='finitem'? obj.code : obj
|
||||
let idx = field.label.indexOf('[')
|
||||
field.label = (idx>0? field.label.substring(0, idx) : field.label) + '[' + (field.api==='finitem'? obj.show : obj) + ']'
|
||||
})
|
||||
return fields
|
||||
}
|
||||
|
||||
Vue.prototype.$updateSeriesFilters = function(filters, fields) {
|
||||
if(fields.filter(v=>v.series).length===0) return filters
|
||||
filters.map(v=>{
|
||||
let found = fields.find(x=>x.name===v.name)
|
||||
if(found? found.series : false) {
|
||||
v.label = found.label
|
||||
}
|
||||
})
|
||||
return filters
|
||||
}
|
||||
}
|
||||
})
|
||||
5
push.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
git add .
|
||||
git commit -m 'changes'
|
||||
git push
|
||||
4
rundev.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
kill -9 $(lsof -i:3001 -t) 2> /dev/null
|
||||
npm run dev
|
||||
BIN
static/favicon.ico
Normal file
|
After Width: | Height: | Size: 30 KiB |
9
static/icons/email.svg
Normal file
|
After Width: | Height: | Size: 28 KiB |
9
static/icons/facebook.svg
Normal file
|
After Width: | Height: | Size: 21 KiB |
9
static/icons/google.svg
Normal file
|
After Width: | Height: | Size: 27 KiB |
9
static/icons/phone-call.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
9
static/icons/shield.svg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/logo-main.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
static/logo.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
190
store/index.js
Normal file
@@ -0,0 +1,190 @@
|
||||
export const state = () => ({
|
||||
login: undefined,
|
||||
menuaction: undefined,
|
||||
modalaction: undefined,
|
||||
reportperiod: undefined,
|
||||
pageparam: undefined,
|
||||
moneyunit: undefined,
|
||||
usertype: undefined,
|
||||
authstatus: undefined,
|
||||
authmethod: undefined,
|
||||
blockreason: undefined,
|
||||
ismobile: undefined,
|
||||
registermethod: undefined,
|
||||
usersetting: undefined,
|
||||
systemsetting: undefined,
|
||||
settingtype: undefined,
|
||||
pagedata: undefined,
|
||||
feature: undefined,
|
||||
datatype: undefined,
|
||||
colorchoice: undefined,
|
||||
filterchoice: undefined,
|
||||
textalign: undefined,
|
||||
placement: undefined,
|
||||
colorscheme: undefined,
|
||||
textcolor: undefined,
|
||||
filtertype: undefined,
|
||||
sorttype: undefined,
|
||||
fielditem: undefined,
|
||||
stockreport: undefined,
|
||||
tablesetting: undefined,
|
||||
stockcomment: undefined,
|
||||
settingchoice: undefined,
|
||||
stockdate: undefined,
|
||||
sharechoice: undefined,
|
||||
originsetting: undefined,
|
||||
menuchoice: undefined,
|
||||
latestperiod: undefined,
|
||||
latestdate: undefined,
|
||||
tradingdate: undefined,
|
||||
currentsetting: undefined,
|
||||
langchoice: undefined,
|
||||
language: undefined,
|
||||
roleright: undefined,
|
||||
employeerights: undefined,
|
||||
employeerole: undefined,
|
||||
settingclass: undefined,
|
||||
chartoption: undefined,
|
||||
help: undefined,
|
||||
servicepack: undefined,
|
||||
sex: undefined,
|
||||
location: undefined,
|
||||
academiclevel: undefined,
|
||||
legaltype: undefined,
|
||||
employeeposition: undefined,
|
||||
link: undefined,
|
||||
dialog: undefined,
|
||||
common: undefined,
|
||||
showmodal: undefined,
|
||||
snackbar: undefined,
|
||||
viewport: undefined,
|
||||
referer: undefined,
|
||||
pagetrack: {},
|
||||
pagedata1: undefined,
|
||||
pagedata2: undefined,
|
||||
pagedata3: undefined,
|
||||
pagedata4: undefined,
|
||||
settings: [],
|
||||
iframe: undefined
|
||||
})
|
||||
|
||||
export const mutations = {
|
||||
updateState (state, payload) { state[payload.name][payload.key] = payload.data },
|
||||
|
||||
updateStore (state, payload) { state[payload.name] = payload.data},
|
||||
|
||||
updateLogin (state, payload) { state.login = payload.login },
|
||||
|
||||
updateMenuAction(state, payload) {state.menuaction = payload.menuaction},
|
||||
|
||||
updateModalAction(state, payload) {state.modalaction = payload.modalaction},
|
||||
|
||||
updateReportPeriod(state, payload) {state.reportperiod = payload.reportperiod},
|
||||
|
||||
updateMoneyUnit(state, payload) {state.moneyunit = payload.moneyunit},
|
||||
|
||||
updatePageParam(state, payload) {state.pageparam = payload.pageparam},
|
||||
|
||||
updateUserType(state, payload) {state.usertype = payload.usertype},
|
||||
|
||||
updateAuthStatus (state, payload) { state.authstatus = payload.authstatus },
|
||||
|
||||
updateAuthMethod (state, payload) { state.authmethod = payload.authmethod },
|
||||
|
||||
updateBlockReason (state, payload) { state.blockreason = payload.blockreason },
|
||||
|
||||
updateIsMobile (state, payload) { state.ismobile = payload.ismobile },
|
||||
|
||||
updateRegisterMethod (state, payload) { state.registermethod = payload.registermethod },
|
||||
|
||||
updateUserSetting (state, payload) { state.usersetting = payload.usersetting },
|
||||
|
||||
updateSystemSetting (state, payload) { state.systemsetting = payload.systemsetting },
|
||||
|
||||
updateSettingType (state, payload) { state.settingtype = payload.settingtype },
|
||||
|
||||
updatePageData (state, payload) { state.pagedata = payload.pagedata },
|
||||
|
||||
updateFeature (state, payload) { state.feature = payload.feature },
|
||||
|
||||
updateDataType (state, payload) { state.datatype = payload.datatype },
|
||||
|
||||
updateColorChoice (state, payload) { state.colorchoice = payload.colorchoice },
|
||||
|
||||
updateFilterChoice (state, payload) { state.filterchoice = payload.filterchoice },
|
||||
|
||||
updateTextAlign (state, payload) { state.textalign = payload.textalign },
|
||||
|
||||
updatePlacement (state, payload) { state.placement = payload.placement },
|
||||
|
||||
updateColorScheme (state, payload) { state.colorscheme = payload.colorscheme },
|
||||
|
||||
updateTextColor (state, payload) { state.textcolor = payload.textcolor },
|
||||
|
||||
updateFilterType (state, payload) { state.filtertype = payload.filtertype },
|
||||
|
||||
updateSortType (state, payload) { state.sorttype = payload.sorttype },
|
||||
|
||||
updateFieldItem (state, payload) { state.fielditem = payload.fielditem },
|
||||
|
||||
updateStockReport (state, payload) { state.stockreport = payload.stockreport },
|
||||
|
||||
updateTableSetting (state, payload) { state.tablesetting = payload.tablesetting },
|
||||
|
||||
updateStockComment (state, payload) { state.stockcomment = payload.stockcomment },
|
||||
|
||||
updateSettingChoice (state, payload) { state.settingchoice = payload.settingchoice },
|
||||
|
||||
updateStockDate (state, payload) { state.stockdate = payload.stockdate },
|
||||
|
||||
updateShareChoice (state, payload) { state.sharechoice = payload.sharechoice },
|
||||
|
||||
updateOriginSetting (state, payload) { state.originsetting = payload.originsetting },
|
||||
|
||||
updateMenuChoice (state, payload) { state.menuchoice = payload.menuchoice },
|
||||
|
||||
updateLatestPeriod (state, payload) { state.latestperiod = payload.latestperiod },
|
||||
|
||||
updateLatestDate (state, payload) { state.latestdate = payload.latestdate },
|
||||
|
||||
updateTradingDate (state, payload) { state.tradingdate = payload.tradingdate },
|
||||
|
||||
updateCurrentSetting (state, payload) { state.currentsetting = payload.currentsetting },
|
||||
|
||||
updateLangChoice (state, payload) { state.langchoice = payload.langchoice },
|
||||
|
||||
updateLanguage (state, payload) { state.language = payload.language },
|
||||
|
||||
updateRoleRight (state, payload) { state.roleright = payload.roleright },
|
||||
|
||||
updateEmployeeRights (state, payload) { state.employeerights = payload.employeerights },
|
||||
|
||||
updateEmployeeRole (state, payload) { state.employeerole = payload.employeerole },
|
||||
|
||||
updateSettingClass (state, payload) { state.settingclass = payload.settingclass },
|
||||
|
||||
updateChartOption (state, payload) { state.chartoption = payload.chartoption },
|
||||
|
||||
updateHelp (state, payload) { state.help = payload.help },
|
||||
|
||||
updateServicePack (state, payload) { state.servicepack = payload.servicepack },
|
||||
|
||||
updateSex (state, payload) { state.sex = payload.sex },
|
||||
|
||||
updateAcademicLevel (state, payload) { state.academiclevel = payload.academiclevel },
|
||||
|
||||
updateLegalType (state, payload) { state.legaltype = payload.legaltype },
|
||||
|
||||
updateEmployeePosition (state, payload) { state.employeeposition = payload.employeeposition },
|
||||
|
||||
updateApproveStatus (state, payload) { state.approvestatus = payload.approvestatus },
|
||||
|
||||
updateLink (state, payload) { state.link = payload.link },
|
||||
|
||||
updateCommon (state, payload) { state.common = payload.common },
|
||||
|
||||
updateViewPort (state, payload) { state.viewport = payload.viewport }
|
||||
}
|
||||
|
||||
export default {state, mutations}
|
||||
|
||||