This commit is contained in:
Xuan Loi
2025-12-05 17:53:49 +07:00
commit 56f3509d4d
187 changed files with 30840 additions and 0 deletions

13
components/Caption.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<div class="mb-2">
<span :class="`icon-text fsb-${size||17} ${type || 'has-text-findata'}`">
<b class="mr-1">{{ title }}</b>
<SvgIcon v-bind="{name: 'right.svg', type: type? type.replace('has-text-', '') : null, size: (size>=30? size*0.7 : size) || 20, alt: 'Arrow'}"></SvgIcon>
</span>
</div>
</template>
<script>
export default {
props: ['type', 'size', 'title']
}
</script>

7
components/Logo.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<div>
<a @click="$router.push('/signin')">
<img width="90px" src="/logo.png" />
</a>
</div>
</template>

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>

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>

40
components/SvgIcon.vue Normal file
View File

@@ -0,0 +1,40 @@
<template>
<nuxt-img loading="lazy" :alt="alt" :class="`svg-${type || 'findata'}`" :style="`width: ${size || 26}px;`" :src="`/icon/${name || 'check.svg'}`"/>
</template>
<script>
export default {
props: ['name', 'size', 'type', 'alt']
}
</script>
<style>
.svg-primary {
filter: invert(39%) sepia(86%) saturate(1828%) hue-rotate(96deg) brightness(104%) contrast(92%);
}
.svg-danger {
filter: brightness(0) saturate(100%) invert(16%) sepia(100%) saturate(2983%) hue-rotate(351deg) brightness(101%) contrast(131%);
}
.svg-findata {
filter: brightness(0) saturate(100%) invert(61%) sepia(85%) saturate(1880%) hue-rotate(340deg) brightness(101%) contrast(101%);
}
.svg-blue {
filter: invert(9%) sepia(98%) saturate(7147%) hue-rotate(248deg) brightness(94%) contrast(145%);
}
.svg-green {
filter: invert(86%) sepia(27%) saturate(7338%) hue-rotate(49deg) brightness(110%) contrast(110%);
}
.svg-grey {
filter: invert(100%) sepia(0%) saturate(213%) hue-rotate(133deg) brightness(86%) contrast(93%);
}
.svg-gray {
filter: invert(35%) sepia(10%) saturate(18%) hue-rotate(338deg) brightness(97%) contrast(82%);
}
.svg-gray1 {
filter: invert(34%) sepia(6%) saturate(71%) hue-rotate(315deg) brightness(95%) contrast(88%);
}
.svg-white {
filter: brightness(0) saturate(100%) invert(100%) sepia(2%) saturate(0%) hue-rotate(101deg) brightness(102%) contrast(102%);
}
.svg-black {
filter: invert(0%) sepia(100%) saturate(0%) hue-rotate(235deg) brightness(107%) contrast(107%);
}
</style>

22
components/TopMenu.vue Executable 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>

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()">Confirm</button>
<button class="button is-dark ml-5" @click="cancel()">Cancel</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,42 @@
<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()">Confirm</button>
<button class="button is-dark ml-5" @click="cancel()">Cancel</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
let id = this.vbind.row.id
let result = await this.$deleteapi(name, id)
if(result==='error') return
this.$snackbar('The data has been deleted from the system.', 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-danger" @click="cancel()">Close</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()">Close</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-primary" @click="cancel()">Close</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: 'Success', 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>