@fui-org/fui-cli 2.1.4 → 2.2.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 (217) hide show
  1. package/README.md +27 -7
  2. package/dist/fui-qrb1j9jz.js +397 -0
  3. package/dist/fui.js +1 -1
  4. package/package.json +1 -1
  5. package/skills/fui/SKILL.md +143 -38
  6. package/skills/fui/agents/fui-builder.md +62 -0
  7. package/skills/fui/agents/fui-tester.md +77 -0
  8. package/skills/{fui-skill → fui}/assets/projectdefaultstyle.css +489 -489
  9. package/skills/{fui-skill → fui}/examples/component.vue +162 -162
  10. package/skills/{fui-skill → fui}/references/INDEX.md +5 -4
  11. package/skills/{fui-skill → fui}/references/advanced-techniques.md +170 -168
  12. package/skills/{fui-skill → fui}/references/component-design.md +2 -2
  13. package/skills/{fui-skill → fui}/references/component-quickref.md +78 -78
  14. package/skills/{fui-skill → fui}/references/component-table.md +287 -287
  15. package/skills/{fui-skill → fui}/references/components-dialog.md +191 -191
  16. package/skills/{fui-skill → fui}/references/components-display.md +141 -141
  17. package/skills/{fui-skill → fui}/references/components-echart.md +316 -316
  18. package/skills/{fui-skill → fui}/references/components-input.md +335 -335
  19. package/skills/{fui-skill → fui}/references/controls-patterns.md +2 -2
  20. package/skills/{fui-skill → fui}/references/controls-styling-vocabulary.md +173 -173
  21. package/skills/{fui-skill → fui}/references/db-workflow.md +9 -9
  22. package/skills/{fui-skill → fui}/references/default-function.md +456 -456
  23. package/skills/{fui-skill → fui}/references/design-modes.md +57 -57
  24. package/skills/{fui-skill → fui}/references/echart-templates.md +489 -489
  25. package/skills/{fui-skill → fui}/references/fastproject.md +99 -99
  26. package/skills/{fui-skill → fui}/references/fullstack-workflow.md +314 -313
  27. package/skills/{fui-skill → fui}/references/module-data-patterns.md +117 -117
  28. package/skills/{fui-skill → fui}/references/module-json-anatomy.md +132 -132
  29. package/skills/{fui-skill → fui}/references/module-structure.md +144 -144
  30. package/skills/{fui-skill → fui}/references/new-session.md +1 -2
  31. package/skills/{fui-skill → fui}/references/pdfmake.md +60 -60
  32. package/skills/{fui-skill → fui}/references/permission-system.md +1 -1
  33. package/skills/{fui-skill → fui}/references/platform-architecture.md +269 -269
  34. package/skills/{fui-skill → fui}/references/project-config.md +303 -303
  35. package/skills/{fui-skill → fui}/references/project-provisioning.md +4 -4
  36. package/skills/{fui-skill → fui}/references/script-map.md +262 -262
  37. package/skills/{fui-skill → fui}/references/sql-clr-functions.md +225 -225
  38. package/skills/{fui-skill → fui}/references/system-design.md +2 -2
  39. package/skills/{fui-skill → fui}/references/tapi-permission-patterns.md +156 -156
  40. package/skills/{fui-skill → fui}/references/tapi-reference.md +474 -474
  41. package/skills/fui/references/team-run.md +141 -0
  42. package/skills/fui/references/tools-registry.md +211 -0
  43. package/skills/{fui-skill → fui}/references/ui-crosswindow-patterns.md +321 -321
  44. package/skills/{fui-skill → fui}/references/ui-dialog-patterns.md +255 -255
  45. package/skills/{fui-skill → fui}/references/ui-layout-patterns.md +176 -176
  46. package/skills/{fui-skill → fui}/references/ui-patterns.md +303 -303
  47. package/skills/{fui-skill → fui}/references/ui-screenshot-review.md +95 -95
  48. package/skills/{fui-skill → fui}/references/ui-table-cell-patterns.md +318 -318
  49. package/skills/{fui-skill → fui}/references/verification.md +11 -11
  50. package/skills/{fui-skill → fui}/references/watcher-patterns.md +163 -163
  51. package/skills/{fui-skill → fui}/references/websocket-realtime.md +271 -271
  52. package/skills/{fui-skill → fui}/scripts/component-3.0.js +2715 -2715
  53. package/skills/{fui-skill → fui}/scripts/component.js +3147 -3147
  54. package/skills/{fui-skill → fui}/scripts/componentTable-3.0.js +1038 -1038
  55. package/skills/{fui-skill → fui}/scripts/componentTable.js +891 -891
  56. package/skills/{fui-skill → fui}/scripts/defaultfunction-3.0.js +866 -866
  57. package/skills/{fui-skill → fui}/scripts/defaultfunction.js +1051 -1051
  58. package/skills/{fui-skill → fui}/scripts/fastproject-3.0.js +870 -870
  59. package/skills/{fui-skill → fui}/scripts/fastproject.js +828 -828
  60. package/dist/fui-h4z7zhk2.js +0 -381
  61. package/skills/fui-skill/SKILL.md +0 -139
  62. package/skills/fui-skill/references/tools-registry.md +0 -84
  63. /package/skills/{fui-skill → fui}/assets/projectdefaultstyle-3.0.css +0 -0
  64. /package/skills/{fui-skill → fui}/design-md/airbnb/DESIGN.md +0 -0
  65. /package/skills/{fui-skill → fui}/design-md/airbnb/README.md +0 -0
  66. /package/skills/{fui-skill → fui}/design-md/airtable/DESIGN.md +0 -0
  67. /package/skills/{fui-skill → fui}/design-md/airtable/README.md +0 -0
  68. /package/skills/{fui-skill → fui}/design-md/apple/DESIGN.md +0 -0
  69. /package/skills/{fui-skill → fui}/design-md/apple/README.md +0 -0
  70. /package/skills/{fui-skill → fui}/design-md/asu/DESIGN.md +0 -0
  71. /package/skills/{fui-skill → fui}/design-md/asu/README.md +0 -0
  72. /package/skills/{fui-skill → fui}/design-md/binance/DESIGN.md +0 -0
  73. /package/skills/{fui-skill → fui}/design-md/binance/README.md +0 -0
  74. /package/skills/{fui-skill → fui}/design-md/bmw/DESIGN.md +0 -0
  75. /package/skills/{fui-skill → fui}/design-md/bmw/README.md +0 -0
  76. /package/skills/{fui-skill → fui}/design-md/bmw-m/DESIGN.md +0 -0
  77. /package/skills/{fui-skill → fui}/design-md/bmw-m/README.md +0 -0
  78. /package/skills/{fui-skill → fui}/design-md/bugatti/DESIGN.md +0 -0
  79. /package/skills/{fui-skill → fui}/design-md/bugatti/README.md +0 -0
  80. /package/skills/{fui-skill → fui}/design-md/cal/DESIGN.md +0 -0
  81. /package/skills/{fui-skill → fui}/design-md/cal/README.md +0 -0
  82. /package/skills/{fui-skill → fui}/design-md/claude/DESIGN.md +0 -0
  83. /package/skills/{fui-skill → fui}/design-md/claude/README.md +0 -0
  84. /package/skills/{fui-skill → fui}/design-md/clay/DESIGN.md +0 -0
  85. /package/skills/{fui-skill → fui}/design-md/clay/README.md +0 -0
  86. /package/skills/{fui-skill → fui}/design-md/clickhouse/DESIGN.md +0 -0
  87. /package/skills/{fui-skill → fui}/design-md/clickhouse/README.md +0 -0
  88. /package/skills/{fui-skill → fui}/design-md/cohere/DESIGN.md +0 -0
  89. /package/skills/{fui-skill → fui}/design-md/cohere/README.md +0 -0
  90. /package/skills/{fui-skill → fui}/design-md/coinbase/DESIGN.md +0 -0
  91. /package/skills/{fui-skill → fui}/design-md/coinbase/README.md +0 -0
  92. /package/skills/{fui-skill → fui}/design-md/composio/DESIGN.md +0 -0
  93. /package/skills/{fui-skill → fui}/design-md/composio/README.md +0 -0
  94. /package/skills/{fui-skill → fui}/design-md/cursor/DESIGN.md +0 -0
  95. /package/skills/{fui-skill → fui}/design-md/cursor/README.md +0 -0
  96. /package/skills/{fui-skill → fui}/design-md/elevenlabs/DESIGN.md +0 -0
  97. /package/skills/{fui-skill → fui}/design-md/elevenlabs/README.md +0 -0
  98. /package/skills/{fui-skill → fui}/design-md/expo/DESIGN.md +0 -0
  99. /package/skills/{fui-skill → fui}/design-md/expo/README.md +0 -0
  100. /package/skills/{fui-skill → fui}/design-md/ferrari/DESIGN.md +0 -0
  101. /package/skills/{fui-skill → fui}/design-md/ferrari/README.md +0 -0
  102. /package/skills/{fui-skill → fui}/design-md/figma/DESIGN.md +0 -0
  103. /package/skills/{fui-skill → fui}/design-md/figma/README.md +0 -0
  104. /package/skills/{fui-skill → fui}/design-md/framer/DESIGN.md +0 -0
  105. /package/skills/{fui-skill → fui}/design-md/framer/README.md +0 -0
  106. /package/skills/{fui-skill → fui}/design-md/fui/DESIGN.md +0 -0
  107. /package/skills/{fui-skill → fui}/design-md/hashicorp/DESIGN.md +0 -0
  108. /package/skills/{fui-skill → fui}/design-md/hashicorp/README.md +0 -0
  109. /package/skills/{fui-skill → fui}/design-md/ibm/DESIGN.md +0 -0
  110. /package/skills/{fui-skill → fui}/design-md/ibm/README.md +0 -0
  111. /package/skills/{fui-skill → fui}/design-md/intercom/DESIGN.md +0 -0
  112. /package/skills/{fui-skill → fui}/design-md/intercom/README.md +0 -0
  113. /package/skills/{fui-skill → fui}/design-md/kraken/DESIGN.md +0 -0
  114. /package/skills/{fui-skill → fui}/design-md/kraken/README.md +0 -0
  115. /package/skills/{fui-skill → fui}/design-md/lamborghini/DESIGN.md +0 -0
  116. /package/skills/{fui-skill → fui}/design-md/lamborghini/README.md +0 -0
  117. /package/skills/{fui-skill → fui}/design-md/linear.app/DESIGN.md +0 -0
  118. /package/skills/{fui-skill → fui}/design-md/linear.app/README.md +0 -0
  119. /package/skills/{fui-skill → fui}/design-md/lovable/DESIGN.md +0 -0
  120. /package/skills/{fui-skill → fui}/design-md/lovable/README.md +0 -0
  121. /package/skills/{fui-skill → fui}/design-md/mastercard/DESIGN.md +0 -0
  122. /package/skills/{fui-skill → fui}/design-md/mastercard/README.md +0 -0
  123. /package/skills/{fui-skill → fui}/design-md/meta/DESIGN.md +0 -0
  124. /package/skills/{fui-skill → fui}/design-md/meta/README.md +0 -0
  125. /package/skills/{fui-skill → fui}/design-md/minimax/DESIGN.md +0 -0
  126. /package/skills/{fui-skill → fui}/design-md/minimax/README.md +0 -0
  127. /package/skills/{fui-skill → fui}/design-md/mintlify/DESIGN.md +0 -0
  128. /package/skills/{fui-skill → fui}/design-md/mintlify/README.md +0 -0
  129. /package/skills/{fui-skill → fui}/design-md/miro/DESIGN.md +0 -0
  130. /package/skills/{fui-skill → fui}/design-md/miro/README.md +0 -0
  131. /package/skills/{fui-skill → fui}/design-md/mistral.ai/DESIGN.md +0 -0
  132. /package/skills/{fui-skill → fui}/design-md/mistral.ai/README.md +0 -0
  133. /package/skills/{fui-skill → fui}/design-md/mongodb/DESIGN.md +0 -0
  134. /package/skills/{fui-skill → fui}/design-md/mongodb/README.md +0 -0
  135. /package/skills/{fui-skill → fui}/design-md/nike/DESIGN.md +0 -0
  136. /package/skills/{fui-skill → fui}/design-md/nike/README.md +0 -0
  137. /package/skills/{fui-skill → fui}/design-md/notion/DESIGN.md +0 -0
  138. /package/skills/{fui-skill → fui}/design-md/notion/README.md +0 -0
  139. /package/skills/{fui-skill → fui}/design-md/nvidia/DESIGN.md +0 -0
  140. /package/skills/{fui-skill → fui}/design-md/nvidia/README.md +0 -0
  141. /package/skills/{fui-skill → fui}/design-md/ollama/DESIGN.md +0 -0
  142. /package/skills/{fui-skill → fui}/design-md/ollama/README.md +0 -0
  143. /package/skills/{fui-skill → fui}/design-md/opencode.ai/DESIGN.md +0 -0
  144. /package/skills/{fui-skill → fui}/design-md/opencode.ai/README.md +0 -0
  145. /package/skills/{fui-skill → fui}/design-md/pinterest/DESIGN.md +0 -0
  146. /package/skills/{fui-skill → fui}/design-md/pinterest/README.md +0 -0
  147. /package/skills/{fui-skill → fui}/design-md/playstation/DESIGN.md +0 -0
  148. /package/skills/{fui-skill → fui}/design-md/playstation/README.md +0 -0
  149. /package/skills/{fui-skill → fui}/design-md/posthog/DESIGN.md +0 -0
  150. /package/skills/{fui-skill → fui}/design-md/posthog/README.md +0 -0
  151. /package/skills/{fui-skill → fui}/design-md/raycast/DESIGN.md +0 -0
  152. /package/skills/{fui-skill → fui}/design-md/raycast/README.md +0 -0
  153. /package/skills/{fui-skill → fui}/design-md/renault/DESIGN.md +0 -0
  154. /package/skills/{fui-skill → fui}/design-md/renault/README.md +0 -0
  155. /package/skills/{fui-skill → fui}/design-md/replicate/DESIGN.md +0 -0
  156. /package/skills/{fui-skill → fui}/design-md/replicate/README.md +0 -0
  157. /package/skills/{fui-skill → fui}/design-md/resend/DESIGN.md +0 -0
  158. /package/skills/{fui-skill → fui}/design-md/resend/README.md +0 -0
  159. /package/skills/{fui-skill → fui}/design-md/revolut/DESIGN.md +0 -0
  160. /package/skills/{fui-skill → fui}/design-md/revolut/README.md +0 -0
  161. /package/skills/{fui-skill → fui}/design-md/runwayml/DESIGN.md +0 -0
  162. /package/skills/{fui-skill → fui}/design-md/runwayml/README.md +0 -0
  163. /package/skills/{fui-skill → fui}/design-md/sanity/DESIGN.md +0 -0
  164. /package/skills/{fui-skill → fui}/design-md/sanity/README.md +0 -0
  165. /package/skills/{fui-skill → fui}/design-md/sentry/DESIGN.md +0 -0
  166. /package/skills/{fui-skill → fui}/design-md/sentry/README.md +0 -0
  167. /package/skills/{fui-skill → fui}/design-md/shopify/DESIGN.md +0 -0
  168. /package/skills/{fui-skill → fui}/design-md/shopify/README.md +0 -0
  169. /package/skills/{fui-skill → fui}/design-md/slack/DESIGN.md +0 -0
  170. /package/skills/{fui-skill → fui}/design-md/spacex/DESIGN.md +0 -0
  171. /package/skills/{fui-skill → fui}/design-md/spacex/README.md +0 -0
  172. /package/skills/{fui-skill → fui}/design-md/spotify/DESIGN.md +0 -0
  173. /package/skills/{fui-skill → fui}/design-md/spotify/README.md +0 -0
  174. /package/skills/{fui-skill → fui}/design-md/starbucks/DESIGN.md +0 -0
  175. /package/skills/{fui-skill → fui}/design-md/starbucks/README.md +0 -0
  176. /package/skills/{fui-skill → fui}/design-md/stripe/DESIGN.md +0 -0
  177. /package/skills/{fui-skill → fui}/design-md/stripe/README.md +0 -0
  178. /package/skills/{fui-skill → fui}/design-md/supabase/DESIGN.md +0 -0
  179. /package/skills/{fui-skill → fui}/design-md/supabase/README.md +0 -0
  180. /package/skills/{fui-skill → fui}/design-md/superhuman/DESIGN.md +0 -0
  181. /package/skills/{fui-skill → fui}/design-md/superhuman/README.md +0 -0
  182. /package/skills/{fui-skill → fui}/design-md/tesla/DESIGN.md +0 -0
  183. /package/skills/{fui-skill → fui}/design-md/tesla/README.md +0 -0
  184. /package/skills/{fui-skill → fui}/design-md/theverge/DESIGN.md +0 -0
  185. /package/skills/{fui-skill → fui}/design-md/theverge/README.md +0 -0
  186. /package/skills/{fui-skill → fui}/design-md/together.ai/DESIGN.md +0 -0
  187. /package/skills/{fui-skill → fui}/design-md/together.ai/README.md +0 -0
  188. /package/skills/{fui-skill → fui}/design-md/uber/DESIGN.md +0 -0
  189. /package/skills/{fui-skill → fui}/design-md/uber/README.md +0 -0
  190. /package/skills/{fui-skill → fui}/design-md/vercel/DESIGN.md +0 -0
  191. /package/skills/{fui-skill → fui}/design-md/vercel/README.md +0 -0
  192. /package/skills/{fui-skill → fui}/design-md/vodafone/DESIGN.md +0 -0
  193. /package/skills/{fui-skill → fui}/design-md/vodafone/README.md +0 -0
  194. /package/skills/{fui-skill → fui}/design-md/voltagent/DESIGN.md +0 -0
  195. /package/skills/{fui-skill → fui}/design-md/voltagent/README.md +0 -0
  196. /package/skills/{fui-skill → fui}/design-md/warp/DESIGN.md +0 -0
  197. /package/skills/{fui-skill → fui}/design-md/warp/README.md +0 -0
  198. /package/skills/{fui-skill → fui}/design-md/webflow/DESIGN.md +0 -0
  199. /package/skills/{fui-skill → fui}/design-md/webflow/README.md +0 -0
  200. /package/skills/{fui-skill → fui}/design-md/wired/DESIGN.md +0 -0
  201. /package/skills/{fui-skill → fui}/design-md/wired/README.md +0 -0
  202. /package/skills/{fui-skill → fui}/design-md/wise/DESIGN.md +0 -0
  203. /package/skills/{fui-skill → fui}/design-md/wise/README.md +0 -0
  204. /package/skills/{fui-skill → fui}/design-md/x.ai/DESIGN.md +0 -0
  205. /package/skills/{fui-skill → fui}/design-md/x.ai/README.md +0 -0
  206. /package/skills/{fui-skill → fui}/design-md/zapier/DESIGN.md +0 -0
  207. /package/skills/{fui-skill → fui}/design-md/zapier/README.md +0 -0
  208. /package/skills/{fui-skill → fui}/examples/f-table-patterns.json +0 -0
  209. /package/skills/{fui-skill → fui}/examples/module-patterns.json +0 -0
  210. /package/skills/{fui-skill → fui}/examples/project-patterns.json +0 -0
  211. /package/skills/{fui-skill → fui}/references/coding-standards.md +0 -0
  212. /package/skills/{fui-skill → fui}/references/db-table-design.md +0 -0
  213. /package/skills/{fui-skill → fui}/references/fsheet.md +0 -0
  214. /package/skills/{fui-skill → fui}/references/tapi-file-api.md +0 -0
  215. /package/skills/{fui-skill → fui}/references/ui-templates.md +0 -0
  216. /package/skills/{fui-skill → fui}/scripts/fechart.js +0 -0
  217. /package/skills/{fui-skill → fui}/scripts/fsheet.js +0 -0
