@fui-org/fui-cli 1.3.2 → 2.1.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.
Files changed (115) hide show
  1. package/README.md +21 -3
  2. package/dist/fui-cx180e19.js +379 -0
  3. package/dist/fui.js +1 -1
  4. package/package.json +3 -6
  5. package/skills/fui/SKILL.md +9 -41
  6. package/skills/fui-skill/SKILL.md +95 -225
  7. package/skills/fui-skill/assets/projectdefaultstyle-3.0.css +555 -0
  8. package/skills/fui-skill/assets/projectdefaultstyle.css +207 -235
  9. package/skills/fui-skill/references/INDEX.md +105 -137
  10. package/skills/fui-skill/references/advanced-techniques.md +76 -68
  11. package/skills/fui-skill/references/coding-standards.md +56 -56
  12. package/skills/fui-skill/references/component-design.md +166 -173
  13. package/skills/fui-skill/references/component-quickref.md +61 -60
  14. package/skills/fui-skill/references/component-table.md +128 -117
  15. package/skills/fui-skill/references/components-dialog.md +55 -56
  16. package/skills/fui-skill/references/components-display.md +24 -30
  17. package/skills/fui-skill/references/components-echart.md +186 -261
  18. package/skills/fui-skill/references/components-input.md +72 -96
  19. package/skills/fui-skill/references/controls-patterns.md +196 -342
  20. package/skills/fui-skill/references/controls-styling-vocabulary.md +130 -97
  21. package/skills/fui-skill/references/db-table-design.md +24 -28
  22. package/skills/fui-skill/references/db-workflow.md +191 -390
  23. package/skills/fui-skill/references/default-function.md +169 -128
  24. package/skills/fui-skill/references/design-modes.md +35 -63
  25. package/skills/fui-skill/references/echart-templates.md +204 -196
  26. package/skills/fui-skill/references/fastproject.md +62 -60
  27. package/skills/fui-skill/references/fsheet.md +109 -124
  28. package/skills/fui-skill/references/fullstack-workflow.md +90 -128
  29. package/skills/fui-skill/references/module-data-patterns.md +31 -40
  30. package/skills/fui-skill/references/module-json-anatomy.md +47 -52
  31. package/skills/fui-skill/references/module-structure.md +80 -196
  32. package/skills/fui-skill/references/new-session.md +49 -51
  33. package/skills/fui-skill/references/pdfmake.md +17 -17
  34. package/skills/fui-skill/references/permission-system.md +89 -108
  35. package/skills/fui-skill/references/platform-architecture.md +128 -153
  36. package/skills/fui-skill/references/project-config.md +102 -134
  37. package/skills/fui-skill/references/project-provisioning.md +139 -244
  38. package/skills/fui-skill/references/script-map.md +208 -242
  39. package/skills/fui-skill/references/sql-clr-functions.md +98 -97
  40. package/skills/fui-skill/references/system-design.md +63 -88
  41. package/skills/fui-skill/references/tapi-file-api.md +46 -52
  42. package/skills/fui-skill/references/tapi-permission-patterns.md +51 -53
  43. package/skills/fui-skill/references/tapi-reference.md +132 -207
  44. package/skills/fui-skill/references/tools-registry.md +84 -460
  45. package/skills/fui-skill/references/ui-crosswindow-patterns.md +79 -75
  46. package/skills/fui-skill/references/ui-dialog-patterns.md +98 -72
  47. package/skills/fui-skill/references/ui-layout-patterns.md +26 -26
  48. package/skills/fui-skill/references/ui-patterns.md +71 -83
  49. package/skills/fui-skill/references/ui-screenshot-review.md +63 -62
  50. package/skills/fui-skill/references/ui-table-cell-patterns.md +61 -59
  51. package/skills/fui-skill/references/ui-templates.md +16 -23
  52. package/skills/fui-skill/references/verification.md +236 -246
  53. package/skills/fui-skill/references/watcher-patterns.md +30 -63
  54. package/skills/fui-skill/references/websocket-realtime.md +83 -66
  55. package/skills/fui-skill/scripts/component-3.0.js +298 -131
  56. package/skills/fui-skill/scripts/component.js +277 -271
  57. package/skills/fui-skill/scripts/componentTable-3.0.js +182 -53
  58. package/skills/fui-skill/scripts/componentTable.js +171 -49
  59. package/skills/fui-skill/scripts/defaultfunction-3.0.js +88 -3
  60. package/skills/fui-skill/scripts/defaultfunction.js +88 -3
  61. package/skills/fui-skill/scripts/fsheet.js +38 -0
  62. package/skills/fui-target/.claude-plugin/plugin.json +6 -0
  63. package/skills/fui-target/hooks/hooks.json +1 -0
  64. package/skills/fui-target/hooks/register.tsx +71 -0
  65. package/skills/fui-target/types/index.d.ts +7 -0
  66. package/dist/fui-y8an39cn.js +0 -420
  67. package/skills/fui-skill/README.md +0 -112
  68. package/skills/fui-skill/metadata.json +0 -75
  69. package/skills/fui-skill-cli/SKILL.md +0 -139
  70. package/skills/fui-skill-cli/references/INDEX.md +0 -110
  71. package/skills/fui-skill-cli/references/advanced-techniques.md +0 -168
  72. package/skills/fui-skill-cli/references/coding-standards.md +0 -112
  73. package/skills/fui-skill-cli/references/component-design.md +0 -448
  74. package/skills/fui-skill-cli/references/component-quickref.md +0 -78
  75. package/skills/fui-skill-cli/references/component-table.md +0 -248
  76. package/skills/fui-skill-cli/references/components-dialog.md +0 -191
  77. package/skills/fui-skill-cli/references/components-display.md +0 -141
  78. package/skills/fui-skill-cli/references/components-echart.md +0 -316
  79. package/skills/fui-skill-cli/references/components-input.md +0 -335
  80. package/skills/fui-skill-cli/references/controls-patterns.md +0 -701
  81. package/skills/fui-skill-cli/references/controls-styling-vocabulary.md +0 -137
  82. package/skills/fui-skill-cli/references/db-table-design.md +0 -73
  83. package/skills/fui-skill-cli/references/db-workflow.md +0 -288
  84. package/skills/fui-skill-cli/references/default-function.md +0 -425
  85. package/skills/fui-skill-cli/references/design-modes.md +0 -57
  86. package/skills/fui-skill-cli/references/echart-templates.md +0 -489
  87. package/skills/fui-skill-cli/references/fastproject.md +0 -99
  88. package/skills/fui-skill-cli/references/fsheet.md +0 -203
  89. package/skills/fui-skill-cli/references/fullstack-workflow.md +0 -313
  90. package/skills/fui-skill-cli/references/module-data-patterns.md +0 -117
  91. package/skills/fui-skill-cli/references/module-json-anatomy.md +0 -132
  92. package/skills/fui-skill-cli/references/module-structure.md +0 -141
  93. package/skills/fui-skill-cli/references/new-session.md +0 -85
  94. package/skills/fui-skill-cli/references/pdfmake.md +0 -60
  95. package/skills/fui-skill-cli/references/permission-system.md +0 -150
  96. package/skills/fui-skill-cli/references/platform-architecture.md +0 -269
  97. package/skills/fui-skill-cli/references/project-config.md +0 -303
  98. package/skills/fui-skill-cli/references/project-provisioning.md +0 -278
  99. package/skills/fui-skill-cli/references/script-map.md +0 -262
  100. package/skills/fui-skill-cli/references/sql-clr-functions.md +0 -225
  101. package/skills/fui-skill-cli/references/system-design.md +0 -89
  102. package/skills/fui-skill-cli/references/tapi-file-api.md +0 -185
  103. package/skills/fui-skill-cli/references/tapi-permission-patterns.md +0 -156
  104. package/skills/fui-skill-cli/references/tapi-reference.md +0 -474
  105. package/skills/fui-skill-cli/references/tools-registry.md +0 -84
  106. package/skills/fui-skill-cli/references/ui-crosswindow-patterns.md +0 -321
  107. package/skills/fui-skill-cli/references/ui-dialog-patterns.md +0 -255
  108. package/skills/fui-skill-cli/references/ui-layout-patterns.md +0 -176
  109. package/skills/fui-skill-cli/references/ui-patterns.md +0 -303
  110. package/skills/fui-skill-cli/references/ui-screenshot-review.md +0 -95
  111. package/skills/fui-skill-cli/references/ui-table-cell-patterns.md +0 -318
  112. package/skills/fui-skill-cli/references/ui-templates.md +0 -22
  113. package/skills/fui-skill-cli/references/verification.md +0 -236
  114. package/skills/fui-skill-cli/references/watcher-patterns.md +0 -163
  115. 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
- fixedTableClass: 'f-table-fixed-' + (this.$ && this.$.uid !== undefined ? this.$.uid : this._uid),
221
- itemselected: [],
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
- fixedTableClass: 'f-table-fixed-' + this._uid,
76
- autoHeaders: this.headers ? null : this.buildAutoHeaders(this.items || []),
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
  //===========================================================================================