@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.
Files changed (111) hide show
  1. package/README.md +19 -3
  2. package/dist/fui-bmg5pnmq.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/dist/fui-y8an39cn.js +0 -420
  63. package/skills/fui-skill/README.md +0 -112
  64. package/skills/fui-skill/metadata.json +0 -75
  65. package/skills/fui-skill-cli/SKILL.md +0 -139
  66. package/skills/fui-skill-cli/references/INDEX.md +0 -110
  67. package/skills/fui-skill-cli/references/advanced-techniques.md +0 -168
  68. package/skills/fui-skill-cli/references/coding-standards.md +0 -112
  69. package/skills/fui-skill-cli/references/component-design.md +0 -448
  70. package/skills/fui-skill-cli/references/component-quickref.md +0 -78
  71. package/skills/fui-skill-cli/references/component-table.md +0 -248
  72. package/skills/fui-skill-cli/references/components-dialog.md +0 -191
  73. package/skills/fui-skill-cli/references/components-display.md +0 -141
  74. package/skills/fui-skill-cli/references/components-echart.md +0 -316
  75. package/skills/fui-skill-cli/references/components-input.md +0 -335
  76. package/skills/fui-skill-cli/references/controls-patterns.md +0 -701
  77. package/skills/fui-skill-cli/references/controls-styling-vocabulary.md +0 -137
  78. package/skills/fui-skill-cli/references/db-table-design.md +0 -73
  79. package/skills/fui-skill-cli/references/db-workflow.md +0 -288
  80. package/skills/fui-skill-cli/references/default-function.md +0 -425
  81. package/skills/fui-skill-cli/references/design-modes.md +0 -57
  82. package/skills/fui-skill-cli/references/echart-templates.md +0 -489
  83. package/skills/fui-skill-cli/references/fastproject.md +0 -99
  84. package/skills/fui-skill-cli/references/fsheet.md +0 -203
  85. package/skills/fui-skill-cli/references/fullstack-workflow.md +0 -313
  86. package/skills/fui-skill-cli/references/module-data-patterns.md +0 -117
  87. package/skills/fui-skill-cli/references/module-json-anatomy.md +0 -132
  88. package/skills/fui-skill-cli/references/module-structure.md +0 -141
  89. package/skills/fui-skill-cli/references/new-session.md +0 -85
  90. package/skills/fui-skill-cli/references/pdfmake.md +0 -60
  91. package/skills/fui-skill-cli/references/permission-system.md +0 -150
  92. package/skills/fui-skill-cli/references/platform-architecture.md +0 -269
  93. package/skills/fui-skill-cli/references/project-config.md +0 -303
  94. package/skills/fui-skill-cli/references/project-provisioning.md +0 -278
  95. package/skills/fui-skill-cli/references/script-map.md +0 -262
  96. package/skills/fui-skill-cli/references/sql-clr-functions.md +0 -225
  97. package/skills/fui-skill-cli/references/system-design.md +0 -89
  98. package/skills/fui-skill-cli/references/tapi-file-api.md +0 -185
  99. package/skills/fui-skill-cli/references/tapi-permission-patterns.md +0 -156
  100. package/skills/fui-skill-cli/references/tapi-reference.md +0 -474
  101. package/skills/fui-skill-cli/references/tools-registry.md +0 -84
  102. package/skills/fui-skill-cli/references/ui-crosswindow-patterns.md +0 -321
  103. package/skills/fui-skill-cli/references/ui-dialog-patterns.md +0 -255
  104. package/skills/fui-skill-cli/references/ui-layout-patterns.md +0 -176
  105. package/skills/fui-skill-cli/references/ui-patterns.md +0 -303
  106. package/skills/fui-skill-cli/references/ui-screenshot-review.md +0 -95
  107. package/skills/fui-skill-cli/references/ui-table-cell-patterns.md +0 -318
  108. package/skills/fui-skill-cli/references/ui-templates.md +0 -22
  109. package/skills/fui-skill-cli/references/verification.md +0 -236
  110. package/skills/fui-skill-cli/references/watcher-patterns.md +0 -163
  111. package/skills/fui-skill-cli/references/websocket-realtime.md +0 -271
@@ -1872,151 +1872,6 @@ Vue.component('fp-profile', {
1872
1872
  },
1873
1873
  });
1874
1874
  //===========================================================================================