@@ -1,891 +1,891 @@
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'],
3
- data: function () {
4
- var headersAttr = bindData(this.headers);
5
- var strTableTemplate = this.template || '';
6
-
7
- if (headersAttr) {
8
- _.forEach(headersAttr.filter(i => !!i.el), function (c) {
9
- var ctl = buildControl([c], false);
10
-
11
- ctl.attr("v-bind", "item.attr");
12
- ctl.attr(":item", "item");
13
- ctl.attr(":index", "items.indexOf(item)");
14
- ctl.attr("v-on:mouseover", "$emit('mouseover:row',item)");
15
- ctl.attr("v-on:mouseleave", "$emit('mouseleave:row',item)");
16
-
17
- if (!c.attr['v-model']) ctl.attr("v-model", `item['${c.value}']`);
18
-
19
- strTableTemplate += `<template v-slot:item.${c.value}="{item}">${ctl[0].outerHTML}</template>`
20
- });
21
- }
22
-
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 || excel || ((newAPI() || newAction()) && !disabledUpdate)" align="end" class="py-2 px-3">
27
- <div class="primary--text text-left" v-html="label" style="font-weight: 500; font-size: 1.1rem;"></div>
28
- <v-spacer></v-spacer><component :is="headercomponent"></component>
29
- <v-btn v-if="excel" icon color="primary" @click="toExcel"><v-icon>mdi-microsoft-excel</v-icon></v-btn>
30
- <div v-if="showSearch" class="d-flex align-center ml-3">
31
- <v-btn icon color="primary" @mousedown.prevent @click="toggleSearch"><v-icon>mdi-magnify</v-icon></v-btn>
32
- <v-expand-x-transition>
33
- <v-text-field v-if="searchOpen" ref="searchField" dense @input="updateSearch" label="" single-line hide-details autofocus style="width: 300px;" @blur="onSearchBlur" class="ml-1"></v-text-field>
34
- </v-expand-x-transition>
35
- </div>
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
- </v-card-title>
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()" v-on:update:options="onTableOptions" :search="search" :custom-filter="customSearch">
40
- <template v-for="(h, index) in headerCols" :key="index" v-slot:['header.'+h.value]="{ header }"><span v-html="header.text"></span></template>
41
- ${strTableTemplate}
42
- <template v-if="!disabledUpdate" v-slot:item.ctrl-update="{item}">
43
- <div class="d-flex justify-center" v-if="item['ctrl-update'] != 0">
44
- <v-btn v-if="deleteAPI()" @click="deleteItem(item, deleteAPI())" x-small fab icon><v-icon color="red darken-3">mdi-trash-can-outline</v-icon></v-btn>
45
- <v-btn v-if="editAPI() || editAction()" @click="editItem(item)" x-small fab icon><v-icon color="green darken-2">mdi-square-edit-outline</v-icon></v-btn>
46
- </div>
47
- </template>
48
- <template v-if="sumFormat && typeof sumFormat === 'object'" v-slot:body.append="{ headers }">
49
- <tr>
50
- <td v-for="h in headers" :key="'sum-'+h.value" class="text-right font-weight-medium">
51
- <span v-if="sumFormat[h.value]">{{ sumByColumn(h.value) }}</span>
52
- </td>
53
- </tr>
54
- </template>
55
- </v-data-table>
56
-
57
- <v-divider v-if="sumFormat && typeof sumFormat === 'string'"></v-divider>
58
- <v-card-actions v-if="sumFormat && typeof sumFormat === 'string'">
59
- <v-spacer></v-spacer><span class="title font-weight-light">{{sumText}}</span>
60
- </v-card-actions>
61
- <f-dialog v-model="editDialog" v-bind="updateFormAttr" :data.sync="editData" :data-out.sync="vueData[dataOut]" :title="formTitle" :button="updateButton" :controls="updateForm"></f-dialog>
62
- </v-card>
63
- `;
64
- //----------------------------------------------------------------------
65
-
66
- return {
67
- editDialog: false,
68
- search: '',
69
- searchOpen: false,
70
- itemselected: [],
71
- returnValue: [],
72
- editData: {},
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 || []),
78
- updateButton: [
79
- {
80
- "v-if": "title=='Cập nhật'",
81
- label: "Lưu",
82
- getoutdata: true,
83
- action: () => {
84
- this.save(this.editAPI());
85
- }
86
- },
87
- {
88
- "v-if": "title=='Thêm mới'",
89
- label: "Thêm mới",
90
- getoutdata: true,
91
- action: () => {
92
- this.addnew(this.newAPI())
93
- }
94
- }
95
- ],
96
- newAction: function () {
97
- if (this.updateApi) return this.updateApi['new-action']
98
- else return null;
99
- },
100
- newAPI: function () {
101
- if (this.updateApi) return this.updateApi.new;
102
- else return null;
103
- },
104
- editAction: function () {
105
- if (this.updateApi) return this.updateApi['edit-action']
106
- else return null;
107
- },
108
- editAPI: function () {
109
- if (this.updateApi) return this.updateApi.edit;
110
- else return null;
111
- },
112
- deleteAPI: function () {
113
- if (this.updateApi) return this.updateApi.delete;
114
- else return null;
115
- },
116
- defaultItem: function () {
117
- if (this.updateApi) {
118
- if (typeof this.updateApi['default-item'] === 'undefined') return {};
119
- var dataItem = {}
120
- mapData(this.updateApi['default-item'], dataItem, vueData);
121
- return dataItem;
122
- }
123
- else return {};
124
- },
125
- }
126
- },
127
- mounted: function () {
128
- this.editData = Object.assign({}, this.defaultItem())
129
- this.editedIndex = -1
130
- this.itemSelectRebuild(this.value);
131
-
132
- if (this.heightNum !== null && this.heightNum <= 0) {
133
- this.updateFillHeight();
134
- this.resizeHandler = this.updateFillHeight.bind(this);
135
- window.addEventListener('resize', this.resizeHandler);
136
- }
137
- },
138
- beforeDestroy: function () { this.removeResize(); },
139
- beforeUnmount: function () { this.removeResize(); },
140
- watch: {
141
- editDialog(val) {
142
- if (!val) {
143
- _.forEach(this.editData, (v, k) => {
144
- this.editData[k] = undefined;
145
- });
146
- this.editedIndex = -1
147
- } else {
148
- this.editData.dialogModel = (this.edit ? 'edit' : 'new')
149
- }
150
- },
151
- value(newVal, oldVal,) {
152
- t = this;
153
- if (t.returnValue != newVal) this.itemSelectRebuild(newVal);
154
- },
155
- items(newVal) {
156
- if (!this.headers) this.autoHeaders = this.buildAutoHeaders(newVal || []);
157
- if (this.resizeHandler) this.updateFillHeight();
158
- }
159
- },
160
- computed: {
161
- // null = không khai height → bảng tự giãn theo số dòng (mặc định của v-data-table)
162
- heightNum() {
163
- if (this.height === undefined || this.height === null || this.height === '') return null;
164
- var n = Number(this.height);
165
- return isNaN(n) ? null : n;
166
- },
167
- tableHeight() {
168
- if (this.heightNum === null) return undefined;
169
- if (this.heightNum > 0) return this.heightNum;
170
- return this.fillHeight || undefined;
171
- },
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() {
205
- var h = this.headers || this.autoHeaders;
206
- if (_.isBoolean(this.disabledUpdate) && this.disabledUpdate) {
207
- _.remove(h, function (n) {
208
- return n.value == 'ctrl-update';
209
- });
210
- };
211
-
212
- return h;
213
- },
214
- _hideDefaultFooter() {
215
- if (this.items) {
216
- if (this.itemsPerPage > 0 && this.itemsPerPage < this.items.length) return false;
217
- }
218
- if (this.hideDefaultFooter) return this.hideDefaultFooter;
219
- return false;
220
- },
221
- edit() {
222
- return this.editedIndex >= 0;
223
- },
224
- formTitle() {
225
- return this.edit ? 'Cập nhật' : 'Thêm mới';
226
- },
227
- dataOut() {
228
- if (this.updateApi) return this.updateApi['data-out']
229
- else return null;
230
- },
231
- sumText() {
232
- var items = this.items;
233
- return _.replace(this.sumFormat, /\[\[(.*?)\]\]/gm, function (s, key) {
234
- return numeral(_.sumBy(items, key)).format('0,0');
235
- });
236
- }
237
- },
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() {
245
- if (this.resizeHandler) {
246
- window.removeEventListener('resize', this.resizeHandler);
247
- this.resizeHandler = null;
248
- }
249
- },
250
- updateFillHeight() {
251
- var t = this;
252
- t.$nextTick(function () {
253
- requestAnimationFrame(function () {
254
- var root = t.$refs.myTable && t.$refs.myTable.$el;
255
- if (!root) return;
256
- var wrapper = root.querySelector('.v-data-table__wrapper');
257
- if (!wrapper) return;
258
- var rootRect = root.getBoundingClientRect();
259
- var wrapRect = wrapper.getBoundingClientRect();
260
- // phần nằm dưới vùng cuộn (footer phân trang, dòng tổng, viền card)
261
- var below = Math.max(0, rootRect.bottom - wrapRect.bottom);
262
- var margin = Math.abs(t.heightNum);
263
- t.fillHeight = Math.max(150, window.innerHeight - wrapRect.top - below - margin);
264
- });
265
- });
266
- },
267
- buildAutoHeaders(items) {
268
- var arrHeaders = buildHeader(items);
269
- var hasEditOrDelete = this.updateApi && (this.updateApi.edit || this.updateApi.delete);
270
- if (arrHeaders.length > 0 && hasEditOrDelete && !this.disabledUpdate) {
271
- arrHeaders.push({ "text": "", "value": "ctrl-update", "divider": true });
272
- }
273
- return arrHeaders;
274
- },
275
- sumByColumn(key) {
276
- return numeral(_.sumBy(this.items, key)).format(this.sumFormat[key]);
277
- },
278
- toggleSearch() {
279
- this.searchOpen = !this.searchOpen;
280
- if (this.searchOpen) {
281
- var t = this;
282
- this.$nextTick(function () {
283
- t.$refs.searchField && t.$refs.searchField.focus();
284
- });
285
- }
286
- },
287
- onSearchBlur() {
288
- if (!this.search) this.searchOpen = false;
289
- },
290
- itemSelect() {
291
- var t = this;
292
-
293
- setTimeout(function () {
294
- if (!t.returnObject) t.returnValue = _.map(t.itemselected, t.itemKey);
295
- else t.returnValue = t.itemselected;
296
-
297
- t.$emit('input', t.returnValue);
298
- }, 10);
299
- },
300
-
301
- itemSelectRebuild(newVal) {
302
- var t = this;
303
- if (t.returnObject) t.itemselected = newVal;
304
- else {
305
- t.itemselected = [];
306
-
307
- _.forEach(newVal, function (val) {
308
- t.itemselected.push(_.find(t.items, [t.itemKey, val]));
309
- });
310
- }
311
- },
312
- newBtnClick() {
313
- if (this.newAction()) runAction(this.newAction());
314
- if (this.newAPI()) {
315
- Object.assign(this.editData, this.defaultItem());
316
- this.editDialog = true;
317
- }
318
- },
319
- editItem(item) {
320
-
321
- if (this.editAction()) runAction(this.editAction(), { item: item });
322
-
323
- if (this.editAPI()) {
324
- this.editedIndex = this.items.indexOf(item);
325
- Object.assign(this.editData, this.defaultItem());
326
- this.editData = Object.assign({}, item);
327
- this.editDialog = true;
328
- }
329
- },
330
-
331
- deleteItem(item, deleteAPI) {
332
- const items = this.items;
333
- var actionAPI = deleteAPI;
334
- var indexDelete = items.indexOf(item)
335
- confirm({
336
- title: (actionAPI.CONFIRM ? bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: item, item: item }, vueData)) : "Bạn có chắc chắn muốn xoá ?"),
337
- action: function () {
338
- vueAction(actionAPI, { $row: item, item: item }, function () {
339
- items.splice(indexDelete, 1);
340
- });
341
- }
342
- });
343
- },
344
-
345
- save(actionAPI) {
346
- var diaCtl = this;
347
- if (diaCtl.editedIndex > -1) {
348
- if (actionAPI) {
349
- if (actionAPI.CONFIRM) {
350
- confirm({
351
- title: bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: diaCtl.editData, item: diaCtl.editData }, vueData)),
352
- action: function () {
353
- vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
354
- if (!d.Message) {
355
- if (_.isArray(d.data)) {
356
- if (d.data[0]) {
357
- _.forEach(d.data[0], function (value, key) {
358
- diaCtl.editData[key] = value;
359
- });
360
- }
361
- }
362
- }
363
- _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
364
- diaCtl.editDialog = false;
365
- });
366
- },
367
- cancel: function () { }
368
- });
369
- } else {
370
- vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
371
- if (!d.Message) {
372
- if (_.isArray(d.data)) {
373
- if (d.data[0]) {
374
- _.forEach(d.data[0], function (value, key) {
375
- diaCtl.editData[key] = value;
376
- });
377
- }
378
- }
379
- }
380
- _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
381
- diaCtl.editDialog = false;
382
- });
383
- }
384
- } else {
385
- _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
386
- diaCtl.editDialog = false;
387
- }
388
- }
389
- },
390
-
391
- addnew(actionAPI) {
392
- var diaCtl = this;
393
- if (diaCtl.editedIndex == -1) {
394
- if (actionAPI) {
395
- if (actionAPI.CONFIRM) {
396
- confirm({
397
- title: bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: diaCtl.editData, item: diaCtl.editData }, vueData)),
398
- action: function () {
399
- vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
400
- if (d) {
401
- if (_.isArray(d.data)) {
402
- if (d.data[0]) {
403
- _.forEach(d.data[0], function (value, key) {
404
- diaCtl.editData[key] = value;
405
- });
406
- }
407
- }
408
- }
409
- diaCtl.items.push(_.cloneDeep(diaCtl.editData));
410
- diaCtl.editDialog = false;
411
- });
412
- },
413
- cancel: function () { }
414
- });
415
- } else {
416
- vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
417
- if (d) {
418
- if (_.isArray(d.data)) {
419
- if (d.data[0]) {
420
- _.forEach(d.data[0], function (value, key) {
421
- diaCtl.editData[key] = value;
422
- });
423
- }
424
- }
425
- }
426
- diaCtl.items.push(_.cloneDeep(diaCtl.editData));
427
- diaCtl.editDialog = false;
428
- });
429
- }
430
- } else {
431
- diaCtl.items.push(_.cloneDeep(diaCtl.editData));
432
- diaCtl.editDialog = false;
433
- }
434
- }
435
- },
436
- updateSearch: _.debounce(function (text) {
437
- this.search = text;
438
- }, 300),
439
- customSearch(value, search, item) {
440
- return Object.values(item).some(v => v && v.toString().toLowerCase().includes(search.toLowerCase()))
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).
444
- toExcel() {
445
- if (typeof this.excel === 'string' && this.excel.trim().toUpperCase() === 'UI') return this.toExcelUI();
446
- jsonToExcel({ data: this.items, filename: (this.label && this.label != '' ? this.label : 'tableFP') + '.xlsx' });
447
- Vue.$toast.success("Đã xuất Excel", { position: 'top' });
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
- },
567
- }
568
- });
569
- //===========================================================================================
570
- //===========================================================================================
571
- //===========================================================================================
572
-
573
- Vue.component('t-html', {
574
- template: `<div class="my-3" v-bind="$attrs" v-html="$attrs['value']"></div>`,
575
- });
576
- //-----------------------------------------------------------------------------------------------------------
577
- Vue.component('t-label', {
578
- template: `<div class="text-no-wrap" v-bind="$attrs">{{$attrs['value']}}</div>`,
579
- });
580
- //-----------------------------------------------------------------------------------------------------------
581
- Vue.component('t-num', {
582
- props: {
583
- value: {
584
- type: Number,
585
- default: 0
586
- },
587
- hidezero: {
588
- type: Boolean,
589
- default: true
590
- },
591
- format: {
592
- type: String,
593
- default: ''
594
- }
595
- },
596
- template: `<div v-if="!_.isNull(value) && !(hidezero && value==0)" class="text-no-wrap text-end" v-bind="$attrs">{{ format != ''? numeral(value).format(format) : Number.isInteger(value)?numeral(value).format('0,0'): numeral(value).format('0,0.0') }}</div>`,
597
- });
598
- //-----------------------------------------------------------------------------------------------------------
599
- Vue.component('t-time', {
600
- props: {
601
- format: {
602
- type: String,
603
- default: 'DD/MM/YYYY'
604
- }
605
- },
606
- template: `<div v-if="$attrs['value']" class="text-no-wrap" v-bind="$attrs">{{ moment($attrs['value']).format(format) }}</div>`,
607
- });
608
- //-----------------------------------------------------------------------------------------------------------
609
- Vue.component('t-boolean', {
610
- props: {
611
- value: {
612
- default: null
613
- },
614
- colorTrue: {
615
- default: "primary"
616
- },
617
- colorFalse: {
618
- default: "red"
619
- },
620
- iconTrue: {
621
- default: "mdi-check"
622
- },
623
- iconFalse: {
624
- default: "mdi-close"
625
- },
626
- },
627
- template: `<v-icon v-bind="$attrs" :color="value?colorTrue:colorFalse">{{value?iconTrue:iconFalse}}</v-icon>`,
628
- });
629
- //-----------------------------------------------------------------------------------------------------------
630
- Vue.component('t-check', {
631
- props: {
632
- value: {
633
- default: null
634
- },
635
- falseValue: {
636
- default: false
637
- }
638
- },
639
- template: `<v-checkbox v-model="ctlValue" v-bind="$attrs" :false-value="falseValue" hide-details class="d-inline-flex ma-1 pa-1" v-on:change="onchange($event)" />`,
640
- data: function () {
641
- return {
642
- ctlValue: this.value
643
- }
644
- },
645
- watch: {
646
- value(newVal) {
647
- this.ctlValue = newVal
648
- }
649
- },
650
- methods: {
651
- onchange: function (e) {
652
- if (!e) this.ctlValue = false;
653
- tableActionEvent(this);
654
- }
655
- }
656
- });
657
- //-----------------------------------------------------------------------------------------------------------
658
- Vue.component('t-text', {
659
- props: ['value'],
660
- template: `<v-text-field v-model="ctlValue" v-bind="$attrs" v-on:change="onchange($event)" dense outlined hide-details/>`,
661
- data: function () {
662
- return {
663
- ctlValue: this.value
664
- }
665
- },
666
- watch: {
667
- value(newVal) {
668
- this.ctlValue = newVal
669
- }
670
- },
671
- methods: {
672
- onchange: function (e) {
673
- console.log(e)
674
- tableActionEvent(this);
675
- }
676
- }
677
- });
678
-
679
- //-----------------------------------------------------------------------------------------------------------
680
- Vue.component('t-select', {
681
- props: ['value'],
682
- template: `<v-select v-model="ctlValue" v-bind="$attrs" v-on:change="onchange($event)" dense hide-details append-icon="mdi-menu-swap"/>`,
683
- data: function () {
684
- return {
685
- ctlValue: this.value
686
- }
687
- },
688
- watch: {
689
- value(newVal) {
690
- this.ctlValue = newVal
691
- }
692
- },
693
- methods: {
694
- onchange: function (e) {
695
- console.log(e)
696
- tableActionEvent(this);
697
- }
698
- }
699
- });
700
- //-----------------------------------------------------------------------------------------------------------
701
- Vue.component('t-combobox', {
702
- props: {
703
- value: {
704
- default: undefined
705
- },
706
- text: {
707
- default: undefined
708
- },
709
- items: {
710
- type: Array,
711
- default: []
712
- },
713
- api: {
714
- type: String,
715
- default: undefined
716
- },
717
- apiData: {
718
- type: Object,
719
- default: {}
720
- },
721
-
722
- },
723
- template: `<v-autocomplete refs="abc" v-model="ctlValue" v-bind="$attrs" :loading="loading" :items="dataItems" :search-input.sync="search" v-on:change="onchange($event)" hide-details dense></v-autocomplete>`,
724
- data: function () {
725
- return {
726
- dataItems: this.items,
727
- ctlValue: this.value,
728
- loading: false,
729
- search: null,
730
- getDataFormAPI: _.debounce((str) => {
731
- if ((this.search && this.search.length < 2) || !this.api) return;
732
- var self = this;
733
- this.loading = true
734
-
735
- if (typeof this.apiData != 'object') this.apiData = JSON.parse(this.apiData);
736
-
737
- mapData(this.apiData, this.apiData, _.assignIn({}, vueData, { TEXT: this.search }));
738
-
739
- console.log("API-CALL: ", this.api)
740
- ajaxCALL(this.api, this.apiData, function (d) {
741
- if (_.isArray(d.data)) self.dataItems = d.data;
742
- else self.dataItems = d;
743
-
744
- self.loading = false
745
- }, function (e) {
746
- console.log("API Error", e);
747
- })
748
-
749
- }, 500)
750
- }
751
- },
752
- watch: {
753
- search(newval, oldval) {
754
- newval && oldval && this.querySelections(newval)
755
- },
756
- items(val) {
757
- this.dataItems = val
758
- },
759
- value(newVal) {
760
- this.ctlValue = newVal
761
- }
762
- },
763
- methods: {
764
- querySelections(val) {
765
- this.getDataFormAPI(val);
766
- },
767
- onchange: function (e) {
768
- if (e) tableActionEvent(this);
769
- }
770
- },
771
- });
772
- //-----------------------------------------------------------------------------------------------------------
773
- Vue.component('t-menu', {
774
- props: {
775
- iconText: {
776
- default: 'mdi-menu'
777
- },
778
- items: {
779
- default: []
780
- }
781
- },
782
- template: `
783
- <v-menu offset-y origin="center center" transition="slide-y-transition" max-height = 500>
784
- <template v-slot:activator="{ on }">
785
- <v-icon v-on="on">{{iconText}}</v-icon>
786
- </template>
787
- <v-list >
788
- <v-list-item v-bind="$attrs" v-for="(item, index) in items" :key="index" @click="onSelect(item)" >
789
- <v-list-item-icon v-if="item['icon-text']" class="mr-3">
790
- <v-icon :color="item.color">{{item['icon-text']}}</v-icon>
791
- </v-list-item-icon>
792
- <v-list-item-content class="mr-2">
793
- <v-list-item-title v-text="item.text"></v-list-item-title>
794
- </v-list-item-content>
795
- </v-list-item>
796
- </v-list>
797
- </v-menu>
798
- `,
799
- data: function () {
800
- return {
801
- }
802
- },
803
- methods: {
804
- onSelect: function (e) {
805
- this.$attrs['action'] = e['action'];
806
- tableActionEvent(this);
807
- }
808
- }
809
- });
810
- //-----------------------------------------------------------------------------------------------------------
811
- Vue.component('t-link', {
812
- props: ['id', 'target', 'url', 'title', 'onclose'],
813
- template: `<a :href="_url" :target="target" v-bind="$attrs" @click="onclick()"><v-icon v-if="$attrs['icon-text']" left color="primary">{{$attrs['icon-text']}}</v-icon> {{ $attrs['value']}}</a>`,
814
- computed: {
815
- _url: function () {
816
- if (this.target != 'dialog' && this.url) return this.url;
817
- return null;
818
- },
819
- },
820
- methods: {
821
- onclick: function (e) {
822
- if (this.target == 'dialog') {
823
- tableActionEvent(this);
824
- openWindow({
825
- id: this.id,
826
- title: this.title,
827
- url: this.url,
828
- onclose: this.onclose,
829
- });
830
- }
831
- }
832
- }
833
- });
834
- //-----------------------------------------------------------------------------------------------------------
835
- Vue.component('t-button', {
836
- props: {
837
- text: {
838
- default: true
839
- },
840
- color: {
841
- default: "primary"
842
- },
843
- label: {
844
- default: null
845
- },
846
- iconText: {
847
- default: null
848
- },
849
- target: {
850
- default: null
851
- },
852
- wid: {
853
- default: null
854
- },
855
- url: {
856
- default: null
857
- },
858
- title: {
859
- default: null
860
- },
861
- onclose: {
862
- default: null
863
- },
864
- quset: {
865
- default: []
866
- },
867
- },
868
- template: `
869
- <v-btn class="custom-transform-class text-none" v-bind="$attrs" :text="text" :color="color" :target="target" :href="_url" @click="clickButton()">
870
- <v-icon v-if="iconText" :left="label" class="ml-0">{{iconText}}</v-icon>{{label}}
871
- </v-btn>`,
872
- computed: {
873
- _url: function () {
874
- if (this.target != 'dialog' && this.url) return this.url;
875
- return null;
876
- },
877
- },
878
- methods: {
879
- clickButton: function (e) {
880
- if (this.target == 'dialog') {
881
- console.log("dialog onclose", this.onclose)
882
- openWindow({
883
- id: this.wid,
884
- title: this.title,
885
- url: this.url,
886
- onclose: this.onclose
887
- });
888
- } else tableActionEvent(this);
889
- }
890
- }
891
- });
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'],
3
+ data: function () {
4
+ var headersAttr = bindData(this.headers);
5
+ var strTableTemplate = this.template || '';
6
+
7
+ if (headersAttr) {
8
+ _.forEach(headersAttr.filter(i => !!i.el), function (c) {
9
+ var ctl = buildControl([c], false);
10
+
11
+ ctl.attr("v-bind", "item.attr");
12
+ ctl.attr(":item", "item");
13
+ ctl.attr(":index", "items.indexOf(item)");
14
+ ctl.attr("v-on:mouseover", "$emit('mouseover:row',item)");
15
+ ctl.attr("v-on:mouseleave", "$emit('mouseleave:row',item)");
16
+
17
+ if (!c.attr['v-model']) ctl.attr("v-model", `item['${c.value}']`);
18
+
19
+ strTableTemplate += `<template v-slot:item.${c.value}="{item}">${ctl[0].outerHTML}</template>`
20
+ });
21
+ }
22
+
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 || excel || ((newAPI() || newAction()) && !disabledUpdate)" align="end" class="py-2 px-3">
27
+ <div class="primary--text text-left" v-html="label" style="font-weight: 500; font-size: 1.1rem;"></div>
28
+ <v-spacer></v-spacer><component :is="headercomponent"></component>
29
+ <v-btn v-if="excel" icon color="primary" @click="toExcel"><v-icon>mdi-microsoft-excel</v-icon></v-btn>
30
+ <div v-if="showSearch" class="d-flex align-center ml-3">
31
+ <v-btn icon color="primary" @mousedown.prevent @click="toggleSearch"><v-icon>mdi-magnify</v-icon></v-btn>
32
+ <v-expand-x-transition>
33
+ <v-text-field v-if="searchOpen" ref="searchField" dense @input="updateSearch" label="" single-line hide-details autofocus style="width: 300px;" @blur="onSearchBlur" class="ml-1"></v-text-field>
34
+ </v-expand-x-transition>
35
+ </div>
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
+ </v-card-title>
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()" v-on:update:options="onTableOptions" :search="search" :custom-filter="customSearch">
40
+ <template v-for="(h, index) in headerCols" :key="index" v-slot:['header.'+h.value]="{ header }"><span v-html="header.text"></span></template>
41
+ ${strTableTemplate}
42
+ <template v-if="!disabledUpdate" v-slot:item.ctrl-update="{item}">
43
+ <div class="d-flex justify-center" v-if="item['ctrl-update'] != 0">
44
+ <v-btn v-if="deleteAPI()" @click="deleteItem(item, deleteAPI())" x-small fab icon><v-icon color="red darken-3">mdi-trash-can-outline</v-icon></v-btn>
45
+ <v-btn v-if="editAPI() || editAction()" @click="editItem(item)" x-small fab icon><v-icon color="green darken-2">mdi-square-edit-outline</v-icon></v-btn>
46
+ </div>
47
+ </template>
48
+ <template v-if="sumFormat && typeof sumFormat === 'object'" v-slot:body.append="{ headers }">
49
+ <tr>
50
+ <td v-for="h in headers" :key="'sum-'+h.value" class="text-right font-weight-medium">
51
+ <span v-if="sumFormat[h.value]">{{ sumByColumn(h.value) }}</span>
52
+ </td>
53
+ </tr>
54
+ </template>
55
+ </v-data-table>
56
+
57
+ <v-divider v-if="sumFormat && typeof sumFormat === 'string'"></v-divider>
58
+ <v-card-actions v-if="sumFormat && typeof sumFormat === 'string'">
59
+ <v-spacer></v-spacer><span class="title font-weight-light">{{sumText}}</span>
60
+ </v-card-actions>
61
+ <f-dialog v-model="editDialog" v-bind="updateFormAttr" :data.sync="editData" :data-out.sync="vueData[dataOut]" :title="formTitle" :button="updateButton" :controls="updateForm"></f-dialog>
62
+ </v-card>
63
+ `;
64
+ //----------------------------------------------------------------------
65
+
66
+ return {
67
+ editDialog: false,
68
+ search: '',
69
+ searchOpen: false,
70
+ itemselected: [],
71
+ returnValue: [],
72
+ editData: {},
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 || []),
78
+ updateButton: [
79
+ {
80
+ "v-if": "title=='Cập nhật'",
81
+ label: "Lưu",
82
+ getoutdata: true,
83
+ action: () => {
84
+ this.save(this.editAPI());
85
+ }
86
+ },
87
+ {
88
+ "v-if": "title=='Thêm mới'",
89
+ label: "Thêm mới",
90
+ getoutdata: true,
91
+ action: () => {
92
+ this.addnew(this.newAPI())
93
+ }
94
+ }
95
+ ],
96
+ newAction: function () {
97
+ if (this.updateApi) return this.updateApi['new-action']
98
+ else return null;
99
+ },
100
+ newAPI: function () {
101
+ if (this.updateApi) return this.updateApi.new;
102
+ else return null;
103
+ },
104
+ editAction: function () {
105
+ if (this.updateApi) return this.updateApi['edit-action']
106
+ else return null;
107
+ },
108
+ editAPI: function () {
109
+ if (this.updateApi) return this.updateApi.edit;
110
+ else return null;
111
+ },
112
+ deleteAPI: function () {
113
+ if (this.updateApi) return this.updateApi.delete;
114
+ else return null;
115
+ },
116
+ defaultItem: function () {
117
+ if (this.updateApi) {
118
+ if (typeof this.updateApi['default-item'] === 'undefined') return {};
119
+ var dataItem = {}
120
+ mapData(this.updateApi['default-item'], dataItem, vueData);
121
+ return dataItem;
122
+ }
123
+ else return {};
124
+ },
125
+ }
126
+ },
127
+ mounted: function () {
128
+ this.editData = Object.assign({}, this.defaultItem())
129
+ this.editedIndex = -1
130
+ this.itemSelectRebuild(this.value);
131
+
132
+ if (this.heightNum !== null && this.heightNum <= 0) {
133
+ this.updateFillHeight();
134
+ this.resizeHandler = this.updateFillHeight.bind(this);
135
+ window.addEventListener('resize', this.resizeHandler);
136
+ }
137
+ },
138
+ beforeDestroy: function () { this.removeResize(); },
139
+ beforeUnmount: function () { this.removeResize(); },
140
+ watch: {
141
+ editDialog(val) {
142
+ if (!val) {
143
+ _.forEach(this.editData, (v, k) => {
144
+ this.editData[k] = undefined;
145
+ });
146
+ this.editedIndex = -1
147
+ } else {
148
+ this.editData.dialogModel = (this.edit ? 'edit' : 'new')
149
+ }
150
+ },
151
+ value(newVal, oldVal,) {
152
+ t = this;
153
+ if (t.returnValue != newVal) this.itemSelectRebuild(newVal);
154
+ },
155
+ items(newVal) {
156
+ if (!this.headers) this.autoHeaders = this.buildAutoHeaders(newVal || []);
157
+ if (this.resizeHandler) this.updateFillHeight();
158
+ }
159
+ },
160
+ computed: {
161
+ // null = không khai height → bảng tự giãn theo số dòng (mặc định của v-data-table)
162
+ heightNum() {
163
+ if (this.height === undefined || this.height === null || this.height === '') return null;
164
+ var n = Number(this.height);
165
+ return isNaN(n) ? null : n;
166
+ },
167
+ tableHeight() {
168
+ if (this.heightNum === null) return undefined;
169
+ if (this.heightNum > 0) return this.heightNum;
170
+ return this.fillHeight || undefined;
171
+ },
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() {
205
+ var h = this.headers || this.autoHeaders;
206
+ if (_.isBoolean(this.disabledUpdate) && this.disabledUpdate) {
207
+ _.remove(h, function (n) {
208
+ return n.value == 'ctrl-update';
209
+ });
210
+ };
211
+
212
+ return h;
213
+ },
214
+ _hideDefaultFooter() {
215
+ if (this.items) {
216
+ if (this.itemsPerPage > 0 && this.itemsPerPage < this.items.length) return false;
217
+ }
218
+ if (this.hideDefaultFooter) return this.hideDefaultFooter;
219
+ return false;
220
+ },
221
+ edit() {
222
+ return this.editedIndex >= 0;
223
+ },
224
+ formTitle() {
225
+ return this.edit ? 'Cập nhật' : 'Thêm mới';
226
+ },
227
+ dataOut() {
228
+ if (this.updateApi) return this.updateApi['data-out']
229
+ else return null;
230
+ },
231
+ sumText() {
232
+ var items = this.items;
233
+ return _.replace(this.sumFormat, /\[\[(.*?)\]\]/gm, function (s, key) {
234
+ return numeral(_.sumBy(items, key)).format('0,0');
235
+ });
236
+ }
237
+ },
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() {
245
+ if (this.resizeHandler) {
246
+ window.removeEventListener('resize', this.resizeHandler);
247
+ this.resizeHandler = null;
248
+ }
249
+ },
250
+ updateFillHeight() {
251
+ var t = this;
252
+ t.$nextTick(function () {
253
+ requestAnimationFrame(function () {
254
+ var root = t.$refs.myTable && t.$refs.myTable.$el;
255
+ if (!root) return;
256
+ var wrapper = root.querySelector('.v-data-table__wrapper');
257
+ if (!wrapper) return;
258
+ var rootRect = root.getBoundingClientRect();
259
+ var wrapRect = wrapper.getBoundingClientRect();
260
+ // phần nằm dưới vùng cuộn (footer phân trang, dòng tổng, viền card)
261
+ var below = Math.max(0, rootRect.bottom - wrapRect.bottom);
262
+ var margin = Math.abs(t.heightNum);
263
+ t.fillHeight = Math.max(150, window.innerHeight - wrapRect.top - below - margin);
264
+ });
265
+ });
266
+ },
267
+ buildAutoHeaders(items) {
268
+ var arrHeaders = buildHeader(items);
269
+ var hasEditOrDelete = this.updateApi && (this.updateApi.edit || this.updateApi.delete);
270
+ if (arrHeaders.length > 0 && hasEditOrDelete && !this.disabledUpdate) {
271
+ arrHeaders.push({ "text": "", "value": "ctrl-update", "divider": true });
272
+ }
273
+ return arrHeaders;
274
+ },
275
+ sumByColumn(key) {
276
+ return numeral(_.sumBy(this.items, key)).format(this.sumFormat[key]);
277
+ },
278
+ toggleSearch() {
279
+ this.searchOpen = !this.searchOpen;
280
+ if (this.searchOpen) {
281
+ var t = this;
282
+ this.$nextTick(function () {
283
+ t.$refs.searchField && t.$refs.searchField.focus();
284
+ });
285
+ }
286
+ },
287
+ onSearchBlur() {
288
+ if (!this.search) this.searchOpen = false;
289
+ },
290
+ itemSelect() {
291
+ var t = this;
292
+
293
+ setTimeout(function () {
294
+ if (!t.returnObject) t.returnValue = _.map(t.itemselected, t.itemKey);
295
+ else t.returnValue = t.itemselected;
296
+
297
+ t.$emit('input', t.returnValue);
298
+ }, 10);
299
+ },
300
+
301
+ itemSelectRebuild(newVal) {
302
+ var t = this;
303
+ if (t.returnObject) t.itemselected = newVal;
304
+ else {
305
+ t.itemselected = [];
306
+
307
+ _.forEach(newVal, function (val) {
308
+ t.itemselected.push(_.find(t.items, [t.itemKey, val]));
309
+ });
310
+ }
311
+ },
312
+ newBtnClick() {
313
+ if (this.newAction()) runAction(this.newAction());
314
+ if (this.newAPI()) {
315
+ Object.assign(this.editData, this.defaultItem());
316
+ this.editDialog = true;
317
+ }
318
+ },
319
+ editItem(item) {
320
+
321
+ if (this.editAction()) runAction(this.editAction(), { item: item });
322
+
323
+ if (this.editAPI()) {
324
+ this.editedIndex = this.items.indexOf(item);
325
+ Object.assign(this.editData, this.defaultItem());
326
+ this.editData = Object.assign({}, item);
327
+ this.editDialog = true;
328
+ }
329
+ },
330
+
331
+ deleteItem(item, deleteAPI) {
332
+ const items = this.items;
333
+ var actionAPI = deleteAPI;
334
+ var indexDelete = items.indexOf(item)
335
+ confirm({
336
+ title: (actionAPI.CONFIRM ? bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: item, item: item }, vueData)) : "Bạn có chắc chắn muốn xoá ?"),
337
+ action: function () {
338
+ vueAction(actionAPI, { $row: item, item: item }, function () {
339
+ items.splice(indexDelete, 1);
340
+ });
341
+ }
342
+ });
343
+ },
344
+
345
+ save(actionAPI) {
346
+ var diaCtl = this;
347
+ if (diaCtl.editedIndex > -1) {
348
+ if (actionAPI) {
349
+ if (actionAPI.CONFIRM) {
350
+ confirm({
351
+ title: bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: diaCtl.editData, item: diaCtl.editData }, vueData)),
352
+ action: function () {
353
+ vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
354
+ if (!d.Message) {
355
+ if (_.isArray(d.data)) {
356
+ if (d.data[0]) {
357
+ _.forEach(d.data[0], function (value, key) {
358
+ diaCtl.editData[key] = value;
359
+ });
360
+ }
361
+ }
362
+ }
363
+ _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
364
+ diaCtl.editDialog = false;
365
+ });
366
+ },
367
+ cancel: function () { }
368
+ });
369
+ } else {
370
+ vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
371
+ if (!d.Message) {
372
+ if (_.isArray(d.data)) {
373
+ if (d.data[0]) {
374
+ _.forEach(d.data[0], function (value, key) {
375
+ diaCtl.editData[key] = value;
376
+ });
377
+ }
378
+ }
379
+ }
380
+ _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
381
+ diaCtl.editDialog = false;
382
+ });
383
+ }
384
+ } else {
385
+ _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
386
+ diaCtl.editDialog = false;
387
+ }
388
+ }
389
+ },
390
+
391
+ addnew(actionAPI) {
392
+ var diaCtl = this;
393
+ if (diaCtl.editedIndex == -1) {
394
+ if (actionAPI) {
395
+ if (actionAPI.CONFIRM) {
396
+ confirm({
397
+ title: bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: diaCtl.editData, item: diaCtl.editData }, vueData)),
398
+ action: function () {
399
+ vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
400
+ if (d) {
401
+ if (_.isArray(d.data)) {
402
+ if (d.data[0]) {
403
+ _.forEach(d.data[0], function (value, key) {
404
+ diaCtl.editData[key] = value;
405
+ });
406
+ }
407
+ }
408
+ }
409
+ diaCtl.items.push(_.cloneDeep(diaCtl.editData));
410
+ diaCtl.editDialog = false;
411
+ });
412
+ },
413
+ cancel: function () { }
414
+ });
415
+ } else {
416
+ vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
417
+ if (d) {
418
+ if (_.isArray(d.data)) {
419
+ if (d.data[0]) {
420
+ _.forEach(d.data[0], function (value, key) {
421
+ diaCtl.editData[key] = value;
422
+ });
423
+ }
424
+ }
425
+ }
426
+ diaCtl.items.push(_.cloneDeep(diaCtl.editData));
427
+ diaCtl.editDialog = false;
428
+ });
429
+ }
430
+ } else {
431
+ diaCtl.items.push(_.cloneDeep(diaCtl.editData));
432
+ diaCtl.editDialog = false;
433
+ }
434
+ }
435
+ },
436
+ updateSearch: _.debounce(function (text) {
437
+ this.search = text;
438
+ }, 300),
439
+ customSearch(value, search, item) {
440
+ return Object.values(item).some(v => v && v.toString().toLowerCase().includes(search.toLowerCase()))
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).
444
+ toExcel() {
445
+ if (typeof this.excel === 'string' && this.excel.trim().toUpperCase() === 'UI') return this.toExcelUI();
446
+ jsonToExcel({ data: this.items, filename: (this.label && this.label != '' ? this.label : 'tableFP') + '.xlsx' });
447
+ Vue.$toast.success("Đã xuất Excel", { position: 'top' });
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
+ },
567
+ }
568
+ });
569
+ //===========================================================================================
570
+ //===========================================================================================
571
+ //===========================================================================================
572
+
573
+ Vue.component('t-html', {
574
+ template: `<div class="my-3" v-bind="$attrs" v-html="$attrs['value']"></div>`,
575
+ });
576
+ //-----------------------------------------------------------------------------------------------------------
577
+ Vue.component('t-label', {
578
+ template: `<div class="text-no-wrap" v-bind="$attrs">{{$attrs['value']}}</div>`,
579
+ });
580
+ //-----------------------------------------------------------------------------------------------------------
581
+ Vue.component('t-num', {
582
+ props: {
583
+ value: {
584
+ type: Number,
585
+ default: 0
586
+ },
587
+ hidezero: {
588
+ type: Boolean,
589
+ default: true
590
+ },
591
+ format: {
592
+ type: String,
593
+ default: ''
594
+ }
595
+ },
596
+ template: `<div v-if="!_.isNull(value) && !(hidezero && value==0)" class="text-no-wrap text-end" v-bind="$attrs">{{ format != ''? numeral(value).format(format) : Number.isInteger(value)?numeral(value).format('0,0'): numeral(value).format('0,0.0') }}</div>`,
597
+ });
598
+ //-----------------------------------------------------------------------------------------------------------
599
+ Vue.component('t-time', {
600
+ props: {
601
+ format: {
602
+ type: String,
603
+ default: 'DD/MM/YYYY'
604
+ }
605
+ },
606
+ template: `<div v-if="$attrs['value']" class="text-no-wrap" v-bind="$attrs">{{ moment($attrs['value']).format(format) }}</div>`,
607
+ });
608
+ //-----------------------------------------------------------------------------------------------------------
609
+ Vue.component('t-boolean', {
610
+ props: {
611
+ value: {
612
+ default: null
613
+ },
614
+ colorTrue: {
615
+ default: "primary"
616
+ },
617
+ colorFalse: {
618
+ default: "red"
619
+ },
620
+ iconTrue: {
621
+ default: "mdi-check"
622
+ },
623
+ iconFalse: {
624
+ default: "mdi-close"
625
+ },
626
+ },
627
+ template: `<v-icon v-bind="$attrs" :color="value?colorTrue:colorFalse">{{value?iconTrue:iconFalse}}</v-icon>`,
628
+ });
629
+ //-----------------------------------------------------------------------------------------------------------
630
+ Vue.component('t-check', {
631
+ props: {
632
+ value: {
633
+ default: null
634
+ },
635
+ falseValue: {
636
+ default: false
637
+ }
638
+ },
639
+ template: `<v-checkbox v-model="ctlValue" v-bind="$attrs" :false-value="falseValue" hide-details class="d-inline-flex ma-1 pa-1" v-on:change="onchange($event)" />`,
640
+ data: function () {
641
+ return {
642
+ ctlValue: this.value
643
+ }
644
+ },
645
+ watch: {
646
+ value(newVal) {
647
+ this.ctlValue = newVal
648
+ }
649
+ },
650
+ methods: {
651
+ onchange: function (e) {
652
+ if (!e) this.ctlValue = false;
653
+ tableActionEvent(this);
654
+ }
655
+ }
656
+ });
657
+ //-----------------------------------------------------------------------------------------------------------
658
+ Vue.component('t-text', {
659
+ props: ['value'],
660
+ template: `<v-text-field v-model="ctlValue" v-bind="$attrs" v-on:change="onchange($event)" dense outlined hide-details/>`,
661
+ data: function () {
662
+ return {
663
+ ctlValue: this.value
664
+ }
665
+ },
666
+ watch: {
667
+ value(newVal) {
668
+ this.ctlValue = newVal
669
+ }
670
+ },
671
+ methods: {
672
+ onchange: function (e) {
673
+ console.log(e)
674
+ tableActionEvent(this);
675
+ }
676
+ }
677
+ });
678
+
679
+ //-----------------------------------------------------------------------------------------------------------
680
+ Vue.component('t-select', {
681
+ props: ['value'],
682
+ template: `<v-select v-model="ctlValue" v-bind="$attrs" v-on:change="onchange($event)" dense hide-details append-icon="mdi-menu-swap"/>`,
683
+ data: function () {
684
+ return {
685
+ ctlValue: this.value
686
+ }
687
+ },
688
+ watch: {
689
+ value(newVal) {
690
+ this.ctlValue = newVal
691
+ }
692
+ },
693
+ methods: {
694
+ onchange: function (e) {
695
+ console.log(e)
696
+ tableActionEvent(this);
697
+ }
698
+ }
699
+ });
700
+ //-----------------------------------------------------------------------------------------------------------
701
+ Vue.component('t-combobox', {
702
+ props: {
703
+ value: {
704
+ default: undefined
705
+ },
706
+ text: {
707
+ default: undefined
708
+ },
709
+ items: {
710
+ type: Array,
711
+ default: []
712
+ },
713
+ api: {
714
+ type: String,
715
+ default: undefined
716
+ },
717
+ apiData: {
718
+ type: Object,
719
+ default: {}
720
+ },
721
+
722
+ },
723
+ template: `<v-autocomplete refs="abc" v-model="ctlValue" v-bind="$attrs" :loading="loading" :items="dataItems" :search-input.sync="search" v-on:change="onchange($event)" hide-details dense></v-autocomplete>`,
724
+ data: function () {
725
+ return {
726
+ dataItems: this.items,
727
+ ctlValue: this.value,
728
+ loading: false,
729
+ search: null,
730
+ getDataFormAPI: _.debounce((str) => {
731
+ if ((this.search && this.search.length < 2) || !this.api) return;
732
+ var self = this;
733
+ this.loading = true
734
+
735
+ if (typeof this.apiData != 'object') this.apiData = JSON.parse(this.apiData);
736
+
737
+ mapData(this.apiData, this.apiData, _.assignIn({}, vueData, { TEXT: this.search }));
738
+
739
+ console.log("API-CALL: ", this.api)
740
+ ajaxCALL(this.api, this.apiData, function (d) {
741
+ if (_.isArray(d.data)) self.dataItems = d.data;
742
+ else self.dataItems = d;
743
+
744
+ self.loading = false
745
+ }, function (e) {
746
+ console.log("API Error", e);
747
+ })
748
+
749
+ }, 500)
750
+ }
751
+ },
752
+ watch: {
753
+ search(newval, oldval) {
754
+ newval && oldval && this.querySelections(newval)
755
+ },
756
+ items(val) {
757
+ this.dataItems = val
758
+ },
759
+ value(newVal) {
760
+ this.ctlValue = newVal
761
+ }
762
+ },
763
+ methods: {
764
+ querySelections(val) {
765
+ this.getDataFormAPI(val);
766
+ },
767
+ onchange: function (e) {
768
+ if (e) tableActionEvent(this);
769
+ }
770
+ },
771
+ });
772
+ //-----------------------------------------------------------------------------------------------------------
773
+ Vue.component('t-menu', {
774
+ props: {
775
+ iconText: {
776
+ default: 'mdi-menu'
777
+ },
778
+ items: {
779
+ default: []
780
+ }
781
+ },
782
+ template: `
783
+ <v-menu offset-y origin="center center" transition="slide-y-transition" max-height = 500>
784
+ <template v-slot:activator="{ on }">
785
+ <v-icon v-on="on">{{iconText}}</v-icon>
786
+ </template>
787
+ <v-list >
788
+ <v-list-item v-bind="$attrs" v-for="(item, index) in items" :key="index" @click="onSelect(item)" >
789
+ <v-list-item-icon v-if="item['icon-text']" class="mr-3">
790
+ <v-icon :color="item.color">{{item['icon-text']}}</v-icon>
791
+ </v-list-item-icon>
792
+ <v-list-item-content class="mr-2">
793
+ <v-list-item-title v-text="item.text"></v-list-item-title>
794
+ </v-list-item-content>
795
+ </v-list-item>
796
+ </v-list>
797
+ </v-menu>
798
+ `,
799
+ data: function () {
800
+ return {
801
+ }
802
+ },
803
+ methods: {
804
+ onSelect: function (e) {
805
+ this.$attrs['action'] = e['action'];
806
+ tableActionEvent(this);
807
+ }
808
+ }
809
+ });
810
+ //-----------------------------------------------------------------------------------------------------------
811
+ Vue.component('t-link', {
812
+ props: ['id', 'target', 'url', 'title', 'onclose'],
813
+ template: `<a :href="_url" :target="target" v-bind="$attrs" @click="onclick()"><v-icon v-if="$attrs['icon-text']" left color="primary">{{$attrs['icon-text']}}</v-icon> {{ $attrs['value']}}</a>`,
814
+ computed: {
815
+ _url: function () {
816
+ if (this.target != 'dialog' && this.url) return this.url;
817
+ return null;
818
+ },
819
+ },
820
+ methods: {
821
+ onclick: function (e) {
822
+ if (this.target == 'dialog') {
823
+ tableActionEvent(this);
824
+ openWindow({
825
+ id: this.id,
826
+ title: this.title,
827
+ url: this.url,
828
+ onclose: this.onclose,
829
+ });
830
+ }
831
+ }
832
+ }
833
+ });
834
+ //-----------------------------------------------------------------------------------------------------------
835
+ Vue.component('t-button', {
836
+ props: {
837
+ text: {
838
+ default: true
839
+ },
840
+ color: {
841
+ default: "primary"
842
+ },
843
+ label: {
844
+ default: null
845
+ },
846
+ iconText: {
847
+ default: null
848
+ },
849
+ target: {
850
+ default: null
851
+ },
852
+ wid: {
853
+ default: null
854
+ },
855
+ url: {
856
+ default: null
857
+ },
858
+ title: {
859
+ default: null
860
+ },
861
+ onclose: {
862
+ default: null
863
+ },
864
+ quset: {
865
+ default: []
866
+ },
867
+ },
868
+ template: `
869
+ <v-btn class="custom-transform-class text-none" v-bind="$attrs" :text="text" :color="color" :target="target" :href="_url" @click="clickButton()">
870
+ <v-icon v-if="iconText" :left="label" class="ml-0">{{iconText}}</v-icon>{{label}}
871
+ </v-btn>`,
872
+ computed: {
873
+ _url: function () {
874
+ if (this.target != 'dialog' && this.url) return this.url;
875
+ return null;
876
+ },
877
+ },
878
+ methods: {
879
+ clickButton: function (e) {
880
+ if (this.target == 'dialog') {
881
+ console.log("dialog onclose", this.onclose)
882
+ openWindow({
883
+ id: this.wid,
884
+ title: this.title,
885
+ url: this.url,
886
+ onclose: this.onclose
887
+ });
888
+ } else tableActionEvent(this);
889
+ }
890
+ }
891
+ });