changes
This commit is contained in:
540
components/datatable/ContextMenu.vue
Normal file
540
components/datatable/ContextMenu.vue
Normal file
@@ -0,0 +1,540 @@
|
||||
<template>
|
||||
<span class="tooltip">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'az' })"
|
||||
>
|
||||
<SvgIcon
|
||||
v-bind="{ name: 'az.svg', type: checkFilter() ? 'grey' : 'primary', size: 22 }"
|
||||
></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
v-html="'Sắp xếp tăng dần'"
|
||||
></span>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'za' })"
|
||||
>
|
||||
<SvgIcon
|
||||
v-bind="{ name: 'az.svg', type: checkFilter() ? 'grey' : 'primary', size: 22 }"
|
||||
></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Sắp xếp giảm dần</span
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="moveLeft()">
|
||||
<SvgIcon v-bind="{ name: 'left5.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Chuyển cột sang trái</span
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="moveRight()">
|
||||
<SvgIcon v-bind="{ name: 'right5.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Chuyển cột sang phải</span
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="resizeWidth()">
|
||||
<SvgIcon v-bind="{ name: 'thick.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Tăng độ rộng cột</span
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="resizeWidth(true)">
|
||||
<SvgIcon v-bind="{ name: 'thin.svg', type: 'primary', size: 23 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Giảm độ rộng cột</span
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="hideField()">
|
||||
<SvgIcon v-bind="{ name: 'eye-off.svg', type: 'primary', size: 23 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Ẩn cột</span
|
||||
>
|
||||
</span>
|
||||
<!-- <template v-if="store.login ? store.login.is_admin : false"> -->
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="currentField.mandatory ? false : doRemove()">
|
||||
<SvgIcon v-bind="{ name: 'bin.svg', type: 'primary', size: 23 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Xóa cột</span
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a
|
||||
class="mr-4"
|
||||
:class="currentField.format === 'number' ? null : 'has-text-grey-light'"
|
||||
@click="
|
||||
currentField.format === 'number'
|
||||
? $emit('modalevent', { name: 'copyfield', data: currentField })
|
||||
: false
|
||||
"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Sao chép cột</span
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="fieldList()">
|
||||
<SvgIcon v-bind="{ name: 'menu4.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Danh sách cột</span
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="createField()">
|
||||
<SvgIcon v-bind="{ name: 'add.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Tạo cột mới</span
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="tableOption()">
|
||||
<SvgIcon v-bind="{ name: 'more.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Tùy chọn bảng</span
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="saveSetting()">
|
||||
<SvgIcon v-bind="{ name: 'save.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Lưu thiết lập</span
|
||||
>
|
||||
</span>
|
||||
<!-- </template> -->
|
||||
<div 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' : 'has-text-primary'"
|
||||
@click="changeTab(v)"
|
||||
>
|
||||
{{ v.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="currentTab === 'detail'">
|
||||
<p class="fs-14 mt-3">
|
||||
<strong> Tên trường: </strong> {{ currentField.name }}
|
||||
<a @click="copyContent(currentField.name)">
|
||||
<span class="tooltip">
|
||||
<SvgIcon
|
||||
class="ml-1"
|
||||
v-bind="{ name: 'copy.svg', type: 'primary', size: 16 }"
|
||||
></SvgIcon>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>Copy</span
|
||||
>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<label class="label fs-14 mt-3">Mô tả<span class="has-text-danger"> *</span></label>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input
|
||||
class="input fs-14"
|
||||
type="text"
|
||||
@change="changeLabel($event.target.value)"
|
||||
v-model="label"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button" @click="editLabel()">
|
||||
<SvgIcon v-bind="{ name: 'pen.svg', type: 'dark', size: 19 }"></SvgIcon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'label')">
|
||||
{{ errors.find((v) => v.name === "label").msg }}
|
||||
</p>
|
||||
<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">
|
||||
<span class="mr-4" v-for="(v, i) in datatype">
|
||||
<span class="icon-text" v-if="radioType === v">
|
||||
<SvgIcon
|
||||
v-bind="{ name: 'radio-checked.svg', type: 'gray', size: 22 }"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
{{ v.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal" v-if="currentField.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">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
vdata: moneyunit,
|
||||
field: 'name',
|
||||
column: ['name'],
|
||||
first: true,
|
||||
position: 'is-top-left',
|
||||
}"
|
||||
@option="selected('_account', $event)"
|
||||
></SearchBox>
|
||||
</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"
|
||||
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">
|
||||
<span
|
||||
class="mr-4"
|
||||
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
|
||||
>
|
||||
<a class="icon-text" @click="changeTemplate(v)">
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: `radio-${radioTemplate === v.code ? '' : 'un'}checked.svg`,
|
||||
type: 'gray',
|
||||
size: 22,
|
||||
}"
|
||||
>
|
||||
</SvgIcon>
|
||||
</a>
|
||||
{{ v.name }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3" v-if="radioTemplate === 'option'">
|
||||
<button class="button is-primary is-small has-text-white" @click="showSidebar()">
|
||||
<span class="fs-14">{{
|
||||
`${currentField.template ? "Sửa" : "Tạo"} định dạng`
|
||||
}}</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="currentTab === 'value'">
|
||||
<ScrollBox
|
||||
v-bind="{
|
||||
data: props.filterData,
|
||||
name: props.field.name,
|
||||
maxheight: '380px',
|
||||
perpage: 20,
|
||||
}"
|
||||
@selected="doSelect"
|
||||
/>
|
||||
</div>
|
||||
<Modal
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
@label="changeLabel"
|
||||
@updatefields="updateFields"
|
||||
@close="close"
|
||||
></Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useStore } from "@/stores/index";
|
||||
import ScrollBox from "~/components/datatable/ScrollBox";
|
||||
const store = useStore();
|
||||
const {
|
||||
$copy,
|
||||
$stripHtml,
|
||||
$clone,
|
||||
$arrayMove,
|
||||
$snackbar,
|
||||
$copyToClipboard,
|
||||
} = useNuxtApp();
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
field: Object,
|
||||
filters: Object,
|
||||
filterData: Object,
|
||||
width: String,
|
||||
});
|
||||
const emit = defineEmits(["modalevent", "changepos", "close"]);
|
||||
var colorchoice = store.colorchoice;
|
||||
var errors = [];
|
||||
var currentTab = ref("value");
|
||||
var currentField = $copy(props.field);
|
||||
var pagedata = store[props.pagename];
|
||||
var fields = [];
|
||||
var label = currentField.label;
|
||||
var showmodal = ref();
|
||||
const checkFilter = function () {};
|
||||
const getMenu = function () {
|
||||
let field = currentField;
|
||||
field.disable = "display,tooltip";
|
||||
let arr = field.disable ? field.disable.split(",") : undefined;
|
||||
let array = arr
|
||||
? store.menuchoice.filter((v) => arr.findIndex((x) => x === v.code) < 0)
|
||||
: store.menuchoice;
|
||||
//if (store.login ? !(store.login.is_admin === false) : true) array = [array[0]];
|
||||
return array;
|
||||
};
|
||||
var selectTab = getMenu()[0];
|
||||
var datatype = store.datatype;
|
||||
var current = 1;
|
||||
var value1 = undefined;
|
||||
var value2 = undefined;
|
||||
var moneyunit = store.moneyunit;
|
||||
var radioType = store.datatype.find((v) => v.code === currentField.format);
|
||||
var selectUnit =
|
||||
currentField.format === "number"
|
||||
? moneyunit.find((v) => v.detail === currentField.unit)
|
||||
: undefined;
|
||||
var bgcolor = undefined;
|
||||
var radioBGcolor = colorchoice.find((v) => v.code === "none");
|
||||
var color = undefined;
|
||||
var radioColor = colorchoice.find((v) => v.code === "none");
|
||||
var textsize = undefined;
|
||||
var radioSize = colorchoice.find((v) => v.code === "none");
|
||||
var minwidth = undefined;
|
||||
var radioWidth = colorchoice.find((v) => v.code === "none");
|
||||
var radioMaxWidth = colorchoice.find((v) => v.code === "none");
|
||||
var maxwidth = undefined;
|
||||
var selectAlign = undefined;
|
||||
var radioAlign = colorchoice.find((v) => v.code === "none");
|
||||
var radioTemplate = ref(
|
||||
colorchoice.find((v) => v.code === (currentField.template ? "option" : "none"))["code"]
|
||||
);
|
||||
var selectPlacement = store.placement.find((v) => v.code === "is-right");
|
||||
var selectScheme = store.colorscheme.find((v) => v.code === "is-primary");
|
||||
var radioTooltip = store.colorchoice.find((v) => v.code === "none");
|
||||
var selectField = undefined;
|
||||
var tags = currentField.tags
|
||||
? currentField.tags.map((v) => fields.find((x) => x.name === v))
|
||||
: [];
|
||||
var formula = currentField.formula ? currentField.formula : undefined;
|
||||
var decimal = currentField.decimal;
|
||||
let shortmenu = store.menuchoice.filter((x) =>
|
||||
currentField.format === "number"
|
||||
? currentField.formula
|
||||
? true
|
||||
: x.code !== "formula"
|
||||
: !["filter", "formula"].find((y) => y === x.code)
|
||||
);
|
||||
var selectTab = shortmenu.find((v) => selectTab.code === v.code)
|
||||
? selectTab
|
||||
: menuchoice.find((v) => v.code === "value");
|
||||
var search = undefined;
|
||||
// if(selectTab.code==='value') {
|
||||
// let self = this
|
||||
// setTimeout(function() {self.$refs[currentField.name]? self.$refs[currentField.name].focus() : false}, 50)
|
||||
// }
|
||||
//==============================================================
|
||||
function moveLeft() {
|
||||
let copy = $clone(pagedata);
|
||||
let i = copy.fields.findIndex((v) => v.name === props.field.name);
|
||||
let idx = i - 1 >= 0 ? i - 1 : copy.fields.length - 1;
|
||||
$arrayMove(copy.fields, i, idx);
|
||||
copy.update = { fields: copy.fields };
|
||||
store.commit(props.pagename, copy);
|
||||
emit("changepos");
|
||||
}
|
||||
function moveRight() {
|
||||
let copy = $clone(pagedata);
|
||||
let i = copy.fields.findIndex((v) => v.name === props.field.name);
|
||||
let idx = copy.fields.length - 1 > i ? i + 1 : 0;
|
||||
$arrayMove(copy.fields, i, idx);
|
||||
copy.update = { fields: copy.fields };
|
||||
store.commit(props.pagename, copy);
|
||||
emit("changepos");
|
||||
}
|
||||
function hideField() {
|
||||
let copy = $clone(store[props.pagename]);
|
||||
let found = copy.fields.find((v) => v.name === props.field.name);
|
||||
found.show = false;
|
||||
copy.update = { fields: copy.fields };
|
||||
store.commit(props.pagename, copy);
|
||||
emit("close");
|
||||
}
|
||||
function doRemove() {
|
||||
let copy = $clone(store[props.pagename]);
|
||||
let idx = copy.fields.findIndex((v) => v.name === props.field.name);
|
||||
copy.fields.splice(idx, 1);
|
||||
copy.update = { fields: copy.fields };
|
||||
store.commit(props.pagename, copy);
|
||||
emit("close");
|
||||
}
|
||||
function fieldList() {
|
||||
showmodal.value = {
|
||||
component: "datatable/TableOption",
|
||||
vbind: { pagename: props.pagename },
|
||||
title: "Danh sách cột",
|
||||
width: "50%",
|
||||
height: "630px",
|
||||
};
|
||||
}
|
||||
function tableOption() {
|
||||
showmodal.value = {
|
||||
component: "datatable/TableSetting",
|
||||
vbind: { pagename: props.pagename },
|
||||
title: "Tùy chọn bảng",
|
||||
width: "40%",
|
||||
height: "400px",
|
||||
};
|
||||
}
|
||||
const getFields = function () {
|
||||
fields = pagedata ? $copy(pagedata.fields) : [];
|
||||
fields.map(
|
||||
(v) => (v.caption = (v.label ? v.label.indexOf("<") >= 0 : false) ? v.name : v.label)
|
||||
);
|
||||
};
|
||||
const doSelect = function (evt) {
|
||||
emit("modalevent", { name: "selected", data: evt[props.field.name] });
|
||||
};
|
||||
const changeLabel = function (text) {
|
||||
currentField.label = text;
|
||||
updateFields(currentField);
|
||||
};
|
||||
function editLabel() {
|
||||
showmodal.value = {
|
||||
component: "datatable/EditLabel",
|
||||
width: "500px",
|
||||
height: "300px",
|
||||
vbind: { label: label },
|
||||
};
|
||||
}
|
||||
const changeTemplate = function (v) {
|
||||
radioTemplate.value = v.code;
|
||||
let copy = $copy(currentField);
|
||||
copy.template = v.code;
|
||||
updateFields(copy);
|
||||
};
|
||||
function createField() {
|
||||
showmodal.value = {
|
||||
component: "datatable/NewField",
|
||||
vbind: { pagename: props.pagename },
|
||||
title: "Tạo cột mới",
|
||||
width: "50%",
|
||||
height: "630px",
|
||||
};
|
||||
}
|
||||
function copyContent(value) {
|
||||
$copyToClipboard(value);
|
||||
}
|
||||
function close() {
|
||||
showmodal.value = undefined;
|
||||
}
|
||||
const updateFields = function (field, type) {
|
||||
let copy = $clone(store[props.pagename]);
|
||||
let idx = copy.fields.findIndex((v) => v.name === field.name);
|
||||
copy.fields[idx] = field;
|
||||
store.commit(props.pagename, copy);
|
||||
};
|
||||
const changeTab = function (v) {
|
||||
currentTab.value = v.code;
|
||||
selectTab = v;
|
||||
};
|
||||
const saveSetting = function () {
|
||||
showmodal.value = {
|
||||
component: "datatable/MenuSave",
|
||||
vbind: { pagename: props.pagename, classify: 4 },
|
||||
title: "Lưu thiết lập",
|
||||
width: "500px",
|
||||
height: "400px",
|
||||
};
|
||||
};
|
||||
const showSidebar = function () {
|
||||
let event = { name: "template", field: currentField };
|
||||
let title = "Danh sách cột";
|
||||
if (event.name === "bgcolor")
|
||||
title = `Đổi màu nền: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
|
||||
else if (event.name === "color")
|
||||
title = `Đổi màu chữ: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
|
||||
else if (event.name === "template")
|
||||
title = `Định dạng nâng cao: ${$stripHtml(event.field.label, 30)}`;
|
||||
showmodal.value = {
|
||||
component: "datatable/FormatOption",
|
||||
vbind: { event: event, currentField: currentField, pagename: props.pagename },
|
||||
width: "850px",
|
||||
height: "700px",
|
||||
title: title,
|
||||
};
|
||||
};
|
||||
function resizeWidth(minus) {
|
||||
let val = maxwidth || minwidth || 80;
|
||||
val = minus ? parseInt(val - 0.1 * val) : parseInt(val + 0.1 * val);
|
||||
if (val > 1000) return $snackbar("Độ rộng cột lớn hơn giới hạn cho phép");
|
||||
else if (val < 20) return $snackbar("Độ rộng cột nhỏ hơn giới hạn cho phép");
|
||||
radioMaxWidth = store.colorchoice.find((v) => v.code === "option");
|
||||
radioWidth = store.colorchoice.find((v) => v.code === "option");
|
||||
maxwidth = val;
|
||||
currentField.maxwidth = val;
|
||||
minwidth = val;
|
||||
currentField.minwidth = val;
|
||||
updateFields(currentField);
|
||||
}
|
||||
</script>
|
||||
431
components/datatable/CreateTemplate.vue
Normal file
431
components/datatable/CreateTemplate.vue
Normal file
@@ -0,0 +1,431 @@
|
||||
<template>
|
||||
<div v-if="docid">
|
||||
<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="changeTab(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)">
|
||||
<SvgIcon v-bind="{name: 'close.svg', type: 'danger', size: 22}"></SvgIcon>
|
||||
</a>
|
||||
</p>
|
||||
<p class="control has-text-right ml-5" v-if="selected? selected.id===v.id : false">
|
||||
<SvgIcon v-bind="{name: 'tick.svg', type: 'primary', size: 22}"></SvgIcon>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab.code==='condition'">
|
||||
<div class="mb-5" v-if="selected">
|
||||
<b-radio v-for="(v,i) in conditions" :key="i" v-model="condition"
|
||||
:native-value="v" @input="changeCondition(v)">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</div>
|
||||
|
||||
<template v-if="condition? condition.code==='yes' : false">
|
||||
<div class="field mt-3">
|
||||
<label class="label fs-14">Chọn trường xây dựng biểu thức <span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<b-taginput
|
||||
size="is-small"
|
||||
v-model="tagsField"
|
||||
:data="pageData? pageData.fields.filter(v=>v.format==='number') : []"
|
||||
type="is-dark is-light"
|
||||
autocomplete
|
||||
:open-on-focus="true"
|
||||
field="name"
|
||||
icon="plus"
|
||||
placeholder="Chọn trường"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<span class="mr-3 has-text-danger has-text-weight-bold"> {{props.option.name}}</span>
|
||||
<span :class="tagsField.find(v=>v.id===props.option.id)? 'has-text-dark' : ''"> {{$stripHtml(props.option.label, 50)}} </span>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
Không có trường thỏa mãn
|
||||
</template>
|
||||
</b-taginput>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tagsField')"> {{errors.find(v=>v.name==='tagsField').message}} </p>
|
||||
</div>
|
||||
<div class="field mt-1" v-if="tagsField.length>0">
|
||||
<p class="help is-primary"> Click đúp vào để thêm vào biểu thức.</p>
|
||||
<div class="tagsField">
|
||||
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name"
|
||||
class="tag is-rounded" v-for="(v,i) in tagsField" :key="i">
|
||||
<span class="tooltip">
|
||||
{{v.name}}
|
||||
<span class="tooltiptext">{{ $stripHtml(v.label) }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14">Biểu thức có dạng Đúng / Sai <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control is-expanded">
|
||||
<input class="input" type="text" v-model="expression" placeholder="Tạo biểu thức tại đây">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='expression')"> {{errors.find(v=>v.name==='expression').message}} </p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab.code==='option' && selected">
|
||||
<div class="field is-horizontal border-bottom pb-2 mt-1">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Màu nền </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioBGcolor"
|
||||
:native-value="v" @input="changeStyle()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioBGcolor? radioBGcolor.code==='option' : false">
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" v-model="bgcolor" @change="changeStyle()">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal border-bottom pb-2">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Màu chữ </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioColor"
|
||||
:native-value="v" @input="changeStyle()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioColor? radioColor.code==='option' : false">
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" v-model="color" @change="changeStyle()">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal border-bottom pb-2">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Cỡ chữ </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioSize"
|
||||
:native-value="v" @input="changeStyle()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioSize? radioSize.code==='option' : false">
|
||||
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" placeholder="Nhập số" v-model="textsize" @change="changeStyle()">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="tab.code==='template'">
|
||||
<p class="mb-3">
|
||||
<a @click="copyContent()" class="mr-6">
|
||||
<span class="icon-text">
|
||||
<SvgIcon class="mr-2" v-bind="{name: 'copy.svg', type: 'primary', siz: 18}"></SvgIcon>
|
||||
<span class="fs-16">Copy</span>
|
||||
</span>
|
||||
</a>
|
||||
<a @click="paste()" class="mr-6">
|
||||
<span class="icon-text">
|
||||
<SvgIcon class="mr-2" v-bind="{name: 'pen1.svg', type: 'primary', siz: 18}"></SvgIcon>
|
||||
<span class="fs-16">Paste</span>
|
||||
</span>
|
||||
</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
|
||||
<SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 22}"></SvgIcon>
|
||||
</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-outlined mt-5" @click="replace()">Replace</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-5">
|
||||
<button class="button is-primary has-text-white" @click="changeTemplate()">Áp dụng</button>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useStore } from '@/stores/index'
|
||||
const store = useStore()
|
||||
const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = useNuxtApp()
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
field: Object
|
||||
})
|
||||
var colorscheme = store.colorscheme
|
||||
var colorchoice = store.colorchoice
|
||||
var pageData = store[props.pagename]
|
||||
var field = props.field
|
||||
var type = undefined
|
||||
var size = undefined
|
||||
var types = [{code: 'span', name: 'span'}, {code: 'tag', name: 'tag'}]
|
||||
var 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'}]
|
||||
var shapes = [{code: 'default', name: 'Mặc định'}, {code: 'is-rounded', name: 'Tròn góc'}]
|
||||
var shape = undefined
|
||||
var outlines = [{code: 'default', name: 'Mặc định'}, {code: 'is-outlined', name: 'Outline'}]
|
||||
var outline = undefined
|
||||
var conditions = [{code: 'no', name: 'Không áp dụng'}, {code: 'yes', name: 'Có áp dụng'}]
|
||||
var condition = undefined
|
||||
var tags = []
|
||||
var selected = undefined
|
||||
var 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'}]
|
||||
var tab = ref(undefined)
|
||||
var tagsField = []
|
||||
var errors = []
|
||||
var expression = ''
|
||||
var text = ref(null)
|
||||
var radioBGcolor = undefined
|
||||
var radioColor = undefined
|
||||
var radioSize = undefined
|
||||
var bgcolor = undefined
|
||||
var color = undefined
|
||||
var textsize = undefined
|
||||
var source = undefined
|
||||
var target = $copy(field.name)
|
||||
|
||||
const initData = function() {
|
||||
type = types.find(v=>v.code==='tag')
|
||||
size = sizes.find(v=>v.code==='is-normal')
|
||||
shape = shapes.find(v=>v.code==='is-rounded')
|
||||
outline = shapes.find(v=>v.code==='default')
|
||||
if($empty(field.template)) tab.value =tabs.find(v=>v.code==='selected')
|
||||
else {
|
||||
text.value =$copy(field.template)
|
||||
tab.value =tabs.find(v=>v.code==='template')
|
||||
}
|
||||
condition =conditions.find(v=>v.code==='no')
|
||||
}
|
||||
/*watch: {
|
||||
expression: function(newVal) {
|
||||
if($empty(newVal)) return
|
||||
elsecheckExpression()
|
||||
},
|
||||
tab: function(newVal, oldVal) {
|
||||
if(oldVal===undefined) return
|
||||
if(newVal.code==='template') {
|
||||
let value = '<div>'
|
||||
tags.map((v,i)=>{
|
||||
value += '<span class="' + v.class + (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>'
|
||||
text = value
|
||||
} else if(newVal.code==='option') {
|
||||
if(!selected) return
|
||||
radioBGcolor =selected.bgcolor?colorchoice.find(v=>v.code==='option') :colorchoice.find(v=>v.code==='none')
|
||||
radioColor =selected.color?colorchoice.find(v=>v.code==='option') :colorchoice.find(v=>v.code==='none')
|
||||
radioSize =selected.textsize?colorchoice.find(v=>v.code==='option') :colorchoice.find(v=>v.code==='none')
|
||||
bgcolor =selected.bgcolor?selected.bgcolor : undefined
|
||||
color =selected.color?selected.color : undefined
|
||||
textsize =selected.textsize?selected.textsize : undefined
|
||||
} else if(newVal.code==='condition') {
|
||||
condition = conditions.find(v=>v.code==='no')
|
||||
tagsField = []
|
||||
expression = ''
|
||||
if(selected?selected.expression : false) {
|
||||
condition =conditions.find(v=>v.code==='yes')
|
||||
tagsField =$copy(selected.tags)
|
||||
expression =$copy(selected.formula)
|
||||
}
|
||||
}
|
||||
}
|
||||
},*/
|
||||
function changeTab(v) {
|
||||
tab.value = v
|
||||
}
|
||||
const paste = async function() {
|
||||
text.value = await navigator.clipboard.readText()
|
||||
}
|
||||
const replace = function() {
|
||||
if($empty(text.value)) return
|
||||
text.value =text.value.replaceAll(source,target)
|
||||
}
|
||||
const doCheck = function() {
|
||||
let text = window.getSelection().toString()
|
||||
if($empty(text)) return
|
||||
source = text
|
||||
}
|
||||
const changeStyle = function() {
|
||||
selected.bgcolor =selected.color =selected.textsize =selected.style = undefined
|
||||
let style = ''
|
||||
if(radioBGcolor.code==='option'? !$empty(bgcolor) : false) {
|
||||
selected.bgcolor =bgcolor
|
||||
style += 'background-color: ' +bgcolor + ' !important; '
|
||||
}
|
||||
if(radioColor.code==='option'? !$empty(color) : false) {
|
||||
selected.color =color
|
||||
style += 'color: ' +color + ' !important; '
|
||||
}
|
||||
if(radioSize.code==='option'?$isNumber(textsize) : false) {
|
||||
selected.textsize =textsize
|
||||
style += 'font-size: ' +textsize + 'px !important; '
|
||||
}
|
||||
$empty(style)? false :selected.style = style
|
||||
}
|
||||
const changeCondition = function(v) {
|
||||
if(v.code==='no')selected.expression = undefined
|
||||
}
|
||||
const copyContent = function() {
|
||||
$copyToClipboard(text.value)
|
||||
}
|
||||
const changeTemplate = function() {
|
||||
let copy = pageData
|
||||
let found = copy.fields.find(v=>v.name===field.name)
|
||||
found.template = text.value
|
||||
store.commit(props.pagename, copy)
|
||||
}
|
||||
const checkExpression = function() {
|
||||
errors = []
|
||||
let val =$copy(expression)
|
||||
let exp =$copy(expression)
|
||||
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 =$calc(val)
|
||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
||||
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
} else if(!(eval(value)===true || eval(value)===false)) {
|
||||
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
} else if(selected) {
|
||||
selected.expression = exp
|
||||
selected.formula =expression
|
||||
selected.tags =$copy(tagsField)
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
}
|
||||
returnerrors.length>0? false : true
|
||||
}
|
||||
const changeType = function(v) {
|
||||
}
|
||||
const doSelect = function(v) {
|
||||
tags.push({id:$id(), name: v.name, class:getClass(v)})
|
||||
tab =tabs.find(v=>v.code==='selected')
|
||||
selected =tags[tags.length-1]
|
||||
}
|
||||
const doSelectSpan = function(v) {
|
||||
tags.push({id:$id(), name: v.name, class:getSpanClass(v)})
|
||||
tab =tabs.find(v=>v.code==='selected')
|
||||
selected =tags[tags.length-1]
|
||||
}
|
||||
const remove = function(i) {
|
||||
$remove(tags, i)
|
||||
}
|
||||
const getClass = function(v) {
|
||||
let value =type.code + ' ' + v.code + ' ' +size.code + (shape.code==='default'? '' : ' ' +shape.code)
|
||||
value += (outline.code==='default'? '' : ' ' +outline.code)
|
||||
return value
|
||||
}
|
||||
const getSpanClass = function(v) {
|
||||
let value = 'has-text-' + v.name.toLowerCase() + ' ' +size.value
|
||||
return value
|
||||
}
|
||||
initData()
|
||||
var docid = $id()
|
||||
</script>
|
||||
193
components/datatable/DataModel.vue
Normal file
193
components/datatable/DataModel.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="columns mx-0">
|
||||
<div class="column is-2">
|
||||
<Caption class="mb-2" v-bind="{title: 'Tên model (bảng)', type: 'has-text-warning'}"></Caption>
|
||||
<div class="mb-2">
|
||||
<input class="input" v-model="text" placeholder="Tìm model" @change="findModel()">
|
||||
</div>
|
||||
<div style="max-height: 80vh; overflow: auto;">
|
||||
<div :class="`py-1 border-bottom is-clickable ${current.model===v.model? 'has-background-primary has-text-white' : ''}`"
|
||||
v-for="v in displayData" @click="changeMenu(v)">
|
||||
{{ v.model}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-10 py-0 px-0">
|
||||
<div class="tabs mb-3">
|
||||
<ul>
|
||||
<li :class="`${v.code===tab? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`" v-for="v in tabs">
|
||||
<a @click="changeTab(v)">{{v.name}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="tab==='datatype'">
|
||||
<Caption class="mb-2" v-bind="{title: 'Kiểu dữ liệu (type)', type: 'has-text-warning'}"></Caption>
|
||||
<div style="max-height:75vh; overflow-y: auto;">
|
||||
<div class="py-1 border-bottom is-clickable" v-for="x in current.fields">
|
||||
{{ x.name}}
|
||||
<span class="ml-6 has-text-grey">{{ x.type }}</span>
|
||||
<a class="ml-6 has-text-primary" v-if="x.model" @click="openModel(x)">{{ x.model }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else-if="tab==='table'">
|
||||
<div class="columns mx-0 mb-0 pb-0">
|
||||
<div class="column is-5">
|
||||
<Caption class="mb-1" v-bind="{title: 'Values', type: 'has-text-warning'}"></Caption>
|
||||
<input class="input" rows="1" v-model="values" placeholder="Tên trường không chứa dấu cách, vd: code,name">
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<Caption class="mb-1" v-bind="{title: 'Filter', type: 'has-text-warning'}"></Caption>
|
||||
<input class="input" rows="1" v-model="filter" placeholder="{'code': 'xyz'}">
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<Caption class="mb-1" v-bind="{title: 'Sort', type: 'has-text-warning'}"></Caption>
|
||||
<input class="input" rows="1" v-model="sort" placeholder="vd: -code,name">
|
||||
</div>
|
||||
<div class="column is-1">
|
||||
<Caption class="mb-1" v-bind="{title: 'Load', type: 'has-text-warning'}"></Caption>
|
||||
<div>
|
||||
<button class="button is-primary has-text-white" @click="loadData()">Load</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Caption class="mb-1" v-bind="{title: 'Query', type: 'has-text-warning'}"></Caption>
|
||||
<div class="mb-2">
|
||||
{{ query }}
|
||||
<a class="has-text-primary ml-5" @click="copy()">copy</a>
|
||||
<p>{{apiUrl}}
|
||||
<a class="has-text-primary ml-5" @click="$copyToClipboard(apiUrl)">copy</a>
|
||||
<a class="has-text-primary ml-5" target="_blank" :href="apiUrl">open</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<DataTable v-bind="{pagename: pagename}" v-if="pagedata" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
<img id="image" :src="filePath" alt="">
|
||||
<p class="pl-5">
|
||||
<a class="mr-5" @click="downloadFile()">
|
||||
<SvgIcon v-bind="{name: 'download.svg', type: 'black', size: 24}"></SvgIcon>
|
||||
</a>
|
||||
<a target="_blank" :href="filePath">
|
||||
<SvgIcon v-bind="{name: 'open.svg', type: 'black', size: 24}"></SvgIcon>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useStore } from '@/stores/index'
|
||||
const { $getdata, $getapi, $createField, $clone, $getpage, $empty, $copyToClipboard, $find, $multiSort, $download, $getpath } = useNuxtApp()
|
||||
const store = useStore()
|
||||
var pagename = 'pagedata3'
|
||||
var pagedata = ref()
|
||||
pagedata.value = $getpage()
|
||||
pagedata.value.perPage = 10
|
||||
store.commit(pagename, pagedata)
|
||||
let list = ['LogEntry', 'Permission', 'ContentType', 'Session', 'Group']
|
||||
var data = (await $getdata('getmodel')).filter(v=>list.findIndex(x=>x===v.model)<0)
|
||||
data = $multiSort(data, {model: 'asc'})
|
||||
var current = ref({fields: []})
|
||||
var tabs = [{code: 'datatype', name: 'Kiểu dữ liệu'}, {code: 'table', name: 'Dữ liệu'}, {code: 'datamodel', name: 'Data model'}]
|
||||
var tab = ref('datatype')
|
||||
var datatable = ref()
|
||||
var query = ref()
|
||||
var values, filter
|
||||
var apiUrl = ref()
|
||||
var showmodal = ref()
|
||||
var text = null
|
||||
var displayData = ref(data)
|
||||
var filePath = `${$getpath()}static/files/datamodel.png`
|
||||
var sort = "-id"
|
||||
current.value = data[0]
|
||||
function changeMenu(v) {
|
||||
values = undefined
|
||||
filter = undefined
|
||||
sort = undefined
|
||||
current.value = v
|
||||
if(tab.value==='table') loadData()
|
||||
}
|
||||
async function changeTab(v) {
|
||||
tab.value = v.code
|
||||
if(v.code==='table') loadData()
|
||||
}
|
||||
async function loadData() {
|
||||
let vfilter = filter? filter.trim() : undefined
|
||||
if(vfilter) {
|
||||
try {
|
||||
vfilter = JSON.parse(vfilter)
|
||||
} catch (error) {
|
||||
alert('Cấu trúc filter có lỗi')
|
||||
vfilter = undefined
|
||||
}
|
||||
}
|
||||
let params = {values: $empty(values)? undefined : values.trim(), filter: filter, sort: $empty(sort)? undefined : sort.trim()}
|
||||
let modelName = current.value.model
|
||||
let found = {name: modelName.toLowerCase().replace('_', ''), url: `data/${modelName}/`, url_detail: `data-detail/${modelName}/`, params: params}
|
||||
query.value = $clone(found)
|
||||
let rs = await $getapi([found])
|
||||
if(rs==='error') return alert('Đã xảy ra lỗi, hãy xem lại câu lệnh.')
|
||||
datatable.value = rs[0].data.rows
|
||||
showData()
|
||||
|
||||
// api query
|
||||
const baseUrl = $getpath() + `${query.value.url}`
|
||||
apiUrl.value = baseUrl
|
||||
let vparams = !$empty(values)? {values: values} : null
|
||||
if(!$empty(filter)) {
|
||||
vparams = vparams? {values: values, filter:filter} : {filter:filter}
|
||||
}
|
||||
if(!$empty(sort)) {
|
||||
if(vparams) {
|
||||
vparams.sort = sort.trim()
|
||||
} else {
|
||||
vparams = {sort: sort.trim()}
|
||||
}
|
||||
}
|
||||
if(vparams) {
|
||||
let url = new URL(baseUrl);
|
||||
let searchParams = new URLSearchParams(vparams);
|
||||
url.search = searchParams.toString();
|
||||
apiUrl.value = baseUrl + url.search
|
||||
}
|
||||
}
|
||||
function showData() {
|
||||
let arr = []
|
||||
if(!$empty(values)) {
|
||||
let arr1 = values.trim().split(',')
|
||||
arr1.map(v=>{
|
||||
let val = v.trim()
|
||||
let field = $createField(val, val, 'string', true)
|
||||
arr.push(field)
|
||||
})
|
||||
} else {
|
||||
current.value.fields.map(v=>{
|
||||
let field = $createField(v.name, v.name, 'string', true)
|
||||
arr.push(field)
|
||||
})
|
||||
}
|
||||
let clone = $clone(pagedata.value)
|
||||
clone.fields = arr
|
||||
clone.data = datatable.value
|
||||
pagedata.value = undefined
|
||||
setTimeout(()=>pagedata.value = clone)
|
||||
}
|
||||
function copy() {
|
||||
$copyToClipboard(JSON.stringify(query.value))
|
||||
}
|
||||
function openModel(x) {
|
||||
showmodal.value = {component: 'datatable/ModelInfo', title: x.model, width: '70%', height: '600px',
|
||||
vbind: {data: data, info: $find(data, {model: x.model})}}
|
||||
}
|
||||
function downloadFile() {
|
||||
$download(`${$getpath()}download/?name=datamodel.png&type=file`, 'datamodel.png')
|
||||
}
|
||||
function findModel() {
|
||||
if($empty(text)) return displayData.value = data
|
||||
displayData.value = data.filter(v=>v.model.toLowerCase().indexOf(text.toLowerCase())>=0)
|
||||
}
|
||||
</script>
|
||||
422
components/datatable/DataTable.vue
Normal file
422
components/datatable/DataTable.vue
Normal file
@@ -0,0 +1,422 @@
|
||||
<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) : '') + '...Σ' + 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`]"
|
||||
@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 { $copy, $empty, $unique, $multiSort, $remove, $calc, $calculate, $find, $formatNumber, $stripHtml, $calculateData, $deleterow } = 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) {
|
||||
let arr = pagedata.fields.filter(v=>v.show)
|
||||
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>
|
||||
402
components/datatable/DataView.vue
Normal file
402
components/datatable/DataView.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<TimeOption
|
||||
v-bind="{ pagename: vpagename, api: api, timeopt: timeopt, filter: optfilter, importdata: props.importdata, newDataAvailable: newDataAvailable, params: vparams }"
|
||||
ref="timeopt" @option="timeOption" @excel="exportExcel" @add="insert" @manual-refresh="manualRefresh" @refresh-data="refreshData"
|
||||
@import="openImportModal" class="mb-3" v-if="timeopt"></TimeOption>
|
||||
<DataTable v-bind="{ pagename: vpagename }" @edit="edit" @insert="insert" @dataevent="dataEvent" v-if="pagedata" />
|
||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TimeOption from '~/components/datatable/TimeOption'
|
||||
import { useStore } from '~/stores/index'
|
||||
import { ref, watch, onBeforeUnmount } from 'vue'
|
||||
|
||||
const emit = defineEmits(['modalevent', 'dataevent', 'dataUpdated'])
|
||||
const store = useStore()
|
||||
|
||||
const props = defineProps({
|
||||
pagename: String,
|
||||
api: String,
|
||||
setting: String,
|
||||
filter: Object,
|
||||
params: Object,
|
||||
data: Object,
|
||||
modal: Object,
|
||||
timeopt: Object,
|
||||
realtime: Object,
|
||||
importdata: Object
|
||||
})
|
||||
|
||||
const { $copy, $find, $findapi, $getapi, $setpage, $clone, $stripHtml, $snackbar, $dayjs } = useNuxtApp()
|
||||
|
||||
const showmodal = ref()
|
||||
const pagedata = ref()
|
||||
const newDataAvailable = ref(false)
|
||||
const pendingNewData = ref(null)
|
||||
const lastDataHash = ref(null)
|
||||
const pollingInterval = ref(null)
|
||||
|
||||
let vpagename = props.pagename
|
||||
let vfilter = props.filter ? $copy(props.filter) : undefined
|
||||
let vparams = props.params ? $copy(props.params) : undefined
|
||||
let connection = undefined
|
||||
let optfilter = props.filter || (props.params ? props.params.filter : undefined)
|
||||
|
||||
const realtimeConfig = ref({ time: 0, update: "true" })
|
||||
|
||||
if (props.realtime) {
|
||||
realtimeConfig.value = { time: props.realtime.time || 0, update: props.realtime.update }
|
||||
}
|
||||
|
||||
if (vparams?.filter) {
|
||||
for (const [key, value] of Object.entries(vparams.filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
vparams.filter[key] = store[value.replace('$', '')].id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateDataHash = (data) => {
|
||||
if (!data) return null
|
||||
try {
|
||||
// Use a replacer with JSON.stringify to create a stable string representation
|
||||
// by sorting the keys of any object. This ensures the hash is consistent
|
||||
// even if the API returns objects with keys in a different order.
|
||||
const replacer = (key, value) =>
|
||||
value && typeof value === 'object' && !Array.isArray(value)
|
||||
? Object.keys(value)
|
||||
.sort()
|
||||
.reduce((sorted, key) => {
|
||||
sorted[key] = value[key];
|
||||
return sorted;
|
||||
}, {})
|
||||
: value;
|
||||
|
||||
const stringToHash = JSON.stringify(data, replacer);
|
||||
|
||||
return stringToHash.split('').reduce((a, b) => {
|
||||
a = ((a << 5) - a) + b.charCodeAt(0)
|
||||
return a & a
|
||||
}, 0)
|
||||
} catch (e) {
|
||||
console.error('Error generating data hash:', e);
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const startAutoCheck = () => {
|
||||
if (pollingInterval.value) clearInterval(pollingInterval.value)
|
||||
if (realtimeConfig.value.time && realtimeConfig.value.time > 0) {
|
||||
pollingInterval.value = setInterval(() => checkDataChanges(), realtimeConfig.value.time * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const checkDataChanges = async () => {
|
||||
try {
|
||||
const connlist = []
|
||||
const conn1 = $findapi(props.api)
|
||||
|
||||
if (vfilter) {
|
||||
const filter = $copy(conn1.params.filter) || {}
|
||||
for (const [key, value] of Object.entries(vfilter)) {
|
||||
filter[key] = value
|
||||
}
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
filter[key] = store[value.replace('$', '')].id
|
||||
}
|
||||
}
|
||||
conn1.params.filter = filter
|
||||
}
|
||||
|
||||
if (vparams) conn1.params = $copy(vparams)
|
||||
|
||||
delete conn1.params.sort
|
||||
delete conn1.params.values
|
||||
|
||||
conn1.params.summary = 'aggregate'
|
||||
conn1.params.distinct_values = JSON.stringify({
|
||||
total_count: { type: 'Count', field: 'id' },
|
||||
last_updated: { type: 'Max', field: 'update_time' },
|
||||
last_created: { type: 'Max', field: 'create_time' }
|
||||
})
|
||||
|
||||
connlist.push(conn1)
|
||||
|
||||
const rs = await $getapi(connlist)
|
||||
const obj = $find(rs, { name: props.api })
|
||||
const newMetadata = obj ? obj.data.rows : {}
|
||||
const newHash = generateDataHash(newMetadata)
|
||||
|
||||
if (lastDataHash.value === null) {
|
||||
lastDataHash.value = newHash
|
||||
return
|
||||
}
|
||||
|
||||
if (newHash !== lastDataHash.value) {
|
||||
lastDataHash.value = newHash
|
||||
|
||||
if (realtimeConfig.value.update === "true") {
|
||||
await loadFullDataAsync()
|
||||
emit('dataUpdated', { newData: store[vpagename].data, autoUpdate: true, hasChanges: true })
|
||||
} else {
|
||||
newDataAvailable.value = true
|
||||
emit('dataUpdated', { newData: null, autoUpdate: false, hasChanges: true })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadFullDataAsync = async () => {
|
||||
try {
|
||||
const connlist = []
|
||||
const conn1 = $findapi(props.api)
|
||||
|
||||
if (vfilter) {
|
||||
const filter = $copy(conn1.params.filter) || {}
|
||||
for (const [key, value] of Object.entries(vfilter)) {
|
||||
filter[key] = value
|
||||
}
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
filter[key] = store[value.replace('$', '')].id
|
||||
}
|
||||
}
|
||||
conn1.params.filter = filter
|
||||
}
|
||||
|
||||
if (vparams) conn1.params = $copy(vparams)
|
||||
|
||||
delete conn1.params.summary
|
||||
delete conn1.params.distinct_values
|
||||
|
||||
connlist.push(conn1)
|
||||
|
||||
const rs = await $getapi(connlist)
|
||||
const obj = $find(rs, { name: props.api })
|
||||
const newData = obj ? $copy(obj.data.rows) : []
|
||||
|
||||
updateDataDisplay(newData)
|
||||
} catch (error) {
|
||||
console.error('Error loading full data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const openImportModal = () => {
|
||||
const copy = $copy(props.importdata)
|
||||
showmodal.value = copy
|
||||
}
|
||||
|
||||
const updateDataDisplay = (newData) => {
|
||||
const copy = $clone(store[vpagename])
|
||||
copy.data = newData
|
||||
copy.update = { data: newData }
|
||||
store.commit(vpagename, copy)
|
||||
newDataAvailable.value = false
|
||||
pendingNewData.value = null
|
||||
}
|
||||
|
||||
const manualRefresh = () => {
|
||||
if (pendingNewData.value) {
|
||||
updateDataDisplay(pendingNewData.value)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshData = async () => {
|
||||
if (pollingInterval.value) clearInterval(pollingInterval.value);
|
||||
newDataAvailable.value = false;
|
||||
pendingNewData.value = null;
|
||||
|
||||
await getApi();
|
||||
|
||||
// After a manual refresh, force a metadata check to get the correct new hash.
|
||||
lastDataHash.value = null;
|
||||
await checkDataChanges();
|
||||
|
||||
newDataAvailable.value = false;
|
||||
startAutoCheck();
|
||||
}
|
||||
|
||||
watch(() => props.realtime, (newVal) => {
|
||||
if (newVal) {
|
||||
realtimeConfig.value.time = newVal.time || 0
|
||||
realtimeConfig.value.update = newVal.update === true
|
||||
startAutoCheck()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (pollingInterval.value) clearInterval(pollingInterval.value)
|
||||
})
|
||||
|
||||
const timeOption = (v) => {
|
||||
if (!v) return getApi()
|
||||
|
||||
if (v.filter_or) {
|
||||
if (vfilter) vfilter = undefined
|
||||
if (vparams) {
|
||||
vparams.filter_or = v.filter_or
|
||||
} else {
|
||||
const found = $copy($findapi(props.api))
|
||||
found.params.filter_or = v.filter_or
|
||||
if (props.filter) {
|
||||
const filter = $copy(props.filter)
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
filter[key] = store[value.replace('$', '')].id
|
||||
}
|
||||
}
|
||||
found.params.filter = filter
|
||||
}
|
||||
vparams = found.params
|
||||
}
|
||||
return getApi()
|
||||
}
|
||||
|
||||
let filter = vfilter ? vfilter : (props.params ? props.params.filter || {} : {})
|
||||
for (const [key, value] of Object.entries(v.filter)) {
|
||||
filter[key] = value
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
filter[key] = store[value.replace('$', '')].id
|
||||
}
|
||||
}
|
||||
|
||||
if (vfilter) {
|
||||
vfilter = filter
|
||||
vparams = undefined
|
||||
} else if (vparams) {
|
||||
vparams.filter = filter
|
||||
vparams.filter_or = undefined
|
||||
}
|
||||
|
||||
if (!vfilter && !vparams) vfilter = filter
|
||||
getApi()
|
||||
}
|
||||
|
||||
const edit = (v) => {
|
||||
const copy = props.modal ? $copy(props.modal) : {}
|
||||
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api, row: v }
|
||||
f.pagename = vpagename
|
||||
f.row = v
|
||||
copy.vbind = f
|
||||
showmodal.value = copy
|
||||
}
|
||||
|
||||
const insert = () => {
|
||||
const copy = props.modal ? $copy(props.modal) : {}
|
||||
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api }
|
||||
f.pagename = vpagename
|
||||
copy.vbind = f
|
||||
showmodal.value = copy
|
||||
}
|
||||
|
||||
const getApi = async () => {
|
||||
const connlist = []
|
||||
let row = props.setting?.id ? $copy(props.setting) : undefined
|
||||
|
||||
if (!row) {
|
||||
const found = $find(store.settings.filter(v => v), props.setting > 0 ? { id: props.setting } : { name: props.setting })
|
||||
if (found) row = $copy(found)
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
const conn = $findapi('usersetting')
|
||||
conn.params.filter = props.setting > 0 ? { id: props.setting } : { name: props.setting }
|
||||
connlist.push(conn)
|
||||
}
|
||||
|
||||
let data = props.data ? $copy(props.data) : undefined
|
||||
|
||||
if (!data) {
|
||||
const conn1 = $findapi(props.api)
|
||||
if (vfilter) {
|
||||
const filter = conn1.params.filter || {}
|
||||
for (const [key, value] of Object.entries(vfilter)) {
|
||||
filter[key] = value
|
||||
}
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
filter[key] = store[value.replace('$', '')].id
|
||||
}
|
||||
}
|
||||
conn1.params.filter = filter
|
||||
}
|
||||
if (vparams) conn1.params = vparams
|
||||
connection = conn1
|
||||
connlist.push(conn1)
|
||||
}
|
||||
|
||||
let obj = undefined
|
||||
if (connlist.length > 0) {
|
||||
const rs = await $getapi(connlist)
|
||||
const ele = $find(rs, { name: 'usersetting' })
|
||||
if (ele) {
|
||||
row = $find(ele.data.rows, { name: props.setting.name || props.setting })
|
||||
const copy = $copy(store.settings)
|
||||
copy.push(row)
|
||||
store.commit('settings', copy)
|
||||
}
|
||||
obj = $find(rs, { name: props.api })
|
||||
if (obj) data = $copy(obj.data.rows)
|
||||
}
|
||||
|
||||
pagedata.value = $setpage(vpagename, row, obj)
|
||||
const copy = $clone(pagedata.value)
|
||||
copy.data = data
|
||||
copy.update = { data: data }
|
||||
store.commit(vpagename, copy)
|
||||
|
||||
// lastDataHash.value = generateDataHash(data); // This is now handled by checkDataChanges after a refresh.
|
||||
}
|
||||
|
||||
const dataEvent = (v, field, data) => {
|
||||
if (data?.modal) {
|
||||
const copy = $copy(data.modal)
|
||||
const f = copy.vbind ? copy.vbind : {}
|
||||
if (!f.api) f.api = props.api
|
||||
f.pagename = vpagename
|
||||
if (!f.row) f.row = v
|
||||
copy.vbind = f
|
||||
copy.field = field
|
||||
showmodal.value = copy
|
||||
}
|
||||
emit('modalevent', { name: 'dataevent', data: { row: v, field: field } })
|
||||
emit('dataevent', v, field)
|
||||
}
|
||||
|
||||
const exportExcel = async () => {
|
||||
if (!props.api) return
|
||||
|
||||
const found = $findapi('exportcsv')
|
||||
found.params = connection.params
|
||||
const fields = pagedata.value.fields
|
||||
.filter(v => (v.show && v.export !== 'no') || v.export === 'yes')
|
||||
.map(x => ({ name: x.name, label: $stripHtml(x.label) }))
|
||||
found.params.fields = JSON.stringify(fields)
|
||||
found.url = connection.url.replace('data/', 'exportcsv/')
|
||||
const rs = await $getapi([found])
|
||||
|
||||
if (rs === 'error') {
|
||||
$snackbar('Đã xảy ra lỗi. Vui lòng thử lại.')
|
||||
return
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([rs[0].data]))
|
||||
const link = document.createElement('a')
|
||||
const fileName = `${$dayjs(new Date()).format('YYYYMMDDhhmmss')}-data.csv`
|
||||
link.href = url
|
||||
link.setAttribute('download', fileName)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
}
|
||||
|
||||
if (!props.timeopt) await getApi()
|
||||
startAutoCheck()
|
||||
</script>
|
||||
82
components/datatable/EditLabel.vue
Normal file
82
components/datatable/EditLabel.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<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 px-2 is-primary" @click="add()">
|
||||
<span>
|
||||
<SvgIcon v-bind="{name: 'add1.png', type: 'white', size: 17}"></SvgIcon></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="control" @click="remove(i)" v-if="(i>0)">
|
||||
<a class="button px-2 is-dark">
|
||||
<span>
|
||||
<SvgIcon v-bind="{name: 'bin.svg', type: 'white', size: 17}"></SvgIcon>
|
||||
</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 has-text-white" @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.$remove(this.arr, i)
|
||||
},
|
||||
checkError() {
|
||||
let error = false
|
||||
this.arr.map(v=>{
|
||||
if(this.$empty(v.label)) {
|
||||
v.error = 'Nội dung không được bỏ trống'
|
||||
error = true
|
||||
}
|
||||
})
|
||||
if(error) this.arr = this.$copy(this.arr)
|
||||
return error
|
||||
},
|
||||
update() {
|
||||
if(this.checkError()) return
|
||||
let label = ''
|
||||
if(this.arr.length>1) {
|
||||
this.arr.map((v,i)=>{
|
||||
label += `<p${i<this.arr.length-1? ' style="border-bottom: 1px solid white;"' : ''}>${v.label.trim()}</p>`
|
||||
})
|
||||
label = `<div>${label}</div>`
|
||||
} else label = this.arr[0].label.trim()
|
||||
this.$emit('modalevent', {name: 'label', data: label})
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
88
components/datatable/FieldAttribute.vue
Normal file
88
components/datatable/FieldAttribute.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="keys.length>0">
|
||||
<div class="field is-horizontal" v-for="(v,i) in keys" :key="i">
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<input class="input fs-14" type="text" placeholder="" v-model="keys[i]">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input fs-14" type="text" placeholder="" v-model="values[i]">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<p class="control">
|
||||
<a @click="addAttr()">
|
||||
<SvgIcon v-bind="{name: 'add1.png', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<a class="ml-2" @click="remove(i)">
|
||||
<SvgIcon v-bind="{name: 'bin1.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<a class="ml-2" @click="jsonData(v, i)">
|
||||
<SvgIcon v-bind="{name: 'apps.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="mb-6" v-else>
|
||||
<button class="button is-primary has-text-white" @click="addAttr()">Thêm thuộc tính</button>
|
||||
</div>
|
||||
<div class="buttons mt-5">
|
||||
<a class="button is-primary has-text-white" @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.$remove(this.keys, i)
|
||||
this.$remove(this.values, i)
|
||||
},
|
||||
update() {
|
||||
let obj = {}
|
||||
this.keys.map((v,i)=>{
|
||||
if(!this.$empty(v)) obj[v] = v.indexOf('__in')>0? this.values[i].split(',') : this.values[i]
|
||||
})
|
||||
this.$emit('update', obj)
|
||||
this.$emit('modalevent', {name: 'update', data: obj})
|
||||
if(this.close) this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
170
components/datatable/FilterOption.vue
Normal file
170
components/datatable/FilterOption.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="field mt-3 mb-1" v-if="field.format==='number'">
|
||||
<label class="label fs-14">Chọn trường<span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<b-taginput
|
||||
size="is-small"
|
||||
v-model="tagsField"
|
||||
:data="pageData? pageData.fields.filter(v=>v.format==='number') : []"
|
||||
type="is-dark is-light"
|
||||
autocomplete
|
||||
:open-on-focus="true"
|
||||
field="name"
|
||||
icon="plus"
|
||||
placeholder="Chọn trường"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<span class="mr-3 has-text-danger has-text-weight-bold"> {{props.option.name}}</span>
|
||||
<span :class="tagsField.find(v=>v.id===props.option.id)? 'has-text-dark' : ''"> {{$stripHtml(props.option.label, 60)}} </span>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
Không có trường thỏa mãn
|
||||
</template>
|
||||
</b-taginput>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tagsField')"> {{errors.find(v=>v.name==='tagsField').message}} </p>
|
||||
</div>
|
||||
<div class="mt-2" v-if="tagsField.length>0">
|
||||
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name"
|
||||
class="tag is-rounded" v-for="(v,i) in tagsField" :key="i">
|
||||
<span class="tooltip">
|
||||
{{ v.name }}
|
||||
<span class="tooltiptext" style="top: 60%; bottom: unset; min-width: max-content; left: 25px;">{{ $stripHtml(v.label) }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-3">
|
||||
<div class="field-body">
|
||||
<div class="field" v-if="field.format==='number'">
|
||||
<label class="label fs-14">Biểu thức có dạng Đúng / Sai <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control is-expanded">
|
||||
<input class="input is-small" type="text" v-model="expression">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='expression')"> {{errors.find(v=>v.name==='expression').message}} </p>
|
||||
</div>
|
||||
|
||||
<div class="field" v-else>
|
||||
<label class="label"> Chuỗi kí tự <span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<p class="control">
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="searchText"
|
||||
@change="changeStyle()"
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'searchText')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "searchText").msg }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field is-narrow" v-if="filterType==='color'">
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" v-model="color" @change="changeStyle()">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='color')"> {{errors.find(v=>v.name==='color').message}} </p>
|
||||
</div>
|
||||
<div class="field is-narrow" v-else-if="filterType==='size'">
|
||||
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" placeholder="Nhập số" v-model="size" @change="changeStyle()">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='size')"> {{errors.find(v=>v.name==='size').message}} </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['filterObj', 'filterType', 'pagename', 'field'],
|
||||
data() {
|
||||
return {
|
||||
tagsField: [],
|
||||
expression: undefined,
|
||||
form: undefined,
|
||||
color: undefined,
|
||||
size: undefined,
|
||||
errors: [],
|
||||
searchText: undefined
|
||||
}
|
||||
},
|
||||
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: {
|
||||
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>
|
||||
207
components/datatable/FormatOption.vue
Normal file
207
components/datatable/FormatOption.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div v-if="['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
|
||||
<p class="has-text-right has-text-grey is-italic fs-13">
|
||||
Màu sắc sẽ hiển thị theo điều kiện Đúng / Sai, mã lệnh do hệ thống tự sinh
|
||||
</p>
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<li v-for="(v, i) in tabs" :key="i" :class="tab.code === v.code ? 'is-active' : ''" @click="tab = v">
|
||||
<a>{{ v.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="tab.code === 'expression' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
|
||||
<template v-if="radio ? radio.code === 'condition' && sideBar === 'bgcolor' : false">
|
||||
<div v-for="(v, i) in bgcolorFilter" :key="v.id" class="px-4">
|
||||
<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" :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">
|
||||
<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" :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">
|
||||
<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" :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>
|
||||
</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-text">
|
||||
<SvgIcon class="mr-2" v-bind="{ name: 'copy.svg', type: 'primary', siz: 18 }"></SvgIcon>
|
||||
<span class="fs-16">Copy</span>
|
||||
</span>
|
||||
</a>
|
||||
<a @click="paste()" class="mr-6">
|
||||
<span class="icon-text">
|
||||
<SvgIcon class="mr-2" v-bind="{ name: 'pen1.svg', type: 'primary', siz: 18 }"></SvgIcon>
|
||||
<span class="fs-16">Paste</span>
|
||||
</span>
|
||||
</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
|
||||
<SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon>
|
||||
</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
|
||||
<SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon>
|
||||
</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>
|
||||
</template>
|
||||
<script setup>
|
||||
// FilterOption: () => import("@/components/datatable/FilterOption"),
|
||||
// TableOption: () => import("@/components/datatable/TableOption"),
|
||||
//CreateTemplate: () => import("@/components/datatable/CreateTemplate")
|
||||
import CreateTemplate from "~/components/datatable/CreateTemplate";
|
||||
const { $id, $copy, $empty, $stripHtml } = useNuxtApp();
|
||||
var props = defineProps({
|
||||
event: Object,
|
||||
currentField: Object,
|
||||
pagename: String,
|
||||
});
|
||||
var currentField = props.currentField;
|
||||
var event = props.event;
|
||||
var openField = {};
|
||||
var bgcolorFilter = [];
|
||||
var colorFilter = [];
|
||||
var sizeFilter = [];
|
||||
var sideBar = undefined;
|
||||
var script = undefined;
|
||||
var radio = undefined;
|
||||
var tabs = [
|
||||
{ code: "expression", name: "Biểu thức" },
|
||||
{ code: "script", name: "Mã lệnh" },
|
||||
];
|
||||
var tab = { code: "expression", name: "Biểu thức" };
|
||||
var source = undefined;
|
||||
var target = $copy(currentField.name);
|
||||
|
||||
const initData = function () {
|
||||
openField = event.field;
|
||||
sideBar = event.name;
|
||||
script = event.script;
|
||||
radio = event.radio;
|
||||
let field = event.field;
|
||||
bgcolorFilter = [{ id: $id() }];
|
||||
if (field.bgcolor) {
|
||||
if (Array.isArray(field.bgcolor)) bgcolorFilter = $copy(field.bgcolor);
|
||||
}
|
||||
colorFilter = [{ id: $id() }];
|
||||
if (field.color) {
|
||||
if (Array.isArray(field.color)) colorFilter = $copy(field.color);
|
||||
}
|
||||
sizeFilter = [{ id: $id() }];
|
||||
if (field.textsize) {
|
||||
if (Array.isArray(field.textsize)) sizeFilter = field.textsize;
|
||||
}
|
||||
};
|
||||
|
||||
const doCheck = function () {
|
||||
let text = window.getSelection().toString();
|
||||
if ($empty(text)) return;
|
||||
source = text;
|
||||
};
|
||||
const replace = function () {
|
||||
if ($empty(script)) return;
|
||||
script = script.replaceAll(source, target);
|
||||
};
|
||||
const paste = async function () {
|
||||
script = await navigator.clipboard.readText();
|
||||
};
|
||||
const addCondition = function (arr) {
|
||||
arr.push({ id: $id() });
|
||||
};
|
||||
const removeCondition = function (arr, i) {
|
||||
$delete(arr, i);
|
||||
};
|
||||
const copyContent = function (value) {
|
||||
$copyToClipboard(value);
|
||||
};
|
||||
const checkScript = function () {
|
||||
if ($empty(script)) return;
|
||||
try {
|
||||
JSON.parse(script);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const changeScript = function () {
|
||||
if (!checkScript()) return;
|
||||
let copy = $copy(openField);
|
||||
copy[sideBar] = JSON.parse(script);
|
||||
$emit("modalevent", { name: "updatefields", data: copy });
|
||||
};
|
||||
const doConditionFilter = function (v, type, id) {
|
||||
v.id = id;
|
||||
let copy = $copy(currentField);
|
||||
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];
|
||||
$emit("modalevent", { name: "updatefields", data: copy });
|
||||
};
|
||||
initData();
|
||||
console.log(sideBar);
|
||||
</script>
|
||||
161
components/datatable/MenuSave.vue
Normal file
161
components/datatable/MenuSave.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="mb-4" v-if="currentsetting ? currentsetting.user === login.id : false">
|
||||
<p class="fs-16 has-text-findata">
|
||||
Đang mở: <b>{{ $stripHtml(currentsetting.name, 40) }}</b>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14">Chọn chế độ lưu <span class="has-text-danger"> * </span></label>
|
||||
<div class="control is-expanded fs-14">
|
||||
<a class="mr-5" v-if="isOverwrite()" @click="changeType('overwrite')">
|
||||
<span class="icon-text">
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: radioSave === 'overwrite' ? 'radio-checked.svg' : 'radio-unchecked.svg',
|
||||
type: 'gray',
|
||||
size: 22,
|
||||
}"
|
||||
></SvgIcon>
|
||||
Ghi đè
|
||||
</span>
|
||||
</a>
|
||||
<a @click="changeType('new')">
|
||||
<span class="icon-text">
|
||||
<SvgIcon
|
||||
v-bind="{ name: radioSave === 'new' ? 'radio-checked.svg' : 'radio-unchecked.svg', type: 'gray', size: 22 }"
|
||||
></SvgIcon>
|
||||
Tạo mới
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="radioSave === 'new'">
|
||||
<div class="field mt-4 px-0 mx-0">
|
||||
<label class="label fs-14">Tên thiết lập <span class="has-text-danger"> * </span></label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="name" ref="name" v-on:keyup.enter="saveSetting" />
|
||||
</div>
|
||||
<div class="help has-text-danger" v-if="errors.find((v) => v.name === 'name')">
|
||||
{{ errors.find((v) => v.name === "name").msg }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-4 px-0 mx-0">
|
||||
<label class="label fs-14"> Mô tả </label>
|
||||
<p class="control is-expanded">
|
||||
<textarea class="textarea" rows="4" v-model="note"></textarea>
|
||||
</p>
|
||||
</div>
|
||||
<!--
|
||||
<div class="field mt-4 px-0 mx-0">
|
||||
<label class="label fs-14">Loại thiết lập <span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<div class="control is-expanded fs-14">
|
||||
<span class="mr-4" v-for="(v,i) in $filter(store.settingtype, {code: ['private', 'public']})">
|
||||
<a class="icon-text" @click="changeOption(v)">
|
||||
<SvgIcon v-bind="{name: `radio-${radioOption===v.code? '' : 'un'}checked.svg`, type: radioOption===v.code? 'primary' : 'gray', size: 22}"></SvgIcon>
|
||||
</a>
|
||||
{{v.name}}
|
||||
</span>
|
||||
</div>
|
||||
</div>-->
|
||||
</template>
|
||||
<div class="field mt-5 px-0 mx-0">
|
||||
<label class="label fs-14" v-if="status !== undefined" :class="status ? 'has-text-primary' : 'has-text-danger'">
|
||||
{{ status ? "Lưu thiết lập thành công." : "Lỗi. Lưu thiết lập thất bại." }}
|
||||
</label>
|
||||
<p class="control is-expanded">
|
||||
<a class="button is-primary has-text-white" @click="saveSetting()">Lưu lại</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useStore } from "@/stores/index";
|
||||
const emit = defineEmits([]);
|
||||
const store = useStore();
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
classify: String,
|
||||
option: String,
|
||||
data: Object,
|
||||
focus: Boolean,
|
||||
});
|
||||
const { $empty, $copy, $filter, $stripHtml, $updateapi, $insertapi, $findIndex, $snackbar } = useNuxtApp();
|
||||
var pagename = props.pagename;
|
||||
var radioOption = ref();
|
||||
var login = { id: 1 };
|
||||
var errors = [];
|
||||
var radioType = undefined;
|
||||
var radioDefault = 0;
|
||||
var radioSave = ref("new");
|
||||
var note = undefined;
|
||||
var status = undefined;
|
||||
var name = undefined;
|
||||
var currentsetting = undefined;
|
||||
var pagedata = store[props.pagename];
|
||||
async function saveSetting() {
|
||||
errors = [];
|
||||
let detail = pagename ? { fields: pagedata.fields } : {};
|
||||
if (pagename) {
|
||||
let element = pagedata.tablesetting || {};
|
||||
if (element !== store.originsetting) detail.tablesetting = element;
|
||||
if (pagedata.filters ? pagedata.filters.length > 0 : false) {
|
||||
detail.filters = pagedata.filters;
|
||||
}
|
||||
}
|
||||
if (props.option) detail.option = props.option;
|
||||
if (props.data) detail.data = props.data;
|
||||
let data = {
|
||||
user: login.id,
|
||||
name: name,
|
||||
detail: detail,
|
||||
note: note,
|
||||
type: radioType.id,
|
||||
classify: props.classify ? props.classify : store.settingclass.find((v) => v.code === "data-field").id,
|
||||
default: radioDefault,
|
||||
update_time: new Date(),
|
||||
};
|
||||
let result;
|
||||
if (radioSave.value === "new") {
|
||||
if ($empty(name)) {
|
||||
return errors.push({ name: "name", msg: "Tên thiết lập không được bỏ trống" });
|
||||
}
|
||||
result = await $insertapi("usersetting", data);
|
||||
} else {
|
||||
let copy = $copy(currentsetting);
|
||||
copy.detail = detail;
|
||||
copy.update_time = new Date();
|
||||
result = await $updateapi("usersetting", copy);
|
||||
}
|
||||
if (radioSave.value === "new") {
|
||||
emit("modalevent", { name: "opensetting", data: result });
|
||||
} else {
|
||||
let idx = $findIndex(store.settings, { id: result.id });
|
||||
if (idx >= 0) {
|
||||
let copy = $copy(store.settings);
|
||||
copy[idx] = result;
|
||||
store.commit("settings", copy);
|
||||
}
|
||||
$snackbar("Lưu thiết lập thành công");
|
||||
emit("modalevent", { name: "updatesetting", data: result });
|
||||
emit("close");
|
||||
}
|
||||
}
|
||||
function isOverwrite() {
|
||||
return true;
|
||||
}
|
||||
function changeType(value) {
|
||||
radioSave.value = value;
|
||||
}
|
||||
function changeOption(v) {
|
||||
radioOption.value = v.code;
|
||||
}
|
||||
function initData() {
|
||||
radioType = store.settingtype.find((v) => v.code === "private");
|
||||
if (props.pagename) currentsetting = $copy(pagedata.setting ? pagedata.setting : undefined);
|
||||
if (!currentsetting) radioSave.value = "new";
|
||||
else if (currentsetting.user !== login.id) radioSave.value = "new";
|
||||
else radioSave.value = "overwrite";
|
||||
}
|
||||
initData();
|
||||
</script>
|
||||
159
components/datatable/ModelInfo.vue
Normal file
159
components/datatable/ModelInfo.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
<li :class="`${v.code === tab ? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`" v-for="v in tabs">
|
||||
<a @click="changeTab(v)">{{ v.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template v-if="tab === 'datatype'">
|
||||
<Caption class="mb-3" v-bind="{ title: 'Kiểu dữ liệu (type)', type: 'has-text-warning' }"></Caption>
|
||||
<div class="py-1 border-bottom is-clickable" v-for="x in current.fields">
|
||||
{{ x.name }}
|
||||
<span class="ml-6 has-text-grey">{{ x.type }}</span>
|
||||
<a class="ml-6 has-text-primary" v-if="x.model" @click="openModel(x)">{{ x.model }}</a>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="columns mx-0 mb-0 pb-0">
|
||||
<div class="column is-7">
|
||||
<Caption class="mb-2" v-bind="{ title: 'Values', type: 'has-text-warning' }"></Caption>
|
||||
<input class="input" rows="1" v-model="values" />
|
||||
</div>
|
||||
<div class="column is-4s">
|
||||
<Caption class="mb-2" v-bind="{ title: 'Filter', type: 'has-text-warning' }"></Caption>
|
||||
<input class="input" rows="1" v-model="filter" />
|
||||
</div>
|
||||
<div class="column is-1">
|
||||
<Caption class="mb-2" v-bind="{ title: 'Load', type: 'has-text-warning' }"></Caption>
|
||||
<div>
|
||||
<button class="button is-primary has-text-white" @click="loadData()">Load</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Caption class="mb-2" v-bind="{ title: 'Query', type: 'has-text-warning' }"></Caption>
|
||||
<div class="mb-4">
|
||||
{{ query }}
|
||||
<a class="has-text-primary ml-5" @click="copy()">copy</a>
|
||||
<p>
|
||||
{{ apiUrl }}
|
||||
<a class="has-text-primary ml-5" @click="$copyToClipboard(apiUrl)">copy</a>
|
||||
<a class="has-text-primary ml-5" target="_blank" :href="apiUrl">open</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Caption class="mb-2" v-bind="{ title: 'Data', type: 'has-text-warning' }"></Caption>
|
||||
<DataTable v-bind="{ pagename: pagename }" v-if="pagedata" />
|
||||
</div>
|
||||
</template>
|
||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useStore } from "@/stores/index";
|
||||
var props = defineProps({
|
||||
data: Array,
|
||||
info: Object,
|
||||
});
|
||||
const { $getdata, $getapi, $createField, $clone, $getpage, $empty, $copyToClipboard, $find } = useNuxtApp();
|
||||
const store = useStore();
|
||||
var pagename = "pagedata99";
|
||||
var pagedata = ref();
|
||||
pagedata.value = $getpage();
|
||||
store.commit(pagename, pagedata);
|
||||
let list = ["LogEntry", "Permission", "ContentType", "Session", "Group"];
|
||||
var current = ref({ fields: [] });
|
||||
var tabs = [
|
||||
{ code: "datatype", name: "Kiểu dữ liệu" },
|
||||
{ code: "table", name: "Dữ liệu" },
|
||||
];
|
||||
var tab = ref("datatype");
|
||||
var datatable = ref();
|
||||
var query = ref();
|
||||
var values, filter;
|
||||
var apiUrl = ref();
|
||||
var showmodal = ref();
|
||||
var data = props.data;
|
||||
current.value = props.info;
|
||||
function changeMenu(v) {
|
||||
values = undefined;
|
||||
filter = undefined;
|
||||
current.value = v;
|
||||
if (tab.value === "table") loadData();
|
||||
}
|
||||
async function changeTab(v) {
|
||||
tab.value = v.code;
|
||||
if (v.code === "table") loadData();
|
||||
}
|
||||
async function loadData() {
|
||||
let vfilter = filter ? filter.trim() : undefined;
|
||||
if (vfilter) {
|
||||
try {
|
||||
vfilter = JSON.parse(vfilter);
|
||||
} catch (error) {
|
||||
alert("Cấu trúc filter có lỗi");
|
||||
vfilter = undefined;
|
||||
}
|
||||
}
|
||||
let params = { values: values ? values.trim() : undefined, filter: filter };
|
||||
let modelName = current.value.model;
|
||||
let found = {
|
||||
name: modelName.toLowerCase().replace("_", ""),
|
||||
url: `data/${modelName}/`,
|
||||
url_detail: `data-detail/${modelName}/`,
|
||||
params: params,
|
||||
};
|
||||
query.value = $clone(found);
|
||||
let rs = await $getapi([found]);
|
||||
if (rs === "error") return alert("Đã xảy ra lỗi, hãy xem lại câu lệnh.");
|
||||
datatable.value = rs[0].data.rows;
|
||||
showData();
|
||||
|
||||
// api query
|
||||
const baseUrl = "https://api.y99.vn/" + `${query.value.url}`;
|
||||
apiUrl.value = baseUrl;
|
||||
let vparams = !$empty(values) ? { values: values } : null;
|
||||
if (!$empty(filter)) {
|
||||
vparams = vparams ? { values: values, filter: filter } : { filter: filter };
|
||||
}
|
||||
if (vparams) {
|
||||
let url = new URL(baseUrl);
|
||||
let searchParams = new URLSearchParams(vparams);
|
||||
url.search = searchParams.toString();
|
||||
apiUrl.value = baseUrl + url.search;
|
||||
}
|
||||
}
|
||||
function showData() {
|
||||
let arr = [];
|
||||
if (!$empty(values)) {
|
||||
let arr1 = values.trim().split(",");
|
||||
arr1.map((v) => {
|
||||
let val = v.trim();
|
||||
let field = $createField(val, val, "string", true);
|
||||
arr.push(field);
|
||||
});
|
||||
} else {
|
||||
current.value.fields.map((v) => {
|
||||
let field = $createField(v.name, v.name, "string", true);
|
||||
arr.push(field);
|
||||
});
|
||||
}
|
||||
let clone = $clone(pagedata.value);
|
||||
clone.fields = arr;
|
||||
clone.data = datatable.value;
|
||||
pagedata.value = undefined;
|
||||
setTimeout(() => (pagedata.value = clone));
|
||||
}
|
||||
function copy() {
|
||||
$copyToClipboard(JSON.stringify(query.value));
|
||||
}
|
||||
function openModel(x) {
|
||||
showmodal.value = {
|
||||
component: "datatable/ModelInfo",
|
||||
title: x.model,
|
||||
width: "70%",
|
||||
height: "600px",
|
||||
vbind: { data: data, info: $find(data, { model: x.model }) },
|
||||
};
|
||||
}
|
||||
</script>
|
||||
s
|
||||
334
components/datatable/NewField.vue
Normal file
334
components/datatable/NewField.vue
Normal file
@@ -0,0 +1,334 @@
|
||||
<template>
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<li :class="selectType.code===v.code? 'is-active fs-16' : 'fs-16'" v-for="v in fieldType">
|
||||
<a @click="selectType = v"><span>{{ v.name }}</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template v-if="selectType.code==='formula'">
|
||||
<b-radio :class="i===1? 'ml-5' : null" v-model="choice" v-for="(v,i) in choices" :key="i"
|
||||
:native-value="v.code">
|
||||
<span :class="v.code===choice? 'fsb-16' : 'fs-16'">{{v.name}}</span>
|
||||
</b-radio>
|
||||
<div class="has-background-light mt-3 px-3 py-3">
|
||||
<div class="tags are-medium mb-0" v-if="choice==='function'">
|
||||
<span :class="`tag ${func===v.code? 'is-primary' : 'is-dark'} is-rounded is-clickable`"
|
||||
v-for="(v,i) in funcs" :key="i" @click="changeFunc(v)" @dblclick="addFunc(v)">{{v.name}}</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="field px-0 mx-0">
|
||||
<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="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 mb-1">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 + ' ' + v.name) : v.name" class="tag is-dark is-rounded is-clickable"
|
||||
v-for="v in tags">
|
||||
{{v.name}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tags">
|
||||
<span 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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<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="3" type="text" :placeholder="placeholder" 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>
|
||||
<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 px-0 mx-0">
|
||||
<label class="label">Tên trường <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control">
|
||||
<input class="input" 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="mt-5">
|
||||
<label class="label">Mô tả<span class="has-text-danger"> *</span></label>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded" >
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="label"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button" @click="editLabel()">
|
||||
<span><SvgIcon v-bind="{name: 'pen.svg', type: 'dark', size: 17}"></SvgIcon></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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-5" v-if="selectType.code==='empty'">
|
||||
<label class="label"
|
||||
>Kiểu dữ liệu
|
||||
<span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<div class="control fs-14">
|
||||
<span class="mr-4" v-for="(v,i) in datatype">
|
||||
<a class="icon-text" @click="changeType(v)">
|
||||
<SvgIcon v-bind="{name: `radio-${radioType.code===v.code? '' : 'un'}checked.svg`, type: 'gray', size: 22}"></SvgIcon>
|
||||
</a>
|
||||
{{v.name}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-5">
|
||||
<p class="control">
|
||||
<a class="button is-primary has-text-white"
|
||||
@click="selectType.code==='formula'? createField() : createEmptyField()">Tạo cột</a>
|
||||
</p>
|
||||
</div>
|
||||
<Modal v-bind="showmodal" v-if="showmodal" @label="changeLabel" @close="close"></Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useStore } from '@/stores/index'
|
||||
import ScrollBox from '~/components/datatable/ScrollBox'
|
||||
const emit = defineEmits(['modalevent'])
|
||||
const store = useStore()
|
||||
const { $id, $copy, $clone, $empty, $stripHtml, $createField, $calc, $isNumber } = useNuxtApp()
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
field: Object,
|
||||
filters: Object,
|
||||
filterData: Object,
|
||||
width: String
|
||||
})
|
||||
const moneyunit = store.moneyunit
|
||||
const datatype = store.datatype
|
||||
var showmodal = ref()
|
||||
var pagedata = store[props.pagename]
|
||||
var selectUnit = moneyunit.find(v=>v.code==='one')
|
||||
var data = []
|
||||
var current = 1
|
||||
var filterData = []
|
||||
var loading = false
|
||||
var fieldType = [{code: 'formula', name: 'Tạo công thức'}, {code: 'empty', name: 'Tạo cột rỗng'}]
|
||||
var errors = []
|
||||
var tags = []
|
||||
var formula = undefined
|
||||
var name = `f${$id().toLocaleLowerCase()}`
|
||||
var label = undefined
|
||||
var errors = []
|
||||
var selectType = fieldType.find(v=>v.code==='empty')
|
||||
var radioType = ref(datatype.find(v=>v.code==='string'))
|
||||
var fields = []
|
||||
var options = undefined
|
||||
var columns = $copy(pagedata.fields.filter(v=>v.format==='number'))
|
||||
var decimal = undefined
|
||||
var choices = [{code: 'column', name: 'Dùng cột dữ liệu'}, {code: 'function', name: 'Dùng hàm số'}]
|
||||
var choice = 'column'
|
||||
var funcs = [{code: 'sum', name: 'Sum'}, {code: 'max', name: 'Max'}, {code: 'min', name: 'Min'}, {code: 'avg', name: 'Avg'}]
|
||||
var func = 'sum'
|
||||
var placeholder = 'Minh hoạ công thức: f10001 + f10002'
|
||||
var args = undefined
|
||||
var 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'}]
|
||||
function editLabel() {
|
||||
if($empty(label)) return
|
||||
showmodal.value = {component: 'datatable/EditLabel', width: '500px', height: '300px', vbind: {label: label}}
|
||||
}
|
||||
function close() {
|
||||
showmodal.value = null
|
||||
}
|
||||
function changeLabel(evt) {
|
||||
label = evt
|
||||
showmodal.value = null
|
||||
}
|
||||
function changeType(v) {
|
||||
radioType.value = v
|
||||
}
|
||||
function addFunc(v) {
|
||||
formula = (formula? formula + ' ' : '') + v.name + '(C0: C2)'
|
||||
}
|
||||
function addOperator(v) {
|
||||
let text = v.code==='iif'? 'a>b? c : d' : v.code
|
||||
formula = `${formula || ''} ${text}`
|
||||
}
|
||||
function changeFunc(v) {
|
||||
placeholder = `${v.name}(C0:C2) hoặc ${v.name}(C0,C1,C2). C là viết tắt của cột dữ liệu, số thứ tự của cột bắt đầu từ 0`
|
||||
func = v.code
|
||||
}
|
||||
function getFields() {
|
||||
fields = pagedata? $copy(pagedata.fields) : []
|
||||
fields.map(v=>v.caption = (v.label? v.label.indexOf('<')>=0 : false)? v.name : v.label)
|
||||
}
|
||||
function checkFunc() {
|
||||
let error = false
|
||||
let val = formula.trim().replaceAll(' ', '')
|
||||
if(val.toLowerCase().indexOf(func)<0) error = true
|
||||
let start = val.toLowerCase().indexOf('(')
|
||||
let end = val.toLowerCase().indexOf(')')
|
||||
if( start<0 || end<0) error = true
|
||||
let content = val.substring(start+1, end)
|
||||
if($empty(content)) error = true
|
||||
let content1 = content.replaceAll(':', ',')
|
||||
let arr = content1.split(',')
|
||||
arr.map(v=>{
|
||||
let arr1 = v.toLowerCase().split('c')
|
||||
if(arr1.length!==2) error = true
|
||||
else if(!$isNumber(arr1[1])) error = true
|
||||
})
|
||||
return error? 'error' : content
|
||||
}
|
||||
function checkValid() {
|
||||
errors = []
|
||||
if(tags.length===0 && choice==='column') {
|
||||
errors.push({name: 'tags', message: 'Chưa chọn trường xây dựng công thức.'})
|
||||
}
|
||||
if(!$empty(formula)? $empty(formula.trim()) : true) {
|
||||
errors.push({name: 'formula', message: 'Công thức không được bỏ trống.'})
|
||||
}
|
||||
if(!$empty(label)? $empty(label.trim()) : true )
|
||||
errors.push({name: 'label', message: 'Mô tả không được bỏ trống.'})
|
||||
else if(pagedata.fields.find(v=>v.label.toLowerCase()===label.toLowerCase())) {
|
||||
errors.push({name: 'label', message: 'Mô tả bị trùng. Hãy đặt mô tả khác.'})
|
||||
}
|
||||
if(errors.length>0) return false
|
||||
//check formula in case use column
|
||||
if(choice==='column') {
|
||||
let val = $copy(formula)
|
||||
tags.forEach(v => {
|
||||
let myRegExp = new RegExp(v.name, 'g')
|
||||
val = val.replace(myRegExp, Math.random())
|
||||
})
|
||||
try {
|
||||
let value = $calc(val)
|
||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
||||
errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err)
|
||||
errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
|
||||
}
|
||||
} else {
|
||||
if(checkFunc()==='error') errors.push({name: 'formula', message: `Hàm ${func.toUpperCase()} không hợp lệ`})
|
||||
}
|
||||
return errors.length>0? false : true
|
||||
}
|
||||
function createField() {
|
||||
if(!checkValid()) return
|
||||
let field = $createField(name.trim(), label.trim(), 'number', true)
|
||||
field.formula = formula.trim().replaceAll(' ', '')
|
||||
if(choice==='function') {
|
||||
field.func = func
|
||||
field.vals = checkFunc()
|
||||
} else field.tags = tags.map(v=>v.name)
|
||||
field.level = Math.max(...pagedata.fields.map(v=>v.level? v.level : 0)) + 1
|
||||
field.unit = selectUnit.detail
|
||||
field.decimal = decimal
|
||||
field.disable = 'search,value'
|
||||
let copy = $copy(pagedata)
|
||||
copy.fields.push(field)
|
||||
store.commit(props.pagename, copy)
|
||||
emit('newfield', field)
|
||||
tags = []
|
||||
formula = undefined
|
||||
label = undefined
|
||||
name = `f${$id()}`
|
||||
emit('close')
|
||||
}
|
||||
function createEmptyField() {
|
||||
errors = []
|
||||
if(!$empty(name)? $empty(name.trim()) : true )
|
||||
errors.push({name: 'name', message: 'Tên không được bỏ trống.'})
|
||||
else if(pagedata.fields.find(v=>v.name.toLowerCase()===name.toLowerCase())) {
|
||||
errors.push({name: 'name', message: 'Tên trường bị trùng. Hãy đặt tên khác.'})
|
||||
}
|
||||
if(!$empty(label)? $empty(label.trim()) : true )
|
||||
errors.push({name: 'label', message: 'Mô tả không được bỏ trống.'})
|
||||
else if(pagedata.fields.find(v=>v.label.toLowerCase()===label.toLowerCase())) {
|
||||
errors.push({name: 'label', message: 'Mô tả bị trùng. Hãy đặt mô tả khác.'})
|
||||
}
|
||||
if(errors.length>0) return
|
||||
let field = $createField(name.trim(), label.trim(), radioType.value.code, true)
|
||||
if(selectType.code==='chart') field = createChartField()
|
||||
let copy = $clone(pagedata)
|
||||
copy.fields.push(field)
|
||||
copy.update = {fields: copy.fields}
|
||||
store.commit(props.pagename, copy)
|
||||
//pagedata = copy
|
||||
emit('newfield', field)
|
||||
label = undefined
|
||||
name = `f${$id()}`
|
||||
emit('close')
|
||||
}
|
||||
function createChartField() {
|
||||
let array = pagedata.fields.filter(v=>v.format==='number' && v.show)
|
||||
if(args) array = $copy(args)
|
||||
let text = ''
|
||||
array.map((v,i)=>text += `'${v.name}${i<array.length-1? "'," : "'"}`)
|
||||
let label = ''
|
||||
array.map((v,i)=>label += `'${$stripHtml(v.label)}${i<array.length-1? "'," : "'"}`)
|
||||
let field = $createField(name.trim(), label.trim(), radioType.value.code, true)
|
||||
field.chart = 'yes'
|
||||
field.template = `<TrendingChart class="is-clickable" v-bind="{row: row, fields: [${text}], labels: [${label}], width: '80', height: '26', 'header': ['stock_code', 'name']}"/>`
|
||||
return field
|
||||
}
|
||||
//============
|
||||
getFields()
|
||||
</script>
|
||||
64
components/datatable/Pagination.vue
Normal file
64
components/datatable/Pagination.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<nav class="pagination mx-0" role="navigation" aria-label="pagination">
|
||||
<ul class="pagination-list" v-if="pageInfo">
|
||||
<li v-for="v in pageInfo">
|
||||
<a v-if="currentPage===v" class="pagination-link is-current has-background-primary has-text-white" :aria-label="`Page ${v}`" aria-current="page">{{ v }}</a>
|
||||
<a v-else href="#" class="pagination-link" :aria-label="`Goto page ${v}`" @click="changePage(v)">{{ v }}</a>
|
||||
</li>
|
||||
<a @click="previous()" class="pagination-previous ml-5">
|
||||
<SvgIcon v-bind="{name: 'left1.svg', type: 'dark', size: 20, alt: 'Tìm kiếm'}"></SvgIcon>
|
||||
</a>
|
||||
<a @click="next()" class="pagination-next">
|
||||
<SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 20, alt: 'Tìm kiếm'}"></SvgIcon>
|
||||
</a>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
<script setup>
|
||||
const emit = defineEmits(['changepage'])
|
||||
var props = defineProps({
|
||||
data: Array,
|
||||
perPage: Number
|
||||
})
|
||||
var currentPage = 1
|
||||
var totalRows = props.data.length
|
||||
var lastPage = parseInt(totalRows / props.perPage)
|
||||
if(lastPage*props.perPage<totalRows) lastPage += 1
|
||||
var pageInfo = ref()
|
||||
function pages(current_page, last_page, onSides = 2) {
|
||||
// pages
|
||||
let pages = [];
|
||||
// Loop through
|
||||
for (let i = 1; i <= last_page; i++) {
|
||||
// Define offset
|
||||
let offset = (i == 1 || last_page) ? onSides + 1 : onSides;
|
||||
// If added
|
||||
if (i == 1 || (current_page - offset <= i && current_page + offset >= i) ||
|
||||
i == current_page || i == last_page) {
|
||||
pages.push(i);
|
||||
} else if (i == current_page - (offset + 1) || i == current_page + (offset + 1)) {
|
||||
pages.push('...');
|
||||
}
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
function changePage(page) {
|
||||
if(page==='...') return
|
||||
currentPage = page
|
||||
pageInfo.value = pages(page, lastPage, 2)
|
||||
emit('changepage', page)
|
||||
}
|
||||
pageInfo.value = pages(1, lastPage, 2)
|
||||
watch(() => props.data, (newVal, oldVal) => {
|
||||
totalRows = props.data.length
|
||||
lastPage = parseInt(totalRows / props.perPage)
|
||||
if(lastPage*props.perPage<totalRows) lastPage += 1
|
||||
pageInfo.value = pages(1, lastPage, 2)
|
||||
})
|
||||
function previous() {
|
||||
if(currentPage>1) changePage(currentPage-1)
|
||||
}
|
||||
function next() {
|
||||
if(currentPage<lastPage) changePage(currentPage+1)
|
||||
}
|
||||
</script>
|
||||
111
components/datatable/ScrollBox.vue
Normal file
111
components/datatable/ScrollBox.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="px-2" :style="`max-height: ${maxheight}; overflow-y: auto;`">
|
||||
<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)">
|
||||
{{ $stripHtml(v[name] || v.fullname || v.code || 'n/a', 75) }}
|
||||
<span class="icon has-text-primary" v-if="checked[i] && notick!==true">
|
||||
<SvgIcon v-bind="{name: 'tick.svg', type: 'primary', size: 15}"></SvgIcon>
|
||||
</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">
|
||||
<SvgIcon v-bind="{name: 'user.svg', type: 'gray', size: 15}"></SvgIcon>
|
||||
<span>{{ v[show.author] }}</span>
|
||||
</span>
|
||||
<span class="icon-text has-text-grey mr-2 fs-13" v-if="show.view">
|
||||
<SvgIcon v-bind="{name: 'view.svg', type: 'gray', size: 15}"></SvgIcon>
|
||||
<span>{{ v[show.view] }}</span>
|
||||
</span>
|
||||
<span class="fs-13 has-text-grey" v-if="show.time">{{$dayjs(v['create_time']).fromNow(true)}}</span>
|
||||
<span class="tooltip">
|
||||
<a class="icon ml-1" v-if="show.link" @click="doClick(v,i, 'newtab')">
|
||||
<SvgIcon v-bind="{name: 'opennew.svg', type: 'gray', size: 15}"></SvgIcon>
|
||||
</a>
|
||||
<span class="tooltiptext">Mở trong tab mớ</span>
|
||||
</span>
|
||||
<span class="tooltip" v-if="show.rename">
|
||||
<a class="icon ml-1" @click="$emit('rename', v, i)">
|
||||
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 15}"></SvgIcon>
|
||||
</a>
|
||||
<span class="tooltiptext">Đổi tên</span>
|
||||
</span>
|
||||
<span class="tooltip" v-if="show.rename">
|
||||
<a class="icon has-text-danger ml-1" @click="$emit('remove', v, i)">
|
||||
<SvgIcon v-bind="{name: 'bin1.svg', type: 'gray', size: 15}"></SvgIcon>
|
||||
</a>
|
||||
<span class="tooltiptext">Xóa</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['data', 'name', 'maxheight', 'perpage', 'sort', 'selects', 'keyval', 'show', 'notick'],
|
||||
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.$remove(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>
|
||||
99
components/datatable/TableOption.vue
Normal file
99
components/datatable/TableOption.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr class="fs-14">
|
||||
<th>#</th>
|
||||
<th>Tên trường</th>
|
||||
<th>Tên cột</th>
|
||||
<th>...</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="fs-14" v-for="(v, i) in fields">
|
||||
<td>{{ i }}</td>
|
||||
<td>
|
||||
<a class="has-text-primary" @click="openField(v, i)">{{ v.name }}</a>
|
||||
</td>
|
||||
<td>{{ $stripHtml(v.label, 50) }}</td>
|
||||
<td>
|
||||
<a class="mr-4" @click="moveDown(v, i)">
|
||||
<SvgIcon v-bind="{ name: 'down1.png', type: 'dark', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a class="mr-4" @click="moveUp(v, i)">
|
||||
<SvgIcon v-bind="{ name: 'up.png', type: 'dark', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a @click="askConfirm(v, i)">
|
||||
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'dark', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Modal @close="showmodal = undefined" @update="update" @confirm="remove" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useStore } from "@/stores/index";
|
||||
const emit = defineEmits(["close"]);
|
||||
const { $stripHtml, $clone, $arrayMove, $remove } = useNuxtApp();
|
||||
const store = useStore();
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
});
|
||||
var showmodal = ref();
|
||||
var current;
|
||||
var pagedata = store[props.pagename];
|
||||
var fields = ref(pagedata.fields);
|
||||
function openField(v, i) {
|
||||
current = { v: v, i: i };
|
||||
showmodal.value = {
|
||||
component: "datatable/FieldAttribute",
|
||||
title: `${v.name} / ${$stripHtml(v.label)}`,
|
||||
width: "50%",
|
||||
height: "400px",
|
||||
vbind: { field: v },
|
||||
};
|
||||
}
|
||||
function update(data) {
|
||||
fields.value[current.i] = data;
|
||||
let copy = $clone(pagedata);
|
||||
copy.fields = fields.value;
|
||||
copy.update = { fields: fields.value };
|
||||
store.commit(props.pagename, copy);
|
||||
showmodal.value = undefined;
|
||||
emit("close");
|
||||
}
|
||||
function updateField() {
|
||||
let copy = $clone(pagedata);
|
||||
copy.fields = fields.value;
|
||||
copy.update = { fields: fields.value };
|
||||
store.commit(props.pagename, copy);
|
||||
}
|
||||
function moveDown(v, i) {
|
||||
let idx = i === fields.value.length - 1 ? 0 : i + 1;
|
||||
fields.value = $arrayMove(fields.value, i, idx);
|
||||
updateField();
|
||||
}
|
||||
function moveUp(v, i) {
|
||||
let idx = i === 0 ? fields.value.length - 1 : i - 1;
|
||||
fields.value = $arrayMove(fields.value, i, idx);
|
||||
updateField();
|
||||
}
|
||||
function askConfirm(v, i) {
|
||||
current = { v: v, i: i };
|
||||
showmodal.value = {
|
||||
component: `dialog/Confirm`,
|
||||
vbind: { content: "Bạn có muốn xóa cột này không?", duration: 10 },
|
||||
title: "Xóa cột",
|
||||
width: "500px",
|
||||
height: "100px",
|
||||
};
|
||||
}
|
||||
function remove() {
|
||||
let arr = [current.v];
|
||||
arr.map((v) => {
|
||||
let idx = fields.value.findIndex((x) => x.name === v.name);
|
||||
$remove(fields.value, idx);
|
||||
});
|
||||
updateField();
|
||||
}
|
||||
</script>
|
||||
112
components/datatable/TableSetting.vue
Normal file
112
components/datatable/TableSetting.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="field is-horizontal">
|
||||
<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="text" :value="tablesetting.find(v=>v.code==='table-font-size').detail"
|
||||
@change="changeSetting($event.target.value, '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="text" :value="tablesetting.find(v=>v.code==='header-font-size').detail"
|
||||
@change="changeSetting($event.target.value, '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="text" :value="tablesetting.find(v=>v.code==='per-page').detail"
|
||||
@change="changeSetting($event.target.value, '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>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useStore } from '@/stores/index'
|
||||
const store = useStore()
|
||||
var props = defineProps({
|
||||
pagename: String
|
||||
})
|
||||
const { $copy, $clone, $empty } = useNuxtApp()
|
||||
var pagedata = $clone(store[props.pagename])
|
||||
var errors = []
|
||||
var radioNote = 'no'
|
||||
var tablesetting = pagedata.tablesetting
|
||||
let found = tablesetting.find(v=>v.code==='note')
|
||||
if(found? found.detail!=='@' : false) radioNote = 'yes'
|
||||
function changeSetting(value, code) {
|
||||
if(code==='note' && $empty(value)) return
|
||||
let copy = $copy(tablesetting)
|
||||
let found = copy.find(v=>v.code===code)
|
||||
if(found) found.detail = value
|
||||
else {
|
||||
found = $copy(tablesetting.find(v=>v.code===code))
|
||||
found.detail = value
|
||||
copy.push(found)
|
||||
}
|
||||
tablesetting = copy
|
||||
pagedata.tablesetting = tablesetting
|
||||
store.commit(props.pagename, pagedata)
|
||||
}
|
||||
</script>
|
||||
378
components/datatable/TimeOption.vue
Normal file
378
components/datatable/TimeOption.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<div class="pb-1" style="border-bottom: 2px solid #3c5b63" v-if="array || !enableTime">
|
||||
<div class="columns mx-0 mb-0">
|
||||
<div class="column is-8 px-0 pb-0" v-if="enableTime">
|
||||
<div class="field is-grouped is-grouped-multiline mb-0">
|
||||
<div class="control mb-0">
|
||||
<Caption v-bind="{ title: lang === 'vi' ? 'Thời gian' : 'Time', type: 'has-text-warning' }" />
|
||||
</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 v-if="newDataAvailable" class="has-text-danger is-italic is-size-6 ml-2">Có dữ liệu mới, vui lòng làm
|
||||
mới.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4 px-0">
|
||||
<div class="field is-grouped is-grouped-multiline mb-0">
|
||||
<div class="control mb-0">
|
||||
<Caption v-bind="{
|
||||
type: 'has-text-warning',
|
||||
title: lang === 'vi' ? `Tìm ${viewport === 1 ? '' : 'kiếm'}` : 'Search',
|
||||
}" />
|
||||
</div>
|
||||
<div class="control mb-0">
|
||||
<input class="input is-small" type="text" v-model="text"
|
||||
:style="`${viewport === 1 ? 'width:150px;' : ''} border: 1px solid #BEBEBE;`" @keyup="startSearch"
|
||||
id="input" :placeholder="lang === 'vi' ? 'Nhập từ khóa...' : 'Enter keyword...'" />
|
||||
</div>
|
||||
<div class="control mb-0">
|
||||
<span class="tooltip" v-if="importdata">
|
||||
<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">
|
||||
<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 class="button is-primary is-loading is-small ml-3" v-if="loading"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useStore } from "@/stores/index"
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const store = useStore()
|
||||
return { store }
|
||||
},
|
||||
|
||||
props: ["pagename", "api", "timeopt", "filter", "realtime", "newDataAvailable", "params", "importdata"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
options: [
|
||||
{ code: 0, name: this.lang === "vi" ? "Hôm nay" : "Today" },
|
||||
{ code: 1, name: "1D" },
|
||||
{ code: 7, name: "7D" },
|
||||
{ code: 30, name: "1M" },
|
||||
{ code: 90, name: "3M" },
|
||||
{ code: 180, name: "6M" },
|
||||
{ code: 360, name: "1Y" },
|
||||
{ code: 36000, name: this.lang === "vi" ? "Tất cả" : "All" },
|
||||
],
|
||||
showmodal: undefined,
|
||||
current: 7,
|
||||
search: undefined,
|
||||
timer: undefined,
|
||||
text: undefined,
|
||||
status: [{ code: 100, name: "Chọn" }],
|
||||
array: undefined,
|
||||
enableAdd: true,
|
||||
enableTime: true,
|
||||
choices: [], // Sẽ được cập nhật động từ pagedata
|
||||
viewport: this.store.viewport,
|
||||
pagedata: undefined,
|
||||
loading: false,
|
||||
pollingInterval: null,
|
||||
searchableFields: [] // Lưu thông tin các field có thể tìm kiếm
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
pagename(newVal) {
|
||||
this.updateSearchableFields()
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
console.log("TimeOption created")
|
||||
|
||||
// Cập nhật searchable fields ngay từ đầu
|
||||
this.updateSearchableFields()
|
||||
|
||||
if (this.viewport < 5) {
|
||||
this.options = [
|
||||
{ code: 0, name: "Hôm nay" },
|
||||
{ code: 1, name: "1D" },
|
||||
{ code: 7, name: "7D" },
|
||||
{ code: 30, name: "1M" },
|
||||
{ code: 90, name: "3M" },
|
||||
{ code: 36000, name: "Tất cả" },
|
||||
]
|
||||
}
|
||||
|
||||
this.checkTimeopt()
|
||||
if (!this.enableTime) return this.$emit("option")
|
||||
|
||||
let found = this.$findapi(this.api)
|
||||
found.commit = undefined
|
||||
|
||||
let filter = this.$copy(this.filter)
|
||||
if (filter) {
|
||||
//dynamic parameter
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf("$") >= 0) {
|
||||
filter[key] = this.store[value.replace("$", "")].id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found.params.filter) {
|
||||
if (!filter) filter = {}
|
||||
for (const [key, value] of Object.entries(found.params.filter)) {
|
||||
filter[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
this.options.map((v) => {
|
||||
let f = filter ? this.$copy(filter) : {}
|
||||
f["create_time__date__gte"] = this.$dayjs()
|
||||
.subtract(v.code, "day")
|
||||
.format("YYYY-MM-DD")
|
||||
v.filter = f
|
||||
})
|
||||
|
||||
this.$emit("option", this.$find(this.options, { code: this.current }))
|
||||
|
||||
let f = {}
|
||||
this.options.map((v) => {
|
||||
f[`${v.code}`] = {
|
||||
type: "Count",
|
||||
field: "create_time__date",
|
||||
filter: v.filter
|
||||
}
|
||||
})
|
||||
|
||||
let params = { summary: "aggregate", distinct_values: f }
|
||||
found.params = params
|
||||
|
||||
try {
|
||||
let rs = await this.$getapi([found])
|
||||
for (const [key, value] of Object.entries(rs[0].data.rows)) {
|
||||
let found = this.$find(this.options, { code: Number(key) })
|
||||
if (found) {
|
||||
found.name = `${found.name} (${value})`
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error)
|
||||
}
|
||||
|
||||
this.array = this.$copy(this.options)
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.realtime) {
|
||||
const interval = typeof this.realtime === "number" ? this.realtime * 1000 : 5000
|
||||
this.pollingInterval = setInterval(this.refresh, interval)
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
if (this.pollingInterval) {
|
||||
clearInterval(this.pollingInterval)
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
lang: function () {
|
||||
return this.store.lang
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
// Lấy các field có thể tìm kiếm từ API params.values
|
||||
updateSearchableFields() {
|
||||
try {
|
||||
// Lấy API config
|
||||
const found = this.$findapi(this.api)
|
||||
|
||||
if (!found) {
|
||||
console.warn('Không tìm thấy API config')
|
||||
this.choices = []
|
||||
this.searchableFields = []
|
||||
return
|
||||
}
|
||||
|
||||
// Ưu tiên lấy values từ props.params, nếu không có thì lấy từ API config
|
||||
let valuesString = ''
|
||||
|
||||
if (this.params && this.params.values) {
|
||||
// Lấy từ props.params (ưu tiên cao nhất)
|
||||
valuesString = this.params.values
|
||||
console.log('Using values from props.params')
|
||||
} else if (found.params && found.params.values) {
|
||||
// Lấy từ API config mặc định
|
||||
valuesString = found.params.values
|
||||
console.log('Using values from API default params')
|
||||
} else {
|
||||
console.warn('Không tìm thấy API values trong props hoặc config')
|
||||
this.choices = []
|
||||
this.searchableFields = []
|
||||
return
|
||||
}
|
||||
|
||||
// Parse values string từ API
|
||||
let fieldNames = valuesString.split(',').map(v => v.trim())
|
||||
|
||||
console.log('Raw fieldNames:', fieldNames)
|
||||
console.log('Total fields:', fieldNames.length)
|
||||
|
||||
// Lấy pagedata để lấy label
|
||||
this.pagedata = this.store[this.pagename]
|
||||
|
||||
// Lấy tất cả các field để search (không lọc format)
|
||||
const searchable = fieldNames.filter(fieldName => {
|
||||
// Loại bỏ các field kỹ thuật
|
||||
if (fieldName === 'id' ||
|
||||
fieldName === 'create_time' ||
|
||||
fieldName === 'update_time' ||
|
||||
fieldName === 'created_at' ||
|
||||
fieldName === 'updated_at') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// Lấy tên và label các field
|
||||
this.choices = searchable
|
||||
this.searchableFields = searchable.map(fieldName => {
|
||||
// Lấy field base name (trước dấu __)
|
||||
const baseFieldName = fieldName.split('__')[0]
|
||||
const fieldInfo = this.pagedata && this.pagedata.fields
|
||||
? this.pagedata.fields.find(f => f.name === baseFieldName)
|
||||
: null
|
||||
|
||||
return {
|
||||
name: fieldName,
|
||||
label: fieldInfo ? fieldInfo.label : fieldName
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Searchable fields:', this.searchableFields)
|
||||
console.log('Choices:', this.choices)
|
||||
console.log('Total searchable fields:', this.choices.length)
|
||||
} catch (error) {
|
||||
console.error('Error updating searchable fields:', error)
|
||||
this.choices = []
|
||||
this.searchableFields = []
|
||||
}
|
||||
},
|
||||
|
||||
refresh() {
|
||||
let found = this.$find(this.options, { code: this.current })
|
||||
this.changeOption(found)
|
||||
},
|
||||
|
||||
changeOption(v) {
|
||||
this.current = v.code
|
||||
if (this.search) {
|
||||
this.text = undefined
|
||||
this.search = undefined
|
||||
}
|
||||
this.$emit("option", this.$find(this.array, { code: this.current }))
|
||||
},
|
||||
|
||||
doSearch() {
|
||||
// Cập nhật choices trước khi search
|
||||
this.updateSearchableFields()
|
||||
|
||||
this.pagedata = this.store[this.pagename]
|
||||
|
||||
if (!this.pagedata || !this.pagedata.fields) {
|
||||
console.warn('Không có pagedata hoặc fields')
|
||||
return
|
||||
}
|
||||
|
||||
let fields = this.pagedata.fields.filter(
|
||||
(v) => this.choices.findIndex((x) => x === v.name) >= 0
|
||||
)
|
||||
|
||||
if (fields.length === 0) {
|
||||
console.warn('Không tìm thấy field để tìm kiếm')
|
||||
return
|
||||
}
|
||||
|
||||
let f = {}
|
||||
fields.map((v) => {
|
||||
f[`${v.name}__icontains`] = this.search
|
||||
})
|
||||
|
||||
console.log('Search filter:', f)
|
||||
this.$emit("option", { filter_or: f })
|
||||
},
|
||||
|
||||
openImport() {
|
||||
if (!this.importdata) return
|
||||
// Emit event lên parent (DataView)
|
||||
this.$emit('import', this.importdata)
|
||||
},
|
||||
|
||||
startSearch(val) {
|
||||
this.search = this.$empty(val.target.value)
|
||||
? ""
|
||||
: val.target.value.trim()
|
||||
|
||||
if (this.timer) clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => this.doSearch(), 300)
|
||||
},
|
||||
|
||||
checkTimeopt() {
|
||||
if (this.timeopt > 0) {
|
||||
let obj = this.$find(this.options, { code: this.$formatNumber(this.timeopt) })
|
||||
if (obj) this.current = obj.code
|
||||
}
|
||||
if (this.timeopt ? this.$empty(this.timeopt.disable) : true) return
|
||||
if (this.timeopt.disable.indexOf("add") >= 0) this.enableAdd = false
|
||||
if (this.timeopt.disable.indexOf("time") >= 0) this.enableTime = false
|
||||
if (this.timeopt.time) {
|
||||
let obj = this.$find(this.options, { code: this.$formatNumber(this.timeopt.time) })
|
||||
if (obj) this.current = obj.code
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
10
components/datatable/format/ColorText.vue
Normal file
10
components/datatable/format/ColorText.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<span :style="{ color: color }">{{ text }}</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
text: { type: String, required: true },
|
||||
color: { type: String, default: "#000" }
|
||||
})
|
||||
</script>
|
||||
10
components/datatable/format/FormatDate.vue
Normal file
10
components/datatable/format/FormatDate.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<span :style="color? `color:${color}` : ''">{{ $dayjs(date).format('DD/MM/YYYY') }}</span>
|
||||
</template>
|
||||
<script setup>
|
||||
const { $dayjs } = useNuxtApp()
|
||||
const props = defineProps({
|
||||
date: String,
|
||||
color: String
|
||||
})
|
||||
</script>
|
||||
10
components/datatable/format/FormatNumber.vue
Normal file
10
components/datatable/format/FormatNumber.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<span :style="color? `color:${color}` : ''">{{ $numtoString(value) }}</span>
|
||||
</template>
|
||||
<script setup>
|
||||
const { $numtoString } = useNuxtApp()
|
||||
const props = defineProps({
|
||||
value: Number,
|
||||
color: String
|
||||
})
|
||||
</script>
|
||||
10
components/datatable/format/FormatTime.vue
Normal file
10
components/datatable/format/FormatTime.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<span :style="color? `color:${color}` : ''">{{ $numtoString(value) }}</span>
|
||||
</template>
|
||||
<script setup>
|
||||
const { $numtoString } = useNuxtApp()
|
||||
const props = defineProps({
|
||||
value: Number,
|
||||
color: String
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user