This commit is contained in:
Viet An
2026-05-08 15:11:02 +07:00
parent ad2d1fbfb6
commit 6f247db4b5
7 changed files with 424 additions and 343 deletions

View File

@@ -1,19 +1,13 @@
<template> <template>
<span :class="`icon-text fsb-${props.size || 17} ${props.type || 'has-text-primary'}`"> <span :class="`icon-text is-flex-wrap-nowrap font-semibold fs-${props.size || 16} ${props.type || ''}`">
<span>{{ title }}</span> <span style="text-wrap: nowrap">{{ title }}</span>
<SvgIcon <span class="icon">
id="ignore" <Icon name="material-symbols:arrow-forward-ios-rounded" />
v-bind="{ </span>
name: 'right.svg',
type: props.type ? props.type.replace('has-text-', '') : null,
size: (props.size >= 30 ? props.size * 0.7 : props.size) || 20,
alt: 'Mũi tên chỉ hướng',
}"
></SvgIcon>
</span> </span>
</template> </template>
<script setup> <script setup>
var props = defineProps({ const props = defineProps({
type: String, type: String,
size: Number, size: Number,
title: String, title: String,

View File

@@ -1,203 +1,262 @@
<template> <template>
<span class="tooltip"> <div class="field has-addons is-justify-content-center">
<a <p class="control">
class="mr-4" <button
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'az' })" class="button is-light is-primary"
> @click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'az' })"
<SvgIcon >
v-bind="{ <span class="icon">
name: 'az.svg', <Icon
type: checkFilter() ? 'grey' : 'primary', name="mdi:sort-alphabetical-ascending"
size: 22, :size="22"
}" :class="checkFilter() && 'has-text-grey-light'"
></SvgIcon> />
</a> </span>
<span </button>
class="tooltiptext" <span
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" class="tooltiptext"
v-html="'Sắp xếp tăng dần'" style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
></span> >Sắp xếp tăng dần</span
</span> >
<span class="tooltip"> </p>
<a <p class="control">
class="mr-4" <button
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'za' })" class="button is-light is-primary"
> @click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'za' })"
<SvgIcon >
v-bind="{ <span class="icon">
name: 'az.svg', <Icon
type: checkFilter() ? 'grey' : 'primary', name="mdi:sort-alphabetical-descending"
size: 22, :size="22"
}" :class="checkFilter() && 'has-text-grey-light'"
></SvgIcon> />
</a> </span>
<span </button>
class="tooltiptext" <span
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" class="tooltiptext"
>Sắp xếp giảm dần</span style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
> >Sắp xếp giảm dần</span
</span> >
<span class="tooltip"> </p>
<a <p class="control">
class="mr-4" <button
@click="moveLeft()" class="button is-light is-primary"
> @click="moveLeft()"
<SvgIcon v-bind="{ name: 'left5.png', type: 'primary', size: 22 }"></SvgIcon> >
</a> <span class="icon">
<span <Icon
class="tooltiptext" name="material-symbols:arrow-back-rounded"
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" :size="22"
>Chuyển cột sang trái</span />
> </span>
</span> </button>
<span class="tooltip"> <span
<a class="tooltiptext"
class="mr-4" style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
@click="moveRight()" >Chuyển cột sang trái</span
> >
<SvgIcon v-bind="{ name: 'right5.png', type: 'primary', size: 22 }"></SvgIcon> </p>
</a> <p class="control">
<span <button
class="tooltiptext" class="button is-light is-primary"
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" @click="moveRight()"
>Chuyển cột sang phải</span >
> <span class="icon">
</span> <Icon
<span class="tooltip"> name="material-symbols:arrow-forward-rounded"
<a :size="22"
class="mr-4" />
@click="resizeWidth()" </span>
> </button>
<SvgIcon v-bind="{ name: 'thick.svg', type: 'primary', size: 22 }"></SvgIcon> <span
</a> class="tooltiptext"
<span style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
class="tooltiptext" >Chuyển cột sang phải</span
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" >
>Tăng độ rộng cột</span </p>
> <p class="control">
</span> <button
<span class="tooltip"> class="button is-light is-primary"
<a @click="resizeWidth()"
class="mr-4" >
@click="resizeWidth(true)" <span class="icon">
> <Icon
<SvgIcon v-bind="{ name: 'thin.svg', type: 'primary', size: 23 }"></SvgIcon> name="fluent:arrow-fit-16-regular"
</a> :size="22"
<span />
class="tooltiptext" </span>
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" </button>
>Giảm độ rộng cột</span <span
> class="tooltiptext"
</span> style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
<span class="tooltip"> >Tăng độ rộng cột</span
<a >
class="mr-4" </p>
@click="hideField()" <p class="control">
> <button
<SvgIcon v-bind="{ name: 'eye-off.svg', type: 'primary', size: 23 }"></SvgIcon> class="button is-light is-primary"
</a> @click="resizeWidth(true)"
<span >
class="tooltiptext" <span class="icon">
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" <Icon
>Ẩn cột</span name="fluent:arrow-fit-in-16-regular"
> :size="22"
</span> />
<!-- <template v-if="store.login ? store.login.is_admin : false"> --> </span>
<span class="tooltip"> </button>
<a <span
class="mr-4" class="tooltiptext"
@click="currentField.mandatory ? false : doRemove()" style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
> >Giảm độ rộng cột</span
<SvgIcon v-bind="{ name: 'bin.svg', type: 'primary', size: 23 }"></SvgIcon> >
</a> </p>
<span <p class="control">
class="tooltiptext" <button
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" class="button is-light is-primary"
>Xóa cột</span @click="hideField()"
> >
</span> <span class="icon">
<span class="tooltip"> <Icon
<a name="material-symbols:visibility-off-outline-rounded"
class="mr-4" :size="22"
:class="currentField.format === 'number' ? null : 'has-text-grey-light'" />
@click="currentField.format === 'number' ? $emit('modalevent', { name: 'copyfield', data: currentField }) : false" </span>
> </button>
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 22 }"></SvgIcon> <span
</a> class="tooltiptext"
<span style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
class="tooltiptext" >Ẩn cột</span
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" >
>Sao chép cột</span </p>
> <p class="control">
</span> <button
<span class="tooltip"> class="button is-light is-primary"
<a @click="currentField.mandatory ? false : doRemove()"
class="mr-4" >
@click="fieldList()" <span class="icon">
> <Icon
<SvgIcon v-bind="{ name: 'menu4.png', type: 'primary', size: 22 }"></SvgIcon> name="material-symbols:delete-outline-rounded"
</a> :size="22"
<span />
class="tooltiptext" </span>
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" </button>
>Danh sách cột</span <span
> class="tooltiptext"
</span> style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
<span class="tooltip"> >Xóa cột</span
<a >
class="mr-4" </p>
@click="createField()" <p class="control">
> <button
<SvgIcon v-bind="{ name: 'add.png', type: 'primary', size: 22 }"></SvgIcon> class="button is-light is-primary"
</a> :class="currentField.format === 'number' ? null : 'has-text-grey-light'"
<span @click="
class="tooltiptext" currentField.format === 'number' ? $emit('modalevent', { name: 'copyfield', data: currentField }) : false
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" "
>Tạo cột mới</span >
> <span class="icon">
</span> <Icon
<span class="tooltip"> name="material-symbols:content-copy-outline-rounded"
<a :size="22"
class="mr-4" />
@click="tableOption()" </span>
> </button>
<SvgIcon v-bind="{ name: 'more.svg', type: 'primary', size: 22 }"></SvgIcon> <span
</a> class="tooltiptext"
<span style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
class="tooltiptext" >Sao chép cột</span
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" >
>Tùy chọn bảng</span </p>
> <p class="control">
</span> <button
<span class="tooltip"> class="button is-light is-primary"
<a @click="fieldList()"
class="mr-4" >
@click="saveSetting()" <span class="icon">
> <Icon
<SvgIcon v-bind="{ name: 'save.svg', type: 'primary', size: 22 }"></SvgIcon> name="material-symbols:menu-rounded"
</a> :size="22"
<span />
class="tooltiptext" </span>
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" </button>
>Lưu thiết lập</span <span
> class="tooltiptext"
</span> style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
<!-- </template> --> >Danh sách cột</span
<div class="panel-tabs mb-2"> >
<a </p>
v-for="(v, i) in getMenu().filter((x) => <p class="control">
currentField.format === 'number' <button
? currentField.formula class="button is-light is-primary"
? true @click="createField()"
: x.code !== 'formula' >
: !['filter', 'formula'].find((y) => y === x.code), <span class="icon">
)" <Icon
:key="i" name="material-symbols:add-2-rounded"
:class="selectTab.code === v.code ? 'is-active' : 'has-text-primary'" :size="22"
@click="changeTab(v)" />
> </span>
{{ v.name }} </button>
</a> <span
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
>Tạo cột mới</span
>
</p>
<p class="control">
<button
class="button is-light is-primary"
@click="tableOption()"
>
<span class="icon">
<Icon
name="material-symbols:settings-outline-rounded"
:size="22"
/>
</span>
</button>
<span
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
>Tùy chọn bảng</span
>
</p>
<p class="control">
<button
class="button is-light is-primary"
@click="saveSetting()"
>
<span class="icon">
<Icon
name="material-symbols:save-outline-rounded"
:size="22"
/>
</span>
</button>
<span
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
>Lưu thiết lập</span
>
</p>
</div>
<div class="tabs is-toggle">
<ul class="is-flex-grow-0 mx-auto">
<li
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' : 'has-text-primary'"
@click="changeTab(v)"
>
<a class="px-4 py-1.5">{{ v.name }}</a>
</li>
</ul>
</div> </div>
<div v-if="currentTab === 'detail'"> <div v-if="currentTab === 'detail'">
<p class="fs-14 mt-3"> <p class="fs-14 mt-3">
@@ -210,7 +269,7 @@
></SvgIcon> ></SvgIcon>
<span <span
class="tooltiptext" class="tooltiptext"
style="top: 100%; bottom: unset; min-width: max-content; left: 25px" style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
>Copy</span >Copy</span
> >
</span> </span>
@@ -574,3 +633,12 @@ function resizeWidth(minus) {
updateFields(currentField); updateFields(currentField);
} }
</script> </script>
<style lang="css" scoped>
p.control {
flex-grow: 1;
> button {
width: 100%;
}
}
</style>

