@fui-org/fui-cli 2.1.3 → 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 (219) 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/skills/fui-target/hooks/register.tsx +2 -0
  61. package/skills/fui-target/types/index.d.ts +1 -0
  62. package/dist/fui-hk5frcn2.js +0 -381
  63. package/skills/fui-skill/SKILL.md +0 -139
  64. package/skills/fui-skill/references/tools-registry.md +0 -84
  65. /package/skills/{fui-skill → fui}/assets/projectdefaultstyle-3.0.css +0 -0
  66. /package/skills/{fui-skill → fui}/design-md/airbnb/DESIGN.md +0 -0
  67. /package/skills/{fui-skill → fui}/design-md/airbnb/README.md +0 -0
  68. /package/skills/{fui-skill → fui}/design-md/airtable/DESIGN.md +0 -0
  69. /package/skills/{fui-skill → fui}/design-md/airtable/README.md +0 -0
  70. /package/skills/{fui-skill → fui}/design-md/apple/DESIGN.md +0 -0
  71. /package/skills/{fui-skill → fui}/design-md/apple/README.md +0 -0
  72. /package/skills/{fui-skill → fui}/design-md/asu/DESIGN.md +0 -0
  73. /package/skills/{fui-skill → fui}/design-md/asu/README.md +0 -0
  74. /package/skills/{fui-skill → fui}/design-md/binance/DESIGN.md +0 -0
  75. /package/skills/{fui-skill → fui}/design-md/binance/README.md +0 -0
  76. /package/skills/{fui-skill → fui}/design-md/bmw/DESIGN.md +0 -0
  77. /package/skills/{fui-skill → fui}/design-md/bmw/README.md +0 -0
  78. /package/skills/{fui-skill → fui}/design-md/bmw-m/DESIGN.md +0 -0
  79. /package/skills/{fui-skill → fui}/design-md/bmw-m/README.md +0 -0
  80. /package/skills/{fui-skill → fui}/design-md/bugatti/DESIGN.md +0 -0
  81. /package/skills/{fui-skill → fui}/design-md/bugatti/README.md +0 -0
  82. /package/skills/{fui-skill → fui}/design-md/cal/DESIGN.md +0 -0
  83. /package/skills/{fui-skill → fui}/design-md/cal/README.md +0 -0
  84. /package/skills/{fui-skill → fui}/design-md/claude/DESIGN.md +0 -0
  85. /package/skills/{fui-skill → fui}/design-md/claude/README.md +0 -0
  86. /package/skills/{fui-skill → fui}/design-md/clay/DESIGN.md +0 -0
  87. /package/skills/{fui-skill → fui}/design-md/clay/README.md +0 -0
  88. /package/skills/{fui-skill → fui}/design-md/clickhouse/DESIGN.md +0 -0
  89. /package/skills/{fui-skill → fui}/design-md/clickhouse/README.md +0 -0
  90. /package/skills/{fui-skill → fui}/design-md/cohere/DESIGN.md +0 -0
  91. /package/skills/{fui-skill → fui}/design-md/cohere/README.md +0 -0
  92. /package/skills/{fui-skill → fui}/design-md/coinbase/DESIGN.md +0 -0
  93. /package/skills/{fui-skill → fui}/design-md/coinbase/README.md +0 -0
  94. /package/skills/{fui-skill → fui}/design-md/composio/DESIGN.md +0 -0
  95. /package/skills/{fui-skill → fui}/design-md/composio/README.md +0 -0
  96. /package/skills/{fui-skill → fui}/design-md/cursor/DESIGN.md +0 -0
  97. /package/skills/{fui-skill → fui}/design-md/cursor/README.md +0 -0
  98. /package/skills/{fui-skill → fui}/design-md/elevenlabs/DESIGN.md +0 -0
  99. /package/skills/{fui-skill → fui}/design-md/elevenlabs/README.md +0 -0
  100. /package/skills/{fui-skill → fui}/design-md/expo/DESIGN.md +0 -0
  101. /package/skills/{fui-skill → fui}/design-md/expo/README.md +0 -0
  102. /package/skills/{fui-skill → fui}/design-md/ferrari/DESIGN.md +0 -0
  103. /package/skills/{fui-skill → fui}/design-md/ferrari/README.md +0 -0
  104. /package/skills/{fui-skill → fui}/design-md/figma/DESIGN.md +0 -0
  105. /package/skills/{fui-skill → fui}/design-md/figma/README.md +0 -0
  106. /package/skills/{fui-skill → fui}/design-md/framer/DESIGN.md +0 -0
  107. /package/skills/{fui-skill → fui}/design-md/framer/README.md +0 -0
  108. /package/skills/{fui-skill → fui}/design-md/fui/DESIGN.md +0 -0
  109. /package/skills/{fui-skill → fui}/design-md/hashicorp/DESIGN.md +0 -0
  110. /package/skills/{fui-skill → fui}/design-md/hashicorp/README.md +0 -0
  111. /package/skills/{fui-skill → fui}/design-md/ibm/DESIGN.md +0 -0
  112. /package/skills/{fui-skill → fui}/design-md/ibm/README.md +0 -0
  113. /package/skills/{fui-skill → fui}/design-md/intercom/DESIGN.md +0 -0
  114. /package/skills/{fui-skill → fui}/design-md/intercom/README.md +0 -0
  115. /package/skills/{fui-skill → fui}/design-md/kraken/DESIGN.md +0 -0
  116. /package/skills/{fui-skill → fui}/design-md/kraken/README.md +0 -0
  117. /package/skills/{fui-skill → fui}/design-md/lamborghini/DESIGN.md +0 -0
  118. /package/skills/{fui-skill → fui}/design-md/lamborghini/README.md +0 -0
  119. /package/skills/{fui-skill → fui}/design-md/linear.app/DESIGN.md +0 -0
  120. /package/skills/{fui-skill → fui}/design-md/linear.app/README.md +0 -0
  121. /package/skills/{fui-skill → fui}/design-md/lovable/DESIGN.md +0 -0
  122. /package/skills/{fui-skill → fui}/design-md/lovable/README.md +0 -0
  123. /package/skills/{fui-skill → fui}/design-md/mastercard/DESIGN.md +0 -0
  124. /package/skills/{fui-skill → fui}/design-md/mastercard/README.md +0 -0
  125. /package/skills/{fui-skill → fui}/design-md/meta/DESIGN.md +0 -0
  126. /package/skills/{fui-skill → fui}/design-md/meta/README.md +0 -0
  127. /package/skills/{fui-skill → fui}/design-md/minimax/DESIGN.md +0 -0
  128. /package/skills/{fui-skill → fui}/design-md/minimax/README.md +0 -0
  129. /package/skills/{fui-skill → fui}/design-md/mintlify/DESIGN.md +0 -0
  130. /package/skills/{fui-skill → fui}/design-md/mintlify/README.md +0 -0
  131. /package/skills/{fui-skill → fui}/design-md/miro/DESIGN.md +0 -0
  132. /package/skills/{fui-skill → fui}/design-md/miro/README.md +0 -0
  133. /package/skills/{fui-skill → fui}/design-md/mistral.ai/DESIGN.md +0 -0
  134. /package/skills/{fui-skill → fui}/design-md/mistral.ai/README.md +0 -0
  135. /package/skills/{fui-skill → fui}/design-md/mongodb/DESIGN.md +0 -0
  136. /package/skills/{fui-skill → fui}/design-md/mongodb/README.md +0 -0
  137. /package/skills/{fui-skill → fui}/design-md/nike/DESIGN.md +0 -0
  138. /package/skills/{fui-skill → fui}/design-md/nike/README.md +0 -0
  139. /package/skills/{fui-skill → fui}/design-md/notion/DESIGN.md +0 -0
  140. /package/skills/{fui-skill → fui}/design-md/notion/README.md +0 -0
  141. /package/skills/{fui-skill → fui}/design-md/nvidia/DESIGN.md +0 -0
  142. /package/skills/{fui-skill → fui}/design-md/nvidia/README.md +0 -0
  143. /package/skills/{fui-skill → fui}/design-md/ollama/DESIGN.md +0 -0
  144. /package/skills/{fui-skill → fui}/design-md/ollama/README.md +0 -0
  145. /package/skills/{fui-skill → fui}/design-md/opencode.ai/DESIGN.md +0 -0
  146. /package/skills/{fui-skill → fui}/design-md/opencode.ai/README.md +0 -0
  147. /package/skills/{fui-skill → fui}/design-md/pinterest/DESIGN.md +0 -0
  148. /package/skills/{fui-skill → fui}/design-md/pinterest/README.md +0 -0
  149. /package/skills/{fui-skill → fui}/design-md/playstation/DESIGN.md +0 -0
  150. /package/skills/{fui-skill → fui}/design-md/playstation/README.md +0 -0
  151. /package/skills/{fui-skill → fui}/design-md/posthog/DESIGN.md +0 -0
  152. /package/skills/{fui-skill → fui}/design-md/posthog/README.md +0 -0
  153. /package/skills/{fui-skill → fui}/design-md/raycast/DESIGN.md +0 -0
  154. /package/skills/{fui-skill → fui}/design-md/raycast/README.md +0 -0
  155. /package/skills/{fui-skill → fui}/design-md/renault/DESIGN.md +0 -0
  156. /package/skills/{fui-skill → fui}/design-md/renault/README.md +0 -0
  157. /package/skills/{fui-skill → fui}/design-md/replicate/DESIGN.md +0 -0
  158. /package/skills/{fui-skill → fui}/design-md/replicate/README.md +0 -0
  159. /package/skills/{fui-skill → fui}/design-md/resend/DESIGN.md +0 -0
  160. /package/skills/{fui-skill → fui}/design-md/resend/README.md +0 -0
  161. /package/skills/{fui-skill → fui}/design-md/revolut/DESIGN.md +0 -0
  162. /package/skills/{fui-skill → fui}/design-md/revolut/README.md +0 -0
  163. /package/skills/{fui-skill → fui}/design-md/runwayml/DESIGN.md +0 -0
  164. /package/skills/{fui-skill → fui}/design-md/runwayml/README.md +0 -0
  165. /package/skills/{fui-skill → fui}/design-md/sanity/DESIGN.md +0 -0
  166. /package/skills/{fui-skill → fui}/design-md/sanity/README.md +0 -0
  167. /package/skills/{fui-skill → fui}/design-md/sentry/DESIGN.md +0 -0
  168. /package/skills/{fui-skill → fui}/design-md/sentry/README.md +0 -0
  169. /package/skills/{fui-skill → fui}/design-md/shopify/DESIGN.md +0 -0
  170. /package/skills/{fui-skill → fui}/design-md/shopify/README.md +0 -0
  171. /package/skills/{fui-skill → fui}/design-md/slack/DESIGN.md +0 -0
  172. /package/skills/{fui-skill → fui}/design-md/spacex/DESIGN.md +0 -0
  173. /package/skills/{fui-skill → fui}/design-md/spacex/README.md +0 -0
  174. /package/skills/{fui-skill → fui}/design-md/spotify/DESIGN.md +0 -0
  175. /package/skills/{fui-skill → fui}/design-md/spotify/README.md +0 -0
  176. /package/skills/{fui-skill → fui}/design-md/starbucks/DESIGN.md +0 -0
  177. /package/skills/{fui-skill → fui}/design-md/starbucks/README.md +0 -0
  178. /package/skills/{fui-skill → fui}/design-md/stripe/DESIGN.md +0 -0
  179. /package/skills/{fui-skill → fui}/design-md/stripe/README.md +0 -0
  180. /package/skills/{fui-skill → fui}/design-md/supabase/DESIGN.md +0 -0
  181. /package/skills/{fui-skill → fui}/design-md/supabase/README.md +0 -0
  182. /package/skills/{fui-skill → fui}/design-md/superhuman/DESIGN.md +0 -0
  183. /package/skills/{fui-skill → fui}/design-md/superhuman/README.md +0 -0
  184. /package/skills/{fui-skill → fui}/design-md/tesla/DESIGN.md +0 -0
  185. /package/skills/{fui-skill → fui}/design-md/tesla/README.md +0 -0
  186. /package/skills/{fui-skill → fui}/design-md/theverge/DESIGN.md +0 -0
  187. /package/skills/{fui-skill → fui}/design-md/theverge/README.md +0 -0
  188. /package/skills/{fui-skill → fui}/design-md/together.ai/DESIGN.md +0 -0
  189. /package/skills/{fui-skill → fui}/design-md/together.ai/README.md +0 -0
  190. /package/skills/{fui-skill → fui}/design-md/uber/DESIGN.md +0 -0
  191. /package/skills/{fui-skill → fui}/design-md/uber/README.md +0 -0
  192. /package/skills/{fui-skill → fui}/design-md/vercel/DESIGN.md +0 -0
  193. /package/skills/{fui-skill → fui}/design-md/vercel/README.md +0 -0
  194. /package/skills/{fui-skill → fui}/design-md/vodafone/DESIGN.md +0 -0
  195. /package/skills/{fui-skill → fui}/design-md/vodafone/README.md +0 -0
  196. /package/skills/{fui-skill → fui}/design-md/voltagent/DESIGN.md +0 -0
  197. /package/skills/{fui-skill → fui}/design-md/voltagent/README.md +0 -0
  198. /package/skills/{fui-skill → fui}/design-md/warp/DESIGN.md +0 -0
  199. /package/skills/{fui-skill → fui}/design-md/warp/README.md +0 -0
  200. /package/skills/{fui-skill → fui}/design-md/webflow/DESIGN.md +0 -0
  201. /package/skills/{fui-skill → fui}/design-md/webflow/README.md +0 -0
  202. /package/skills/{fui-skill → fui}/design-md/wired/DESIGN.md +0 -0
  203. /package/skills/{fui-skill → fui}/design-md/wired/README.md +0 -0
  204. /package/skills/{fui-skill → fui}/design-md/wise/DESIGN.md +0 -0
  205. /package/skills/{fui-skill → fui}/design-md/wise/README.md +0 -0
  206. /package/skills/{fui-skill → fui}/design-md/x.ai/DESIGN.md +0 -0
  207. /package/skills/{fui-skill → fui}/design-md/x.ai/README.md +0 -0
  208. /package/skills/{fui-skill → fui}/design-md/zapier/DESIGN.md +0 -0
  209. /package/skills/{fui-skill → fui}/design-md/zapier/README.md +0 -0
  210. /package/skills/{fui-skill → fui}/examples/f-table-patterns.json +0 -0
  211. /package/skills/{fui-skill → fui}/examples/module-patterns.json +0 -0
  212. /package/skills/{fui-skill → fui}/examples/project-patterns.json +0 -0
  213. /package/skills/{fui-skill → fui}/references/coding-standards.md +0 -0
  214. /package/skills/{fui-skill → fui}/references/db-table-design.md +0 -0
  215. /package/skills/{fui-skill → fui}/references/fsheet.md +0 -0
  216. /package/skills/{fui-skill → fui}/references/tapi-file-api.md +0 -0
  217. /package/skills/{fui-skill → fui}/references/ui-templates.md +0 -0
  218. /package/skills/{fui-skill → fui}/scripts/fechart.js +0 -0
  219. /package/skills/{fui-skill → fui}/scripts/fsheet.js +0 -0
