@fui-org/fui-cli 1.3.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/dist/fui-bmg5pnmq.js +379 -0
- package/dist/fui.js +1 -1
- package/package.json +3 -6
- package/skills/fui/SKILL.md +9 -41
- package/skills/fui-skill/SKILL.md +95 -225
- package/skills/fui-skill/assets/projectdefaultstyle-3.0.css +555 -0
- package/skills/fui-skill/assets/projectdefaultstyle.css +207 -235
- package/skills/fui-skill/references/INDEX.md +105 -137
- package/skills/fui-skill/references/advanced-techniques.md +76 -68
- package/skills/fui-skill/references/coding-standards.md +56 -56
- package/skills/fui-skill/references/component-design.md +166 -173
- package/skills/fui-skill/references/component-quickref.md +61 -60
- package/skills/fui-skill/references/component-table.md +128 -117
- package/skills/fui-skill/references/components-dialog.md +55 -56
- package/skills/fui-skill/references/components-display.md +24 -30
- package/skills/fui-skill/references/components-echart.md +186 -261
- package/skills/fui-skill/references/components-input.md +72 -96
- package/skills/fui-skill/references/controls-patterns.md +196 -342
- package/skills/fui-skill/references/controls-styling-vocabulary.md +130 -97
- package/skills/fui-skill/references/db-table-design.md +24 -28
- package/skills/fui-skill/references/db-workflow.md +191 -390
- package/skills/fui-skill/references/default-function.md +169 -128
- package/skills/fui-skill/references/design-modes.md +35 -63
- package/skills/fui-skill/references/echart-templates.md +204 -196
- package/skills/fui-skill/references/fastproject.md +62 -60
- package/skills/fui-skill/references/fsheet.md +109 -124
- package/skills/fui-skill/references/fullstack-workflow.md +90 -128
- package/skills/fui-skill/references/module-data-patterns.md +31 -40
- package/skills/fui-skill/references/module-json-anatomy.md +47 -52
- package/skills/fui-skill/references/module-structure.md +80 -196
- package/skills/fui-skill/references/new-session.md +49 -51
- package/skills/fui-skill/references/pdfmake.md +17 -17
- package/skills/fui-skill/references/permission-system.md +89 -108
- package/skills/fui-skill/references/platform-architecture.md +128 -153
- package/skills/fui-skill/references/project-config.md +102 -134
- package/skills/fui-skill/references/project-provisioning.md +139 -244
- package/skills/fui-skill/references/script-map.md +208 -242
- package/skills/fui-skill/references/sql-clr-functions.md +98 -97
- package/skills/fui-skill/references/system-design.md +63 -88
- package/skills/fui-skill/references/tapi-file-api.md +46 -52
- package/skills/fui-skill/references/tapi-permission-patterns.md +51 -53
- package/skills/fui-skill/references/tapi-reference.md +132 -207
- package/skills/fui-skill/references/tools-registry.md +84 -460
- package/skills/fui-skill/references/ui-crosswindow-patterns.md +79 -75
- package/skills/fui-skill/references/ui-dialog-patterns.md +98 -72
- package/skills/fui-skill/references/ui-layout-patterns.md +26 -26
- package/skills/fui-skill/references/ui-patterns.md +71 -83
- package/skills/fui-skill/references/ui-screenshot-review.md +63 -62
- package/skills/fui-skill/references/ui-table-cell-patterns.md +61 -59
- package/skills/fui-skill/references/ui-templates.md +16 -23
- package/skills/fui-skill/references/verification.md +236 -246
- package/skills/fui-skill/references/watcher-patterns.md +30 -63
- package/skills/fui-skill/references/websocket-realtime.md +83 -66
- package/skills/fui-skill/scripts/component-3.0.js +298 -131
- package/skills/fui-skill/scripts/component.js +277 -271
- package/skills/fui-skill/scripts/componentTable-3.0.js +182 -53
- package/skills/fui-skill/scripts/componentTable.js +171 -49
- package/skills/fui-skill/scripts/defaultfunction-3.0.js +88 -3
- package/skills/fui-skill/scripts/defaultfunction.js +88 -3
- package/skills/fui-skill/scripts/fsheet.js +38 -0
- package/dist/fui-y8an39cn.js +0 -420
- package/skills/fui-skill/README.md +0 -112
- package/skills/fui-skill/metadata.json +0 -75
- package/skills/fui-skill-cli/SKILL.md +0 -139
- package/skills/fui-skill-cli/references/INDEX.md +0 -110
- package/skills/fui-skill-cli/references/advanced-techniques.md +0 -168
- package/skills/fui-skill-cli/references/coding-standards.md +0 -112
- package/skills/fui-skill-cli/references/component-design.md +0 -448
- package/skills/fui-skill-cli/references/component-quickref.md +0 -78
- package/skills/fui-skill-cli/references/component-table.md +0 -248
- package/skills/fui-skill-cli/references/components-dialog.md +0 -191
- package/skills/fui-skill-cli/references/components-display.md +0 -141
- package/skills/fui-skill-cli/references/components-echart.md +0 -316
- package/skills/fui-skill-cli/references/components-input.md +0 -335
- package/skills/fui-skill-cli/references/controls-patterns.md +0 -701
- package/skills/fui-skill-cli/references/controls-styling-vocabulary.md +0 -137
- package/skills/fui-skill-cli/references/db-table-design.md +0 -73
- package/skills/fui-skill-cli/references/db-workflow.md +0 -288
- package/skills/fui-skill-cli/references/default-function.md +0 -425
- package/skills/fui-skill-cli/references/design-modes.md +0 -57
- package/skills/fui-skill-cli/references/echart-templates.md +0 -489
- package/skills/fui-skill-cli/references/fastproject.md +0 -99
- package/skills/fui-skill-cli/references/fsheet.md +0 -203
- package/skills/fui-skill-cli/references/fullstack-workflow.md +0 -313
- package/skills/fui-skill-cli/references/module-data-patterns.md +0 -117
- package/skills/fui-skill-cli/references/module-json-anatomy.md +0 -132
- package/skills/fui-skill-cli/references/module-structure.md +0 -141
- package/skills/fui-skill-cli/references/new-session.md +0 -85
- package/skills/fui-skill-cli/references/pdfmake.md +0 -60
- package/skills/fui-skill-cli/references/permission-system.md +0 -150
- package/skills/fui-skill-cli/references/platform-architecture.md +0 -269
- package/skills/fui-skill-cli/references/project-config.md +0 -303
- package/skills/fui-skill-cli/references/project-provisioning.md +0 -278
- package/skills/fui-skill-cli/references/script-map.md +0 -262
- package/skills/fui-skill-cli/references/sql-clr-functions.md +0 -225
- package/skills/fui-skill-cli/references/system-design.md +0 -89
- package/skills/fui-skill-cli/references/tapi-file-api.md +0 -185
- package/skills/fui-skill-cli/references/tapi-permission-patterns.md +0 -156
- package/skills/fui-skill-cli/references/tapi-reference.md +0 -474
- package/skills/fui-skill-cli/references/tools-registry.md +0 -84
- package/skills/fui-skill-cli/references/ui-crosswindow-patterns.md +0 -321
- package/skills/fui-skill-cli/references/ui-dialog-patterns.md +0 -255
- package/skills/fui-skill-cli/references/ui-layout-patterns.md +0 -176
- package/skills/fui-skill-cli/references/ui-patterns.md +0 -303
- package/skills/fui-skill-cli/references/ui-screenshot-review.md +0 -95
- package/skills/fui-skill-cli/references/ui-table-cell-patterns.md +0 -318
- package/skills/fui-skill-cli/references/ui-templates.md +0 -22
- package/skills/fui-skill-cli/references/verification.md +0 -236
- package/skills/fui-skill-cli/references/watcher-patterns.md +0 -163
- package/skills/fui-skill-cli/references/websocket-realtime.md +0 -271
|
@@ -101,12 +101,12 @@ Vue.component("FTable", {
|
|
|
101
101
|
returnObject: {
|
|
102
102
|
default: true
|
|
103
103
|
},
|
|
104
|
-
fixedHeader: {
|
|
105
|
-
default: true
|
|
106
|
-
},
|
|
107
|
-
fixedCols: {
|
|
108
|
-
default: 0
|
|
109
|
-
},
|
|
104
|
+
fixedHeader: {
|
|
105
|
+
default: true
|
|
106
|
+
},
|
|
107
|
+
fixedCols: {
|
|
108
|
+
default: 0
|
|
109
|
+
},
|
|
110
110
|
// null/bỏ trống = bảng tự giãn theo số dòng; <=0 = fill tới đáy màn hình chừa
|
|
111
111
|
// abs(n) px (tự đo vị trí thật, theo dõi resize); >0 = px cố định.
|
|
112
112
|
height: {
|
|
@@ -142,9 +142,9 @@ Vue.component("FTable", {
|
|
|
142
142
|
},
|
|
143
143
|
},
|
|
144
144
|
template: `
|
|
145
|
-
<v-card ref="myTable" variant="outlined" v-if="headersArray" class="border" style="text-align: initial;">
|
|
146
|
-
<component is="style" v-if="fixedColStyles" v-html="fixedColStyles"></component>
|
|
147
|
-
<v-card-actions>
|
|
145
|
+
<v-card ref="myTable" variant="outlined" v-if="headersArray" class="border" style="text-align: initial;">
|
|
146
|
+
<component is="style" v-if="fixedColStyles" v-html="fixedColStyles"></component>
|
|
147
|
+
<v-card-actions v-if="showSearch || label || excel || ((newAPI() || newAction()) && !disabledUpdate)">
|
|
148
148
|
<div v-if="showSearch || label || ((newAPI() || newAction()) && !disabledUpdate)" class="text-primary text-darken-2 text-left w-66 pl-2 text-h6" v-html="label"></div>
|
|
149
149
|
<v-spacer></v-spacer>
|
|
150
150
|
<v-btn v-if="excel" class="mr-4" small icon density="compact" color="primary" @click="toExcel"><v-icon>mdi-microsoft-excel</v-icon></v-btn>
|
|
@@ -156,7 +156,7 @@ Vue.component("FTable", {
|
|
|
156
156
|
</div>
|
|
157
157
|
<v-btn v-if="(newAPI() || newAction()) && !disabledUpdate" class="ml-5 mr-2" density="compact" variant="outlined" icon="mdi-plus" color="primary" @click="newBtnClick"></v-btn>
|
|
158
158
|
</v-card-actions>
|
|
159
|
-
<v-data-table class="tTable" :class="fixedTableClass" v-bind="$attrs" :height="tableHeight" :fixed-header="tableFixedHeader" :hover="hover" :items="items" :item-key="itemKey" :return-object="returnObject" :headers="headersArray" :density="density" :items-per-page="itemsPerPage" :hide-default-footer="hideDefaultFooter_cacl" v-model="itemselected" calculate-widths v-on:click:row="$emit('click:row', $event)" v-on:item-selected="itemSelect()" v-on:toggle-select-all="itemSelect()" :search="search" :custom-filter="customSearch">
|
|
159
|
+
<v-data-table class="tTable" :class="fixedTableClass" v-bind="$attrs" :height="tableHeight" :fixed-header="tableFixedHeader" :hover="hover" :items="items" :item-key="itemKey" :return-object="returnObject" :headers="headersArray" :density="density" :items-per-page="itemsPerPage" :hide-default-footer="hideDefaultFooter_cacl" v-model="itemselected" calculate-widths v-on:click:row="$emit('click:row', $event)" v-on:item-selected="itemSelect()" v-on:toggle-select-all="itemSelect()" @update:options="onTableOptions" :search="search" :custom-filter="customSearch">
|
|
160
160
|
<template v-slot:headers="{ headers, isSorted, getSortIcon, toggleSort, allSelected, selectAll, someSelected, groupHeaders }">
|
|
161
161
|
<tr v-for="columns in headers">
|
|
162
162
|
<template v-for="column in columns" :key="column.key">
|
|
@@ -215,10 +215,11 @@ Vue.component("FTable", {
|
|
|
215
215
|
return {
|
|
216
216
|
editDialog: false,
|
|
217
217
|
search: '',
|
|
218
|
-
searchOpen: false,
|
|
219
|
-
fillHeight: 0,
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
searchOpen: false,
|
|
219
|
+
fillHeight: 0,
|
|
220
|
+
tableOptions: null, // sortBy/groupBy hiện hành của v-data-table (update:options) — cho excel="UI"
|
|
221
|
+
fixedTableClass: 'f-table-fixed-' + (this.$ && this.$.uid !== undefined ? this.$.uid : this._uid),
|
|
222
|
+
itemselected: [],
|
|
222
223
|
returnValue: [],
|
|
223
224
|
editData: {},
|
|
224
225
|
editedIndex: -1,
|
|
@@ -335,38 +336,38 @@ Vue.component("FTable", {
|
|
|
335
336
|
if (this.heightNum > 0) return this.heightNum;
|
|
336
337
|
return this.fillHeight || undefined;
|
|
337
338
|
},
|
|
338
|
-
tableFixedHeader() {
|
|
339
|
-
return this.tableHeight ? this.fixedHeader : false;
|
|
340
|
-
},
|
|
341
|
-
fixedColStyles() {
|
|
342
|
-
var fixedCols = parseInt(this.fixedCols, 10);
|
|
343
|
-
var headers = this.headersArray || [];
|
|
344
|
-
if (isNaN(fixedCols) || fixedCols <= 0 || headers.length === 0) return '';
|
|
345
|
-
|
|
346
|
-
fixedCols = Math.min(fixedCols, headers.length);
|
|
347
|
-
var scope = '.' + this.fixedTableClass;
|
|
348
|
-
var css = scope + ' th:nth-child(-n+' + fixedCols + '),' +
|
|
349
|
-
scope + ' td:nth-child(-n+' + fixedCols + '){' +
|
|
350
|
-
'position:sticky!important;background-color:#fff;' +
|
|
351
|
-
'background-color:rgb(var(--v-theme-surface,255,255,255));z-index:1;}';
|
|
352
|
-
css += '.v-theme--dark ' + scope + ' th:nth-child(-n+' + fixedCols + '),' +
|
|
353
|
-
'.v-theme--dark ' + scope + ' td:nth-child(-n+' + fixedCols + '){background-color:#1e1e1e;}';
|
|
354
|
-
// Vuetify fixed-header đặt th trên body; giữ fixed body ở lớp 1 và nâng riêng
|
|
355
|
-
// giao điểm header + cột cố định lên lớp 4 để không thua specificity của framework.
|
|
356
|
-
css += scope + ' th:nth-child(-n+' + fixedCols + '){z-index:4!important;}';
|
|
357
|
-
|
|
358
|
-
var left = [];
|
|
359
|
-
for (var i = 0; i < fixedCols; i++) {
|
|
360
|
-
var width = this.fixedColumnWidth(headers[i] && headers[i].width);
|
|
361
|
-
css += scope + ' th:nth-child(' + (i + 1) + '),' +
|
|
362
|
-
scope + ' td:nth-child(' + (i + 1) + '){' +
|
|
363
|
-
'left:calc(' + (left.length ? left.join(' + ') : '0px') + ')!important;' +
|
|
364
|
-
'min-width:' + width + '!important;max-width:' + width + '!important;width:' + width + '!important;}';
|
|
365
|
-
left.push(width);
|
|
366
|
-
}
|
|
367
|
-
return css;
|
|
368
|
-
},
|
|
369
|
-
edit() {
|
|
339
|
+
tableFixedHeader() {
|
|
340
|
+
return this.tableHeight ? this.fixedHeader : false;
|
|
341
|
+
},
|
|
342
|
+
fixedColStyles() {
|
|
343
|
+
var fixedCols = parseInt(this.fixedCols, 10);
|
|
344
|
+
var headers = this.headersArray || [];
|
|
345
|
+
if (isNaN(fixedCols) || fixedCols <= 0 || headers.length === 0) return '';
|
|
346
|
+
|
|
347
|
+
fixedCols = Math.min(fixedCols, headers.length);
|
|
348
|
+
var scope = '.' + this.fixedTableClass;
|
|
349
|
+
var css = scope + ' th:nth-child(-n+' + fixedCols + '),' +
|
|
350
|
+
scope + ' td:nth-child(-n+' + fixedCols + '){' +
|
|
351
|
+
'position:sticky!important;background-color:#fff;' +
|
|
352
|
+
'background-color:rgb(var(--v-theme-surface,255,255,255));z-index:1;}';
|
|
353
|
+
css += '.v-theme--dark ' + scope + ' th:nth-child(-n+' + fixedCols + '),' +
|
|
354
|
+
'.v-theme--dark ' + scope + ' td:nth-child(-n+' + fixedCols + '){background-color:#1e1e1e;}';
|
|
355
|
+
// Vuetify fixed-header đặt th trên body; giữ fixed body ở lớp 1 và nâng riêng
|
|
356
|
+
// giao điểm header + cột cố định lên lớp 4 để không thua specificity của framework.
|
|
357
|
+
css += scope + ' th:nth-child(-n+' + fixedCols + '){z-index:4!important;}';
|
|
358
|
+
|
|
359
|
+
var left = [];
|
|
360
|
+
for (var i = 0; i < fixedCols; i++) {
|
|
361
|
+
var width = this.fixedColumnWidth(headers[i] && headers[i].width);
|
|
362
|
+
css += scope + ' th:nth-child(' + (i + 1) + '),' +
|
|
363
|
+
scope + ' td:nth-child(' + (i + 1) + '){' +
|
|
364
|
+
'left:calc(' + (left.length ? left.join(' + ') : '0px') + ')!important;' +
|
|
365
|
+
'min-width:' + width + '!important;max-width:' + width + '!important;width:' + width + '!important;}';
|
|
366
|
+
left.push(width);
|
|
367
|
+
}
|
|
368
|
+
return css;
|
|
369
|
+
},
|
|
370
|
+
edit() {
|
|
370
371
|
return this.editedIndex >= 0;
|
|
371
372
|
},
|
|
372
373
|
formTitle() {
|
|
@@ -383,13 +384,13 @@ Vue.component("FTable", {
|
|
|
383
384
|
});
|
|
384
385
|
}
|
|
385
386
|
},
|
|
386
|
-
methods: {
|
|
387
|
-
fixedColumnWidth(width) {
|
|
388
|
-
if (width === undefined || width === null || width === '') return '150px';
|
|
389
|
-
var value = String(width).trim();
|
|
390
|
-
return /^\d+(\.\d+)?$/.test(value) ? value + 'px' : value;
|
|
391
|
-
},
|
|
392
|
-
removeResize() {
|
|
387
|
+
methods: {
|
|
388
|
+
fixedColumnWidth(width) {
|
|
389
|
+
if (width === undefined || width === null || width === '') return '150px';
|
|
390
|
+
var value = String(width).trim();
|
|
391
|
+
return /^\d+(\.\d+)?$/.test(value) ? value + 'px' : value;
|
|
392
|
+
},
|
|
393
|
+
removeResize() {
|
|
393
394
|
if (this.resizeHandler) {
|
|
394
395
|
window.removeEventListener('resize', this.resizeHandler);
|
|
395
396
|
this.resizeHandler = null;
|
|
@@ -624,10 +625,138 @@ Vue.component("FTable", {
|
|
|
624
625
|
customSearch(value, search, item) {
|
|
625
626
|
return Object.values(item.columns).some(v=>v&&v.toString().toLowerCase().includes(search.toLowerCase()))
|
|
626
627
|
},
|
|
628
|
+
// excel = true (mặc định): xuất nguyên items gốc — giữ nguyên hành vi cũ.
|
|
629
|
+
// excel = "UI": xuất đúng những gì bảng đang hiển thị (xem toExcelUI).
|
|
627
630
|
toExcel() {
|
|
631
|
+
if (typeof this.excel === 'string' && this.excel.trim().toUpperCase() === 'UI') return this.toExcelUI();
|
|
628
632
|
jsonToExcel({ data: this.items, filename: (this.label && this.label != '' ? this.label : 'tableFP') + '.xlsx' });
|
|
629
633
|
Vue.$toast.success("Đã xuất Excel", { position: 'top' });
|
|
630
634
|
},
|
|
635
|
+
onTableOptions(opt) {
|
|
636
|
+
this.tableOptions = opt;
|
|
637
|
+
},
|
|
638
|
+
//------------------------------------------------------------------ excel="UI"
|
|
639
|
+
// Lấy từ giao diện: DÒNG = items sau lọc (ô tìm kiếm) + sắp xếp (kể cả group-by), gồm MỌI trang;
|
|
640
|
+
// CỘT = header lá theo thứ tự hiển thị, tên cột = tiêu đề trên bảng. GIÁ TRỊ ô = dữ liệu gốc của
|
|
641
|
+
// field, KHÔNG định dạng (không numFmt, không đổi ngày, không bỏ HTML) — không có hàng tổng.
|
|
642
|
+
// Không đọc DOM: DOM chỉ chứa trang hiện tại, còn ô t-* là component nên không lấy lại được giá trị.
|
|
643
|
+
toExcelUI() {
|
|
644
|
+
var t = this;
|
|
645
|
+
var columns = t.uiExcelColumns();
|
|
646
|
+
if (!columns.length) { showMessage({ title: 'Bảng không có cột nào để xuất Excel' }); return; }
|
|
647
|
+
var rows = t.uiExcelRows(columns);
|
|
648
|
+
if (!rows.length) { showMessage({ title: 'Không có dòng nào đang hiển thị để xuất Excel' }); return; }
|
|
649
|
+
var name = t.plainText(t.label) || 'tableFP';
|
|
650
|
+
jsonToExcel({
|
|
651
|
+
data: rows,
|
|
652
|
+
columns: columns,
|
|
653
|
+
filename: name.replace(/[\\\/:*?"<>|]/g, '_') + '.xlsx',
|
|
654
|
+
worksheet: name.replace(/[\[\]:*?\/\\]/g, ' ').slice(0, 31).trim() || 'SheetFP'
|
|
655
|
+
});
|
|
656
|
+
Vue.$toast.success("Đã xuất Excel", { position: 'top' });
|
|
657
|
+
},
|
|
658
|
+
uiExcelColumns() {
|
|
659
|
+
var t = this, out = [];
|
|
660
|
+
var skip = { 'ctrl-update': 1, 'data-table-select': 1, 'data-table-group': 1, 'data-table-expand': 1 };
|
|
661
|
+
(function walk(list, prefix) {
|
|
662
|
+
_.forEach(list, function (h) {
|
|
663
|
+
if (!h || typeof h !== 'object') return;
|
|
664
|
+
var title = t.plainText(h.title !== undefined ? h.title : h.text);
|
|
665
|
+
var full = prefix && title ? prefix + ' / ' + title : (title || prefix);
|
|
666
|
+
if (Array.isArray(h.children) && h.children.length) { walk(h.children, full); return; }
|
|
667
|
+
var key = typeof h.key === 'string' ? h.key : (typeof h.value === 'string' ? h.value : null);
|
|
668
|
+
if (!key || skip[key]) return;
|
|
669
|
+
// Vuetify 3 lấy giá trị ô theo `value` (path) nếu có, không thì theo `key`
|
|
670
|
+
out.push(t.uiExcelColumn(h, key, typeof h.value === 'string' ? h.value : key, full || key));
|
|
671
|
+
});
|
|
672
|
+
})(t.headersArray, '');
|
|
673
|
+
return out;
|
|
674
|
+
},
|
|
675
|
+
uiExcelColumn(h, key, path, header) {
|
|
676
|
+
var attr = h.attr || {};
|
|
677
|
+
// Ô t-* hiển thị field mà v-model trỏ tới ("item.X"), không nhất thiết trùng key của cột
|
|
678
|
+
var vm = typeof attr['v-model'] === 'string' && /^\s*item\.([\w$.]+)\s*$/.exec(attr['v-model']);
|
|
679
|
+
var tablePath = path;
|
|
680
|
+
if (vm) path = vm[1];
|
|
681
|
+
var col = { header: header, key: path, tablePath: tablePath, sortKey: key, compare: typeof h.sort === 'function' ? h.sort : null };
|
|
682
|
+
var wm = /^\s*(\d+(?:\.\d+)?)\s*(px)?\s*$/.exec(h.width === undefined || h.width === null ? '' : String(h.width));
|
|
683
|
+
if (wm) col.width = Math.max(8, Math.round(Number(wm[1]) / 7));
|
|
684
|
+
return col;
|
|
685
|
+
},
|
|
686
|
+
uiExcelRows(columns) {
|
|
687
|
+
var t = this;
|
|
688
|
+
var rows = (t.items || []).slice();
|
|
689
|
+
if (t.search) {
|
|
690
|
+
// Dùng lại chính customSearch mà v-data-table đang dùng (item.columns = giá trị theo từng cột)
|
|
691
|
+
rows = rows.filter(function (r) {
|
|
692
|
+
var cols = {};
|
|
693
|
+
_.forEach(columns, function (c) { cols[c.sortKey] = t.readCell(r, c.tablePath); });
|
|
694
|
+
return t.customSearch(null, t.search, { columns: cols });
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
var spec = t.uiSortSpec();
|
|
698
|
+
if (spec.length) {
|
|
699
|
+
var byKey = _.keyBy(columns, 'sortKey');
|
|
700
|
+
var collator = t.uiCollator();
|
|
701
|
+
var fields = spec.map(function (s) {
|
|
702
|
+
var c = byKey[s.key];
|
|
703
|
+
return { path: c ? c.tablePath : s.key, desc: s.desc, compare: c && c.compare };
|
|
704
|
+
});
|
|
705
|
+
rows.sort(function (a, b) {
|
|
706
|
+
for (var i = 0; i < fields.length; i++) {
|
|
707
|
+
var f = fields[i];
|
|
708
|
+
var va = t.readCell(a, f.path), vb = t.readCell(b, f.path);
|
|
709
|
+
if (f.desc) { var tmp = va; va = vb; vb = tmp; }
|
|
710
|
+
var r = f.compare ? f.compare(va, vb) : t.compareCell(va, vb, collator);
|
|
711
|
+
if (r) return r;
|
|
712
|
+
}
|
|
713
|
+
return 0;
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return rows;
|
|
717
|
+
},
|
|
718
|
+
// group-by đứng trước sort-by — đúng thứ tự v-data-table sắp dòng khi gộp nhóm.
|
|
719
|
+
// Chưa nhận update:options nào thì lấy từ attr khai sẵn.
|
|
720
|
+
uiSortSpec() {
|
|
721
|
+
var o = this.tableOptions, a = this.$attrs || {};
|
|
722
|
+
var group = o ? o.groupBy : (a['group-by'] || a.groupBy);
|
|
723
|
+
var sort = o ? o.sortBy : (a['sort-by'] || a.sortBy);
|
|
724
|
+
return this.normalizeSort(group).concat(this.normalizeSort(sort));
|
|
725
|
+
},
|
|
726
|
+
normalizeSort(list) {
|
|
727
|
+
return _.castArray(list === undefined || list === null ? [] : list).map(function (s) {
|
|
728
|
+
if (s && typeof s === 'object') return { key: s.key, desc: s.order === 'desc' || s.order === true };
|
|
729
|
+
return { key: s, desc: false };
|
|
730
|
+
}).filter(function (s) { return typeof s.key === 'string' && s.key !== ''; });
|
|
731
|
+
},
|
|
732
|
+
// Bám thuật toán sortItems của Vuetify: ngày so theo thời gian, rỗng lên đầu, hai chuỗi số so
|
|
733
|
+
// theo số, còn lại so bằng Intl.Collator (có dấu tiếng Việt).
|
|
734
|
+
compareCell(a, b, collator) {
|
|
735
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
|
|
736
|
+
var sa = a === null || a === undefined ? '' : String(a).toLocaleLowerCase();
|
|
737
|
+
var sb = b === null || b === undefined ? '' : String(b).toLocaleLowerCase();
|
|
738
|
+
if (sa === sb) return 0;
|
|
739
|
+
if (sa === '') return -1;
|
|
740
|
+
if (sb === '') return 1;
|
|
741
|
+
if (!isNaN(sa) && !isNaN(sb)) return Number(sa) - Number(sb);
|
|
742
|
+
return collator.compare(sa, sb);
|
|
743
|
+
},
|
|
744
|
+
uiCollator() {
|
|
745
|
+
var loc = _.get(this, '$vuetify.locale.current');
|
|
746
|
+
if (loc && typeof loc === 'object' && 'value' in loc) loc = loc.value;
|
|
747
|
+
try { return new Intl.Collator(loc || undefined, { sensitivity: 'accent', usage: 'sort' }); }
|
|
748
|
+
catch (e) { return new Intl.Collator(undefined, { sensitivity: 'accent', usage: 'sort' }); }
|
|
749
|
+
},
|
|
750
|
+
readCell(row, path) {
|
|
751
|
+
if (row === null || row === undefined) return null;
|
|
752
|
+
return row[path] !== undefined ? row[path] : _.get(row, path);
|
|
753
|
+
},
|
|
754
|
+
plainText(v) {
|
|
755
|
+
if (v === null || v === undefined) return '';
|
|
756
|
+
var s = String(v);
|
|
757
|
+
if (s.indexOf('<') < 0 && s.indexOf('&') < 0) return s.trim();
|
|
758
|
+
return (new DOMParser().parseFromString(s, 'text/html').body.textContent || '').trim();
|
|
759
|
+
},
|
|
631
760
|
}
|
|
632
761
|
});
|
|
633
762
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Vue.component("f-table", {
|
|
2
|
-
props: ['value', 'headers', 'updateForm', 'updateFormAttr', "updateApi", 'items', 'label', 'excel', 'showSearch', 'sumFormat', 'disabledHover', 'returnObject', "itemKey", 'itemsPerPage', "disabledUpdate", 'hideDefaultFooter', 'template', 'headercomponent', 'height', 'fixedHeader', 'fixedCols'],
|
|
2
|
+
props: ['value', 'headers', 'updateForm', 'updateFormAttr', "updateApi", 'items', 'label', 'excel', 'showSearch', 'sumFormat', 'disabledHover', 'returnObject', "itemKey", 'itemsPerPage', "disabledUpdate", 'hideDefaultFooter', 'template', 'headercomponent', 'height', 'fixedHeader', 'fixedCols'],
|
|
3
3
|
data: function () {
|
|
4
4
|
var headersAttr = bindData(this.headers);
|
|
5
5
|
var strTableTemplate = this.template || '';
|
|
@@ -21,9 +21,9 @@ Vue.component("f-table", {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
this.$options.template = `
|
|
24
|
-
<v-card outlined v-if="headerCols" ref="myTable">
|
|
25
|
-
<component is="style" v-if="fixedColStyles" v-html="fixedColStyles"></component>
|
|
26
|
-
<v-card-title v-if="showSearch || label || ((newAPI() || newAction()) && !disabledUpdate)" align="end" class="py-2 px-3">
|
|
24
|
+
<v-card outlined v-if="headerCols" ref="myTable">
|
|
25
|
+
<component is="style" v-if="fixedColStyles" v-html="fixedColStyles"></component>
|
|
26
|
+
<v-card-title v-if="showSearch || label || excel || ((newAPI() || newAction()) && !disabledUpdate)" align="end" class="py-2 px-3">
|
|
27
27
|
<div class="primary--text text-left" v-html="label" style="font-weight: 500; font-size: 1.1rem;"></div>
|
|
28
28
|
<v-spacer></v-spacer><component :is="headercomponent"></component>
|
|
29
29
|
<v-btn v-if="excel" icon color="primary" @click="toExcel"><v-icon>mdi-microsoft-excel</v-icon></v-btn>
|
|
@@ -36,7 +36,7 @@ Vue.component("f-table", {
|
|
|
36
36
|
<v-btn v-if="(newAPI() || newAction()) && !disabledUpdate" class="ml-3" outlined x-small fab dark color="primary" @click="newBtnClick"><v-icon dark>mdi-plus</v-icon></v-btn>
|
|
37
37
|
</v-card-title>
|
|
38
38
|
|
|
39
|
-
<v-data-table class="tTable" :class="[{tTableNoHover: disabledHover}, fixedTableClass]" v-bind="$attrs" :height="tableHeight" :fixed-header="tableFixedHeader" :items="items" :item-key="itemKey" :headers="headerCols" :items-per-page="itemsPerPage" :hide-default-footer="_hideDefaultFooter" v-model="itemselected" calculate-widths v-on:click:row="$emit('click:row', $event)" v-on:item-selected="itemSelect()" v-on:toggle-select-all="itemSelect()" :search="search" :custom-filter="customSearch">
|
|
39
|
+
<v-data-table class="tTable" :class="[{tTableNoHover: disabledHover}, fixedTableClass]" v-bind="$attrs" :height="tableHeight" :fixed-header="tableFixedHeader" :items="items" :item-key="itemKey" :headers="headerCols" :items-per-page="itemsPerPage" :hide-default-footer="_hideDefaultFooter" v-model="itemselected" calculate-widths v-on:click:row="$emit('click:row', $event)" v-on:item-selected="itemSelect()" v-on:toggle-select-all="itemSelect()" v-on:update:options="onTableOptions" :search="search" :custom-filter="customSearch">
|
|
40
40
|
<template v-for="(h, index) in headerCols" :key="index" v-slot:['header.'+h.value]="{ header }"><span v-html="header.text"></span></template>
|
|
41
41
|
${strTableTemplate}
|
|
42
42
|
<template v-if="!disabledUpdate" v-slot:item.ctrl-update="{item}">
|
|
@@ -70,10 +70,11 @@ Vue.component("f-table", {
|
|
|
70
70
|
itemselected: [],
|
|
71
71
|
returnValue: [],
|
|
72
72
|
editData: {},
|
|
73
|
-
editedIndex: -1,
|
|
74
|
-
fillHeight: 0,
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
editedIndex: -1,
|
|
74
|
+
fillHeight: 0,
|
|
75
|
+
tableOptions: null, // sortBy/sortDesc/groupBy hiện hành của v-data-table (update:options) — cho excel="UI"
|
|
76
|
+
fixedTableClass: 'f-table-fixed-' + this._uid,
|
|
77
|
+
autoHeaders: this.headers ? null : this.buildAutoHeaders(this.items || []),
|
|
77
78
|
updateButton: [
|
|
78
79
|
{
|
|
79
80
|
"v-if": "title=='Cập nhật'",
|
|
@@ -168,39 +169,39 @@ Vue.component("f-table", {
|
|
|
168
169
|
if (this.heightNum > 0) return this.heightNum;
|
|
169
170
|
return this.fillHeight || undefined;
|
|
170
171
|
},
|
|
171
|
-
tableFixedHeader() {
|
|
172
|
-
if (this.fixedHeader !== undefined) return this.fixedHeader;
|
|
173
|
-
return this.tableHeight ? true : undefined;
|
|
174
|
-
},
|
|
175
|
-
fixedColStyles() {
|
|
176
|
-
var fixedCols = parseInt(this.fixedCols, 10);
|
|
177
|
-
var headers = this.headerCols || [];
|
|
178
|
-
if (isNaN(fixedCols) || fixedCols <= 0 || headers.length === 0) return '';
|
|
179
|
-
|
|
180
|
-
fixedCols = Math.min(fixedCols, headers.length);
|
|
181
|
-
var scope = '.' + this.fixedTableClass;
|
|
182
|
-
var css = scope + ' th:nth-child(-n+' + fixedCols + '),' +
|
|
183
|
-
scope + ' td:nth-child(-n+' + fixedCols + '){' +
|
|
184
|
-
'position:sticky!important;background-color:#fff;' +
|
|
185
|
-
'background-color:rgb(var(--v-theme-surface,255,255,255));z-index:1;}';
|
|
186
|
-
css += '.theme--dark ' + scope + ' th:nth-child(-n+' + fixedCols + '),' +
|
|
187
|
-
'.theme--dark ' + scope + ' td:nth-child(-n+' + fixedCols + '){background-color:#1e1e1e;}';
|
|
188
|
-
// Vuetify fixed-header đặt th ở z-index 2 bằng selector có specificity cao.
|
|
189
|
-
// Ô body cố định phải ở dưới mức đó; giao điểm header + cột cố định phải thắng tuyệt đối.
|
|
190
|
-
css += scope + ' th:nth-child(-n+' + fixedCols + '){z-index:4!important;}';
|
|
191
|
-
|
|
192
|
-
var left = [];
|
|
193
|
-
for (var i = 0; i < fixedCols; i++) {
|
|
194
|
-
var width = this.fixedColumnWidth(headers[i] && headers[i].width);
|
|
195
|
-
css += scope + ' th:nth-child(' + (i + 1) + '),' +
|
|
196
|
-
scope + ' td:nth-child(' + (i + 1) + '){' +
|
|
197
|
-
'left:calc(' + (left.length ? left.join(' + ') : '0px') + ')!important;' +
|
|
198
|
-
'min-width:' + width + '!important;max-width:' + width + '!important;width:' + width + '!important;}';
|
|
199
|
-
left.push(width);
|
|
200
|
-
}
|
|
201
|
-
return css;
|
|
202
|
-
},
|
|
203
|
-
headerCols() {
|
|
172
|
+
tableFixedHeader() {
|
|
173
|
+
if (this.fixedHeader !== undefined) return this.fixedHeader;
|
|
174
|
+
return this.tableHeight ? true : undefined;
|
|
175
|
+
},
|
|
176
|
+
fixedColStyles() {
|
|
177
|
+
var fixedCols = parseInt(this.fixedCols, 10);
|
|
178
|
+
var headers = this.headerCols || [];
|
|
179
|
+
if (isNaN(fixedCols) || fixedCols <= 0 || headers.length === 0) return '';
|
|
180
|
+
|
|
181
|
+
fixedCols = Math.min(fixedCols, headers.length);
|
|
182
|
+
var scope = '.' + this.fixedTableClass;
|
|
183
|
+
var css = scope + ' th:nth-child(-n+' + fixedCols + '),' +
|
|
184
|
+
scope + ' td:nth-child(-n+' + fixedCols + '){' +
|
|
185
|
+
'position:sticky!important;background-color:#fff;' +
|
|
186
|
+
'background-color:rgb(var(--v-theme-surface,255,255,255));z-index:1;}';
|
|
187
|
+
css += '.theme--dark ' + scope + ' th:nth-child(-n+' + fixedCols + '),' +
|
|
188
|
+
'.theme--dark ' + scope + ' td:nth-child(-n+' + fixedCols + '){background-color:#1e1e1e;}';
|
|
189
|
+
// Vuetify fixed-header đặt th ở z-index 2 bằng selector có specificity cao.
|
|
190
|
+
// Ô body cố định phải ở dưới mức đó; giao điểm header + cột cố định phải thắng tuyệt đối.
|
|
191
|
+
css += scope + ' th:nth-child(-n+' + fixedCols + '){z-index:4!important;}';
|
|
192
|
+
|
|
193
|
+
var left = [];
|
|
194
|
+
for (var i = 0; i < fixedCols; i++) {
|
|
195
|
+
var width = this.fixedColumnWidth(headers[i] && headers[i].width);
|
|
196
|
+
css += scope + ' th:nth-child(' + (i + 1) + '),' +
|
|
197
|
+
scope + ' td:nth-child(' + (i + 1) + '){' +
|
|
198
|
+
'left:calc(' + (left.length ? left.join(' + ') : '0px') + ')!important;' +
|
|
199
|
+
'min-width:' + width + '!important;max-width:' + width + '!important;width:' + width + '!important;}';
|
|
200
|
+
left.push(width);
|
|
201
|
+
}
|
|
202
|
+
return css;
|
|
203
|
+
},
|
|
204
|
+
headerCols() {
|
|
204
205
|
var h = this.headers || this.autoHeaders;
|
|
205
206
|
if (_.isBoolean(this.disabledUpdate) && this.disabledUpdate) {
|
|
206
207
|
_.remove(h, function (n) {
|
|
@@ -234,13 +235,13 @@ Vue.component("f-table", {
|
|
|
234
235
|
});
|
|
235
236
|
}
|
|
236
237
|
},
|
|
237
|
-
methods: {
|
|
238
|
-
fixedColumnWidth(width) {
|
|
239
|
-
if (width === undefined || width === null || width === '') return '150px';
|
|
240
|
-
var value = String(width).trim();
|
|
241
|
-
return /^\d+(\.\d+)?$/.test(value) ? value + 'px' : value;
|
|
242
|
-
},
|
|
243
|
-
removeResize() {
|
|
238
|
+
methods: {
|
|
239
|
+
fixedColumnWidth(width) {
|
|
240
|
+
if (width === undefined || width === null || width === '') return '150px';
|
|
241
|
+
var value = String(width).trim();
|
|
242
|
+
return /^\d+(\.\d+)?$/.test(value) ? value + 'px' : value;
|
|
243
|
+
},
|
|
244
|
+
removeResize() {
|
|
244
245
|
if (this.resizeHandler) {
|
|
245
246
|
window.removeEventListener('resize', this.resizeHandler);
|
|
246
247
|
this.resizeHandler = null;
|
|
@@ -438,10 +439,131 @@ Vue.component("f-table", {
|
|
|
438
439
|
customSearch(value, search, item) {
|
|
439
440
|
return Object.values(item).some(v => v && v.toString().toLowerCase().includes(search.toLowerCase()))
|
|
440
441
|
},
|
|
442
|
+
// excel = true: xuất nguyên items gốc — giữ nguyên hành vi cũ.
|
|
443
|
+
// excel = "UI": xuất đúng những gì bảng đang hiển thị (xem toExcelUI).
|
|
441
444
|
toExcel() {
|
|
445
|
+
if (typeof this.excel === 'string' && this.excel.trim().toUpperCase() === 'UI') return this.toExcelUI();
|
|
442
446
|
jsonToExcel({ data: this.items, filename: (this.label && this.label != '' ? this.label : 'tableFP') + '.xlsx' });
|
|
443
447
|
Vue.$toast.success("Đã xuất Excel", { position: 'top' });
|
|
444
448
|
},
|
|
449
|
+
onTableOptions(opt) {
|
|
450
|
+
this.tableOptions = opt;
|
|
451
|
+
},
|
|
452
|
+
//------------------------------------------------------------------ excel="UI"
|
|
453
|
+
// Lấy từ giao diện: DÒNG = items sau lọc (ô tìm kiếm) + sắp xếp (kể cả group-by), gồm MỌI trang;
|
|
454
|
+
// CỘT = header lá theo thứ tự hiển thị, tên cột = tiêu đề trên bảng. GIÁ TRỊ ô = dữ liệu gốc của
|
|
455
|
+
// field, KHÔNG định dạng (không numFmt, không đổi ngày, không bỏ HTML) — không có hàng tổng.
|
|
456
|
+
// Không đọc DOM: DOM chỉ chứa trang hiện tại, còn ô t-* là component nên không lấy lại được giá trị.
|
|
457
|
+
toExcelUI() {
|
|
458
|
+
var t = this;
|
|
459
|
+
var columns = t.uiExcelColumns();
|
|
460
|
+
if (!columns.length) { showMessage({ title: 'Bảng không có cột nào để xuất Excel' }); return; }
|
|
461
|
+
var rows = t.uiExcelRows(columns);
|
|
462
|
+
if (!rows.length) { showMessage({ title: 'Không có dòng nào đang hiển thị để xuất Excel' }); return; }
|
|
463
|
+
var name = t.plainText(t.label) || 'tableFP';
|
|
464
|
+
jsonToExcel({
|
|
465
|
+
data: rows,
|
|
466
|
+
columns: columns,
|
|
467
|
+
filename: name.replace(/[\\\/:*?"<>|]/g, '_') + '.xlsx',
|
|
468
|
+
worksheet: name.replace(/[\[\]:*?\/\\]/g, ' ').slice(0, 31).trim() || 'SheetFP'
|
|
469
|
+
});
|
|
470
|
+
Vue.$toast.success("Đã xuất Excel", { position: 'top' });
|
|
471
|
+
},
|
|
472
|
+
uiExcelColumns() {
|
|
473
|
+
var t = this, out = [];
|
|
474
|
+
var skip = { 'ctrl-update': 1, 'data-table-select': 1, 'data-table-expand': 1 };
|
|
475
|
+
(function walk(list, prefix) {
|
|
476
|
+
_.forEach(list, function (h) {
|
|
477
|
+
if (!h || typeof h !== 'object') return;
|
|
478
|
+
var title = t.plainText(h.text !== undefined ? h.text : h.title);
|
|
479
|
+
var full = prefix && title ? prefix + ' / ' + title : (title || prefix);
|
|
480
|
+
if (Array.isArray(h.children) && h.children.length) { walk(h.children, full); return; }
|
|
481
|
+
var key = typeof h.value === 'string' ? h.value : null;
|
|
482
|
+
if (!key || skip[key]) return;
|
|
483
|
+
out.push(t.uiExcelColumn(h, key, key, full || key));
|
|
484
|
+
});
|
|
485
|
+
})(t.headerCols, '');
|
|
486
|
+
return out;
|
|
487
|
+
},
|
|
488
|
+
uiExcelColumn(h, key, path, header) {
|
|
489
|
+
var attr = h.attr || {};
|
|
490
|
+
// Ô t-* hiển thị field mà v-model trỏ tới ("item.X"), không nhất thiết trùng key của cột
|
|
491
|
+
var vm = typeof attr['v-model'] === 'string' && /^\s*item\.([\w$.]+)\s*$/.exec(attr['v-model']);
|
|
492
|
+
var tablePath = path;
|
|
493
|
+
if (vm) path = vm[1];
|
|
494
|
+
var col = { header: header, key: path, tablePath: tablePath, sortKey: key, compare: typeof h.sort === 'function' ? h.sort : null };
|
|
495
|
+
var wm = /^\s*(\d+(?:\.\d+)?)\s*(px)?\s*$/.exec(h.width === undefined || h.width === null ? '' : String(h.width));
|
|
496
|
+
if (wm) col.width = Math.max(8, Math.round(Number(wm[1]) / 7));
|
|
497
|
+
return col;
|
|
498
|
+
},
|
|
499
|
+
uiExcelRows(columns) {
|
|
500
|
+
var t = this;
|
|
501
|
+
var rows = (t.items || []).slice();
|
|
502
|
+
// Vuetify 2 trim chuỗi tìm kiếm trước khi lọc; customSearch của bảng quét mọi field của dòng gốc
|
|
503
|
+
var q = typeof t.search === 'string' ? t.search.trim() : '';
|
|
504
|
+
if (q) rows = rows.filter(function (r) { return t.customSearch(null, q, r); });
|
|
505
|
+
var spec = t.uiSortSpec();
|
|
506
|
+
if (spec.length) {
|
|
507
|
+
var byKey = _.keyBy(columns, 'sortKey');
|
|
508
|
+
var collator = t.uiCollator();
|
|
509
|
+
var fields = spec.map(function (s) {
|
|
510
|
+
var c = byKey[s.key];
|
|
511
|
+
return { path: c ? c.tablePath : s.key, desc: s.desc, compare: c && c.compare };
|
|
512
|
+
});
|
|
513
|
+
rows.sort(function (a, b) {
|
|
514
|
+
for (var i = 0; i < fields.length; i++) {
|
|
515
|
+
var f = fields[i];
|
|
516
|
+
var va = t.readCell(a, f.path), vb = t.readCell(b, f.path);
|
|
517
|
+
if (f.desc) { var tmp = va; va = vb; vb = tmp; }
|
|
518
|
+
var r = f.compare ? f.compare(va, vb) : t.compareCell(va, vb, collator);
|
|
519
|
+
if (r) return r;
|
|
520
|
+
}
|
|
521
|
+
return 0;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return rows;
|
|
525
|
+
},
|
|
526
|
+
// group-by đứng trước sort-by — đúng thứ tự v-data-table sắp dòng khi gộp nhóm.
|
|
527
|
+
// Chưa nhận update:options nào thì lấy từ attr khai sẵn.
|
|
528
|
+
uiSortSpec() {
|
|
529
|
+
var o = this.tableOptions, a = this.$attrs || {};
|
|
530
|
+
var gb = o ? o.groupBy : (a['group-by'] || a.groupBy), gd = o ? o.groupDesc : (a['group-desc'] || a.groupDesc);
|
|
531
|
+
var sb = o ? o.sortBy : (a['sort-by'] || a.sortBy), sd = o ? o.sortDesc : (a['sort-desc'] || a.sortDesc);
|
|
532
|
+
return this.normalizeSort(gb, gd).concat(this.normalizeSort(sb, sd));
|
|
533
|
+
},
|
|
534
|
+
normalizeSort(by, desc) {
|
|
535
|
+
var d = _.castArray(desc === undefined || desc === null ? [] : desc);
|
|
536
|
+
return _.castArray(by === undefined || by === null ? [] : by).map(function (k, i) {
|
|
537
|
+
return { key: k, desc: d[i] === true || d[i] === 'true' };
|
|
538
|
+
}).filter(function (s) { return typeof s.key === 'string' && s.key !== ''; });
|
|
539
|
+
},
|
|
540
|
+
// Bám thuật toán sortItems của Vuetify: ngày so theo thời gian, rỗng lên đầu, hai chuỗi số so
|
|
541
|
+
// theo số, còn lại so bằng Intl.Collator (có dấu tiếng Việt).
|
|
542
|
+
compareCell(a, b, collator) {
|
|
543
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
|
|
544
|
+
var sa = a === null || a === undefined ? '' : String(a).toLocaleLowerCase();
|
|
545
|
+
var sb = b === null || b === undefined ? '' : String(b).toLocaleLowerCase();
|
|
546
|
+
if (sa === sb) return 0;
|
|
547
|
+
if (sa === '') return -1;
|
|
548
|
+
if (sb === '') return 1;
|
|
549
|
+
if (!isNaN(sa) && !isNaN(sb)) return Number(sa) - Number(sb);
|
|
550
|
+
return collator.compare(sa, sb);
|
|
551
|
+
},
|
|
552
|
+
uiCollator() {
|
|
553
|
+
var loc = _.get(this, '$vuetify.lang.current');
|
|
554
|
+
try { return new Intl.Collator(loc || undefined, { sensitivity: 'accent', usage: 'sort' }); }
|
|
555
|
+
catch (e) { return new Intl.Collator(undefined, { sensitivity: 'accent', usage: 'sort' }); }
|
|
556
|
+
},
|
|
557
|
+
readCell(row, path) {
|
|
558
|
+
if (row === null || row === undefined) return null;
|
|
559
|
+
return row[path] !== undefined ? row[path] : _.get(row, path);
|
|
560
|
+
},
|
|
561
|
+
plainText(v) {
|
|
562
|
+
if (v === null || v === undefined) return '';
|
|
563
|
+
var s = String(v);
|
|
564
|
+
if (s.indexOf('<') < 0 && s.indexOf('&') < 0) return s.trim();
|
|
565
|
+
return (new DOMParser().parseFromString(s, 'text/html').body.textContent || '').trim();
|
|
566
|
+
},
|
|
445
567
|
}
|
|
446
568
|
});
|
|
447
569
|
//===========================================================================================
|