View File

@@ -64,7 +64,11 @@
@click="showField(field)" @click="showField(field)"
:style="field.dropStyle" :style="field.dropStyle"
> >
<a v-if="field.label.indexOf('<') < 0">{{ field.label }}</a> <a
v-if="field.label.indexOf('<') < 0"
class="has-text-white font-semibold"
>{{ field.label }}</a
>
<a v-else> <a v-else>
<component <component
:is="dynamicComponent(field.label)" :is="dynamicComponent(field.label)"
@@ -156,7 +160,13 @@ var currentPage = 1;
var displayFields = ref([]); var displayFields = ref([]);
var displayData = []; var displayData = [];
var pagedata = store[props.pagename]; var pagedata = store[props.pagename];
var tablesetting = $copy(pagedata.tablesetting || store.tablesetting); console.log("props.pagename", props.pagename);
console.log("store", toRaw(store));
console.log("store[props.pagename]", toRaw(store[props.pagename]));
console.log("pagedata.tablesetting", toRaw(pagedata.tablesetting)); // table-font-size is 13
console.log("store.tablesetting", toRaw(store.tablesetting)); // table-font-size is 12
// var tablesetting = $copy(pagedata.tablesetting || store.tablesetting);
var tablesetting = $copy(store.tablesetting);
var perPage = Number($find(tablesetting, { code: "per-page" }, "detail")) || 20; var perPage = Number($find(tablesetting, { code: "per-page" }, "detail")) || 20;
var filters = $copy(pagedata.filters || []); var filters = $copy(pagedata.filters || []);
var currentField; var currentField;

