Files
web/app/components/datatable/DataTable.vue
2026-03-02 09:45:33 +07:00

551 lines
17 KiB
Vue

<template>
<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-small has-text-white has-text-weight-bold" @click="updateData({filters: []})">
<span class="fs-14">Xóa lọc</span>
</a>
</div>
<div class="control pr-2 mr-5">
<span class="icon-text">
<SvgIcon v-bind="{name: 'sigma.svg', type: 'primary', size: 20}"></SvgIcon>
<span class="fsb-18 has-text-primary">{{totalRows}}</span>
</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 has-text-white 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 class="table-container mb-0" ref="container" id="docid">
<table class="table is-fullwidth is-bordered is-narrow is-hoverable" :style="tableStyle">
<thead>
<tr>
<th v-for="(field,i) in displayFields" :key="i" :style="field.headerStyle">
<div @click="showField(field)" :style="field.dropStyle">
<a v-if="field.label.indexOf('<')<0">{{field.label}}</a>
<a v-else>
<component :is="dynamicComponent(field.label)" :row="v" @clickevent="clickEvent($event, v, field)" />
</a>
</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(v,i) in displayData" :key="i">
<td
v-for="(field, j) in displayFields"
:key="j"
:id="field.name"
:style="v[`${field.name}color`]"
style="
overflow: hidden;
text-overflow: ellipsis;
"
@dblclick="doubleClick(field, v)">
<component :is="dynamicComponent(field.template)" :row="v" v-if="field.template" @clickevent="clickEvent($event, v, field)" />
<span v-else>{{ v[field.name] }}</span>
</td>
</tr>
</tbody>
</table>
<DatatablePagination
v-bind="{ data: data, perPage: perPage }"
@changepage="changePage"
v-if="showPaging"
></DatatablePagination>
</div>
<Modal
@close="close"
@selected="doSelect"
@confirm="confirmRemove"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</template>
<script setup>
import { createApp } from "vue/dist/vue.esm-bundler.js";
import { ref, defineComponent } from "vue";
import { useStore } from "~/stores/index";
const emit = defineEmits(["edit", "insert", "dataevent"]);
const {
$calc,
$calculate,
$calculateData,
$copy,
$deleterow,
$empty,
$find,
$getEditRights,
$formatNumber,
$multiSort,
$remove,
$stripHtml,
$unique,
} = useNuxtApp();
const store = useStore();
var props = defineProps({
pagename: String,
});
function dynamicComponent(htmlString) {
return defineComponent({
template: htmlString,
props: {
row: {
type: Object,
default: () => ({}),
},
},
});
}
var timer = undefined;
var showPaging = ref(false);
var totalRows = ref(0);
var currentPage = 1;
var displayFields = ref([]);
var displayData = [];
var pagedata = store[props.pagename];
var tablesetting = $copy(pagedata.tablesetting || store.tablesetting);
var perPage = Number($find(tablesetting, { code: "per-page" }, "detail")) || 20;
var filters = $copy(pagedata.filters || []);
var currentField;
var filterData = [];
var currentsetting;
var scrollbar;
var fields;
var currentRow;
var data = $copy(pagedata.data);
var showmodal = ref();
watch(
() => store[props.pagename],
(newVal, oldVal) => {
updateChange();
}
);
function updateChange() {
pagedata = store[props.pagename];
if (!pagedata.update) return;
if (pagedata.update.data) data = $copy(pagedata.update.data);
if (pagedata.update.filters) {
doFilter(pagedata.update.filters);
updateShow();
return; //exit
}
if (filters.length > 0) doFilter(filters);
if (pagedata.update.fields || pagedata.update.data) updateShow();
}
const updateShow = function (full_data) {
// allowed JS expressions - should return a boolean
const allowedFns = {
'$getEditRights()': $getEditRights,
};
const arr = pagedata.fields.filter(({ show }) => {
if (typeof show === 'boolean') return show;
else {
// show is a string
if (show === 'true') return true;
if (show === 'false') return false;
return allowedFns[show]?.() || false;
}
});
if (full_data === false) displayData = $copy(data);
else
displayData = $copy(
data.filter(
(ele, index) =>
index >= (currentPage - 1) * perPage && index < currentPage * perPage
)
);
displayData.map((v) => {
arr.map((x) => (v[`${x.name}color`] = getStyle(x, v)));
});
arr.map((v) => {
v.headerStyle = getSettingStyle("header", v);
v.dropStyle = getSettingStyle("dropdown", v);
});
displayFields.value = arr;
showPagination();
};
function confirmRemove() {
$deleterow(pagedata.api.name, currentRow.id, props.pagename, true);
}
const clickEvent = function (event, row, field) {
let name = typeof event === "string" ? event : event.name;
let data = typeof event === "string" ? event : event.data;
if (name === "remove") {
currentRow = row;
showmodal.value = {
component: `dialog/Confirm`,
vbind: { content: "Bạn có muốn xóa bản ghi này không?", duration: 10 },
title: "Xác nhận",
width: "500px",
height: "100px",
};
}
emit(name, row, field, data);
};
const showField = async function (field) {
if (pagedata.contextMenu === false || field.menu === "no") return;
currentField = field;
filterData = $unique(pagedata.data, [field.name]);
//let doc = this.$refs[`th${field.name}`]
//let width = (doc? doc.length>0 : false)? doc[0].getBoundingClientRect().width : 100
let width = 100;
if (pagedata.setting) currentsetting = $copy(pagedata.setting);
showmodal.value = {
vbind: {
pagename: props.pagename,
field: field,
filters: filters,
filterData: filterData,
width: width,
},
component: "datatable/ContextMenu",
title: field.name,
width: "650px",
height: "500px",
}; //$stripHtml(field.label)
};
const getStyle = function (field, record) {
var stop = false;
let val = tablesetting.find((v) => v.code === "td-border")
? 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 = $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 = $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 = $calculate(record, v.tags, v.expression);
if (res.success && res.value && !stop) {
val += ` font-size:${v.size}px; `;
stop = true;
}
}
});
} else
val += ` font-size:${
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;
};
const getSettingStyle = function (name, field) {
let value = "";
if (name === "container") {
value =
"min-height:" +
tablesetting.find((v) => v.code === "container-height").detail +
"rem; ";
} else if (name === "table") {
value +=
"background-color:" +
tablesetting.find((v) => v.code === "table-background").detail +
"; ";
value +=
"font-size:" +
tablesetting.find((v) => v.code === "table-font-size").detail +
"px;";
value +=
"color:" + tablesetting.find((v) => v.code === "table-font-color").detail + "; ";
} else if (name === "header") {
value +=
"background-color:" +
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 = tablesetting.find((v) => v.code === "menu-width").detail;
arg = field ? (field.menuwidth ? field.menuwidth : arg) : arg;
value += "width:" + arg + "rem; ";
value +=
"min-height:" +
tablesetting.find((v) => v.code === "menu-min-height").detail +
"rem; ";
value +=
"max-height:" +
tablesetting.find((v) => v.code === "menu-max-height").detail +
"rem; ";
value += "overflow:auto; ";
} else if (name === "dropdown") {
value +=
"font-size:" +
tablesetting.find((v) => v.code === "header-font-size").detail +
"px; ";
let found = filters.find((v) => v.name === field.name);
found
? (value +=
"color:" +
tablesetting.find((v) => v.code === "header-filter-color").detail +
"; ")
: (value +=
"color:" +
tablesetting.find((v) => v.code === "header-font-color").detail +
"; ");
}
return value;
};
function changePage(page) {
currentPage = page;
updateShow();
}
const showPagination = function () {
showPaging.value = pagedata.pagination === false ? false : true;
totalRows.value = data.length;
if (showPaging.value && pagedata.api) {
if (pagedata.api.full_data === false) totalRows.value = pagedata.api.total_rows;
showPaging.value = totalRows.value > perPage;
}
};
const close = function () {
showmodal.value = undefined;
};
const frontendFilter = function (newVal) {
let checkValid = function (name, x, filter) {
if ($empty(x[name])) return false;
else {
let text = "";
filter.map((y, k) => {
text += `${
k > 0 ? (filter[k - 1].operator === "and" ? " &&" : " ||") : ""
} ${$formatNumber(x[name])}
${
y.condition === "=" ? "==" : y.condition === "<>" ? "!==" : y.condition
} ${$formatNumber(y.value)}`;
});
return $calc(text);
}
};
newVal = $copy(newVal);
var data = $copy(pagedata.data);
newVal
.filter((m) => m.select || m.filter)
.map((v) => {
if (v.select) {
data = data.filter(
(x) =>
v.select.findIndex((y) => ($empty(y) ? $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 = 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 ? $multiSort(data, sort, format) : data;
};
const backendFilter = function (newVal) {};
const doFilter = function (newVal, nonset) {
if (currentPage > 1 && nonset !== true) currentPage = 1;
if (pagedata.api.full_data) {
data = frontendFilter(newVal);
pagedata.dataFilter = $copy(data);
store.commit(props.pagename, pagedata);
emit("changedata", newVal);
} else {
if (timer) clearTimeout(timer);
timer = setTimeout(() => backendFilter(newVal), 200);
}
pagedata.filters = newVal;
store.commit(props.pagename, pagedata);
emit("changefilter", newVal ? newVal.length > 0 : false);
};
const doSelect = function (value) {
showmodal.value = undefined;
let field = currentField;
let found = filters.find((v) => v.name === field.name);
if (found) {
!found.select ? (found.select = []) : false;
let idx = found.select.findIndex((x) => x === value);
idx >= 0 ? $remove(found.select, idx) : found.select.push(value);
if (found.select.length === 0) {
idx = filters.findIndex((v) => v.name === field.name);
if (idx >= 0) $remove(filters, idx);
}
} else {
filters.push({
name: field.name,
label: field.label,
select: [value],
format: field.format,
});
}
doFilter(filters);
updateShow();
};
const doubleScroll = function (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;
scrollbar = _scrollbar;
};
const removeFilter = function (i) {
$remove(filters, i);
doFilter(filters);
updateShow();
};
const scrollbarVisible = function () {
let element = this.$refs["container"];
if (!element) return;
let result = element.scrollWidth > element.clientWidth ? true : false;
if (scrollbar) {
element.parentNode.removeChild(scrollbar);
scrollbar = undefined;
}
if (result) doubleScroll(element);
};
const updateData = async function (newVal) {
if (newVal.columns) {
//change attribute
fields = $copy(newVal.columns);
let _fields = fields.filter((v) => v.show);
data.map((v) => {
_fields.map((x) => (v[`${x.name}color`] = getStyle(x, v)));
});
return updateShow();
}
if (newVal.tablesetting) {
tablesetting = newVal.tablesetting;
perPage = $formatNumber(tablesetting.find((v) => v.code == "per-page").detail);
currentPage = 1;
}
tablesetting = $copy(pagedata.tablesetting || gridsetting);
if (tablesetting) {
perPage = pagedata.perPage
? pagedata.perPage
: Number(tablesetting.find((v) => v.code === "per-page").detail);
}
if (newVal.fields) {
fields = $copy(newVal.fields);
} else fields = $copy(pagedata.fields);
if (newVal.data || newVal.fields) {
let copy = $copy(newVal.data || data);
this.data = $calculateData(copy, fields);
let fields = fields.filter((v) => v.show);
data.map((v) => {
fields.map((x) => (v[`${x.name}color`] = getStyle(x, v)));
});
}
if (newVal.filters) filters = $copy(newVal.filters);
else if (pagedata.filters) filters = $copy(pagedata.filters);
if (newVal.data || newVal.fields || newVal.filters) {
let copy = $copy(filters);
filters.map((v, i) => {
let idx = $findIndex(fields, { name: v.name });
let index = $findIndex(copy, { name: v.name });
if (idx < 0 && index >= 0) $delete(copy, index);
else if (idx >= 0 && index >= 0) copy[index].label = fields[idx].label;
});
filters = copy;
doFilter(filters);
}
if (newVal.data || newVal.fields || newVal.filters || newVal.tablesetting) updateShow();
if (newVal.data || newVal.fields) setTimeout(() => scrollbarVisible(), 100);
if (newVal.highlight) setTimeout(() => highlight(newVal.highlight), 50);
};
const doubleClick = function (field, v) {
currentField = field;
doSelect(v[field.name]);
};
var tableStyle = getSettingStyle("table");
setTimeout(() => updateShow(), 200);
</script>
<style scoped>
:deep(.table tbody tr:hover td, .table tbody tr:hover th) {
background-color: hsl(0, 0%, 78%);
color: rgb(0, 0, 0);
}
</style>