@@ -1,1038 +1,1038 @@
1
- //===========================================================================================
2
- Vue.component("FTableItemComponent", {
3
- template: `<component :is="dynamicComponent"></component>`,
4
- props: {
5
- template: {
6
- required: true
7
- },
8
- data: {
9
- type: Object,
10
- default: () => ({}),
11
- },
12
- },
13
- data() {
14
- return {
15
- }
16
- },
17
- computed: {
18
- dynamicComponent() {
19
- return {
20
- template: this.buildTemplateString(this.template),
21
- data: () => { return this.data },
22
- }
23
- },
24
- },
25
- methods: {
26
- buildTemplateString(jObj) {
27
- if (Array.isArray(jObj)) {
28
- var templateString = '';
29
- jObj.forEach((o) => {
30
- templateString = templateString + this.buildTemplateString(o)
31
- });
32
- return templateString;
33
- }
34
-
35
- if (typeof jObj === 'object') {
36
- if (!jObj.attr) jObj.attr = {}
37
- jObj.attr[":item"] = "item"
38
- let d = $(`<${jObj.el}>`, this.fixAttrValueObject(jObj.attr)).append(this.buildTemplateString(jObj.innerHTML));
39
- return $('<div>').append(d).html();
40
- }
41
- if (typeof jObj === 'string') return jObj;
42
- return ""
43
- },
44
- fixAttrValueObject(jObj) {
45
- let returnObj = {}
46
- if (jObj) {
47
- for (const [key, value] of Object.entries(jObj)) {
48
- if (typeof value === 'object') returnObj[key] = JSON.stringify(value);
49
- else returnObj[key] = value;
50
- }
51
- }
52
- return returnObj
53
- }
54
- }
55
- });
56
- //===========================================================================================
57
- Vue.component("FTable", {
58
- props: {
59
- modelValue: {
60
- default: null
61
- },
62
- headers: {
63
- default: null
64
- },
65
- updateForm: {
66
- default: null
67
- },
68
- updateApi: {
69
- default: null
70
- },
71
- items: {
72
- default: null
73
- },
74
- label: {
75
- default: null
76
- },
77
- sumFormat: {
78
- default: null
79
- },
80
- returnObject: {
81
- default: null
82
- },
83
- itemKey: {
84
- default: null
85
- },
86
- disabledUpdate: {
87
- default: null
88
- },
89
- template: {
90
- default: null
91
- },
92
- excel: {
93
- default: true
94
- },
95
- hover: {
96
- default: true
97
- },
98
- showSearch: {
99
- default: true
100
- },
101
- returnObject: {
102
- default: true
103
- },
104
- fixedHeader: {
105
- default: true
106
- },
107
- fixedCols: {
108
- default: 0
109
- },
110
- // null/bỏ trống = bảng tự giãn theo số dòng; <=0 = fill tới đáy màn hình chừa
111
- // abs(n) px (tự đo vị trí thật, theo dõi resize); >0 = px cố định.
112
- height: {
113
- default: null
114
- },
115
- disableSort: {
116
- default: false
117
- },
118
- density: {
119
- default: 'compact'
120
- },
121
- hideDefaultFooter: {
122
- default: true
123
- },
124
- returnObject: {
125
- default: true
126
- },
127
- itemsPerPage: {
128
- default: 50
129
- },
130
- footerProps:
131
- {
132
- default: "{'items-per-page-options':[50, 100, 150, 200, -1]}"
133
- },
134
- updateFormAttr: {
135
- default: null
136
- },
137
- slots: {
138
- default: null
139
- },
140
- groupTemplate: {
141
- default: null
142
- },
143
- },
144
- template: `
145
- <v-card ref="myTable" variant="outlined" v-if="headersArray" class="border" style="text-align: initial;">
146
- <component is="style" v-if="fixedColStyles" v-html="fixedColStyles"></component>
147
- <v-card-actions v-if="showSearch || label || excel || ((newAPI() || newAction()) && !disabledUpdate)">
148
- <div v-if="showSearch || label || ((newAPI() || newAction()) && !disabledUpdate)" class="text-primary text-darken-2 text-left w-66 pl-2 text-h6" v-html="label"></div>
149
- <v-spacer></v-spacer>
150
- <v-btn v-if="excel" class="mr-4" small icon density="compact" color="primary" @click="toExcel"><v-icon>mdi-microsoft-excel</v-icon></v-btn>
151
- <div v-if="showSearch" class="d-flex align-center">
152
- <v-btn icon density="compact" color="primary" @click="toggleSearch"><v-icon>mdi-magnify</v-icon></v-btn>
153
- <v-expand-x-transition>
154
- <v-text-field v-if="searchOpen" ref="searchField" density="compact" @update:modelValue="updateSearch" label="" variant="underlined" hide-details autofocus style="width: 300px;" @blur="onSearchBlur" class="ml-1"></v-text-field>
155
- </v-expand-x-transition>
156
- </div>
157
- <v-btn v-if="(newAPI() || newAction()) && !disabledUpdate" class="ml-5 mr-2" density="compact" variant="outlined" icon="mdi-plus" color="primary" @click="newBtnClick"></v-btn>
158
- </v-card-actions>
159
- <v-data-table class="tTable" :class="fixedTableClass" v-bind="$attrs" :height="tableHeight" :fixed-header="tableFixedHeader" :hover="hover" :items="items" :item-key="itemKey" :return-object="returnObject" :headers="headersArray" :density="density" :items-per-page="itemsPerPage" :hide-default-footer="hideDefaultFooter_cacl" v-model="itemselected" calculate-widths v-on:click:row="$emit('click:row', $event)" v-on:item-selected="itemSelect()" v-on:toggle-select-all="itemSelect()" @update:options="onTableOptions" :search="search" :custom-filter="customSearch">
160
- <template v-slot:headers="{ headers, isSorted, getSortIcon, toggleSort, allSelected, selectAll, someSelected, groupHeaders }">
161
- <tr v-for="columns in headers">
162
- <template v-for="column in columns" :key="column.key">
163
- <th v-if="column.key=='data-table-select'" class="px-2" :colspan="column.colspan || 1" :rowspan="column.rowspan || 1">
164
- <v-checkbox-btn :indeterminate="someSelected && !allSelected" :model-value="allSelected" color="primary" @update:model-value="selectAll(!allSelected)" ></v-checkbox-btn>
165
- </th>
166
- <th v-else-if="column.key=='data-table-group'" v-bind="column.headerProps" :colspan="column.colspan || 1" :rowspan="column.rowspan || 1" style="width:40px" class="px-0 text-center">
167
- <VIcon icon="mdi-format-list-group" size="small"></VIcon>
168
- </th>
169
- <th v-else v-bind="column.headerProps" :colspan="column.colspan || 1" :rowspan="column.rowspan || 1">
170
- <div :class="['cursor-pointer', 'd-flex', 'justify-' + column.align]" @click="() => toggleSort(column)"><span v-html="column.title"></span><template v-if="isSorted(column)"><v-icon class="ml-1" :icon="getSortIcon(column)"></v-icon></template></div>
171
- </th>
172
- </template>
173
- </tr>
174
- </template>
175
-
176
- <template v-slot:group-header="{ item, columns, toggleGroup, isGroupOpen }">
177
- <tr><td :colspan="columns.length" class="px-0">
178
- <VBtn :icon="isGroupOpen(item) ? '$expand' : '$next'" size="small" variant="text" @click="toggleGroup(item)"></VBtn>
179
- <FDynamicComponent :componentData="{template:groupTemplate,data: () => { return { vueData, ...this.$props, item, columns, toggleGroup, isGroupOpen } }}"/>
180
- </td></tr>
181
- </template>
182
-
183
- <template v-for="(c, index) in headersArray.filter(i => !!i.el)" :key="index" v-slot:['item.'+c.key]="{item}">
184
- <FTableItemComponent :template="c" :data="{item, vueData}" />
185
- </template>
186
-
187
- <template v-if="!disabledUpdate" v-slot:item.ctrl-update="{item}">
188
- <div style="min-width: 70px;" v-if="item['ctrl-update'] != 0">
189
- <v-btn v-if="deleteAPI()" @click="deleteItem(item, deleteAPI())" icon="mdi-trash-can-outline" size="small" color="red-darken-1" density="comfortable" variant="plain"></v-btn>
190
- <v-btn v-if="editAPI() || editAction()" @click="editItem(item)" icon="mdi-square-edit-outline" size="small" color="green-darken-2" density="comfortable" variant="plain"></v-btn>
191
- </div>
192
- </template>
193
-
194
- <template v-for="(sData, index) in slots" :key="'dynamic-slot-' + index" v-slot:[sData.name]="{item, columns}">
195
- <FDynamicComponent :componentData="{template:sData.html,data: () => { return { vueData, ...this.$props, item, columns} }}"/>
196
- </template>
197
-
198
- <template v-if="sumFormat && typeof sumFormat === 'object'" v-slot:body.append="{ columns }">
199
- <tr class="tTableSumRow">
200
- <td v-for="col in columns" :key="'sum-' + col.key" class="text-right font-weight-medium">
201
- <span v-if="sumFormat[col.key]">{{ sumByColumn(col.key) }}</span>
202
- </td>
203
- </tr>
204
- </template>
205
- </v-data-table>
206
-
207
- <v-divider v-if="sumFormat && typeof sumFormat === 'string'"></v-divider>
208
- <v-card-actions v-if="sumFormat && typeof sumFormat === 'string'">
209
- <v-spacer></v-spacer><span class="text-h6 font-weight-light">{{sumText}}</span>
210
- </v-card-actions>
211
- <f-dialog v-model="editDialog" v-model:data="editData" v-bind="updateFormAttr" :title="formTitle" :button="updateButton" :controls="updateForm"></f-dialog>
212
- </v-card>
213
- `,
214
- data: function () {
215
- return {
216
- editDialog: false,
217
- search: '',
218
- searchOpen: false,
219
- fillHeight: 0,
220
- tableOptions: null, // sortBy/groupBy hiện hành của v-data-table (update:options) — cho excel="UI"
221
- fixedTableClass: 'f-table-fixed-' + (this.$ && this.$.uid !== undefined ? this.$.uid : this._uid),
222
- itemselected: [],
223
- returnValue: [],
224
- editData: {},
225
- editedIndex: -1,
226
- updateButton: [
227
- {
228
- "v-if": "title=='Cập nhật'",
229
- label: "Lưu",
230
- type: "submit",
231
- action: () => {
232
- this.save(this.editAPI());
233
- }
234
- },
235
- {
236
- "v-if": "title=='Thêm mới'",
237
- label: "Thêm mới",
238
- type: "submit",
239
- action: () => {
240
- this.addnew(this.newAPI())
241
- }
242
- }
243
- ],
244
- newAction: function () {
245
- if (this.updateApi) return this.updateApi['new-action']
246
- else return null;
247
- },
248
- newAPI: function () {
249
- if (this.updateApi) return this.updateApi.new;
250
- else return null;
251
- },
252
- editAction: function () {
253
- if (this.updateApi) return this.updateApi['edit-action']
254
- else return null;
255
- },
256
- editAPI: function () {
257
- if (this.updateApi) return this.updateApi.edit;
258
- else return null;
259
- },
260
- deleteAPI: function () {
261
- if (this.updateApi) return this.updateApi.delete;
262
- else return null;
263
- },
264
- defaultItem: function () {
265
- if (this.updateApi) {
266
- if (typeof this.updateApi['default-item'] === 'undefined') return {};
267
- var dataItem = {}
268
- mapData(this.updateApi['default-item'], dataItem, vueData);
269
- return dataItem;
270
- }
271
- else return {};
272
- },
273
- }
274
- },
275
- mounted: function () {
276
- this.editData = Object.assign({}, this.defaultItem())
277
- this.editedIndex = -1
278
- this.itemSelectRebuild(this.modelValue);
279
-
280
- if (this.heightNum !== null && this.heightNum <= 0) {
281
- this.updateFillHeight();
282
- this.resizeHandler = this.updateFillHeight.bind(this);
283
- window.addEventListener('resize', this.resizeHandler);
284
- }
285
- },
286
- beforeUnmount: function () { this.removeResize(); },
287
- beforeDestroy: function () { this.removeResize(); },
288
- watch: {
289
- items() {
290
- if (this.resizeHandler) this.updateFillHeight();
291
- },
292
- editDialog(val) {
293
- if (!val) {
294
- _.forEach(this.editData, (v, k) => {
295
- this.editData[k] = undefined;
296
- });
297
- this.editedIndex = -1
298
- } else {
299
- this.editData.dMode = (this.edit ? 'edit' : 'new')
300
- }
301
- },
302
- modelValue(newVal, oldVal,) {
303
- t = this;
304
- if (t.returnValue != newVal) this.itemSelectRebuild(newVal);
305
- },
306
- },
307
- computed: {
308
- bindAttr(attr, item) {
309
- var attrTmp = {}
310
- if (attr && attr['v-model']) attrTmp.modelvalue = eval(attr['v-model'])
311
- return attrTmp
312
- },
313
- headersArray() {
314
- var headersBuild = [];
315
- if (this.headers) {
316
- headersBuild = this.headerSetDefaultProps(bindData(this.headers));
317
- }
318
- else if (this.items.length > 0) headersBuild = this.buildHeaderData(this.items[0]);
319
- return headersBuild;
320
- },
321
- hideDefaultFooter_cacl() {
322
- if (this.items) {
323
- if (this.itemsPerPage > 0 && this.itemsPerPage < this.items.length) return false;
324
- }
325
- if (this.hideDefaultFooter) return this.hideDefaultFooter;
326
- return false;
327
- },
328
- // null = không khai height → bảng tự giãn theo số dòng
329
- heightNum() {
330
- if (this.height === undefined || this.height === null || this.height === '') return null;
331
- var n = Number(this.height);
332
- return isNaN(n) ? null : n;
333
- },
334
- tableHeight() {
335
- if (this.heightNum === null) return undefined;
336
- if (this.heightNum > 0) return this.heightNum;
337
- return this.fillHeight || undefined;
338
- },
339
- tableFixedHeader() {
340
- return this.tableHeight ? this.fixedHeader : false;
341
- },
342
- fixedColStyles() {
343
- var fixedCols = parseInt(this.fixedCols, 10);
344
- var headers = this.headersArray || [];
345
- if (isNaN(fixedCols) || fixedCols <= 0 || headers.length === 0) return '';
346
-
347
- fixedCols = Math.min(fixedCols, headers.length);
348
- var scope = '.' + this.fixedTableClass;
349
- var css = scope + ' th:nth-child(-n+' + fixedCols + '),' +
350
- scope + ' td:nth-child(-n+' + fixedCols + '){' +
351
- 'position:sticky!important;background-color:#fff;' +
352
- 'background-color:rgb(var(--v-theme-surface,255,255,255));z-index:1;}';
353
- css += '.v-theme--dark ' + scope + ' th:nth-child(-n+' + fixedCols + '),' +
354
- '.v-theme--dark ' + scope + ' td:nth-child(-n+' + fixedCols + '){background-color:#1e1e1e;}';
355
- // Vuetify fixed-header đặt th trên body; giữ fixed body ở lớp 1 và nâng riêng
356
- // giao điểm header + cột cố định lên lớp 4 để không thua specificity của framework.
357
- css += scope + ' th:nth-child(-n+' + fixedCols + '){z-index:4!important;}';
358
-
359
- var left = [];
360
- for (var i = 0; i < fixedCols; i++) {
361
- var width = this.fixedColumnWidth(headers[i] && headers[i].width);
362
- css += scope + ' th:nth-child(' + (i + 1) + '),' +
363
- scope + ' td:nth-child(' + (i + 1) + '){' +
364
- 'left:calc(' + (left.length ? left.join(' + ') : '0px') + ')!important;' +
365
- 'min-width:' + width + '!important;max-width:' + width + '!important;width:' + width + '!important;}';
366
- left.push(width);
367
- }
368
- return css;
369
- },
370
- edit() {
371
- return this.editedIndex >= 0;
372
- },
373
- formTitle() {
374
- return this.edit ? 'Cập nhật' : 'Thêm mới';
375
- },
376
- dataOut() {
377
- if (this.updateApi) return this.updateApi['data-out']
378
- else return null;
379
- },
380
- sumText() {
381
- var items = this.items;
382
- return _.replace(this.sumFormat, /\[\[(.*?)\]\]/gm, function (s, key) {
383
- return numeral(_.sumBy(items, key)).format('0,0');
384
- });
385
- }
386
- },
387
- methods: {
388
- fixedColumnWidth(width) {
389
- if (width === undefined || width === null || width === '') return '150px';
390
- var value = String(width).trim();
391
- return /^\d+(\.\d+)?$/.test(value) ? value + 'px' : value;
392
- },
393
- removeResize() {
394
- if (this.resizeHandler) {
395
- window.removeEventListener('resize', this.resizeHandler);
396
- this.resizeHandler = null;
397
- }
398
- },
399
- // Đo vị trí thật của vùng cuộn sau khi layout xong rồi fill tới đáy màn hình.
400
- // Footer phân trang / dòng tổng nằm NGOÀI wrapper nên phải trừ phần dưới đó ra.
401
- updateFillHeight() {
402
- var t = this;
403
- t.$nextTick(function () {
404
- requestAnimationFrame(function () {
405
- var root = t.$refs.myTable && t.$refs.myTable.$el;
406
- if (!root) return;
407
- var wrapper = root.querySelector('.v-table__wrapper, .v-data-table__wrapper');
408
- if (!wrapper) return;
409
- var rootRect = root.getBoundingClientRect();
410
- var wrapRect = wrapper.getBoundingClientRect();
411
- var below = Math.max(0, rootRect.bottom - wrapRect.bottom);
412
- var margin = Math.abs(t.heightNum);
413
- t.fillHeight = Math.max(150, window.innerHeight - wrapRect.top - below - margin);
414
- });
415
- });
416
- },
417
- // NEW: sum theo từng cột, format riêng cho mỗi cột theo sumFormat[key] (dùng khi sumFormat là object)
418
- sumByColumn(key) {
419
- return numeral(_.sumBy(this.items, key)).format(this.sumFormat[key]);
420
- },
421
- itemSelect() {
422
- var t = this;
423
-
424
- setTimeout(function () {
425
- if (!t.returnObject) t.returnValue = _.map(t.itemselected, t.itemKey);
426
- else t.returnValue = t.itemselected;
427
-
428
- t.$emit('input', t.returnValue);
429
- }, 10);
430
- },
431
- isDateValid(dateStr) {
432
- return typeof dateStr == 'string' && !isNaN(new Date(dateStr));
433
- },
434
- toggleSearch() {
435
- this.searchOpen = !this.searchOpen;
436
- if (this.searchOpen) {
437
- this.$nextTick(() => {
438
- this.$refs.searchField && this.$refs.searchField.focus();
439
- });
440
- }
441
- },
442
- onSearchBlur() {
443
- if (!this.search) this.searchOpen = false;
444
- },
445
- buildHeader(item_0) {
446
- var header = [];
447
-
448
- if (item_0) {
449
- _.forEach(item_0, (value, key) => {
450
- if (_.isNumber(value)) header.push({ "el": "t-num", "title": key, "key": key, headerProps: { class: "divider" }, cellProps: { class: "divider" }, attr: { "v-model": "item." + key } });
451
- else if (this.isDateValid(value)) header.push({ "el": "t-time", "title": key, "key": key, headerProps: { class: "divider" }, cellProps: { class: "divider" }, attr: { "v-model": "item." + key } });
452
- else header.push({ "title": key, "key": key, headerProps: { class: "divider" }, cellProps: { class: "divider" }, });
453
- });
454
- }
455
- console.log("Build Header: ", header)
456
- return header;
457
- },
458
- buildHeaderData(item_0) {
459
- var arrHeaders = this.buildHeader(item_0);
460
- // NEW: chỉ tự thêm cột ctrl-update khi có khai báo edit hoặc delete API (đồng bộ với bản Vue 2)
461
- var hasEditOrDelete = this.updateApi && (this.updateApi.edit || this.updateApi.delete);
462
- if (arrHeaders.length > 0 && this.items && hasEditOrDelete && _.isBoolean(this.disabledUpdate) && !this.disabledUpdate) arrHeaders.push({ "title": "update", "key": "ctrl-update" });
463
-
464
- return arrHeaders;
465
- },
466
- headerSetDefaultProps(col) {
467
- _.forEach(col, (c) => {
468
- if (c.divider) {
469
- if (c.headerProps) {
470
- if (c.headerProps.class) c.headerProps.class = c.headerProps.class + ' divider'
471
- else c.headerProps.class = "divider";
472
- }
473
- else c.headerProps = { class: "divider" }
474
-
475
- if (c.cellProps) {
476
- if (c.cellProps.class) c.cellProps.class = c.cellProps.class + ' divider'
477
- else c.cellProps.class = "divider";
478
- }
479
- else c.cellProps = { class: "divider" }
480
-
481
- if (c.children) c.children = this.headerSetDefaultProps(c.children);
482
- }
483
- });
484
- return col
485
- },
486
- itemSelectRebuild(newVal) {
487
- var t = this;
488
- if (t.returnObject) t.itemselected = newVal;
489
- else {
490
- t.itemselected = [];
491
-
492
- _.forEach(newVal, function (val) {
493
- t.itemselected.push(_.find(t.items, [t.itemKey, val]));
494
- });
495
- }
496
- },
497
- newBtnClick() {
498
- if (this.newAction()) runAction(this.newAction());
499
- if (this.newAPI()) {
500
- Object.assign(this.editData, this.defaultItem());
501
- this.editDialog = true;
502
- }
503
- },
504
- editItem(item) {
505
-
506
- if (this.editAction()) runAction(this.editAction(), { item: item });
507
-
508
- if (this.editAPI()) {
509
- this.editedIndex = this.items.indexOf(item);
510
- Object.assign(this.editData, this.defaultItem());
511
- this.editData = Object.assign({}, item);
512
- this.editDialog = true;
513
- }
514
- },
515
-
516
- deleteItem(item, deleteAPI) {
517
- const items = this.items;
518
- var actionAPI = deleteAPI;
519
- var indexDelete = items.indexOf(item)
520
- confirm({
521
- title: (actionAPI.CONFIRM ? bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: item, item: item }, vueData)) : "Bạn có chắc chắn muốn xoá ?"),
522
- action: function () {
523
- vueAction(actionAPI, { $row: item, item: item }, function () {
524
- items.splice(indexDelete, 1);
525
- });
526
- }
527
- });
528
- },
529
-
530
- save(actionAPI) {
531
- var diaCtl = this;
532
- if (diaCtl.editedIndex > -1) {
533
- if (actionAPI) {
534
- if (actionAPI.CONFIRM) {
535
- confirm({
536
- title: bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: diaCtl.editData, item: diaCtl.editData }, vueData)),
537
- content: '',
538
- action: function () {
539
- vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
540
- if (!d.Message) {
541
- if (_.isArray(d.data)) {
542
- if (d.data[0]) {
543
- _.forEach(d.data[0], function (value, key) {
544
- diaCtl.editData[key] = value;
545
- });
546
- }
547
- }
548
- }
549
- _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
550
- diaCtl.editDialog = false;
551
- });
552
- },
553
- cancel: function () { }
554
- });
555
- } else {
556
- vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
557
- if (!d.Message) {
558
- if (_.isArray(d.data)) {
559
- if (d.data[0]) {
560
- _.forEach(d.data[0], function (value, key) {
561
- diaCtl.editData[key] = value;
562
- });
563
- }
564
- }
565
- }
566
- _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
567
- diaCtl.editDialog = false;
568
- });
569
- }
570
- } else {
571
- _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
572
- diaCtl.editDialog = false;
573
- }
574
- }
575
- },
576
-
577
- addnew(actionAPI) {
578
- var diaCtl = this;
579
- if (diaCtl.editedIndex == -1) {
580
- if (actionAPI) {
581
- if (actionAPI.CONFIRM) {
582
- confirm({
583
- title: bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: diaCtl.editData, item: diaCtl.editData }, vueData)),
584
- action: function () {
585
- vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
586
- if (d) {
587
- if (_.isArray(d.data)) {
588
- if (d.data[0]) {
589
- _.forEach(d.data[0], function (value, key) {
590
- diaCtl.editData[key] = value;
591
- });
592
- }
593
- }
594
- }
595
- diaCtl.items.push(_.cloneDeep(diaCtl.editData));
596
- diaCtl.editDialog = false;
597
- });
598
- },
599
- cancel: function () { }
600
- });
601
- } else {
602
- vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
603
- if (d) {
604
- if (_.isArray(d.data)) {
605
- if (d.data[0]) {
606
- _.forEach(d.data[0], function (value, key) {
607
- diaCtl.editData[key] = value;
608
- });
609
- }
610
- }
611
- }
612
- diaCtl.items.push(_.cloneDeep(diaCtl.editData));
613
- diaCtl.editDialog = false;
614
- });
615
- }
616
- } else {
617
- diaCtl.items.push(_.cloneDeep(diaCtl.editData));
618
- diaCtl.editDialog = false;
619
- }
620
- }
621
- },
622
- updateSearch: _.debounce(function (text) {
623
- this.search = text;
624
- }, 300),
625
- customSearch(value, search, item) {
626
- return Object.values(item.columns).some(v=>v&&v.toString().toLowerCase().includes(search.toLowerCase()))
627
- },
628
- // excel = true (mặc định): xuất nguyên items gốc — giữ nguyên hành vi cũ.
629
- // excel = "UI": xuất đúng những gì bảng đang hiển thị (xem toExcelUI).
630
- toExcel() {
631
- if (typeof this.excel === 'string' && this.excel.trim().toUpperCase() === 'UI') return this.toExcelUI();
632
- jsonToExcel({ data: this.items, filename: (this.label && this.label != '' ? this.label : 'tableFP') + '.xlsx' });
633
- Vue.$toast.success("Đã xuất Excel", { position: 'top' });
634
- },
635
- onTableOptions(opt) {
636
- this.tableOptions = opt;
637
- },
638
- //------------------------------------------------------------------ excel="UI"
639
- // Lấy từ giao diện: DÒNG = items sau lọc (ô tìm kiếm) + sắp xếp (kể cả group-by), gồm MỌI trang;
640
- // CỘT = header lá theo thứ tự hiển thị, tên cột = tiêu đề trên bảng. GIÁ TRỊ ô = dữ liệu gốc của
641
- // field, KHÔNG định dạng (không numFmt, không đổi ngày, không bỏ HTML) — không có hàng tổng.
642
- // Không đọc DOM: DOM chỉ chứa trang hiện tại, còn ô t-* là component nên không lấy lại được giá trị.
643
- toExcelUI() {
644
- var t = this;
645
- var columns = t.uiExcelColumns();
646
- if (!columns.length) { showMessage({ title: 'Bảng không có cột nào để xuất Excel' }); return; }
647
- var rows = t.uiExcelRows(columns);
648
- if (!rows.length) { showMessage({ title: 'Không có dòng nào đang hiển thị để xuất Excel' }); return; }
649
- var name = t.plainText(t.label) || 'tableFP';
650
- jsonToExcel({
651
- data: rows,
652
- columns: columns,
653
- filename: name.replace(/[\\\/:*?"<>|]/g, '_') + '.xlsx',
654
- worksheet: name.replace(/[\[\]:*?\/\\]/g, ' ').slice(0, 31).trim() || 'SheetFP'
655
- });
656
- Vue.$toast.success("Đã xuất Excel", { position: 'top' });
657
- },
658
- uiExcelColumns() {
659
- var t = this, out = [];
660
- var skip = { 'ctrl-update': 1, 'data-table-select': 1, 'data-table-group': 1, 'data-table-expand': 1 };
661
- (function walk(list, prefix) {
662
- _.forEach(list, function (h) {
663
- if (!h || typeof h !== 'object') return;
664
- var title = t.plainText(h.title !== undefined ? h.title : h.text);
665
- var full = prefix && title ? prefix + ' / ' + title : (title || prefix);
666
- if (Array.isArray(h.children) && h.children.length) { walk(h.children, full); return; }
667
- var key = typeof h.key === 'string' ? h.key : (typeof h.value === 'string' ? h.value : null);
668
- if (!key || skip[key]) return;
669
- // Vuetify 3 lấy giá trị ô theo `value` (path) nếu có, không thì theo `key`
670
- out.push(t.uiExcelColumn(h, key, typeof h.value === 'string' ? h.value : key, full || key));
671
- });
672
- })(t.headersArray, '');
673
- return out;
674
- },
675
- uiExcelColumn(h, key, path, header) {
676
- var attr = h.attr || {};
677
- // Ô t-* hiển thị field mà v-model trỏ tới ("item.X"), không nhất thiết trùng key của cột
678
- var vm = typeof attr['v-model'] === 'string' && /^\s*item\.([\w$.]+)\s*$/.exec(attr['v-model']);
679
- var tablePath = path;
680
- if (vm) path = vm[1];
681
- var col = { header: header, key: path, tablePath: tablePath, sortKey: key, compare: typeof h.sort === 'function' ? h.sort : null };
682
- var wm = /^\s*(\d+(?:\.\d+)?)\s*(px)?\s*$/.exec(h.width === undefined || h.width === null ? '' : String(h.width));
683
- if (wm) col.width = Math.max(8, Math.round(Number(wm[1]) / 7));
684
- return col;
685
- },
686
- uiExcelRows(columns) {
687
- var t = this;
688
- var rows = (t.items || []).slice();
689
- if (t.search) {
690
- // Dùng lại chính customSearch mà v-data-table đang dùng (item.columns = giá trị theo từng cột)
691
- rows = rows.filter(function (r) {
692
- var cols = {};
693
- _.forEach(columns, function (c) { cols[c.sortKey] = t.readCell(r, c.tablePath); });
694
- return t.customSearch(null, t.search, { columns: cols });
695
- });
696
- }
697
- var spec = t.uiSortSpec();
698
- if (spec.length) {
699
- var byKey = _.keyBy(columns, 'sortKey');
700
- var collator = t.uiCollator();
701
- var fields = spec.map(function (s) {
702
- var c = byKey[s.key];
703
- return { path: c ? c.tablePath : s.key, desc: s.desc, compare: c && c.compare };
704
- });
705
- rows.sort(function (a, b) {
706
- for (var i = 0; i < fields.length; i++) {
707
- var f = fields[i];
708
- var va = t.readCell(a, f.path), vb = t.readCell(b, f.path);
709
- if (f.desc) { var tmp = va; va = vb; vb = tmp; }
710
- var r = f.compare ? f.compare(va, vb) : t.compareCell(va, vb, collator);
711
- if (r) return r;
712
- }
713
- return 0;
714
- });
715
- }
716
- return rows;
717
- },
718
- // group-by đứng trước sort-by — đúng thứ tự v-data-table sắp dòng khi gộp nhóm.
719
- // Chưa nhận update:options nào thì lấy từ attr khai sẵn.
720
- uiSortSpec() {
721
- var o = this.tableOptions, a = this.$attrs || {};
722
- var group = o ? o.groupBy : (a['group-by'] || a.groupBy);
723
- var sort = o ? o.sortBy : (a['sort-by'] || a.sortBy);
724
- return this.normalizeSort(group).concat(this.normalizeSort(sort));
725
- },
726
- normalizeSort(list) {
727
- return _.castArray(list === undefined || list === null ? [] : list).map(function (s) {
728
- if (s && typeof s === 'object') return { key: s.key, desc: s.order === 'desc' || s.order === true };
729
- return { key: s, desc: false };
730
- }).filter(function (s) { return typeof s.key === 'string' && s.key !== ''; });
731
- },
732
- // Bám thuật toán sortItems của Vuetify: ngày so theo thời gian, rỗng lên đầu, hai chuỗi số so
733
- // theo số, còn lại so bằng Intl.Collator (có dấu tiếng Việt).
734
- compareCell(a, b, collator) {
735
- if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
736
- var sa = a === null || a === undefined ? '' : String(a).toLocaleLowerCase();
737
- var sb = b === null || b === undefined ? '' : String(b).toLocaleLowerCase();
738
- if (sa === sb) return 0;
739
- if (sa === '') return -1;
740
- if (sb === '') return 1;
741
- if (!isNaN(sa) && !isNaN(sb)) return Number(sa) - Number(sb);
742
- return collator.compare(sa, sb);
743
- },
744
- uiCollator() {
745
- var loc = _.get(this, '$vuetify.locale.current');
746
- if (loc && typeof loc === 'object' && 'value' in loc) loc = loc.value;
747
- try { return new Intl.Collator(loc || undefined, { sensitivity: 'accent', usage: 'sort' }); }
748
- catch (e) { return new Intl.Collator(undefined, { sensitivity: 'accent', usage: 'sort' }); }
749
- },
750
- readCell(row, path) {
751
- if (row === null || row === undefined) return null;
752
- return row[path] !== undefined ? row[path] : _.get(row, path);
753
- },
754
- plainText(v) {
755
- if (v === null || v === undefined) return '';
756
- var s = String(v);
757
- if (s.indexOf('<') < 0 && s.indexOf('&') < 0) return s.trim();
758
- return (new DOMParser().parseFromString(s, 'text/html').body.textContent || '').trim();
759
- },
760
- }
761
- });
762
-
763
- //===========================================================================================
764
-
765
- Vue.component('THtml', {
766
- props: {
767
- modelValue: {
768
- default: null
769
- },
770
- },
771
- template: `<div class="my-2" v-bind="$attrs" v-html="modelValue"></div>`,
772
- });
773
- //-----------------------------------------------------------------------------------------------------------
774
- Vue.component('TLabel', {
775
- props: {
776
- modelValue: {
777
- default: null
778
- },
779
- },
780
- template: `<div class="text-no-wrap" v-bind="$attrs">{{modelValue}}</div>`,
781
- });
782
- //-----------------------------------------------------------------------------------------------------------
783
- Vue.component('TNum', {
784
- props: {
785
- modelValue: {
786
- type: Number,
787
- default: 0
788
- },
789
- hidezero: {
790
- type: Boolean,
791
- default: true
792
- },
793
- format: {
794
- type: String,
795
- default: ''
796
- }
797
- },
798
- template: `<div v-if="showData" class="text-no-wrap text-end" v-bind="$attrs">{{ format != ''? numeral(modelValue).format(format) : Number.isInteger(modelValue)?numeral(modelValue).format('0,0'): numeral(modelValue).format('0,0.0') }}</div>`,
799
- data: function () {
800
- return {
801
- numeral: numeral
802
- }
803
- },
804
- computed: {
805
- showData: function () {
806
- return !_.isNull(this.modelValue) && !(this.hidezero && this.modelValue == 0);
807
- },
808
- },
809
- });
810
- //-----------------------------------------------------------------------------------------------------------
811
- Vue.component('TTime', {
812
- props: {
813
- format: {
814
- type: String,
815
- default: 'DD/MM/YYYY'
816
- }
817
- },
818
- template: `<div v-if="$attrs['modelValue']" class="text-no-wrap" v-bind="$attrs">{{ dayjs($attrs['modelValue']).format(format) }}</div>`,
819
- data: function () {
820
- return {
821
- dayjs: dayjs
822
- }
823
- },
824
- });
825
- //-----------------------------------------------------------------------------------------------------------
826
- Vue.component('TCheck', {
827
- props: {
828
- modelValue: {
829
- default: null
830
- },
831
- falseValue: {
832
- default: false
833
- },
834
- },
835
- template: `<v-checkbox v-bind="$attrs" v-model="ctlValue" :false-value="falseValue" @change="onchange($event)" class="d-inline-flex" hide-details />`,
836
- data: function () {
837
- return {
838
- ctlValue: Boolean(this.modelValue)
839
- }
840
- },
841
- watch: {
842
- modelValue(newVal) {
843
- this.ctlValue = Boolean(newVal)
844
- },
845
- },
846
- methods: {
847
- onchange: function (e) {
848
- console.log("onchange", this)
849
- tableActionEvent(this.$attrs);
850
- }
851
- }
852
- });
853
- //-----------------------------------------------------------------------------------------------------------
854
- Vue.component('TSelect', {
855
- props: ['modelValue'],
856
- template: `<v-select v-model="ctlValue" v-bind="$attrs" @update:modelValue="onchange" variant="plain" density="compact" hide-details menu-icon="mdi-menu-swap"></v-select>`,
857
- data: function () {
858
- return {
859
- ctlValue: this.modelValue
860
- }
861
- },
862
- watch: {
863
- modelValue(newVal) {
864
- this.ctlValue = newVal
865
- },
866
- },
867
- methods: {
868
- onchange: function (e) {
869
- tableActionEvent(this.$attrs);
870
- }
871
- }
872
- });
873
- //-----------------------------------------------------------------------------------------------------------
874
- Vue.component('TCombobox', {
875
- props: {
876
- modelValue: {
877
- default: undefined
878
- },
879
- text: {
880
- default: undefined
881
- },
882
- items: {
883
- type: Array,
884
- default: []
885
- },
886
- api: {
887
- type: String,
888
- default: undefined
889
- },
890
- apiData: {
891
- type: Object,
892
- default: {}
893
- },
894
-
895
- },
896
- template: `<v-autocomplete v-model="ctlValue" v-bind="$attrs" @update:modelValue="onchange" :loading="loading" :items="dataItems" @update:search="querySelections" auto-select-first density="compact" hide-details></v-autocomplete>`,
897
- data: function () {
898
- return {
899
- dataItems: this.items,
900
- ctlValue: this.modelValue,
901
- loading: false,
902
- getDataFormAPI: _.debounce((searchText) => {
903
- if ((searchText && searchText.length < 2) || !this.api) return;
904
- var self = this;
905
- this.loading = true
906
-
907
- if (typeof this.apiData != 'object') this.apiData = JSON.parse(this.apiData);
908
-
909
- mapData(this.apiData, this.apiData, _.assignIn({}, vueData, { TEXT: searchText }));
910
-
911
- ajaxCALL(this.api, this.apiData, function (d) {
912
- if (_.isArray(d.data)) self.dataItems = d.data;
913
- else self.dataItems = d;
914
-
915
- self.loading = false
916
- }, function (e) {
917
- console.log("API Error", e);
918
- })
919
-
920
- }, 500)
921
- }
922
- },
923
- watch: {
924
- items(val) {
925
- this.dataItems = val
926
- },
927
- modelValue(newVal) {
928
- this.ctlValue = newVal
929
- }
930
- },
931
- methods: {
932
- querySelections(val) {
933
- if (!val) return false
934
- this.getDataFormAPI(val);
935
- },
936
- onchange: function (e) {
937
- if (e) tableActionEvent(this.$attrs);
938
- }
939
- },
940
- });
941
- //-----------------------------------------------------------------------------------------------------------
942
- Vue.component('TMenu', {
943
- props: {
944
- iconText: {
945
- default: 'mdi-menu'
946
- },
947
- items: {
948
- default: []
949
- }
950
- },
951
- template: `
952
- <v-menu offset-y origin="center center" transition="slide-y-transition" max-height = 500>
953
- <template v-slot:activator="{ props }"><v-icon color="primary" v-bind="props" :icon="iconText"></v-icon></template>
954
- <v-list >
955
- <v-list-item v-bind="$attrs" v-for="(item, index) in items" :key="index" @click="onSelect(item)" >
956
- <template v-slot:prepend><v-icon :color="item.color" :icon="item['icon-text']"></v-icon></template>
957
- <v-list-item-title v-text="item.text"></v-list-item-title>
958
- </v-list-item>
959
- </v-list>
960
- </v-menu>
961
- `,
962
- methods: {
963
- onSelect: function (e) {
964
- tableActionEvent({ action: e.action, ...this.$attrs });
965
- }
966
- }
967
- });
968
- //-----------------------------------------------------------------------------------------------------------
969
- Vue.component('TLink', {
970
- props: ['id', 'target', 'url', 'title', 'onclose'],
971
- template: `<a :href="iURL" :target="target" v-bind="$attrs" @click="onclick()"><v-icon v-if="$attrs['icon-text']" left color="primary">{{$attrs['icon-text']}}</v-icon>{{$attrs['modelValue']}}</a>`,
972
- data: function () {
973
- return {}
974
- },
975
- computed: {
976
- iURL: function () {
977
- if (this.target != 'dialog' && this.url) return this.url;
978
- return null;
979
- },
980
- },
981
- methods: {
982
- onclick: function (e) {
983
- if (this.target == 'dialog') {
984
- tableActionEvent(this.$attrs);
985
- openWindow({
986
- id: this.id,
987
- title: this.title,
988
- url: this.url,
989
- onclose: this.onclose,
990
- });
991
- }
992
- }
993
- }
994
- });
995
- //-----------------------------------------------------------------------------------------------------------
996
- Vue.component('TButton', {
997
- props: {
998
- label: {
999
- default: null
1000
- },
1001
- variant: {
1002
- default: "text"
1003
- },
1004
- color: {
1005
- default: "primary"
1006
- },
1007
- wid: {
1008
- default: null
1009
- },
1010
- url: {
1011
- default: null
1012
- },
1013
- title: {
1014
- default: null
1015
- },
1016
- density: {
1017
- default: 'compact'
1018
- },
1019
- onclose: {
1020
- default: null
1021
- },
1022
- },
1023
- template: `<v-btn v-bind="$attrs" :variant="variant" :text="label" :color="color" :density="density" @click="clickButton"></v-btn>`,
1024
- methods: {
1025
- clickButton: function (e) {
1026
- if (this.url) {
1027
- console.log("dialog onclose", this.onclose)
1028
- openWindow({
1029
- id: this.wid,
1030
- title: this.title,
1031
- url: this.url,
1032
- onclose: this.onclose
1033
- });
1034
- } else tableActionEvent(this.$attrs);
1035
- }
1036
- }
1037
-
1038
- });
1
+ //===========================================================================================
2
+ Vue.component("FTableItemComponent", {
3
+ template: `<component :is="dynamicComponent"></component>`,
4
+ props: {
5
+ template: {
6
+ required: true
7
+ },
8
+ data: {
9
+ type: Object,
10
+ default: () => ({}),
11
+ },
12
+ },
13
+ data() {
14
+ return {
15
+ }
16
+ },
17
+ computed: {
18
+ dynamicComponent() {
19
+ return {
20
+ template: this.buildTemplateString(this.template),
21
+ data: () => { return this.data },
22
+ }
23
+ },
24
+ },
25
+ methods: {
26
+ buildTemplateString(jObj) {
27
+ if (Array.isArray(jObj)) {
28
+ var templateString = '';
29
+ jObj.forEach((o) => {
30
+ templateString = templateString + this.buildTemplateString(o)
31
+ });
32
+ return templateString;
33
+ }
34
+
35
+ if (typeof jObj === 'object') {
36
+ if (!jObj.attr) jObj.attr = {}
37
+ jObj.attr[":item"] = "item"
38
+ let d = $(`<${jObj.el}>`, this.fixAttrValueObject(jObj.attr)).append(this.buildTemplateString(jObj.innerHTML));
39
+ return $('<div>').append(d).html();
40
+ }
41
+ if (typeof jObj === 'string') return jObj;
42
+ return ""
43
+ },
44
+ fixAttrValueObject(jObj) {
45
+ let returnObj = {}
46
+ if (jObj) {
47
+ for (const [key, value] of Object.entries(jObj)) {
48
+ if (typeof value === 'object') returnObj[key] = JSON.stringify(value);
49
+ else returnObj[key] = value;
50
+ }
51
+ }
52
+ return returnObj
53
+ }
54
+ }
55
+ });
56
+ //===========================================================================================
57
+ Vue.component("FTable", {
58
+ props: {
59
+ modelValue: {
60
+ default: null
61
+ },
62
+ headers: {
63
+ default: null
64
+ },
65
+ updateForm: {
66
+ default: null
67
+ },
68
+ updateApi: {
69
+ default: null
70
+ },
71
+ items: {
72
+ default: null
73
+ },
74
+ label: {
75
+ default: null
76
+ },
77
+ sumFormat: {
78
+ default: null
79
+ },
80
+ returnObject: {
81
+ default: null
82
+ },
83
+ itemKey: {
84
+ default: null
85
+ },
86
+ disabledUpdate: {
87
+ default: null
88
+ },
89
+ template: {
90
+ default: null
91
+ },
92
+ excel: {
93
+ default: true
94
+ },
95
+ hover: {
96
+ default: true
97
+ },
98
+ showSearch: {
99
+ default: true
100
+ },
101
+ returnObject: {
102
+ default: true
103
+ },
104
+ fixedHeader: {
105
+ default: true
106
+ },
107
+ fixedCols: {
108
+ default: 0
109
+ },
110
+ // null/bỏ trống = bảng tự giãn theo số dòng; <=0 = fill tới đáy màn hình chừa
111
+ // abs(n) px (tự đo vị trí thật, theo dõi resize); >0 = px cố định.
112
+ height: {
113
+ default: null
114
+ },
115
+ disableSort: {
116
+ default: false
117
+ },
118
+ density: {
119
+ default: 'compact'
120
+ },
121
+ hideDefaultFooter: {
122
+ default: true
123
+ },
124
+ returnObject: {
125
+ default: true
126
+ },
127
+ itemsPerPage: {
128
+ default: 50
129
+ },
130
+ footerProps:
131
+ {
132
+ default: "{'items-per-page-options':[50, 100, 150, 200, -1]}"
133
+ },
134
+ updateFormAttr: {
135
+ default: null
136
+ },
137
+ slots: {
138
+ default: null
139
+ },
140
+ groupTemplate: {
141
+ default: null
142
+ },
143
+ },
144
+ template: `
145
+ <v-card ref="myTable" variant="outlined" v-if="headersArray" class="border" style="text-align: initial;">
146
+ <component is="style" v-if="fixedColStyles" v-html="fixedColStyles"></component>
147
+ <v-card-actions v-if="showSearch || label || excel || ((newAPI() || newAction()) && !disabledUpdate)">
148
+ <div v-if="showSearch || label || ((newAPI() || newAction()) && !disabledUpdate)" class="text-primary text-darken-2 text-left w-66 pl-2 text-h6" v-html="label"></div>
149
+ <v-spacer></v-spacer>
150
+ <v-btn v-if="excel" class="mr-4" small icon density="compact" color="primary" @click="toExcel"><v-icon>mdi-microsoft-excel</v-icon></v-btn>
151
+ <div v-if="showSearch" class="d-flex align-center">
152
+ <v-btn icon density="compact" color="primary" @click="toggleSearch"><v-icon>mdi-magnify</v-icon></v-btn>
153
+ <v-expand-x-transition>
154
+ <v-text-field v-if="searchOpen" ref="searchField" density="compact" @update:modelValue="updateSearch" label="" variant="underlined" hide-details autofocus style="width: 300px;" @blur="onSearchBlur" class="ml-1"></v-text-field>
155
+ </v-expand-x-transition>
156
+ </div>
157
+ <v-btn v-if="(newAPI() || newAction()) && !disabledUpdate" class="ml-5 mr-2" density="compact" variant="outlined" icon="mdi-plus" color="primary" @click="newBtnClick"></v-btn>
158
+ </v-card-actions>
159
+ <v-data-table class="tTable" :class="fixedTableClass" v-bind="$attrs" :height="tableHeight" :fixed-header="tableFixedHeader" :hover="hover" :items="items" :item-key="itemKey" :return-object="returnObject" :headers="headersArray" :density="density" :items-per-page="itemsPerPage" :hide-default-footer="hideDefaultFooter_cacl" v-model="itemselected" calculate-widths v-on:click:row="$emit('click:row', $event)" v-on:item-selected="itemSelect()" v-on:toggle-select-all="itemSelect()" @update:options="onTableOptions" :search="search" :custom-filter="customSearch">
160
+ <template v-slot:headers="{ headers, isSorted, getSortIcon, toggleSort, allSelected, selectAll, someSelected, groupHeaders }">
161
+ <tr v-for="columns in headers">
162
+ <template v-for="column in columns" :key="column.key">
163
+ <th v-if="column.key=='data-table-select'" class="px-2" :colspan="column.colspan || 1" :rowspan="column.rowspan || 1">
164
+ <v-checkbox-btn :indeterminate="someSelected && !allSelected" :model-value="allSelected" color="primary" @update:model-value="selectAll(!allSelected)" ></v-checkbox-btn>
165
+ </th>
166
+ <th v-else-if="column.key=='data-table-group'" v-bind="column.headerProps" :colspan="column.colspan || 1" :rowspan="column.rowspan || 1" style="width:40px" class="px-0 text-center">
167
+ <VIcon icon="mdi-format-list-group" size="small"></VIcon>
168
+ </th>
169
+ <th v-else v-bind="column.headerProps" :colspan="column.colspan || 1" :rowspan="column.rowspan || 1">
170
+ <div :class="['cursor-pointer', 'd-flex', 'justify-' + column.align]" @click="() => toggleSort(column)"><span v-html="column.title"></span><template v-if="isSorted(column)"><v-icon class="ml-1" :icon="getSortIcon(column)"></v-icon></template></div>
171
+ </th>
172
+ </template>
173
+ </tr>
174
+ </template>
175
+
176
+ <template v-slot:group-header="{ item, columns, toggleGroup, isGroupOpen }">
177
+ <tr><td :colspan="columns.length" class="px-0">
178
+ <VBtn :icon="isGroupOpen(item) ? '$expand' : '$next'" size="small" variant="text" @click="toggleGroup(item)"></VBtn>
179
+ <FDynamicComponent :componentData="{template:groupTemplate,data: () => { return { vueData, ...this.$props, item, columns, toggleGroup, isGroupOpen } }}"/>
180
+ </td></tr>
181
+ </template>
182
+
183
+ <template v-for="(c, index) in headersArray.filter(i => !!i.el)" :key="index" v-slot:['item.'+c.key]="{item}">
184
+ <FTableItemComponent :template="c" :data="{item, vueData}" />
185
+ </template>
186
+
187
+ <template v-if="!disabledUpdate" v-slot:item.ctrl-update="{item}">
188
+ <div style="min-width: 70px;" v-if="item['ctrl-update'] != 0">
189
+ <v-btn v-if="deleteAPI()" @click="deleteItem(item, deleteAPI())" icon="mdi-trash-can-outline" size="small" color="red-darken-1" density="comfortable" variant="plain"></v-btn>
190
+ <v-btn v-if="editAPI() || editAction()" @click="editItem(item)" icon="mdi-square-edit-outline" size="small" color="green-darken-2" density="comfortable" variant="plain"></v-btn>
191
+ </div>
192
+ </template>
193
+
194
+ <template v-for="(sData, index) in slots" :key="'dynamic-slot-' + index" v-slot:[sData.name]="{item, columns}">
195
+ <FDynamicComponent :componentData="{template:sData.html,data: () => { return { vueData, ...this.$props, item, columns} }}"/>
196
+ </template>
197
+
198
+ <template v-if="sumFormat && typeof sumFormat === 'object'" v-slot:body.append="{ columns }">
199
+ <tr class="tTableSumRow">
200
+ <td v-for="col in columns" :key="'sum-' + col.key" class="text-right font-weight-medium">
201
+ <span v-if="sumFormat[col.key]">{{ sumByColumn(col.key) }}</span>
202
+ </td>
203
+ </tr>
204
+ </template>
205
+ </v-data-table>
206
+
207
+ <v-divider v-if="sumFormat && typeof sumFormat === 'string'"></v-divider>
208
+ <v-card-actions v-if="sumFormat && typeof sumFormat === 'string'">
209
+ <v-spacer></v-spacer><span class="text-h6 font-weight-light">{{sumText}}</span>
210
+ </v-card-actions>
211
+ <f-dialog v-model="editDialog" v-model:data="editData" v-bind="updateFormAttr" :title="formTitle" :button="updateButton" :controls="updateForm"></f-dialog>
212
+ </v-card>
213
+ `,
214
+ data: function () {
215
+ return {
216
+ editDialog: false,
217
+ search: '',
218
+ searchOpen: false,
219
+ fillHeight: 0,
220
+ tableOptions: null, // sortBy/groupBy hiện hành của v-data-table (update:options) — cho excel="UI"
221
+ fixedTableClass: 'f-table-fixed-' + (this.$ && this.$.uid !== undefined ? this.$.uid : this._uid),
222
+ itemselected: [],
223
+ returnValue: [],
224
+ editData: {},
225
+ editedIndex: -1,
226
+ updateButton: [
227
+ {
228
+ "v-if": "title=='Cập nhật'",
229
+ label: "Lưu",
230
+ type: "submit",
231
+ action: () => {
232
+ this.save(this.editAPI());
233
+ }
234
+ },
235
+ {
236
+ "v-if": "title=='Thêm mới'",
237
+ label: "Thêm mới",
238
+ type: "submit",
239
+ action: () => {
240
+ this.addnew(this.newAPI())
241
+ }
242
+ }
243
+ ],
244
+ newAction: function () {
245
+ if (this.updateApi) return this.updateApi['new-action']
246
+ else return null;
247
+ },
248
+ newAPI: function () {
249
+ if (this.updateApi) return this.updateApi.new;
250
+ else return null;
251
+ },
252
+ editAction: function () {
253
+ if (this.updateApi) return this.updateApi['edit-action']
254
+ else return null;
255
+ },
256
+ editAPI: function () {
257
+ if (this.updateApi) return this.updateApi.edit;
258
+ else return null;
259
+ },
260
+ deleteAPI: function () {
261
+ if (this.updateApi) return this.updateApi.delete;
262
+ else return null;
263
+ },
264
+ defaultItem: function () {
265
+ if (this.updateApi) {
266
+ if (typeof this.updateApi['default-item'] === 'undefined') return {};
267
+ var dataItem = {}
268
+ mapData(this.updateApi['default-item'], dataItem, vueData);
269
+ return dataItem;
270
+ }
271
+ else return {};
272
+ },
273
+ }
274
+ },
275
+ mounted: function () {
276
+ this.editData = Object.assign({}, this.defaultItem())
277
+ this.editedIndex = -1
278
+ this.itemSelectRebuild(this.modelValue);
279
+
280
+ if (this.heightNum !== null && this.heightNum <= 0) {
281
+ this.updateFillHeight();
282
+ this.resizeHandler = this.updateFillHeight.bind(this);
283
+ window.addEventListener('resize', this.resizeHandler);
284
+ }
285
+ },
286
+ beforeUnmount: function () { this.removeResize(); },
287
+ beforeDestroy: function () { this.removeResize(); },
288
+ watch: {
289
+ items() {
290
+ if (this.resizeHandler) this.updateFillHeight();
291
+ },
292
+ editDialog(val) {
293
+ if (!val) {
294
+ _.forEach(this.editData, (v, k) => {
295
+ this.editData[k] = undefined;
296
+ });
297
+ this.editedIndex = -1
298
+ } else {
299
+ this.editData.dMode = (this.edit ? 'edit' : 'new')
300
+ }
301
+ },
302
+ modelValue(newVal, oldVal,) {
303
+ t = this;
304
+ if (t.returnValue != newVal) this.itemSelectRebuild(newVal);
305
+ },
306
+ },
307
+ computed: {
308
+ bindAttr(attr, item) {
309
+ var attrTmp = {}
310
+ if (attr && attr['v-model']) attrTmp.modelvalue = eval(attr['v-model'])
311
+ return attrTmp
312
+ },
313
+ headersArray() {
314
+ var headersBuild = [];
315
+ if (this.headers) {
316
+ headersBuild = this.headerSetDefaultProps(bindData(this.headers));
317
+ }
318
+ else if (this.items.length > 0) headersBuild = this.buildHeaderData(this.items[0]);
319
+ return headersBuild;
320
+ },
321
+ hideDefaultFooter_cacl() {
322
+ if (this.items) {
323
+ if (this.itemsPerPage > 0 && this.itemsPerPage < this.items.length) return false;
324
+ }
325
+ if (this.hideDefaultFooter) return this.hideDefaultFooter;
326
+ return false;
327
+ },
328
+ // null = không khai height → bảng tự giãn theo số dòng
329
+ heightNum() {
330
+ if (this.height === undefined || this.height === null || this.height === '') return null;
331
+ var n = Number(this.height);
332
+ return isNaN(n) ? null : n;
333
+ },
334
+ tableHeight() {
335
+ if (this.heightNum === null) return undefined;
336
+ if (this.heightNum > 0) return this.heightNum;
337
+ return this.fillHeight || undefined;
338
+ },
339
+ tableFixedHeader() {
340
+ return this.tableHeight ? this.fixedHeader : false;
341
+ },
342
+ fixedColStyles() {
343
+ var fixedCols = parseInt(this.fixedCols, 10);
344
+ var headers = this.headersArray || [];
345
+ if (isNaN(fixedCols) || fixedCols <= 0 || headers.length === 0) return '';
346
+
347
+ fixedCols = Math.min(fixedCols, headers.length);
348
+ var scope = '.' + this.fixedTableClass;
349
+ var css = scope + ' th:nth-child(-n+' + fixedCols + '),' +
350
+ scope + ' td:nth-child(-n+' + fixedCols + '){' +
351
+ 'position:sticky!important;background-color:#fff;' +
352
+ 'background-color:rgb(var(--v-theme-surface,255,255,255));z-index:1;}';
353
+ css += '.v-theme--dark ' + scope + ' th:nth-child(-n+' + fixedCols + '),' +
354
+ '.v-theme--dark ' + scope + ' td:nth-child(-n+' + fixedCols + '){background-color:#1e1e1e;}';
355
+ // Vuetify fixed-header đặt th trên body; giữ fixed body ở lớp 1 và nâng riêng
356
+ // giao điểm header + cột cố định lên lớp 4 để không thua specificity của framework.
357
+ css += scope + ' th:nth-child(-n+' + fixedCols + '){z-index:4!important;}';
358
+
359
+ var left = [];
360
+ for (var i = 0; i < fixedCols; i++) {
361
+ var width = this.fixedColumnWidth(headers[i] && headers[i].width);
362
+ css += scope + ' th:nth-child(' + (i + 1) + '),' +
363
+ scope + ' td:nth-child(' + (i + 1) + '){' +
364
+ 'left:calc(' + (left.length ? left.join(' + ') : '0px') + ')!important;' +
365
+ 'min-width:' + width + '!important;max-width:' + width + '!important;width:' + width + '!important;}';
366
+ left.push(width);
367
+ }
368
+ return css;
369
+ },
370
+ edit() {
371
+ return this.editedIndex >= 0;
372
+ },
373
+ formTitle() {
374
+ return this.edit ? 'Cập nhật' : 'Thêm mới';
375
+ },
376
+ dataOut() {
377
+ if (this.updateApi) return this.updateApi['data-out']
378
+ else return null;
379
+ },
380
+ sumText() {
381
+ var items = this.items;
382
+ return _.replace(this.sumFormat, /\[\[(.*?)\]\]/gm, function (s, key) {
383
+ return numeral(_.sumBy(items, key)).format('0,0');
384
+ });
385
+ }
386
+ },
387
+ methods: {
388
+ fixedColumnWidth(width) {
389
+ if (width === undefined || width === null || width === '') return '150px';
390
+ var value = String(width).trim();
391
+ return /^\d+(\.\d+)?$/.test(value) ? value + 'px' : value;
392
+ },
393
+ removeResize() {
394
+ if (this.resizeHandler) {
395
+ window.removeEventListener('resize', this.resizeHandler);
396
+ this.resizeHandler = null;
397
+ }
398
+ },
399
+ // Đo vị trí thật của vùng cuộn sau khi layout xong rồi fill tới đáy màn hình.
400
+ // Footer phân trang / dòng tổng nằm NGOÀI wrapper nên phải trừ phần dưới đó ra.
401
+ updateFillHeight() {
402
+ var t = this;
403
+ t.$nextTick(function () {
404
+ requestAnimationFrame(function () {
405
+ var root = t.$refs.myTable && t.$refs.myTable.$el;
406
+ if (!root) return;
407
+ var wrapper = root.querySelector('.v-table__wrapper, .v-data-table__wrapper');
408
+ if (!wrapper) return;
409
+ var rootRect = root.getBoundingClientRect();
410
+ var wrapRect = wrapper.getBoundingClientRect();
411
+ var below = Math.max(0, rootRect.bottom - wrapRect.bottom);
412
+ var margin = Math.abs(t.heightNum);
413
+ t.fillHeight = Math.max(150, window.innerHeight - wrapRect.top - below - margin);
414
+ });
415
+ });
416
+ },
417
+ // NEW: sum theo từng cột, format riêng cho mỗi cột theo sumFormat[key] (dùng khi sumFormat là object)
418
+ sumByColumn(key) {
419
+ return numeral(_.sumBy(this.items, key)).format(this.sumFormat[key]);
420
+ },
421
+ itemSelect() {
422
+ var t = this;
423
+
424
+ setTimeout(function () {
425
+ if (!t.returnObject) t.returnValue = _.map(t.itemselected, t.itemKey);
426
+ else t.returnValue = t.itemselected;
427
+
428
+ t.$emit('input', t.returnValue);
429
+ }, 10);
430
+ },
431
+ isDateValid(dateStr) {
432
+ return typeof dateStr == 'string' && !isNaN(new Date(dateStr));
433
+ },
434
+ toggleSearch() {
435
+ this.searchOpen = !this.searchOpen;
436
+ if (this.searchOpen) {
437
+ this.$nextTick(() => {
438
+ this.$refs.searchField && this.$refs.searchField.focus();
439
+ });
440
+ }
441
+ },
442
+ onSearchBlur() {
443
+ if (!this.search) this.searchOpen = false;
444
+ },
445
+ buildHeader(item_0) {
446
+ var header = [];
447
+
448
+ if (item_0) {
449
+ _.forEach(item_0, (value, key) => {
450
+ if (_.isNumber(value)) header.push({ "el": "t-num", "title": key, "key": key, headerProps: { class: "divider" }, cellProps: { class: "divider" }, attr: { "v-model": "item." + key } });
451
+ else if (this.isDateValid(value)) header.push({ "el": "t-time", "title": key, "key": key, headerProps: { class: "divider" }, cellProps: { class: "divider" }, attr: { "v-model": "item." + key } });
452
+ else header.push({ "title": key, "key": key, headerProps: { class: "divider" }, cellProps: { class: "divider" }, });
453
+ });
454
+ }
455
+ console.log("Build Header: ", header)
456
+ return header;
457
+ },
458
+ buildHeaderData(item_0) {
459
+ var arrHeaders = this.buildHeader(item_0);
460
+ // NEW: chỉ tự thêm cột ctrl-update khi có khai báo edit hoặc delete API (đồng bộ với bản Vue 2)
461
+ var hasEditOrDelete = this.updateApi && (this.updateApi.edit || this.updateApi.delete);
462
+ if (arrHeaders.length > 0 && this.items && hasEditOrDelete && _.isBoolean(this.disabledUpdate) && !this.disabledUpdate) arrHeaders.push({ "title": "update", "key": "ctrl-update" });
463
+
464
+ return arrHeaders;
465
+ },
466
+ headerSetDefaultProps(col) {
467
+ _.forEach(col, (c) => {
468
+ if (c.divider) {
469
+ if (c.headerProps) {
470
+ if (c.headerProps.class) c.headerProps.class = c.headerProps.class + ' divider'
471
+ else c.headerProps.class = "divider";
472
+ }
473
+ else c.headerProps = { class: "divider" }
474
+
475
+ if (c.cellProps) {
476
+ if (c.cellProps.class) c.cellProps.class = c.cellProps.class + ' divider'
477
+ else c.cellProps.class = "divider";
478
+ }
479
+ else c.cellProps = { class: "divider" }
480
+
481
+ if (c.children) c.children = this.headerSetDefaultProps(c.children);
482
+ }
483
+ });
484
+ return col
485
+ },
486
+ itemSelectRebuild(newVal) {
487
+ var t = this;
488
+ if (t.returnObject) t.itemselected = newVal;
489
+ else {
490
+ t.itemselected = [];
491
+
492
+ _.forEach(newVal, function (val) {
493
+ t.itemselected.push(_.find(t.items, [t.itemKey, val]));
494
+ });
495
+ }
496
+ },
497
+ newBtnClick() {
498
+ if (this.newAction()) runAction(this.newAction());
499
+ if (this.newAPI()) {
500
+ Object.assign(this.editData, this.defaultItem());
501
+ this.editDialog = true;
502
+ }
503
+ },
504
+ editItem(item) {
505
+
506
+ if (this.editAction()) runAction(this.editAction(), { item: item });
507
+
508
+ if (this.editAPI()) {
509
+ this.editedIndex = this.items.indexOf(item);
510
+ Object.assign(this.editData, this.defaultItem());
511
+ this.editData = Object.assign({}, item);
512
+ this.editDialog = true;
513
+ }
514
+ },
515
+
516
+ deleteItem(item, deleteAPI) {
517
+ const items = this.items;
518
+ var actionAPI = deleteAPI;
519
+ var indexDelete = items.indexOf(item)
520
+ confirm({
521
+ title: (actionAPI.CONFIRM ? bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: item, item: item }, vueData)) : "Bạn có chắc chắn muốn xoá ?"),
522
+ action: function () {
523
+ vueAction(actionAPI, { $row: item, item: item }, function () {
524
+ items.splice(indexDelete, 1);
525
+ });
526
+ }
527
+ });
528
+ },
529
+
530
+ save(actionAPI) {
531
+ var diaCtl = this;
532
+ if (diaCtl.editedIndex > -1) {
533
+ if (actionAPI) {
534
+ if (actionAPI.CONFIRM) {
535
+ confirm({
536
+ title: bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: diaCtl.editData, item: diaCtl.editData }, vueData)),
537
+ content: '',
538
+ action: function () {
539
+ vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
540
+ if (!d.Message) {
541
+ if (_.isArray(d.data)) {
542
+ if (d.data[0]) {
543
+ _.forEach(d.data[0], function (value, key) {
544
+ diaCtl.editData[key] = value;
545
+ });
546
+ }
547
+ }
548
+ }
549
+ _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
550
+ diaCtl.editDialog = false;
551
+ });
552
+ },
553
+ cancel: function () { }
554
+ });
555
+ } else {
556
+ vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
557
+ if (!d.Message) {
558
+ if (_.isArray(d.data)) {
559
+ if (d.data[0]) {
560
+ _.forEach(d.data[0], function (value, key) {
561
+ diaCtl.editData[key] = value;
562
+ });
563
+ }
564
+ }
565
+ }
566
+ _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
567
+ diaCtl.editDialog = false;
568
+ });
569
+ }
570
+ } else {
571
+ _.assignIn(diaCtl.items[diaCtl.editedIndex], diaCtl.editData);
572
+ diaCtl.editDialog = false;
573
+ }
574
+ }
575
+ },
576
+
577
+ addnew(actionAPI) {
578
+ var diaCtl = this;
579
+ if (diaCtl.editedIndex == -1) {
580
+ if (actionAPI) {
581
+ if (actionAPI.CONFIRM) {
582
+ confirm({
583
+ title: bindDataToString(actionAPI.CONFIRM, _.assignIn({ $row: diaCtl.editData, item: diaCtl.editData }, vueData)),
584
+ action: function () {
585
+ vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
586
+ if (d) {
587
+ if (_.isArray(d.data)) {
588
+ if (d.data[0]) {
589
+ _.forEach(d.data[0], function (value, key) {
590
+ diaCtl.editData[key] = value;
591
+ });
592
+ }
593
+ }
594
+ }
595
+ diaCtl.items.push(_.cloneDeep(diaCtl.editData));
596
+ diaCtl.editDialog = false;
597
+ });
598
+ },
599
+ cancel: function () { }
600
+ });
601
+ } else {
602
+ vueAction(actionAPI, { $row: diaCtl.editData, item: diaCtl.editData }, function (d) {
603
+ if (d) {
604
+ if (_.isArray(d.data)) {
605
+ if (d.data[0]) {
606
+ _.forEach(d.data[0], function (value, key) {
607
+ diaCtl.editData[key] = value;
608
+ });
609
+ }
610
+ }
611
+ }
612
+ diaCtl.items.push(_.cloneDeep(diaCtl.editData));
613
+ diaCtl.editDialog = false;
614
+ });
615
+ }
616
+ } else {
617
+ diaCtl.items.push(_.cloneDeep(diaCtl.editData));
618
+ diaCtl.editDialog = false;
619
+ }
620
+ }
621
+ },
622
+ updateSearch: _.debounce(function (text) {
623
+ this.search = text;
624
+ }, 300),
625
+ customSearch(value, search, item) {
626
+ return Object.values(item.columns).some(v=>v&&v.toString().toLowerCase().includes(search.toLowerCase()))
627
+ },
628
+ // excel = true (mặc định): xuất nguyên items gốc — giữ nguyên hành vi cũ.
629
+ // excel = "UI": xuất đúng những gì bảng đang hiển thị (xem toExcelUI).
630
+ toExcel() {
631
+ if (typeof this.excel === 'string' && this.excel.trim().toUpperCase() === 'UI') return this.toExcelUI();
632
+ jsonToExcel({ data: this.items, filename: (this.label && this.label != '' ? this.label : 'tableFP') + '.xlsx' });
633
+ Vue.$toast.success("Đã xuất Excel", { position: 'top' });
634
+ },
635
+ onTableOptions(opt) {
636
+ this.tableOptions = opt;
637
+ },
638
+ //------------------------------------------------------------------ excel="UI"
639
+ // Lấy từ giao diện: DÒNG = items sau lọc (ô tìm kiếm) + sắp xếp (kể cả group-by), gồm MỌI trang;
640
+ // CỘT = header lá theo thứ tự hiển thị, tên cột = tiêu đề trên bảng. GIÁ TRỊ ô = dữ liệu gốc của
641
+ // field, KHÔNG định dạng (không numFmt, không đổi ngày, không bỏ HTML) — không có hàng tổng.
642
+ // Không đọc DOM: DOM chỉ chứa trang hiện tại, còn ô t-* là component nên không lấy lại được giá trị.
643
+ toExcelUI() {
644
+ var t = this;
645
+ var columns = t.uiExcelColumns();
646
+ if (!columns.length) { showMessage({ title: 'Bảng không có cột nào để xuất Excel' }); return; }
647
+ var rows = t.uiExcelRows(columns);
648
+ if (!rows.length) { showMessage({ title: 'Không có dòng nào đang hiển thị để xuất Excel' }); return; }
649
+ var name = t.plainText(t.label) || 'tableFP';
650
+ jsonToExcel({
651
+ data: rows,
652
+ columns: columns,
653
+ filename: name.replace(/[\\\/:*?"<>|]/g, '_') + '.xlsx',
654
+ worksheet: name.replace(/[\[\]:*?\/\\]/g, ' ').slice(0, 31).trim() || 'SheetFP'
655
+ });
656
+ Vue.$toast.success("Đã xuất Excel", { position: 'top' });
657
+ },
658
+ uiExcelColumns() {
659
+ var t = this, out = [];
660
+ var skip = { 'ctrl-update': 1, 'data-table-select': 1, 'data-table-group': 1, 'data-table-expand': 1 };
661
+ (function walk(list, prefix) {
662
+ _.forEach(list, function (h) {
663
+ if (!h || typeof h !== 'object') return;
664
+ var title = t.plainText(h.title !== undefined ? h.title : h.text);
665
+ var full = prefix && title ? prefix + ' / ' + title : (title || prefix);
666
+ if (Array.isArray(h.children) && h.children.length) { walk(h.children, full); return; }
667
+ var key = typeof h.key === 'string' ? h.key : (typeof h.value === 'string' ? h.value : null);
668
+ if (!key || skip[key]) return;
669
+ // Vuetify 3 lấy giá trị ô theo `value` (path) nếu có, không thì theo `key`
670
+ out.push(t.uiExcelColumn(h, key, typeof h.value === 'string' ? h.value : key, full || key));
671
+ });
672
+ })(t.headersArray, '');
673
+ return out;
674
+ },
675
+ uiExcelColumn(h, key, path, header) {
676
+ var attr = h.attr || {};
677
+ // Ô t-* hiển thị field mà v-model trỏ tới ("item.X"), không nhất thiết trùng key của cột
678
+ var vm = typeof attr['v-model'] === 'string' && /^\s*item\.([\w$.]+)\s*$/.exec(attr['v-model']);
679
+ var tablePath = path;
680
+ if (vm) path = vm[1];
681
+ var col = { header: header, key: path, tablePath: tablePath, sortKey: key, compare: typeof h.sort === 'function' ? h.sort : null };
682
+ var wm = /^\s*(\d+(?:\.\d+)?)\s*(px)?\s*$/.exec(h.width === undefined || h.width === null ? '' : String(h.width));
683
+ if (wm) col.width = Math.max(8, Math.round(Number(wm[1]) / 7));
684
+ return col;
685
+ },
686
+ uiExcelRows(columns) {
687
+ var t = this;
688
+ var rows = (t.items || []).slice();
689
+ if (t.search) {
690
+ // Dùng lại chính customSearch mà v-data-table đang dùng (item.columns = giá trị theo từng cột)
691
+ rows = rows.filter(function (r) {
692
+ var cols = {};
693
+ _.forEach(columns, function (c) { cols[c.sortKey] = t.readCell(r, c.tablePath); });
694
+ return t.customSearch(null, t.search, { columns: cols });
695
+ });
696
+ }
697
+ var spec = t.uiSortSpec();
698
+ if (spec.length) {
699
+ var byKey = _.keyBy(columns, 'sortKey');
700
+ var collator = t.uiCollator();
701
+ var fields = spec.map(function (s) {
702
+ var c = byKey[s.key];
703
+ return { path: c ? c.tablePath : s.key, desc: s.desc, compare: c && c.compare };
704
+ });
705
+ rows.sort(function (a, b) {
706
+ for (var i = 0; i < fields.length; i++) {
707
+ var f = fields[i];
708
+ var va = t.readCell(a, f.path), vb = t.readCell(b, f.path);
709
+ if (f.desc) { var tmp = va; va = vb; vb = tmp; }
710
+ var r = f.compare ? f.compare(va, vb) : t.compareCell(va, vb, collator);
711
+ if (r) return r;
712
+ }
713
+ return 0;
714
+ });
715
+ }
716
+ return rows;
717
+ },
718
+ // group-by đứng trước sort-by — đúng thứ tự v-data-table sắp dòng khi gộp nhóm.
719
+ // Chưa nhận update:options nào thì lấy từ attr khai sẵn.
720
+ uiSortSpec() {
721
+ var o = this.tableOptions, a = this.$attrs || {};
722
+ var group = o ? o.groupBy : (a['group-by'] || a.groupBy);
723
+ var sort = o ? o.sortBy : (a['sort-by'] || a.sortBy);
724
+ return this.normalizeSort(group).concat(this.normalizeSort(sort));
725
+ },
726
+ normalizeSort(list) {
727
+ return _.castArray(list === undefined || list === null ? [] : list).map(function (s) {
728
+ if (s && typeof s === 'object') return { key: s.key, desc: s.order === 'desc' || s.order === true };
729
+ return { key: s, desc: false };
730
+ }).filter(function (s) { return typeof s.key === 'string' && s.key !== ''; });
731
+ },
732
+ // Bám thuật toán sortItems của Vuetify: ngày so theo thời gian, rỗng lên đầu, hai chuỗi số so
733
+ // theo số, còn lại so bằng Intl.Collator (có dấu tiếng Việt).
734
+ compareCell(a, b, collator) {
735
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
736
+ var sa = a === null || a === undefined ? '' : String(a).toLocaleLowerCase();
737
+ var sb = b === null || b === undefined ? '' : String(b).toLocaleLowerCase();
738
+ if (sa === sb) return 0;
739
+ if (sa === '') return -1;
740
+ if (sb === '') return 1;
741
+ if (!isNaN(sa) && !isNaN(sb)) return Number(sa) - Number(sb);
742
+ return collator.compare(sa, sb);
743
+ },
744
+ uiCollator() {
745
+ var loc = _.get(this, '$vuetify.locale.current');
746
+ if (loc && typeof loc === 'object' && 'value' in loc) loc = loc.value;
747
+ try { return new Intl.Collator(loc || undefined, { sensitivity: 'accent', usage: 'sort' }); }
748
+ catch (e) { return new Intl.Collator(undefined, { sensitivity: 'accent', usage: 'sort' }); }
749
+ },
750
+ readCell(row, path) {
751
+ if (row === null || row === undefined) return null;
752
+ return row[path] !== undefined ? row[path] : _.get(row, path);
753
+ },
754
+ plainText(v) {
755
+ if (v === null || v === undefined) return '';
756
+ var s = String(v);
757
+ if (s.indexOf('<') < 0 && s.indexOf('&') < 0) return s.trim();
758
+ return (new DOMParser().parseFromString(s, 'text/html').body.textContent || '').trim();
759
+ },
760
+ }
761
+ });
762
+
763
+ //===========================================================================================
764
+
765
+ Vue.component('THtml', {
766
+ props: {
767
+ modelValue: {
768
+ default: null
769
+ },
770
+ },
771
+ template: `<div class="my-2" v-bind="$attrs" v-html="modelValue"></div>`,
772
+ });
773
+ //-----------------------------------------------------------------------------------------------------------
774
+ Vue.component('TLabel', {
775
+ props: {
776
+ modelValue: {
777
+ default: null
778
+ },
779
+ },
780
+ template: `<div class="text-no-wrap" v-bind="$attrs">{{modelValue}}</div>`,
781
+ });
782
+ //-----------------------------------------------------------------------------------------------------------
783
+ Vue.component('TNum', {
784
+ props: {
785
+ modelValue: {
786
+ type: Number,
787
+ default: 0
788
+ },
789
+ hidezero: {
790
+ type: Boolean,
791
+ default: true
792
+ },
793
+ format: {
794
+ type: String,
795
+ default: ''
796
+ }
797
+ },
798
+ template: `<div v-if="showData" class="text-no-wrap text-end" v-bind="$attrs">{{ format != ''? numeral(modelValue).format(format) : Number.isInteger(modelValue)?numeral(modelValue).format('0,0'): numeral(modelValue).format('0,0.0') }}</div>`,
799
+ data: function () {
800
+ return {
801
+ numeral: numeral
802
+ }
803
+ },
804
+ computed: {
805
+ showData: function () {
806
+ return !_.isNull(this.modelValue) && !(this.hidezero && this.modelValue == 0);
807
+ },
808
+ },
809
+ });
810
+ //-----------------------------------------------------------------------------------------------------------
811
+ Vue.component('TTime', {
812
+ props: {
813
+ format: {
814
+ type: String,
815
+ default: 'DD/MM/YYYY'
816
+ }
817
+ },
818
+ template: `<div v-if="$attrs['modelValue']" class="text-no-wrap" v-bind="$attrs">{{ dayjs($attrs['modelValue']).format(format) }}</div>`,
819
+ data: function () {
820
+ return {
821
+ dayjs: dayjs
822
+ }
823
+ },
824
+ });
825
+ //-----------------------------------------------------------------------------------------------------------
826
+ Vue.component('TCheck', {
827
+ props: {
828
+ modelValue: {
829
+ default: null
830
+ },
831
+ falseValue: {
832
+ default: false
833
+ },
834
+ },
835
+ template: `<v-checkbox v-bind="$attrs" v-model="ctlValue" :false-value="falseValue" @change="onchange($event)" class="d-inline-flex" hide-details />`,
836
+ data: function () {
837
+ return {
838
+ ctlValue: Boolean(this.modelValue)
839
+ }
840
+ },
841
+ watch: {
842
+ modelValue(newVal) {
843
+ this.ctlValue = Boolean(newVal)
844
+ },
845
+ },
846
+ methods: {
847
+ onchange: function (e) {
848
+ console.log("onchange", this)
849
+ tableActionEvent(this.$attrs);
850
+ }
851
+ }
852
+ });
853
+ //-----------------------------------------------------------------------------------------------------------
854
+ Vue.component('TSelect', {
855
+ props: ['modelValue'],
856
+ template: `<v-select v-model="ctlValue" v-bind="$attrs" @update:modelValue="onchange" variant="plain" density="compact" hide-details menu-icon="mdi-menu-swap"></v-select>`,
857
+ data: function () {
858
+ return {
859
+ ctlValue: this.modelValue
860
+ }
861
+ },
862
+ watch: {
863
+ modelValue(newVal) {
864
+ this.ctlValue = newVal
865
+ },
866
+ },
867
+ methods: {
868
+ onchange: function (e) {
869
+ tableActionEvent(this.$attrs);
870
+ }
871
+ }
872
+ });
873
+ //-----------------------------------------------------------------------------------------------------------
874
+ Vue.component('TCombobox', {
875
+ props: {
876
+ modelValue: {
877
+ default: undefined
878
+ },
879
+ text: {
880
+ default: undefined
881
+ },
882
+ items: {
883
+ type: Array,
884
+ default: []
885
+ },
886
+ api: {
887
+ type: String,
888
+ default: undefined
889
+ },
890
+ apiData: {
891
+ type: Object,
892
+ default: {}
893
+ },
894
+
895
+ },
896
+ template: `<v-autocomplete v-model="ctlValue" v-bind="$attrs" @update:modelValue="onchange" :loading="loading" :items="dataItems" @update:search="querySelections" auto-select-first density="compact" hide-details></v-autocomplete>`,
897
+ data: function () {
898
+ return {
899
+ dataItems: this.items,
900
+ ctlValue: this.modelValue,
901
+ loading: false,
902
+ getDataFormAPI: _.debounce((searchText) => {
903
+ if ((searchText && searchText.length < 2) || !this.api) return;
904
+ var self = this;
905
+ this.loading = true
906
+
907
+ if (typeof this.apiData != 'object') this.apiData = JSON.parse(this.apiData);
908
+
909
+ mapData(this.apiData, this.apiData, _.assignIn({}, vueData, { TEXT: searchText }));
910
+
911
+ ajaxCALL(this.api, this.apiData, function (d) {
912
+ if (_.isArray(d.data)) self.dataItems = d.data;
913
+ else self.dataItems = d;
914
+
915
+ self.loading = false
916
+ }, function (e) {
917
+ console.log("API Error", e);
918
+ })
919
+
920
+ }, 500)
921
+ }
922
+ },
923
+ watch: {
924
+ items(val) {
925
+ this.dataItems = val
926
+ },
927
+ modelValue(newVal) {
928
+ this.ctlValue = newVal
929
+ }
930
+ },
931
+ methods: {
932
+ querySelections(val) {
933
+ if (!val) return false
934
+ this.getDataFormAPI(val);
935
+ },
936
+ onchange: function (e) {
937
+ if (e) tableActionEvent(this.$attrs);
938
+ }
939
+ },
940
+ });
941
+ //-----------------------------------------------------------------------------------------------------------
942
+ Vue.component('TMenu', {
943
+ props: {
944
+ iconText: {
945
+ default: 'mdi-menu'
946
+ },
947
+ items: {
948
+ default: []
949
+ }
950
+ },
951
+ template: `
952
+ <v-menu offset-y origin="center center" transition="slide-y-transition" max-height = 500>
953
+ <template v-slot:activator="{ props }"><v-icon color="primary" v-bind="props" :icon="iconText"></v-icon></template>
954
+ <v-list >
955
+ <v-list-item v-bind="$attrs" v-for="(item, index) in items" :key="index" @click="onSelect(item)" >
956
+ <template v-slot:prepend><v-icon :color="item.color" :icon="item['icon-text']"></v-icon></template>
957
+ <v-list-item-title v-text="item.text"></v-list-item-title>
958
+ </v-list-item>
959
+ </v-list>
960
+ </v-menu>
961
+ `,
962
+ methods: {
963
+ onSelect: function (e) {
964
+ tableActionEvent({ action: e.action, ...this.$attrs });
965
+ }
966
+ }
967
+ });
968
+ //-----------------------------------------------------------------------------------------------------------
969
+ Vue.component('TLink', {
970
+ props: ['id', 'target', 'url', 'title', 'onclose'],
971
+ template: `<a :href="iURL" :target="target" v-bind="$attrs" @click="onclick()"><v-icon v-if="$attrs['icon-text']" left color="primary">{{$attrs['icon-text']}}</v-icon>{{$attrs['modelValue']}}</a>`,
972
+ data: function () {
973
+ return {}
974
+ },
975
+ computed: {
976
+ iURL: function () {
977
+ if (this.target != 'dialog' && this.url) return this.url;
978
+ return null;
979
+ },
980
+ },
981
+ methods: {
982
+ onclick: function (e) {
983
+ if (this.target == 'dialog') {
984
+ tableActionEvent(this.$attrs);
985
+ openWindow({
986
+ id: this.id,
987
+ title: this.title,
988
+ url: this.url,
989
+ onclose: this.onclose,
990
+ });
991
+ }
992
+ }
993
+ }
994
+ });
995
+ //-----------------------------------------------------------------------------------------------------------
996
+ Vue.component('TButton', {
997
+ props: {
998
+ label: {
999
+ default: null
1000
+ },
1001
+ variant: {
1002
+ default: "text"
1003
+ },
1004
+ color: {
1005
+ default: "primary"
1006
+ },
1007
+ wid: {
1008
+ default: null
1009
+ },
1010
+ url: {
1011
+ default: null
1012
+ },
1013
+ title: {
1014
+ default: null
1015
+ },
1016
+ density: {
1017
+ default: 'compact'
1018
+ },
1019
+ onclose: {
1020
+ default: null
1021
+ },
1022
+ },
1023
+ template: `<v-btn v-bind="$attrs" :variant="variant" :text="label" :color="color" :density="density" @click="clickButton"></v-btn>`,
1024
+ methods: {
1025
+ clickButton: function (e) {
1026
+ if (this.url) {
1027
+ console.log("dialog onclose", this.onclose)
1028
+ openWindow({
1029
+ id: this.wid,
1030
+ title: this.title,
1031
+ url: this.url,
1032
+ onclose: this.onclose
1033
+ });
1034
+ } else tableActionEvent(this.$attrs);
1035
+ }
1036
+ }
1037
+
1038
+ });