@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
|
@@ -241,17 +241,6 @@ Vue.component("FDynamicComponent", {
|
|
|
241
241
|
},
|
|
242
242
|
|
|
243
243
|
});
|
|
244
|
-
// //===========================================================================================
|
|
245
|
-
// Vue.component("FOpenwindowControl", {
|
|
246
|
-
// template: `<component v-for="(w,index) in windowList" v-bind:is="w.el" v-bind="w.attr">{{w.innerHTML}}</component>`,
|
|
247
|
-
// props: {
|
|
248
|
-
// windowList: {
|
|
249
|
-
// default: [],
|
|
250
|
-
// required: true
|
|
251
|
-
// },
|
|
252
|
-
// },
|
|
253
|
-
// });
|
|
254
|
-
|
|
255
244
|
//===========================================================================================
|
|
256
245
|
Vue.component('FLabel', {
|
|
257
246
|
props: ['items', "text", "color", 'iconText'],
|
|
@@ -1533,7 +1522,7 @@ Vue.component('HeaderBar', {
|
|
|
1533
1522
|
<v-navigation-drawer
|
|
1534
1523
|
v-if="leftMenuAndMobile"
|
|
1535
1524
|
:color="menuStyle.leftmenu.bgcolor"
|
|
1536
|
-
:class="['text-' + menuStyle.leftmenu.textcolor, 'fp-
|
|
1525
|
+
:class="['text-' + menuStyle.leftmenu.textcolor, 'fp-left-menu', 'fp-stable-left-menu']"
|
|
1537
1526
|
v-model="openLeftMenu"
|
|
1538
1527
|
app
|
|
1539
1528
|
clipped
|
|
@@ -1559,6 +1548,7 @@ Vue.component('HeaderBar', {
|
|
|
1559
1548
|
:target="item.target"
|
|
1560
1549
|
:title="item.name"
|
|
1561
1550
|
:class="['text-' + menuStyle.leftmenu.textcolor]"
|
|
1551
|
+
v-on:click="rememberMenuPosition"
|
|
1562
1552
|
:data-active="isActiveUrl(item.url) ? 'true' : null"
|
|
1563
1553
|
:style="isActiveUrl(item.url) ? {backgroundColor: activeColor} : {}">
|
|
1564
1554
|
<template v-slot:prepend v-if="item.icon">
|
|
@@ -1578,6 +1568,7 @@ Vue.component('HeaderBar', {
|
|
|
1578
1568
|
:href="i.url"
|
|
1579
1569
|
:target="i.target"
|
|
1580
1570
|
class="fp-submenu-item"
|
|
1571
|
+
v-on:click="rememberMenuPosition"
|
|
1581
1572
|
:data-active="isActiveUrl(i.url) ? 'true' : null"
|
|
1582
1573
|
:style="isActiveUrl(i.url) ? {backgroundColor: activeColor} : {}">
|
|
1583
1574
|
<template v-slot:prepend v-if="i.icon"><v-icon :icon="i.icon" class="opacity-100"></v-icon></template>
|
|
@@ -1662,58 +1653,71 @@ Vue.component('HeaderBar', {
|
|
|
1662
1653
|
if (!url) return false;
|
|
1663
1654
|
return this.currentFullUrl === url;
|
|
1664
1655
|
},
|
|
1665
|
-
|
|
1656
|
+
getDrawerContent: function () {
|
|
1657
|
+
return this.$el.querySelector('.v-navigation-drawer__content');
|
|
1658
|
+
},
|
|
1659
|
+
rememberMenuPosition: function () {
|
|
1660
|
+
var content = this.getDrawerContent();
|
|
1661
|
+
if (!content) return;
|
|
1662
|
+
|
|
1663
|
+
sessionStorage.setItem('fpLeftMenuScrollTop', String(content.scrollTop));
|
|
1664
|
+
},
|
|
1665
|
+
restoreMenuPosition: function () {
|
|
1666
1666
|
if (!this.leftMenuAndMobile || this.leftMenuAndMobile.length === 0) return;
|
|
1667
|
-
|
|
1667
|
+
|
|
1668
1668
|
var self = this;
|
|
1669
1669
|
this.$nextTick(function() {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1670
|
+
var content = self.getDrawerContent();
|
|
1671
|
+
if (!content) return;
|
|
1672
|
+
|
|
1673
|
+
var savedTop = Number(sessionStorage.getItem('fpLeftMenuScrollTop'));
|
|
1674
|
+
if (Number.isFinite(savedTop) && savedTop >= 0) {
|
|
1675
|
+
content.scrollTop = savedTop;
|
|
1676
|
+
}
|
|
1677
|
+
content.scrollLeft = 0;
|
|
1678
|
+
|
|
1679
|
+
var activeEl = self.$el.querySelector('[data-active="true"]');
|
|
1680
|
+
if (activeEl) {
|
|
1681
|
+
var containerRect = content.getBoundingClientRect();
|
|
1682
|
+
var activeRect = activeEl.getBoundingClientRect();
|
|
1683
|
+
var isOutside = activeRect.top < containerRect.top || activeRect.bottom > containerRect.bottom;
|
|
1684
|
+
|
|
1685
|
+
if (isOutside) {
|
|
1686
|
+
content.scrollTop += activeRect.top - containerRect.top
|
|
1687
|
+
- ((containerRect.height - activeRect.height) / 2);
|
|
1679
1688
|
}
|
|
1680
|
-
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
self._menuScrollListener = function() {
|
|
1692
|
+
if (content.scrollLeft !== 0) content.scrollLeft = 0;
|
|
1693
|
+
sessionStorage.setItem('fpLeftMenuScrollTop', String(content.scrollTop));
|
|
1694
|
+
};
|
|
1695
|
+
content.addEventListener('scroll', self._menuScrollListener, { passive: true });
|
|
1681
1696
|
});
|
|
1682
1697
|
},
|
|
1683
|
-
|
|
1698
|
+
injectLeftMenuStyles: function () {
|
|
1684
1699
|
if (!this.leftMenuAndMobile || this.leftMenuAndMobile.length === 0) return;
|
|
1685
|
-
|
|
1686
|
-
var styleId = 'fp-
|
|
1700
|
+
|
|
1701
|
+
var styleId = 'fp-left-menu-style';
|
|
1687
1702
|
if (document.getElementById(styleId)) return;
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
var trackColor = 'rgba(0,0,0,0.1)';
|
|
1692
|
-
|
|
1703
|
+
|
|
1704
|
+
// Thanh cuộn KHÔNG vẽ ở đây: lấy từ .fpScrollbar trong projectdefaultstyle.css
|
|
1705
|
+
// (rule đó đã phủ sẵn .v-navigation-drawer__content).
|
|
1693
1706
|
var style = document.createElement('style');
|
|
1694
1707
|
style.id = styleId;
|
|
1695
1708
|
style.textContent = [
|
|
1696
|
-
'.fp-
|
|
1697
|
-
'
|
|
1698
|
-
'
|
|
1699
|
-
'.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar-track {',
|
|
1700
|
-
' background: ' + trackColor + ';',
|
|
1709
|
+
'.fp-left-menu .v-navigation-drawer__content {',
|
|
1710
|
+
' overflow-x: hidden !important;',
|
|
1711
|
+
' scroll-behavior: auto !important;',
|
|
1701
1712
|
'}',
|
|
1702
|
-
'.fp-
|
|
1703
|
-
'
|
|
1704
|
-
'
|
|
1713
|
+
'.fp-stable-left-menu,',
|
|
1714
|
+
'.fp-stable-left-menu.v-navigation-drawer {',
|
|
1715
|
+
' transition: none !important;',
|
|
1705
1716
|
'}',
|
|
1706
|
-
'.fp-
|
|
1707
|
-
' background: ' + thumbHoverColor + ';',
|
|
1708
|
-
'}',
|
|
1709
|
-
'.fp-custom-scrollbar .v-navigation-drawer__content {',
|
|
1710
|
-
' scrollbar-width: thin;',
|
|
1711
|
-
' scrollbar-color: ' + thumbColor + ' ' + trackColor + ';',
|
|
1712
|
-
'}',
|
|
1713
|
-
'.fp-custom-scrollbar .v-list-group__items .fp-submenu-item {',
|
|
1717
|
+
'.fp-left-menu .v-list-group__items .fp-submenu-item {',
|
|
1714
1718
|
' padding-left: 32px !important;',
|
|
1715
1719
|
'}',
|
|
1716
|
-
'.fp-
|
|
1720
|
+
'.fp-left-menu .v-list-group__items .v-list-item__prepend {',
|
|
1717
1721
|
' width: auto;',
|
|
1718
1722
|
' margin-right: 8px;',
|
|
1719
1723
|
'}'
|
|
@@ -1734,11 +1738,16 @@ Vue.component('HeaderBar', {
|
|
|
1734
1738
|
},
|
|
1735
1739
|
mounted: function() {
|
|
1736
1740
|
if (this.leftMenuAndMobile && this.leftMenuAndMobile.length > 0) {
|
|
1737
|
-
this.
|
|
1738
|
-
this.
|
|
1741
|
+
this.injectLeftMenuStyles();
|
|
1742
|
+
this.restoreMenuPosition();
|
|
1739
1743
|
}
|
|
1740
1744
|
},
|
|
1741
1745
|
beforeDestroy: function() {
|
|
1746
|
+
this.rememberMenuPosition();
|
|
1747
|
+
var content = this.getDrawerContent();
|
|
1748
|
+
if (content && this._menuScrollListener) {
|
|
1749
|
+
content.removeEventListener('scroll', this._menuScrollListener);
|
|
1750
|
+
}
|
|
1742
1751
|
window.removeEventListener('scroll', this._scrollListener);
|
|
1743
1752
|
}
|
|
1744
1753
|
});
|
|
@@ -2350,7 +2359,8 @@ Vue.component('FPdfmake', {
|
|
|
2350
2359
|
|
|
2351
2360
|
//===========================================================================================
|
|
2352
2361
|
Vue.component('FExcelReader', {
|
|
2353
|
-
|
|
2362
|
+
inheritAttrs: false,
|
|
2363
|
+
emits: ['update:modelValue', 'load', 'error', 'action-error', 'cancel'],
|
|
2354
2364
|
props: {
|
|
2355
2365
|
modelValue: { type: Array, default: () => [] },
|
|
2356
2366
|
|
|
@@ -2359,27 +2369,75 @@ Vue.component('FExcelReader', {
|
|
|
2359
2369
|
dateFormat: { type: String, default: 'dd/mm/yyyy' },
|
|
2360
2370
|
defval: { type: [String, Number], default: '' },
|
|
2361
2371
|
|
|
2362
|
-
sheet:
|
|
2363
|
-
|
|
2372
|
+
sheet: { type: [String, Number], default: null },
|
|
2373
|
+
sheetPicker: { type: [Boolean, String], default: true },
|
|
2374
|
+
sheetStats: { type: [Boolean, String], default: false },
|
|
2375
|
+
skipRows: { type: [String, Number], default: 0 },
|
|
2364
2376
|
|
|
2365
|
-
label:
|
|
2366
|
-
iconText:
|
|
2367
|
-
accept:
|
|
2368
|
-
maxSize:
|
|
2377
|
+
label: { type: String, default: 'Load Excel' },
|
|
2378
|
+
iconText: { type: String, default: 'mdi-grid' },
|
|
2379
|
+
accept: { type: String, default: '.xlsx,.xlsb,.xlsm,.xls,.csv,.txt,.ods,.dbf,.prn,.html,.htm' },
|
|
2380
|
+
maxSize: { type: [String, Number], default: 20 },
|
|
2369
2381
|
loadTimeout: { type: [String, Number], default: 15000 },
|
|
2382
|
+
loadingText: { type: String, default: 'Đang xử lý dữ liệu...' },
|
|
2383
|
+
|
|
2384
|
+
notify: { type: [Boolean, String], default: true },
|
|
2385
|
+
notifyTimeout: { type: [String, Number], default: 6000 },
|
|
2370
2386
|
|
|
2371
2387
|
action: { type: Object, default: null }
|
|
2372
2388
|
},
|
|
2373
2389
|
template: `
|
|
2374
2390
|
<div>
|
|
2375
2391
|
<v-btn :text="label" v-bind="$attrs" :prepend-icon="iconText"
|
|
2376
|
-
:disabled="parsing"
|
|
2377
|
-
<input ref="uploader" style="display:none" type="file" :multiple="false"
|
|
2378
|
-
|
|
2392
|
+
:disabled="parsing" @click="onButtonClick"></v-btn>
|
|
2393
|
+
<input ref="uploader" style="display:none" type="file" :multiple="false" :accept="accept" @change="onchange" onclick="this.value=null;">
|
|
2394
|
+
|
|
2395
|
+
<v-dialog v-model="loading" persistent width="320">
|
|
2396
|
+
<v-card class="pa-8 d-flex flex-column align-center justify-center text-center">
|
|
2397
|
+
<div class="fui-loading"></div>
|
|
2398
|
+
<div class="mt-5 body-2">{{ loadingMsg }}</div>
|
|
2399
|
+
<div v-if="loadingSub" class="mt-1 caption grey--text text-truncate" style="max-width:100%;">{{ loadingSub }}</div>
|
|
2400
|
+
</v-card>
|
|
2401
|
+
</v-dialog>
|
|
2402
|
+
|
|
2403
|
+
<v-dialog v-model="dialog" max-width="440" persistent>
|
|
2404
|
+
<v-card>
|
|
2405
|
+
<v-card-title class="text-subtitle-1">Chọn sheet cần đọc</v-card-title>
|
|
2406
|
+
<v-card-text class="pb-0">
|
|
2407
|
+
<div class="text-caption mb-2">{{ pendingName }}</div>
|
|
2408
|
+
<v-list density="compact" nav max-height="300" class="overflow-y-auto">
|
|
2409
|
+
<v-list-item v-for="s in sheetList" :key="s.name"
|
|
2410
|
+
:active="s.name === sheetChoice"
|
|
2411
|
+
@click="sheetChoice = s.name">
|
|
2412
|
+
<v-list-item-title>{{ s.name }}</v-list-item-title>
|
|
2413
|
+
<v-list-item-subtitle v-if="s.info">{{ s.info }}</v-list-item-subtitle>
|
|
2414
|
+
</v-list-item>
|
|
2415
|
+
</v-list>
|
|
2416
|
+
</v-card-text>
|
|
2417
|
+
<v-card-actions>
|
|
2418
|
+
<v-spacer></v-spacer>
|
|
2419
|
+
<v-btn text="Hủy" @click="pickSheet(null)"></v-btn>
|
|
2420
|
+
<v-btn text="Chọn" color="primary" :disabled="!sheetChoice"
|
|
2421
|
+
@click="pickSheet(sheetChoice)"></v-btn>
|
|
2422
|
+
</v-card-actions>
|
|
2423
|
+
</v-card>
|
|
2424
|
+
</v-dialog>
|
|
2425
|
+
|
|
2426
|
+
<v-snackbar v-model="snack" :color="snackColor" :timeout="Number(notifyTimeout)" location="top">
|
|
2427
|
+
{{ snackMsg }}
|
|
2428
|
+
<template #actions>
|
|
2429
|
+
<v-btn variant="text" @click="snack = false">Đóng</v-btn>
|
|
2430
|
+
</template>
|
|
2431
|
+
</v-snackbar>
|
|
2379
2432
|
</div>
|
|
2380
2433
|
`,
|
|
2381
2434
|
data() {
|
|
2382
|
-
return {
|
|
2435
|
+
return {
|
|
2436
|
+
parsing: false, reqId: 0,
|
|
2437
|
+
loading: false, loadingMsg: '', loadingSub: '',
|
|
2438
|
+
dialog: false, sheetList: [], sheetChoice: null, pendingName: '',
|
|
2439
|
+
snack: false, snackMsg: '', snackColor: 'error'
|
|
2440
|
+
};
|
|
2383
2441
|
},
|
|
2384
2442
|
computed: {
|
|
2385
2443
|
optHeader() {
|
|
@@ -2391,26 +2449,51 @@ Vue.component('FExcelReader', {
|
|
|
2391
2449
|
if (s.toUpperCase() === 'A') return 'A';
|
|
2392
2450
|
return h;
|
|
2393
2451
|
},
|
|
2394
|
-
optRaw()
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
},
|
|
2452
|
+
optRaw() { return this.toBool(this.raw, false); },
|
|
2453
|
+
usePicker() { return this.toBool(this.sheetPicker, true); },
|
|
2454
|
+
useStats() { return this.toBool(this.sheetStats, false); },
|
|
2455
|
+
useNotify() { return this.toBool(this.notify, true); },
|
|
2398
2456
|
optRange() {
|
|
2399
2457
|
const n = parseInt(this.skipRows, 10);
|
|
2400
2458
|
return n > 0 ? n : undefined;
|
|
2401
2459
|
},
|
|
2402
2460
|
limitMB() {
|
|
2403
|
-
const
|
|
2404
|
-
|
|
2461
|
+
const n = parseFloat(this.maxSize);
|
|
2462
|
+
if (isNaN(n)) return 20;
|
|
2463
|
+
return n > 0 ? n : 0;
|
|
2405
2464
|
}
|
|
2406
2465
|
},
|
|
2407
2466
|
methods: {
|
|
2467
|
+
toBool(v, def) {
|
|
2468
|
+
if (typeof v === 'boolean') return v;
|
|
2469
|
+
if (v === null || v === undefined || v === '') return def;
|
|
2470
|
+
const s = String(v).trim().toLowerCase();
|
|
2471
|
+
if (['true', '1', 'yes'].includes(s)) return true;
|
|
2472
|
+
if (['false', '0', 'no'].includes(s)) return false;
|
|
2473
|
+
return def;
|
|
2474
|
+
},
|
|
2475
|
+
|
|
2476
|
+
// ⭐ nhường 1 khung hình để trình duyệt kịp vẽ dialog trước khi chạy code đồng bộ nặng
|
|
2477
|
+
nextPaint() {
|
|
2478
|
+
return new Promise(resolve => {
|
|
2479
|
+
requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 0)));
|
|
2480
|
+
});
|
|
2481
|
+
},
|
|
2482
|
+
|
|
2483
|
+
showLoading(msg, sub) {
|
|
2484
|
+
this.loadingMsg = msg || this.loadingText;
|
|
2485
|
+
this.loadingSub = sub || '';
|
|
2486
|
+
this.loading = true;
|
|
2487
|
+
return this.$nextTick().then(() => this.nextPaint());
|
|
2488
|
+
},
|
|
2489
|
+
|
|
2490
|
+
hideLoading() {
|
|
2491
|
+
this.loading = false;
|
|
2492
|
+
this.loadingSub = '';
|
|
2493
|
+
},
|
|
2494
|
+
|
|
2408
2495
|
onButtonClick() {
|
|
2409
|
-
if (this.parsing) return;
|
|
2410
|
-
this.busy = true;
|
|
2411
|
-
window.addEventListener('focus', () => {
|
|
2412
|
-
setTimeout(() => { if (!this.parsing) this.busy = false; }, 300);
|
|
2413
|
-
}, { once: true });
|
|
2496
|
+
if (this.parsing) return;
|
|
2414
2497
|
this.$refs.uploader.click();
|
|
2415
2498
|
},
|
|
2416
2499
|
|
|
@@ -2418,38 +2501,141 @@ Vue.component('FExcelReader', {
|
|
|
2418
2501
|
const file = evt.target.files && evt.target.files[0];
|
|
2419
2502
|
evt.target.value = '';
|
|
2420
2503
|
|
|
2421
|
-
if (this.parsing) return;
|
|
2422
|
-
if (!file)
|
|
2504
|
+
if (this.parsing) return;
|
|
2505
|
+
if (!file) return;
|
|
2423
2506
|
|
|
2424
|
-
const id = ++this.reqId;
|
|
2507
|
+
const id = ++this.reqId;
|
|
2425
2508
|
this.parsing = true;
|
|
2426
|
-
this.busy = true;
|
|
2427
2509
|
|
|
2428
2510
|
let result;
|
|
2429
2511
|
try {
|
|
2430
|
-
if (this.limitMB > 0 && file.size > this.limitMB *
|
|
2431
|
-
throw new Error('File
|
|
2512
|
+
if (this.limitMB > 0 && file.size > this.limitMB * 1048576)
|
|
2513
|
+
throw new Error('File "' + file.name + '" nặng ' + this.fmtSize(file.size)
|
|
2514
|
+
+ ', vượt giới hạn ' + this.limitMB + 'MB');
|
|
2432
2515
|
|
|
2516
|
+
await this.showLoading('Đang mở tập tin...', file.name);
|
|
2433
2517
|
await this.ensureXLSX();
|
|
2434
|
-
result = await this.parseFile(file);
|
|
2435
2518
|
|
|
2436
|
-
|
|
2519
|
+
const buf = await this.readBuffer(file);
|
|
2520
|
+
if (id !== this.reqId) return;
|
|
2521
|
+
const u8 = new Uint8Array(buf);
|
|
2522
|
+
|
|
2523
|
+
await this.showLoading('Đang đọc cấu trúc tập tin...', file.name);
|
|
2524
|
+
const wsname = await this.resolveSheet(u8, file);
|
|
2525
|
+
if (id !== this.reqId) return;
|
|
2526
|
+
|
|
2527
|
+
if (!wsname) {
|
|
2528
|
+
this.$emit('cancel', { fileName: file.name, reason: 'sheet' });
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// ⭐ dialog loading cho MỌI trường hợp, kể cả file chỉ 1 sheet
|
|
2533
|
+
await this.showLoading(this.loadingText, 'Sheet: ' + wsname);
|
|
2534
|
+
result = this.parseSheet(u8, wsname, file);
|
|
2535
|
+
if (id !== this.reqId) return;
|
|
2437
2536
|
} catch (err) {
|
|
2438
2537
|
if (id === this.reqId) this.fail(err, file);
|
|
2439
2538
|
else console.warn('[FExcelReader] bỏ qua lỗi của lượt cũ:', err);
|
|
2440
2539
|
return;
|
|
2441
2540
|
} finally {
|
|
2442
|
-
if (id === this.reqId) { this.parsing = false; this.
|
|
2541
|
+
if (id === this.reqId) { this.parsing = false; this.hideLoading(); }
|
|
2443
2542
|
}
|
|
2444
2543
|
|
|
2445
|
-
// =====
|
|
2544
|
+
// ===== Excel ĐÃ đọc xong, từ đây không thể thành error =====
|
|
2446
2545
|
this.$emit('update:modelValue', result.rows);
|
|
2447
2546
|
this.$emit('load', result.meta);
|
|
2448
2547
|
|
|
2449
2548
|
await this.$nextTick();
|
|
2450
|
-
this.runAction(file);
|
|
2549
|
+
this.runAction(file);
|
|
2550
|
+
},
|
|
2551
|
+
|
|
2552
|
+
// ---------- PHA 1: chỉ lấy tên sheet ----------
|
|
2553
|
+
listSheets(u8) {
|
|
2554
|
+
if (this.useStats) {
|
|
2555
|
+
const wb = XLSX.read(u8, { bookSheets: true, bookProps: true, dense: true });
|
|
2556
|
+
return { names: wb.SheetNames, wb };
|
|
2557
|
+
}
|
|
2558
|
+
const wb = XLSX.read(u8, { bookSheets: true });
|
|
2559
|
+
return { names: wb.SheetNames, wb: null };
|
|
2560
|
+
},
|
|
2561
|
+
|
|
2562
|
+
resolveSheet(u8, file) {
|
|
2563
|
+
const sp = this.sheet;
|
|
2564
|
+
const { names, wb } = this.listSheets(u8);
|
|
2565
|
+
if (!names || !names.length) throw new Error('File "' + file.name + '" không có sheet nào');
|
|
2566
|
+
|
|
2567
|
+
if (sp !== null && sp !== undefined && sp !== '') {
|
|
2568
|
+
const s = String(sp).trim();
|
|
2569
|
+
if (/^\d+$/.test(s)) {
|
|
2570
|
+
const n = names[parseInt(s, 10)];
|
|
2571
|
+
if (!n) throw new Error('Không có sheet ở vị trí ' + s
|
|
2572
|
+
+ ' (file chỉ có ' + names.length + ' sheet)');
|
|
2573
|
+
return Promise.resolve(n);
|
|
2574
|
+
}
|
|
2575
|
+
if (names.indexOf(s) === -1)
|
|
2576
|
+
throw new Error('Không tìm thấy sheet "' + s + '". File có: ' + names.join(', '));
|
|
2577
|
+
return Promise.resolve(s);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
if (names.length === 1 || !this.usePicker) return Promise.resolve(names[0]);
|
|
2581
|
+
|
|
2582
|
+
this.sheetList = names.map(n => this.sheetInfo(wb, n));
|
|
2583
|
+
this.sheetChoice = (this.sheetList.find(s => !s.empty) || this.sheetList[0]).name;
|
|
2584
|
+
this.pendingName = file.name + ' — ' + names.length + ' sheet';
|
|
2585
|
+
this.hideLoading(); // ẩn loading trong lúc chờ người dùng
|
|
2586
|
+
this.dialog = true;
|
|
2587
|
+
return new Promise(resolve => { this._pickSheet = resolve; });
|
|
2451
2588
|
},
|
|
2452
2589
|
|
|
2590
|
+
sheetInfo(wb, name) {
|
|
2591
|
+
if (!wb) return { name, info: '', empty: false };
|
|
2592
|
+
const ref = wb.Sheets[name] && wb.Sheets[name]['!ref'];
|
|
2593
|
+
if (!ref) return { name, info: 'sheet rỗng', empty: true };
|
|
2594
|
+
const r = XLSX.utils.decode_range(ref);
|
|
2595
|
+
return {
|
|
2596
|
+
name,
|
|
2597
|
+
info: (r.e.r - r.s.r + 1) + ' dòng × ' + (r.e.c - r.s.c + 1) + ' cột',
|
|
2598
|
+
empty: false
|
|
2599
|
+
};
|
|
2600
|
+
},
|
|
2601
|
+
|
|
2602
|
+
pickSheet(name) {
|
|
2603
|
+
this.dialog = false;
|
|
2604
|
+
const done = this._pickSheet;
|
|
2605
|
+
this._pickSheet = null;
|
|
2606
|
+
if (done) done(name || null);
|
|
2607
|
+
},
|
|
2608
|
+
|
|
2609
|
+
// ---------- PHA 2: parse đúng 1 sheet ----------
|
|
2610
|
+
parseSheet(u8, wsname, file) {
|
|
2611
|
+
const wb = XLSX.read(u8, { type: 'array', cellDates: true, sheets: wsname });
|
|
2612
|
+
const ws = wb.Sheets[wsname];
|
|
2613
|
+
if (!ws) throw new Error('Không đọc được sheet "' + wsname + '"');
|
|
2614
|
+
|
|
2615
|
+
const rows = XLSX.utils.sheet_to_json(ws, {
|
|
2616
|
+
header: this.optHeader,
|
|
2617
|
+
raw: this.optRaw,
|
|
2618
|
+
dateNF: this.dateFormat,
|
|
2619
|
+
defval: this.defval,
|
|
2620
|
+
range: this.optRange,
|
|
2621
|
+
blankrows: false
|
|
2622
|
+
});
|
|
2623
|
+
|
|
2624
|
+
return {
|
|
2625
|
+
rows,
|
|
2626
|
+
meta: {
|
|
2627
|
+
rows,
|
|
2628
|
+
rowCount: rows.length,
|
|
2629
|
+
headers: this.optHeader === 1 && rows.length ? rows[0] : Object.keys(rows[0] || {}),
|
|
2630
|
+
sheetName: wsname,
|
|
2631
|
+
sheetNames: wb.SheetNames,
|
|
2632
|
+
fileName: file.name,
|
|
2633
|
+
fileSize: file.size
|
|
2634
|
+
}
|
|
2635
|
+
};
|
|
2636
|
+
},
|
|
2637
|
+
|
|
2638
|
+
// ---------- phụ trợ ----------
|
|
2453
2639
|
ensureXLSX() {
|
|
2454
2640
|
return new Promise((resolve, reject) => {
|
|
2455
2641
|
if (typeof XLSX !== 'undefined') return resolve();
|
|
@@ -2461,7 +2647,7 @@ Vue.component('FExcelReader', {
|
|
|
2461
2647
|
const timer = setTimeout(() => {
|
|
2462
2648
|
if (settled) return;
|
|
2463
2649
|
settled = true;
|
|
2464
|
-
reject(new Error('Quá thời gian nạp thư viện
|
|
2650
|
+
reject(new Error('Quá thời gian nạp thư viện đọc Excel'));
|
|
2465
2651
|
}, ms);
|
|
2466
2652
|
|
|
2467
2653
|
new Loader().require(['/include/excel/xlsx.full.min.js'], () => {
|
|
@@ -2470,7 +2656,7 @@ Vue.component('FExcelReader', {
|
|
|
2470
2656
|
clearTimeout(timer);
|
|
2471
2657
|
typeof XLSX !== 'undefined'
|
|
2472
2658
|
? resolve()
|
|
2473
|
-
: reject(new Error('Nạp thư viện
|
|
2659
|
+
: reject(new Error('Nạp thư viện đọc Excel thất bại'));
|
|
2474
2660
|
});
|
|
2475
2661
|
});
|
|
2476
2662
|
},
|
|
@@ -2479,71 +2665,52 @@ Vue.component('FExcelReader', {
|
|
|
2479
2665
|
return new Promise((resolve, reject) => {
|
|
2480
2666
|
const reader = new FileReader();
|
|
2481
2667
|
reader.onload = e => resolve(e.target.result);
|
|
2482
|
-
reader.onerror = () => reject(new Error('Không đọc được file
|
|
2668
|
+
reader.onerror = () => reject(new Error('Không đọc được file "' + file.name + '"'));
|
|
2483
2669
|
reader.onabort = () => reject(new Error('Việc đọc file bị hủy'));
|
|
2484
2670
|
reader.readAsArrayBuffer(file);
|
|
2485
2671
|
});
|
|
2486
2672
|
},
|
|
2487
2673
|
|
|
2488
|
-
async parseFile(file) {
|
|
2489
|
-
const buf = await this.readBuffer(file);
|
|
2490
|
-
const wb = XLSX.read(new Uint8Array(buf), { type: 'array', cellDates: true });
|
|
2491
|
-
|
|
2492
|
-
const isIndex = typeof this.sheet === 'number' || /^\d+$/.test(String(this.sheet));
|
|
2493
|
-
const wsname = isIndex
|
|
2494
|
-
? wb.SheetNames[parseInt(this.sheet, 10)]
|
|
2495
|
-
: (wb.SheetNames.includes(this.sheet) ? this.sheet : wb.SheetNames[0]);
|
|
2496
|
-
|
|
2497
|
-
const ws = wb.Sheets[wsname];
|
|
2498
|
-
if (!ws) throw new Error('Không tìm thấy sheet: ' + this.sheet);
|
|
2499
|
-
|
|
2500
|
-
const rows = XLSX.utils.sheet_to_json(ws, {
|
|
2501
|
-
header: this.optHeader,
|
|
2502
|
-
raw: this.optRaw,
|
|
2503
|
-
dateNF: this.dateFormat,
|
|
2504
|
-
defval: this.defval,
|
|
2505
|
-
range: this.optRange,
|
|
2506
|
-
blankrows: false
|
|
2507
|
-
});
|
|
2508
|
-
|
|
2509
|
-
return {
|
|
2510
|
-
rows,
|
|
2511
|
-
meta: {
|
|
2512
|
-
rows,
|
|
2513
|
-
rowCount: rows.length,
|
|
2514
|
-
headers: this.optHeader === 1 && rows.length ? rows[0] : Object.keys(rows[0] || {}),
|
|
2515
|
-
sheetName: wsname,
|
|
2516
|
-
sheetNames: wb.SheetNames,
|
|
2517
|
-
fileName: file.name
|
|
2518
|
-
}
|
|
2519
|
-
};
|
|
2520
|
-
},
|
|
2521
|
-
|
|
2522
2674
|
runAction(file) {
|
|
2523
2675
|
if (!this.action || typeof CALL !== 'function') return;
|
|
2524
2676
|
const onErr = (err) => {
|
|
2677
|
+
const msg = (err && err.message) || String(err);
|
|
2525
2678
|
console.error('[FExcelReader] action lỗi (dữ liệu Excel vẫn hợp lệ):', err);
|
|
2526
|
-
this
|
|
2527
|
-
|
|
2528
|
-
fileName: file.name
|
|
2529
|
-
});
|
|
2679
|
+
this.toast('Đọc Excel thành công nhưng xử lý sau đó lỗi: ' + msg, 'warning');
|
|
2680
|
+
this.$emit('action-error', { message: msg, fileName: file.name });
|
|
2530
2681
|
};
|
|
2531
2682
|
try {
|
|
2532
2683
|
const r = CALL(this.action);
|
|
2533
|
-
if (r && typeof r.then === 'function') r.then(null, onErr);
|
|
2684
|
+
if (r && typeof r.then === 'function') r.then(null, onErr);
|
|
2534
2685
|
} catch (err) {
|
|
2535
2686
|
onErr(err);
|
|
2536
2687
|
}
|
|
2537
2688
|
},
|
|
2538
2689
|
|
|
2690
|
+
toast(msg, color) {
|
|
2691
|
+
if (!this.useNotify) return;
|
|
2692
|
+
this.snackColor = color || 'error';
|
|
2693
|
+
this.snackMsg = msg;
|
|
2694
|
+
this.snack = true;
|
|
2695
|
+
},
|
|
2696
|
+
|
|
2697
|
+
fmtSize(bytes) {
|
|
2698
|
+
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + 'MB';
|
|
2699
|
+
if (bytes >= 1024) return (bytes / 1024).toFixed(0) + 'KB';
|
|
2700
|
+
return bytes + 'B';
|
|
2701
|
+
},
|
|
2702
|
+
|
|
2539
2703
|
fail(err, file) {
|
|
2704
|
+
const msg = (err && err.message) || String(err);
|
|
2540
2705
|
console.error('[FExcelReader]', err);
|
|
2541
2706
|
this.parsing = false;
|
|
2542
|
-
this.
|
|
2543
|
-
this
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
});
|
|
2707
|
+
this.dialog = false;
|
|
2708
|
+
this.hideLoading();
|
|
2709
|
+
this.toast(msg, 'error');
|
|
2710
|
+
this.$emit('error', { message: msg, fileName: file ? file.name : null });
|
|
2547
2711
|
}
|
|
2712
|
+
},
|
|
2713
|
+
beforeUnmount() {
|
|
2714
|
+
if (this._pickSheet) { this._pickSheet(null); this._pickSheet = null; }
|
|
2548
2715
|
}
|
|
2549
2716
|
});
|