Files
web/app/components/datatable/NewField.vue
2026-05-15 13:29:39 +07:00

484 lines
15 KiB
Vue

<template>
<div class="tabs is-boxed">
<ul>
<li
v-for="v in fieldTypes"
:class="selectType.code === v.code && 'is-active'"
>
<a
class="has-text-inherit"
@click="selectType = v"
><span>{{ v.name }}</span></a
>
</li>
</ul>
</div>
<template v-if="selectType.code === 'formula'">
<div class="control is-flex is-gap-2">
<label
v-for="(v, i) in choices"
:key="i"
class="radio"
>
<input
v-model="choice"
:value="v.code"
@input="changeType(v)"
type="radio"
name="choice"
/>
{{ v.name }}
</label>
</div>
<div class="has-background-white-bis mt-3 p-3 rounded-md">
<div
class="tags mb-0"
v-if="choice === 'function'"
>
<span
:class="`tag is-primary ${func !== v.code && 'is-light'} fs-13 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 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">
<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-grey"
v-else
>
Tên trường do hệ thống tự sinh.
</p>
</div>
<div class="field">
<label class="label"> 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 class="icon">
<Icon
name="material-symbols:edit-outline-rounded"
:size="18"
/>
</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"
v-if="selectType.code === 'empty'"
>
<label class="label"
>Kiểu dữ liệu
<span class="has-text-danger"> * </span>
</label>
<div class="control">
<button
v-for="(v, i) in datatype"
class="button is-white fs-14"
@click="changeType(v)"
>
<span class="icon">
<Icon
:name="
radioType.code === v.code
? 'material-symbols:radio-button-checked-outline'
: 'material-symbols:radio-button-unchecked'
"
:size="22"
/>
</span>
<span>{{ v.name }}</span>
</button>
</div>
</div>
<button
class="button is-primary"
@click="selectType.code === 'formula' ? createField() : createEmptyField()"
>
Tạo cột
</button>
<Modal
v-bind="showmodal"
@label="changeLabel"
@close="close"
/>
</template>
<script setup>
import { useStore } from "@/stores/index";
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;
const fieldTypes = [
{ code: "empty", name: "Tạo cột rỗng" },
{ code: "formula", name: "Tạo công thức" },
];
var tags = [];
var formula = undefined;
var name = `f${$id().toLocaleLowerCase()}`;
var label = undefined;
const errors = ref([]);
var selectType = fieldTypes.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ố" },
];
const choice = ref("column");
const 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.value = [];
if (tags.length === 0 && choice.value === "column") {
errors.value.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.value.push({ name: "formula", message: "Công thức không được bỏ trống." });
}
if (!$empty(label) ? $empty(label.trim()) : true)
errors.value.push({ name: "label", message: "Mô tả không được bỏ trống." });
else if (pagedata.fields.find((v) => v.label.toLowerCase() === label.toLowerCase())) {
errors.value.push({
name: "label",
message: "Mô tả bị trùng. Hãy đặt mô tả khác.",
});
}
if (errors.value.length > 0) return false;
//check formula in case use column
if (choice.value === "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.value.push({ name: "formula", message: "Công thức không hợp lệ" });
}
} catch (err) {
console.log(err);
errors.value.push({ name: "formula", message: "Công thức không hợp lệ" });
}
} else {
if (checkFunc() === "error")
errors.value.push({
name: "formula",
message: `Hàm ${func.toUpperCase()} không hợp lệ`,
});
}
return errors.value.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.value === "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.value = [];
if (!$empty(name) ? $empty(name.trim()) : true)
errors.value.push({ name: "name", message: "Tên không được bỏ trống." });
else if (pagedata.fields.find((v) => v.name.toLowerCase() === name.toLowerCase())) {
errors.value.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.value.push({ name: "label", message: "Mô tả không được bỏ trống." });
else if (pagedata.fields.find((v) => v.label.toLowerCase() === label.toLowerCase())) {
errors.value.push({
name: "label",
message: "Mô tả bị trùng. Hãy đặt mô tả khác.",
});
}
if (errors.value.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>