1875
- // Vue.component('header-bar', {
1876
- // props: {
1877
- // menu: {
1878
- // default: null
1879
- // },
1880
- // menuLeft: {
1881
- // default: null
1882
- // },
1883
- // title: {
1884
- // default: ''
1885
- // },
1886
- // logo: {
1887
- // default: ""
1888
- // },
1889
- // expand: {
1890
- // default: true
1891
- // },
1892
- // menuStyle: {
1893
- // default: {
1894
- // topmenu: {
1895
- // bgcolor: "primary",
1896
- // textcolor: "white"
1897
- // },
1898
- // leftmenu: {
1899
- // bgcolor: "primary",
1900
- // textcolor: "white"
1901
- // }
1902
- // }
1903
- // },
1904
- // component: {
1905
- // default: null
1906
- // },
1907
- // userProfile: {
1908
- // default: null
1909
- // },
1910
- // },
1911
- // template: `<div>
1912
- // <v-app-bar v-if="isMobile || menu" light app dense flat clipped-left :color="menuStyle.topmenu.bgcolor" :style="{ 'box-shadow': 'rgba(0, 0, 0, 0.5) 0px 0px '+ boxShadow +'px 0px !important'}">
1913
- // <v-img class="mr-3" v-if="logo" :src="logo" height="26" width="26" max-height="26" max-width="26" contain></v-img>
1914
- // <v-toolbar-title v-if="title" :class="[menuStyle.topmenu.textcolor + '--text']">{{title}}</v-toolbar-title>
1915
- // <v-spacer></v-spacer>
1916
- // <v-toolbar-items class="hidden-sm-and-down" v-if="menu">
1917
- // <v-menu offset-y v-for="(item, index) in menu.filter(info => checkright(info))" :key="index">
1918
- // <template v-slot:activator="{ on }">
1919
- // <v-btn v-on="on" :href="(!item.submenu?item.url:'')" :target="item.target" text :class="[menuStyle.topmenu.textcolor + '--text']">
1920
- // <v-icon v-if="item.icon" v-text="item.icon" left class="mr-1"></v-icon>
1921
- // {{ item.name }}<v-icon v-if="!!item.submenu">mdi-chevron-down</v-icon>
1922
- // </v-btn>
1923
- // </template>
1924
- // <v-list v-if="!!item.submenu">
1925
- // <v-list-item v-for="(i, index) in item.submenu.filter(info => checkright(info))" :key="index" :href="i.url" :target="i.target">
1926
- // <v-list-item-title>{{ i.name }}</v-list-item-title>
1927
- // </v-list-item>
1928
- // </v-list>
1929
- // </v-menu>
1930
- // </v-toolbar-items>
1931
- // <component v-if="!isMobile && component" v-for="(c, index) in component" :key="index" v-bind:is="c.el" v-bind="c.attr"></component>
1932
- // <fp-profile v-if="!isMobile && userProfile" v-bind="userProfile"></fp-profile>
1933
- // <v-app-bar-nav-icon v-if="isMobile && leftMenuAndMobile" :color="menuStyle.topmenu.textcolor" @click.stop="openLeftMenu=!openLeftMenu"></v-app-bar-nav-icon>
1934
- // </v-app-bar>
1935
-
1936
- // <v-navigation-drawer v-if="leftMenuAndMobile" :class="[menuStyle.leftmenu.bgcolor, menuStyle.leftmenu.textcolor + '--text','fpScrollbar']" v-model="openLeftMenu" app clipped :right="isMobile" :permanent="!isMobile" :mini-variant="pinLeftMenu && !isMobile" :expand-on-hover="pinLeftMenu && !isMobile">
1937
- // <template v-slot:prepend v-if="!isMobile && !menu">
1938
- // <v-list-item class="px-0 pt-1 pl-3">
1939
- // <v-img class="mr-1" v-if="logo" :src="logo" height="30" width="30" max-height="30" max-width="30" contain></v-img>
1940
- // <v-toolbar-title v-if="title" :class="[menuStyle.leftmenu.textcolor + '--text']">{{title}}</v-toolbar-title>
1941
- // </v-list-item>
1942
- // </template>
1943
- // <v-list class="text-left pa-0" :color="menuStyle.leftmenu.bgcolor" dark v-if="leftMenuAndMobile">
1944
- // <template v-for="(item, index) in leftMenuAndMobile.filter(info => checkright(info))" :key="index">
1945
- // <v-list-item v-if="!item.submenu" :href="item.url" :target="item.target">
1946
- // <v-icon v-if="item.icon" v-text="item.icon" class="mr-2"></v-icon>
1947
- // <v-list-item-title :class="[menuStyle.leftmenu.textcolor + '--text']">{{ item.name }}</v-list-item-title>
1948
- // </v-list-item>
1949
- // <v-list-group v-if="item.submenu" v-model="item.active" :color="menuStyle.leftmenu.textcolor">
1950
- // <template v-slot:activator>
1951
- // <v-icon v-if="item.icon" v-text="item.icon" class="mr-2"></v-icon>
1952
- // <v-list-item-title :class="[menuStyle.leftmenu.textcolor + '--text']">{{ item.name }}</v-list-item-title>
1953
- // </template>
1954
- // <v-list-item v-for="(i, index) in item.submenu.filter(info => checkright(info))" :key="index" :href="i.url" :target="i.target">
1955
- // <v-list-item-subtitle class="ml-6" :class="[menuStyle.leftmenu.textcolor + '--text']">{{ i.name }}</v-list-item-title>
1956
- // </v-list-item>
1957
- // </v-list-group>
1958
- // </template>
1959
- // </v-list>
1960
- // <template v-slot:append>
1961
- // <component v-if="!menu && component || isMobile" v-for="(c, index) in component" :key="index" v-bind:is="c.el" v-bind="c.attr"></component>
1962
- // <v-list-item class="pa-0">
1963
- // <fp-profile v-if="!menu && userProfile || isMobile" v-bind="userProfile" open-on-hover></fp-profile>
1964
- // <v-spacer></v-spacer>
1965
- // <v-btn @click.stop="on_of_menu" v-if="(menu || leftMenuAndMobile) && !isMobile" :color="menuStyle.topmenu.textcolor" icon><v-icon :color="menuStyle.leftmenu.textcolor">{{ pinLeftMenu?'mdi-pin-outline':'mdi-pin-off-outline'}}</v-icon></v-btn>
1966
- // </v-list-item>
1967
- // </template>
1968
- // </v-navigation-drawer>
1969
- // </div>`,
1970
- // data: function () {
1971
- // var leftMenuAndMobile = []
1972
- // if (this.$vuetify.breakpoint.smAndDown) {
1973
- // _.forEach(_.union(this.menu, this.menuLeft), function (value) {
1974
- // if (rightTest(value.right)) leftMenuAndMobile.push(value);
1975
- // });
1976
-
1977
- // if (leftMenuAndMobile.length == 0) leftMenuAndMobile = null;
1978
- // }
1979
- // else leftMenuAndMobile = this.menuLeft;
1980
-
1981
- // return {
1982
- // leftMenuAndMobile: leftMenuAndMobile,
1983
- // openLeftMenu: this.isMobile,
1984
- // pinLeftMenu: (sessionStorage.getItem("fpOpenmenuAppBar") == null ? !this.expand : sessionStorage.getItem("fpOpenmenuAppBar") == 'true'),
1985
- // boxShadow: 0
1986
- // }
1987
- // },
1988
- // computed: {
1989
- // isMobile() {
1990
- // if (this.$vuetify.breakpoint.smAndDown) return true
1991
- // else return false;
1992
- // }
1993
- // },
1994
- // methods: {
1995
- // on_of_menu: function (e) {
1996
- // this.pinLeftMenu = !this.pinLeftMenu
1997
- // sessionStorage.setItem("fpOpenmenuAppBar", this.pinLeftMenu);
1998
- // },
1999
- // checkright: function (item) {
2000
- // if (!item.right) return true;
2001
- // if (!vueData.user) return false;
2002
-
2003
- // return rightTest(item.right);
2004
- // }
2005
- // },
2006
- // created() {
2007
- // if (!this.$isServer) {
2008
- // this._scrollListener = () => {
2009
- // this.boxShadow = Math.round(window.pageYOffset) / 10;
2010
- // if (this.boxShadow > 10) this.boxShadow = 10;
2011
- // }
2012
- // this._scrollListener()
2013
- // window.addEventListener('scroll', this._scrollListener)
2014
- // }
2015
- // },
2016
- // beforeDestroy() {
2017
- // window.removeEventListener('scroll', this._scrollListener)
2018
- // }
2019
- // });
2020
1875
  Vue.component('header-bar', {
2021
1876
  props: {
2022
1877
  menu: {
@@ -2083,7 +1938,7 @@ Vue.component('header-bar', {
2083
1938
  <v-navigation-drawer \
2084
1939
  v-if=\"leftMenuAndMobile\" \
2085
1940
  ref=\"navDrawer\"\
2086
- :class=\"[menuStyle.leftmenu.bgcolor, menuStyle.leftmenu.textcolor + '--text', 'fp-custom-scrollbar']\" \
1941
+ :class=\"[menuStyle.leftmenu.bgcolor, menuStyle.leftmenu.textcolor + '--text']\" \
2087
1942
  v-model=\"openLeftMenu\" \
2088
1943
  app \
2089
1944
  clipped \
@@ -2216,39 +2071,6 @@ Vue.component('header-bar', {
2216
2071
  }
2217
2072
  }, 300);
2218
2073
  });
2219
- },
2220
- injectScrollbarStyles: function () {
2221
- if (!this.leftMenuAndMobile || this.leftMenuAndMobile.length === 0) return;
2222
-
2223
- var styleId = 'fp-custom-scrollbar-style';
2224
- if (document.getElementById(styleId)) return;
2225
-
2226
- var thumbColor = 'rgba(255,255,255,0.3)';
2227
- var thumbHoverColor = 'rgba(255,255,255,0.5)';
2228
- var trackColor = 'rgba(0,0,0,0.1)';
2229
-
2230
- var style = document.createElement('style');
2231
- style.id = styleId;
2232
- style.textContent = [
2233
- '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar {',
2234
- ' width: 5px;',
2235
- '}',
2236
- '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar-track {',
2237
- ' background: ' + trackColor + ';',
2238
- '}',
2239
- '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar-thumb {',
2240
- ' background: ' + thumbColor + ';',
2241
- ' border-radius: 3px;',
2242
- '}',
2243
- '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar-thumb:hover {',
2244
- ' background: ' + thumbHoverColor + ';',
2245
- '}',
2246
- '.fp-custom-scrollbar .v-navigation-drawer__content {',
2247
- ' scrollbar-width: thin;',
2248
- ' scrollbar-color: ' + thumbColor + ' ' + trackColor + ';',
2249
- '}'
2250
- ].join('\n');
2251
- document.head.appendChild(style);
2252
2074
  }
2253
2075
  },
2254
2076
  created: function () {
@@ -2264,7 +2086,8 @@ Vue.component('header-bar', {
2264
2086
  },
2265
2087
  mounted: function () {
2266
2088
  if (this.leftMenuAndMobile && this.leftMenuAndMobile.length > 0) {
2267
- this.injectScrollbarStyles();
2089
+ // Thanh cuộn menu trái lấy từ .fpScrollbar trong projectdefaultstyle.css
2090
+ // (rule đó đã phủ sẵn .v-navigation-drawer__content).
2268
2091
  this.scrollToActiveMenu();
2269
2092
  }
2270
2093
  },
@@ -2941,30 +2764,82 @@ Vue.component('f-excel-reader', {
2941
2764
  dateFormat: { type: String, default: 'dd/mm/yyyy' },
2942
2765
  defval: { type: [String, Number], default: '' },
2943
2766
 
2944
- sheet: { type: [String, Number], default: 0 },
2945
- skipRows: { type: [String, Number], default: 0 },
2767
+ sheet: { type: [String, Number], default: null },
2768
+ sheetPicker: { type: [Boolean, String], default: true },
2769
+ sheetStats: { type: [Boolean, String], default: false },
2770
+ skipRows: { type: [String, Number], default: 0 },
2946
2771
 
2947
- label: { type: String, default: 'Load Excel' },
2948
- iconText: { type: String, default: 'mdi-grid' },
2949
- accept: { type: String, default: '.xlsx,.xlsb,.xlsm,.xls,.csv,.txt,.ods,.dbf,.prn,.html,.htm' },
2950
- maxSize: { type: [String, Number], default: 20 },
2772
+ label: { type: String, default: 'Load Excel' },
2773
+ iconText: { type: String, default: 'mdi-grid' },
2774
+ accept: { type: String, default: '.xlsx,.xlsb,.xlsm,.xls,.csv,.txt,.ods,.dbf,.prn,.html,.htm' },
2775
+ maxSize: { type: [String, Number], default: 20 },
2951
2776
  loadTimeout: { type: [String, Number], default: 15000 },
2777
+ loadingText: { type: String, default: 'Đang xử lý dữ liệu...' },
2778
+
2779
+ notify: { type: [Boolean, String], default: true },
2780
+ notifyTimeout: { type: [String, Number], default: 6000 },
2952
2781
 
2953
2782
  action: { type: Object, default: null }
2954
2783
  },
2955
2784
  template: `
2956
2785
  <div>
2957
2786
  <v-btn v-bind="$attrs" v-on="$listeners"
2958
- :disabled="parsing" :loading="busy" @click="onButtonClick">
2787
+ :disabled="parsing" @click="onButtonClick">
2959
2788
  <v-icon v-if="iconText" left>{{ iconText }}</v-icon>
2960
2789
  {{ label }}
2961
2790
  </v-btn>
2962
- <input ref="uploader" style="display:none" type="file" :multiple="false"
2963
- :accept="accept" @change="onchange" onclick="this.value=null;">
2791
+ <input ref="uploader" style="display:none" type="file" :multiple="false" :accept="accept" @change="onchange" onclick="this.value=null;">
2792
+
2793
+ <v-dialog v-model="loading" persistent width="320">
2794
+ <v-card class="pa-8 d-flex flex-column align-center justify-center text-center">
2795
+ <div class="fui-loading"></div>
2796
+ <div class="mt-5 body-2">{{ loadingMsg }}</div>
2797
+ <div v-if="loadingSub" class="mt-1 caption grey--text text-truncate" style="max-width:100%;">{{ loadingSub }}</div>
2798
+ </v-card>
2799
+ </v-dialog>
2800
+
2801
+ <v-dialog v-model="dialog" max-width="440" persistent>
2802
+ <v-card>
2803
+ <v-card-title class="subtitle-1">Chọn sheet cần đọc</v-card-title>
2804
+ <v-card-text class="pb-0">
2805
+ <div class="caption mb-2">{{ pendingName }}</div>
2806
+ <v-list dense style="max-height:300px; overflow-y:auto;">
2807
+ <v-list-item v-for="s in sheetList" :key="s.name"
2808
+ @click="sheetChoice = s.name">
2809
+ <v-list-item-content>
2810
+ <v-list-item-title
2811
+ :class="{ 'primary--text font-weight-bold': s.name === sheetChoice }">
2812
+ {{ s.name }}
2813
+ </v-list-item-title>
2814
+ <v-list-item-subtitle v-if="s.info">{{ s.info }}</v-list-item-subtitle>
2815
+ </v-list-item-content>
2816
+ </v-list-item>
2817
+ </v-list>
2818
+ </v-card-text>
2819
+ <v-card-actions>
2820
+ <v-spacer></v-spacer>
2821
+ <v-btn text @click="pickSheet(null)">Hủy</v-btn>
2822
+ <v-btn text color="primary" :disabled="!sheetChoice"
2823
+ @click="pickSheet(sheetChoice)">Chọn</v-btn>
2824
+ </v-card-actions>
2825
+ </v-card>
2826
+ </v-dialog>
2827
+
2828
+ <v-snackbar v-model="snack" :color="snackColor" :timeout="Number(notifyTimeout)" top>
2829
+ {{ snackMsg }}
2830
+ <template #action="{ attrs }">
2831
+ <v-btn text v-bind="attrs" @click="snack = false">Đóng</v-btn>
2832
+ </template>
2833
+ </v-snackbar>
2964
2834
  </div>
2965
2835
  `,
2966
2836
  data: function () {
2967
- return { busy: false, parsing: false, reqId: 0 };
2837
+ return {
2838
+ parsing: false, reqId: 0,
2839
+ loading: false, loadingMsg: '', loadingSub: '',
2840
+ dialog: false, sheetList: [], sheetChoice: null, pendingName: '',
2841
+ snack: false, snackMsg: '', snackColor: 'error'
2842
+ };
2968
2843
  },
2969
2844
  computed: {
2970
2845
  optHeader: function () {
@@ -2976,29 +2851,56 @@ Vue.component('f-excel-reader', {
2976
2851
  if (s.toUpperCase() === 'A') return 'A';
2977
2852
  return h;
2978
2853
  },
2979
- optRaw: function () {
2980
- if (typeof this.raw === 'boolean') return this.raw;
2981
- var s = String(this.raw).trim().toLowerCase();
2982
- return s === 'true' || s === '1' || s === 'yes';
2983
- },
2854
+ optRaw: function () { return this.toBool(this.raw, false); },
2855
+ usePicker: function () { return this.toBool(this.sheetPicker, true); },
2856
+ useStats: function () { return this.toBool(this.sheetStats, false); },
2857
+ useNotify: function () { return this.toBool(this.notify, true); },
2984
2858
  optRange: function () {
2985
2859
  var n = parseInt(this.skipRows, 10);
2986
2860
  return n > 0 ? n : undefined;
2987
2861
  },
2988
2862
  limitMB: function () {
2989
- var mb = parseFloat(this.maxSize);
2990
- return isNaN(mb) ? 20 : mb;
2863
+ var n = parseFloat(this.maxSize);
2864
+ if (isNaN(n)) return 20;
2865
+ return n > 0 ? n : 0;
2991
2866
  }
2992
2867
  },
2993
2868
  methods: {
2994
- onButtonClick: function () {
2869
+ toBool: function (v, def) {
2870
+ if (typeof v === 'boolean') return v;
2871
+ if (v === null || v === undefined || v === '') return def;
2872
+ var s = String(v).trim().toLowerCase();
2873
+ if (s === 'true' || s === '1' || s === 'yes') return true;
2874
+ if (s === 'false' || s === '0' || s === 'no') return false;
2875
+ return def;
2876
+ },
2877
+
2878
+ nextPaint: function () {
2879
+ return new Promise(function (resolve) {
2880
+ requestAnimationFrame(function () {
2881
+ requestAnimationFrame(function () { setTimeout(resolve, 0); });
2882
+ });
2883
+ });
2884
+ },
2885
+
2886
+ showLoading: function (msg, sub) {
2995
2887
  var ctrl = this;
2996
- if (ctrl.parsing) return; // (2)
2997
- ctrl.busy = true;
2998
- window.addEventListener('focus', function () {
2999
- setTimeout(function () { if (!ctrl.parsing) ctrl.busy = false; }, 300);
3000
- }, { once: true });
3001
- ctrl.$refs.uploader.click();
2888
+ ctrl.loadingMsg = msg || ctrl.loadingText;
2889
+ ctrl.loadingSub = sub || '';
2890
+ ctrl.loading = true;
2891
+ return new Promise(function (resolve) {
2892
+ ctrl.$nextTick(function () { ctrl.nextPaint().then(resolve); });
2893
+ });
2894
+ },
2895
+
2896
+ hideLoading: function () {
2897
+ this.loading = false;
2898
+ this.loadingSub = '';
2899
+ },
2900
+
2901
+ onButtonClick: function () {
2902
+ if (this.parsing) return;
2903
+ this.$refs.uploader.click();
3002
2904
  },
3003
2905
 
3004
2906
  onchange: function (evt) {
@@ -3006,33 +2908,57 @@ Vue.component('f-excel-reader', {
3006
2908
  var file = evt.target.files && evt.target.files[0];
3007
2909
  evt.target.value = '';
3008
2910
 
3009
- if (ctrl.parsing) return; // (2)
3010
- if (!file) { ctrl.busy = false; return; }
2911
+ if (ctrl.parsing) return;
2912
+ if (!file) return;
3011
2913
 
3012
- var id = ++ctrl.reqId; // (1)
3013
- var parsed = false; // (3) cờ chốt: đã đọc xong Excel
2914
+ var id = ++ctrl.reqId;
2915
+ var parsed = false;
2916
+ var u8 = null;
3014
2917
  ctrl.parsing = true;
3015
- ctrl.busy = true;
3016
2918
 
3017
2919
  Promise.resolve()
3018
2920
  .then(function () {
3019
- if (ctrl.limitMB > 0 && file.size > ctrl.limitMB * 1024 * 1024)
3020
- throw new Error('File vượt quá ' + ctrl.limitMB + 'MB');
3021
- return ctrl.ensureXLSX();
2921
+ if (ctrl.limitMB > 0 && file.size > ctrl.limitMB * 1048576)
2922
+ throw new Error('File "' + file.name + '" nặng ' + ctrl.fmtSize(file.size)
2923
+ + ', vượt giới hạn ' + ctrl.limitMB + 'MB');
2924
+ return ctrl.showLoading('Đang mở tập tin...', file.name);
3022
2925
  })
3023
- .then(function () { return ctrl.parseFile(file); })
3024
- .then(function (res) {
3025
- if (id !== ctrl.reqId) return; // (1) lượt cũ -> vứt
3026
- ctrl.parsing = false;
3027
- ctrl.busy = false;
3028
- parsed = true;
3029
-
3030
- ctrl.$emit('input', res.rows);
3031
- ctrl.$emit('load', res.meta);
3032
- ctrl.$nextTick(function () { ctrl.runAction(file); }); // (3)
2926
+ .then(function () { return ctrl.ensureXLSX(); })
2927
+ .then(function () { return ctrl.readBuffer(file); })
2928
+ .then(function (buf) {
2929
+ if (id !== ctrl.reqId) return null;
2930
+ u8 = new Uint8Array(buf);
2931
+ return ctrl.showLoading('Đang đọc cấu trúc tập tin...', file.name)
2932
+ .then(function () { return ctrl.resolveSheet(u8, file); });
2933
+ })
2934
+ .then(function (wsname) {
2935
+ if (id !== ctrl.reqId) return;
2936
+
2937
+ if (!wsname) {
2938
+ ctrl.parsing = false;
2939
+ ctrl.hideLoading();
2940
+ parsed = true;
2941
+ ctrl.$emit('cancel', { fileName: file.name, reason: 'sheet' });
2942
+ return;
2943
+ }
2944
+
2945
+ // dialog loading cho MỌI trường hợp, kể cả file 1 sheet
2946
+ return ctrl.showLoading(ctrl.loadingText, 'Sheet: ' + wsname)
2947
+ .then(function () {
2948
+ if (id !== ctrl.reqId) return;
2949
+
2950
+ var res = ctrl.parseSheet(u8, wsname, file);
2951
+ ctrl.parsing = false;
2952
+ ctrl.hideLoading();
2953
+ parsed = true;
2954
+
2955
+ ctrl.$emit('input', res.rows);
2956
+ ctrl.$emit('load', res.meta);
2957
+ ctrl.$nextTick(function () { ctrl.runAction(file); });
2958
+ });
3033
2959
  })
3034
2960
  .catch(function (err) {
3035
- if (id !== ctrl.reqId || parsed) { // (1)(3)
2961
+ if (id !== ctrl.reqId || parsed) {
3036
2962
  console.warn('[FExcelReader] bỏ qua lỗi ngoài phạm vi đọc Excel:', err);
3037
2963
  return;
3038
2964
  }
@@ -3040,6 +2966,107 @@ Vue.component('f-excel-reader', {
3040
2966
  });
3041
2967
  },
3042
2968
 
2969
+ // ---------- PHA 1 ----------
2970
+ listSheets: function (u8) {
2971
+ if (this.useStats) {
2972
+ var wbFull = XLSX.read(u8, { bookSheets: true, bookProps: true, dense: true });
2973
+ return { names: wbFull.SheetNames, wb: wbFull };
2974
+ }
2975
+ var wb = XLSX.read(u8, { bookSheets: true });
2976
+ return { names: wb.SheetNames, wb: null };
2977
+ },
2978
+
2979
+ resolveSheet: function (u8, file) {
2980
+ var ctrl = this;
2981
+ var sp = ctrl.sheet;
2982
+
2983
+ var info = ctrl.listSheets(u8);
2984
+ var names = info.names;
2985
+ if (!names || !names.length)
2986
+ return Promise.reject(new Error('File "' + file.name + '" không có sheet nào'));
2987
+
2988
+ if (sp !== null && sp !== undefined && sp !== '') {
2989
+ var s = String(sp).trim();
2990
+ if (/^\d+$/.test(s)) {
2991
+ var n = names[parseInt(s, 10)];
2992
+ if (!n) return Promise.reject(new Error('Không có sheet ở vị trí ' + s
2993
+ + ' (file chỉ có ' + names.length + ' sheet)'));
2994
+ return Promise.resolve(n);
2995
+ }
2996
+ if (names.indexOf(s) === -1)
2997
+ return Promise.reject(new Error('Không tìm thấy sheet "' + s
2998
+ + '". File có: ' + names.join(', ')));
2999
+ return Promise.resolve(s);
3000
+ }
3001
+
3002
+ if (names.length === 1 || !ctrl.usePicker) return Promise.resolve(names[0]);
3003
+
3004
+ var list = [];
3005
+ for (var i = 0; i < names.length; i++) list.push(ctrl.sheetInfo(info.wb, names[i]));
3006
+
3007
+ var pick = list[0];
3008
+ for (var j = 0; j < list.length; j++) {
3009
+ if (!list[j].empty) { pick = list[j]; break; }
3010
+ }
3011
+
3012
+ ctrl.sheetList = list;
3013
+ ctrl.sheetChoice = pick.name;
3014
+ ctrl.pendingName = file.name + ' — ' + names.length + ' sheet';
3015
+ ctrl.hideLoading();
3016
+ ctrl.dialog = true;
3017
+
3018
+ return new Promise(function (resolve) { ctrl._pickSheet = resolve; });
3019
+ },
3020
+
3021
+ sheetInfo: function (wb, name) {
3022
+ if (!wb) return { name: name, info: '', empty: false };
3023
+ var ref = wb.Sheets[name] && wb.Sheets[name]['!ref'];
3024
+ if (!ref) return { name: name, info: 'sheet rỗng', empty: true };
3025
+ var r = XLSX.utils.decode_range(ref);
3026
+ return {
3027
+ name: name,
3028
+ info: (r.e.r - r.s.r + 1) + ' dòng × ' + (r.e.c - r.s.c + 1) + ' cột',
3029
+ empty: false
3030
+ };
3031
+ },
3032
+
3033
+ pickSheet: function (name) {
3034
+ this.dialog = false;
3035
+ var done = this._pickSheet;
3036
+ this._pickSheet = null;
3037
+ if (done) done(name || null);
3038
+ },
3039
+
3040
+ // ---------- PHA 2 ----------
3041
+ parseSheet: function (u8, wsname, file) {
3042
+ var wb = XLSX.read(u8, { type: 'array', cellDates: true, sheets: wsname });
3043
+ var ws = wb.Sheets[wsname];
3044
+ if (!ws) throw new Error('Không đọc được sheet "' + wsname + '"');
3045
+
3046
+ var rows = XLSX.utils.sheet_to_json(ws, {
3047
+ header: this.optHeader,
3048
+ raw: this.optRaw,
3049
+ dateNF: this.dateFormat,
3050
+ defval: this.defval,
3051
+ range: this.optRange,
3052
+ blankrows: false
3053
+ });
3054
+
3055
+ return {
3056
+ rows: rows,
3057
+ meta: {
3058
+ rows: rows,
3059
+ rowCount: rows.length,
3060
+ headers: (this.optHeader === 1 && rows.length) ? rows[0] : Object.keys(rows[0] || {}),
3061
+ sheetName: wsname,
3062
+ sheetNames: wb.SheetNames,
3063
+ fileName: file.name,
3064
+ fileSize: file.size
3065
+ }
3066
+ };
3067
+ },
3068
+
3069
+ // ---------- phụ trợ ----------
3043
3070
  ensureXLSX: function () {
3044
3071
  var ctrl = this;
3045
3072
  return new Promise(function (resolve, reject) {
@@ -3052,7 +3079,7 @@ Vue.component('f-excel-reader', {
3052
3079
  var timer = setTimeout(function () {
3053
3080
  if (settled) return;
3054
3081
  settled = true;
3055
- reject(new Error('Quá thời gian nạp thư viện XLSX'));
3082
+ reject(new Error('Quá thời gian nạp thư viện đọc Excel'));
3056
3083
  }, ms);
3057
3084
 
3058
3085
  new Loader().require(['/include/excel/xlsx.full.min.js'], function () {
@@ -3060,7 +3087,7 @@ Vue.component('f-excel-reader', {
3060
3087
  settled = true;
3061
3088
  clearTimeout(timer);
3062
3089
  if (typeof XLSX !== 'undefined') resolve();
3063
- else reject(new Error('Nạp thư viện XLSX thất bại'));
3090
+ else reject(new Error('Nạp thư viện đọc Excel thất bại'));
3064
3091
  });
3065
3092
  });
3066
3093
  },
@@ -3069,57 +3096,20 @@ Vue.component('f-excel-reader', {
3069
3096
  return new Promise(function (resolve, reject) {
3070
3097
  var reader = new FileReader();
3071
3098
  reader.onload = function (e) { resolve(e.target.result); };
3072
- reader.onerror = function () { reject(new Error('Không đọc được file: ' + file.name)); };
3099
+ reader.onerror = function () { reject(new Error('Không đọc được file "' + file.name + '"')); };
3073
3100
  reader.onabort = function () { reject(new Error('Việc đọc file bị hủy')); };
3074
3101
  reader.readAsArrayBuffer(file);
3075
3102
  });
3076
3103
  },
3077
3104
 
3078
- parseFile: function (file) {
3079
- var ctrl = this;
3080
- return ctrl.readBuffer(file).then(function (buf) {
3081
- var wb = XLSX.read(new Uint8Array(buf), { type: 'array', cellDates: true });
3082
-
3083
- var isIndex = typeof ctrl.sheet === 'number' || /^\d+$/.test(String(ctrl.sheet));
3084
- var wsname = isIndex
3085
- ? wb.SheetNames[parseInt(ctrl.sheet, 10)]
3086
- : (wb.SheetNames.indexOf(ctrl.sheet) !== -1 ? ctrl.sheet : wb.SheetNames[0]);
3087
-
3088
- var ws = wb.Sheets[wsname];
3089
- if (!ws) throw new Error('Không tìm thấy sheet: ' + ctrl.sheet);
3090
-
3091
- var rows = XLSX.utils.sheet_to_json(ws, {
3092
- header: ctrl.optHeader,
3093
- raw: ctrl.optRaw,
3094
- dateNF: ctrl.dateFormat,
3095
- defval: ctrl.defval,
3096
- range: ctrl.optRange,
3097
- blankrows: false
3098
- });
3099
-
3100
- return {
3101
- rows: rows,
3102
- meta: {
3103
- rows: rows,
3104
- rowCount: rows.length,
3105
- headers: (ctrl.optHeader === 1 && rows.length) ? rows[0] : Object.keys(rows[0] || {}),
3106
- sheetName: wsname,
3107
- sheetNames: wb.SheetNames,
3108
- fileName: file.name
3109
- }
3110
- };
3111
- });
3112
- },
3113
-
3114
3105
  runAction: function (file) {
3115
3106
  var ctrl = this;
3116
3107
  if (!ctrl.action || typeof CALL !== 'function') return;
3117
3108
  var onErr = function (err) {
3109
+ var msg = (err && err.message) || String(err);
3118
3110
  console.error('[FExcelReader] action lỗi (dữ liệu Excel vẫn hợp lệ):', err);
3119
- ctrl.$emit('action-error', {
3120
- message: (err && err.message) || String(err),
3121
- fileName: file.name
3122
- });
3111
+ ctrl.toast('Đọc Excel thành công nhưng xử lý sau đó lỗi: ' + msg, 'warning');
3112
+ ctrl.$emit('action-error', { message: msg, fileName: file.name });
3123
3113
  };
3124
3114
  try {
3125
3115
  var r = CALL(ctrl.action);
@@ -3129,14 +3119,30 @@ Vue.component('f-excel-reader', {
3129
3119
  }
3130
3120
  },
3131
3121
 
3122
+ toast: function (msg, color) {
3123
+ if (!this.useNotify) return;
3124
+ this.snackColor = color || 'error';
3125
+ this.snackMsg = msg;
3126
+ this.snack = true;
3127
+ },
3128
+
3129
+ fmtSize: function (bytes) {
3130
+ if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + 'MB';
3131
+ if (bytes >= 1024) return (bytes / 1024).toFixed(0) + 'KB';
3132
+ return bytes + 'B';
3133
+ },
3134
+
3132
3135
  fail: function (err, file) {
3136
+ var msg = (err && err.message) || String(err);
3133
3137
  console.error('[FExcelReader]', err);
3134
3138
  this.parsing = false;
3135
- this.busy = false;
3136
- this.$emit('error', {
3137
- message: (err && err.message) || String(err),
3138
- fileName: file ? file.name : null
3139
- });
3139
+ this.dialog = false;
3140
+ this.hideLoading();
3141
+ this.toast(msg, 'error');
3142
+ this.$emit('error', { message: msg, fileName: file ? file.name : null });
3140
3143
  }
3144
+ },
3145
+ beforeDestroy: function () {
3146
+ if (this._pickSheet) { this._pickSheet(null); this._pickSheet = null; }
3141
3147
  }
3142
3148
  });