Base Login

This commit is contained in:
ThienPhamVan
2026-03-25 10:06:01 +07:00
commit 3a2e16cf19
81 changed files with 27983 additions and 0 deletions

24
.gitignore vendored Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

48
assets/styles/main.scss Normal file
View 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
View 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);
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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 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
View 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
View 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
View 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>

View 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"> 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ố </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 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>

View 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 / <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 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 </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>

View 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 <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 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>

View 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>

View 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>

View 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 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"> 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>

View 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 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 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à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à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>

View 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) : '') + '...&#931;' + 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>

View 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>

View 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>

View 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>

View 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 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 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 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à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>

View 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, 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["./*"],
"@/*": ["./*"],
"~~/*": ["./*"],
"@@/*": ["./*"]
}
},
"exclude": ["node_modules", ".nuxt", "dist"]
}

83
layouts/colud.vue Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View 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
View 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> xác thực tài khoản </strong> chuỗi gồm <strong> 9 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 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 xác thực?</span>
<button :class="`button is-dark is-light ${loading ? 'is-loading' : ''} ml-4`" @click="sendCode()">
Gửi lại
</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
View 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 kiểm tra. Nếu thông tin 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"> 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
View File

@@ -0,0 +1,8 @@
<template></template>
<script>
export default {
created() {
this.$router.push('/signin');
},
};
</script>

22
pages/notice.vue Normal file
View 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
View 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 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 HIỆU LỰC CHO CẢ KHÁCH HÀNG CHÍNH THỨC KHÁCH HÀNG DÙNG THỬ</p>
</div>
<p>
Nội dung Điều khoản Chính sách đang được cập nhật để đảm bảo tính đầy đủ 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
View 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 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> &nbsp;
<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
View 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ọ 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 đ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 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 với Facebook </span>
</button>
</div>
</div>
</div>
<p class="mt-5 has-text-centered">
Bạn đã 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> &nbsp;
<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
View 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 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 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 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 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
View File

@@ -0,0 +1,4 @@
import Vue from 'vue'
import Buefy from 'buefy'
Vue.use(Buefy)

301
plugins/common.js Normal file
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
#!/bin/bash
git add .
git commit -m 'changes'
git push

4
rundev.sh Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

9
static/icons/email.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

9
static/icons/google.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

9
static/icons/shield.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/logo-main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

190
store/index.js Normal file
View 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}