View File

@@ -6,7 +6,10 @@
<div <div
v-for="(v, i) in rows" v-for="(v, i) in rows"
:key="i" :key="i"
:class="['field is-grouped py-1 my-0', i !== rows.length - 1 && 'border-bottom']" class="field is-grouped py-1 my-0"
:style="{
borderBottom: i !== rows.length - 1 && '1px solid red',
}"
> >
<p <p
class="control is-expanded py-0 fs-14 hyperlink" class="control is-expanded py-0 fs-14 hyperlink"
@@ -18,6 +21,10 @@
v-if="checked[i] && notick !== true" v-if="checked[i] && notick !== true"
> >
<SvgIcon v-bind="{ name: 'tick.svg', type: 'primary', size: 15 }"></SvgIcon> <SvgIcon v-bind="{ name: 'tick.svg', type: 'primary', size: 15 }"></SvgIcon>
<Icon
name="material-symbols:check-rounded"
:size="15"
/>
</span> </span>
</p> </p>
<p <p

View File

@@ -1,147 +1,144 @@
<template> <template>
<div <div
class="pb-1"
style="border-bottom: 2px solid #3c5b63"
v-if="array || !enableTime" v-if="array || !enableTime"
class="has-text-primary fixed-grid has-12-cols"
style="border-bottom: 2px solid var(--bulma-grey-80)"
> >
<div class="columns mx-0 mb-0"> <div class="grid mb-3">
<div <div
class="column is-8 px-0 pb-0"
v-if="enableTime" v-if="enableTime"
class="cell is-col-span-7 is-flex is-align-items-center is-flex-wrap-wrap"
style="row-gap: 0.5rem; column-gap: 1rem"
> >
<div class="field is-grouped is-grouped-multiline mb-0"> <Caption
<div class="control mb-0"> :title="lang === 'vi' ? 'Thời gian' : 'Time'"
<Caption type="has-text-orange"
v-bind="{ />
title: lang === 'vi' ? 'Thời gian' : 'Time', <div
type: 'has-text-warning', v-for="v in array"
}" :key="v.code"
/> >
</div>
<div
class="control mb-0"
v-for="v in array"
:key="v.code"
>
<span
class="icon-text fsb-16 has-text-warning px-1"
v-if="v.code === current"
>
<SvgIcon v-bind="{ name: 'tick.png', size: 20 }"></SvgIcon>
<span>{{ v.name }}</span>
</span>
<span
class="icon-text has-text-grey hyperlink px-1 fsb-16"
@click="changeOption(v)"
v-else
>{{ v.name }}</span
>
</div>
<span <span
v-if="newDataAvailable" v-if="v.code === current"
class="has-text-danger is-italic is-size-6 ml-2" class="icon-text is-flex-wrap-nowrap font-semibold has-text-orange"
>Có dữ liệu mới, vui lòng làm mới.</span
> >
<span class="icon">
<Icon
name="material-symbols:check-rounded"
:size="22"
/>
</span>
<span style="text-wrap: nowrap">{{ v.name }}</span>
</span>
<span
v-else
class="icon-text font-semibold has-text-grey is-clickable"
style="text-wrap: nowrap"
@click="changeOption(v)"
>
{{ v.name }}
</span>
</div> </div>
<span
v-if="newDataAvailable"
class="has-text-danger is-italic is-size-6 ml-2"
> dữ liệu mới, vui lòng làm mới.</span
>
</div> </div>
<div class="column is-4 px-0"> <div class="cell is-col-span-5 is-flex is-align-items-center is-gap-1 is-flex-wrap-wrap">
<div class="field is-grouped is-grouped-multiline mb-0"> <Caption
<div class="control mb-0"> :title="lang === 'vi' ? `Tìm ${viewport === 1 ? '' : 'kiếm'}` : 'Search'"
<Caption type="has-text-orange"
v-bind="{ />
type: 'has-text-warning', <input
title: lang === 'vi' ? `Tìm ${viewport === 1 ? '' : 'kiếm'}` : 'Search', class="input is-small is-orange"
}" type="text"
/> v-model="text"
</div> :style="`${viewport === 1 ? 'width:150px;' : ''} border: 1px solid var(--bulma-grey-80); max-width: 160px`"
<div class="control mb-0"> @keyup="startSearch"
<input id="input"
class="input is-small" :placeholder="lang === 'vi' ? 'Nhập từ khóa...' : 'Enter keyword...'"
type="text" />
v-model="text" <div class="field has-addons is-flex is-flex-wrap-wrap is-gap-0 is-align-items-center">
:style="`${viewport === 1 ? 'width:150px;' : ''} border: 1px solid #BEBEBE;`" <span
@keyup="startSearch" class="tooltip"
id="input" v-if="importdata && $getEditRights()"
:placeholder="lang === 'vi' ? 'Nhập từ khóa...' : 'Enter keyword...'" >
/>
</div>
<div class="control mb-0">
<span
class="tooltip"
v-if="importdata && $getEditRights()"
>
<a
class="mr-2"
@click="openImport()"
>
<SvgIcon
v-bind="{
name: 'upload.svg',
type: 'findata',
size: 22,
}"
></SvgIcon>
</a>
<span
class="tooltiptext"
style="min-width: max-content"
>
{{ lang === "vi" ? "Nhập dữ liệu" : "Import data" }}
</span>
</span>
<span
class="tooltip"
v-if="enableAdd && $getEditRights()"
>
<a
class="mr-2"
@click="$emit('add')"
>
<SvgIcon v-bind="{ name: 'add1.png', type: 'findata', size: 22 }"></SvgIcon>
</a>
<span
class="tooltiptext"
style="min-width: max-content"
>{{ lang === "vi" ? "Thêm mới" : "Add new" }}</span
>
</span>
<span class="tooltip">
<a
class="mr-2"
@click="$emit('excel')"
>
<SvgIcon v-bind="{ name: 'excel.png', type: 'findata', size: 22 }"></SvgIcon>
</a>
<span
class="tooltiptext"
style="min-width: max-content"
>{{ lang === "vi" ? "Xuất excel" : "Export excel" }}</span
>
</span>
<span class="tooltip">
<a @click="$emit('refresh-data')">
<SvgIcon v-bind="{ name: 'refresh.svg', type: 'findata', size: 22 }"></SvgIcon>
</a>
<span
class="tooltiptext"
style="min-width: max-content"
>{{ lang === "vi" ? "Làm mới" : "Refresh" }}</span
>
</span>
<a <a
class="button is-primary is-loading is-small ml-3" class="mr-2"
v-if="loading" @click="openImport()"
></a> >
</div> <SvgIcon
v-bind="{
name: 'upload.svg',
type: 'findata',
size: 22,
}"
></SvgIcon>
</a>
<span
class="tooltiptext"
style="min-width: max-content"
>
{{ lang === "vi" ? "Nhập dữ liệu" : "Import data" }}
</span>
</span>
<span
class="tooltip"
v-if="enableAdd && $getEditRights()"
>
<a
class="mr-2"
@click="$emit('add')"
>
<SvgIcon v-bind="{ name: 'add1.png', type: 'findata', size: 22 }"></SvgIcon>
</a>
<span
class="tooltiptext"
style="min-width: max-content"
>{{ lang === "vi" ? "Thêm mới" : "Add new" }}</span
>
</span>
<p class="control">
<button
class="button is-ghost has-text-orange fs-14"
@click="$emit('excel')"
>
<span class="icon">
<Icon
name="mdi:microsoft-excel"
:size="22"
/>
</span>
<span>Xuất excel</span>
</button>
</p>
<p class="control">
<button
class="button is-ghost has-text-orange fs-14"
@click="$emit('refresh-data')"
>
<span class="icon">
<Icon
name="material-symbols:refresh-rounded"
:size="22"
/>
</span>
<span>Làm mới</span>
</button>
</p>
<Icon
v-if="loading"
name="svg-spinners:90-ring"
:size="22"
/>
</div> </div>
</div> </div>
</div> </div>
<Modal <Modal
@close="showmodal = undefined"
v-bind="showmodal" v-bind="showmodal"
v-if="showmodal" @close="showmodal = undefined"
></Modal> />
</div> </div>
</template> </template>

View File

@@ -98,7 +98,6 @@ function doChange() {
dataFiles.value.push(file); dataFiles.value.push(file);
}); });
console.log("dataFiles.value", dataFiles.value);
showmodal.value = { showmodal.value = {
component: "media/UploadProgress", component: "media/UploadProgress",
title: "Upload files", title: "Upload files",

View File

@@ -36,6 +36,12 @@ export default defineNuxtPlugin(() => {
const filter = function (arr, obj, attr) { const filter = function (arr, obj, attr) {
const keys = Object.keys(obj); const keys = Object.keys(obj);
if (!arr) {
console.error(`$filter: arr is ${arr}`);
return [];
}
let rows = arr.filter((v) => { let rows = arr.filter((v) => {
let valid = true; let valid = true;
keys.map((key) => { keys.map((key) => {