@fui-org/fui-cli 0.2.0 → 0.4.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 (212) hide show
  1. package/README.md +20 -2
  2. package/dist/fui.js +347 -44
  3. package/package.json +15 -3
  4. package/skills/fui/SKILL.md +149 -66
  5. package/skills/fui-skill/README.md +112 -0
  6. package/skills/fui-skill/SKILL.md +269 -0
  7. package/skills/fui-skill/assets/projectdefaultstyle.css +518 -0
  8. package/skills/fui-skill/design-md/airbnb/DESIGN.md +545 -0
  9. package/skills/fui-skill/design-md/airbnb/README.md +5 -0
  10. package/skills/fui-skill/design-md/airtable/DESIGN.md +554 -0
  11. package/skills/fui-skill/design-md/airtable/README.md +5 -0
  12. package/skills/fui-skill/design-md/apple/DESIGN.md +562 -0
  13. package/skills/fui-skill/design-md/apple/README.md +5 -0
  14. package/skills/fui-skill/design-md/asu/DESIGN.md +179 -0
  15. package/skills/fui-skill/design-md/asu/README.md +107 -0
  16. package/skills/fui-skill/design-md/binance/DESIGN.md +634 -0
  17. package/skills/fui-skill/design-md/binance/README.md +5 -0
  18. package/skills/fui-skill/design-md/bmw/DESIGN.md +544 -0
  19. package/skills/fui-skill/design-md/bmw/README.md +5 -0
  20. package/skills/fui-skill/design-md/bmw-m/DESIGN.md +503 -0
  21. package/skills/fui-skill/design-md/bmw-m/README.md +5 -0
  22. package/skills/fui-skill/design-md/bugatti/DESIGN.md +454 -0
  23. package/skills/fui-skill/design-md/bugatti/README.md +5 -0
  24. package/skills/fui-skill/design-md/cal/DESIGN.md +542 -0
  25. package/skills/fui-skill/design-md/cal/README.md +5 -0
  26. package/skills/fui-skill/design-md/claude/DESIGN.md +589 -0
  27. package/skills/fui-skill/design-md/claude/README.md +5 -0
  28. package/skills/fui-skill/design-md/clay/DESIGN.md +541 -0
  29. package/skills/fui-skill/design-md/clay/README.md +5 -0
  30. package/skills/fui-skill/design-md/clickhouse/DESIGN.md +544 -0
  31. package/skills/fui-skill/design-md/clickhouse/README.md +5 -0
  32. package/skills/fui-skill/design-md/cohere/DESIGN.md +451 -0
  33. package/skills/fui-skill/design-md/cohere/README.md +5 -0
  34. package/skills/fui-skill/design-md/coinbase/DESIGN.md +570 -0
  35. package/skills/fui-skill/design-md/coinbase/README.md +5 -0
  36. package/skills/fui-skill/design-md/composio/DESIGN.md +506 -0
  37. package/skills/fui-skill/design-md/composio/README.md +5 -0
  38. package/skills/fui-skill/design-md/cursor/DESIGN.md +537 -0
  39. package/skills/fui-skill/design-md/cursor/README.md +5 -0
  40. package/skills/fui-skill/design-md/elevenlabs/DESIGN.md +504 -0
  41. package/skills/fui-skill/design-md/elevenlabs/README.md +5 -0
  42. package/skills/fui-skill/design-md/expo/DESIGN.md +526 -0
  43. package/skills/fui-skill/design-md/expo/README.md +5 -0
  44. package/skills/fui-skill/design-md/ferrari/DESIGN.md +531 -0
  45. package/skills/fui-skill/design-md/ferrari/README.md +5 -0
  46. package/skills/fui-skill/design-md/figma/DESIGN.md +578 -0
  47. package/skills/fui-skill/design-md/figma/README.md +5 -0
  48. package/skills/fui-skill/design-md/framer/DESIGN.md +544 -0
  49. package/skills/fui-skill/design-md/framer/README.md +5 -0
  50. package/skills/fui-skill/design-md/fui/DESIGN.md +532 -0
  51. package/skills/fui-skill/design-md/hashicorp/DESIGN.md +575 -0
  52. package/skills/fui-skill/design-md/hashicorp/README.md +5 -0
  53. package/skills/fui-skill/design-md/ibm/DESIGN.md +550 -0
  54. package/skills/fui-skill/design-md/ibm/README.md +5 -0
  55. package/skills/fui-skill/design-md/intercom/DESIGN.md +546 -0
  56. package/skills/fui-skill/design-md/intercom/README.md +5 -0
  57. package/skills/fui-skill/design-md/kraken/DESIGN.md +125 -0
  58. package/skills/fui-skill/design-md/kraken/README.md +5 -0
  59. package/skills/fui-skill/design-md/lamborghini/DESIGN.md +288 -0
  60. package/skills/fui-skill/design-md/lamborghini/README.md +5 -0
  61. package/skills/fui-skill/design-md/linear.app/DESIGN.md +548 -0
  62. package/skills/fui-skill/design-md/linear.app/README.md +5 -0
  63. package/skills/fui-skill/design-md/lovable/DESIGN.md +298 -0
  64. package/skills/fui-skill/design-md/lovable/README.md +5 -0
  65. package/skills/fui-skill/design-md/mastercard/DESIGN.md +365 -0
  66. package/skills/fui-skill/design-md/mastercard/README.md +5 -0
  67. package/skills/fui-skill/design-md/meta/DESIGN.md +683 -0
  68. package/skills/fui-skill/design-md/meta/README.md +5 -0
  69. package/skills/fui-skill/design-md/minimax/DESIGN.md +746 -0
  70. package/skills/fui-skill/design-md/minimax/README.md +5 -0
  71. package/skills/fui-skill/design-md/mintlify/DESIGN.md +852 -0
  72. package/skills/fui-skill/design-md/mintlify/README.md +5 -0
  73. package/skills/fui-skill/design-md/miro/DESIGN.md +825 -0
  74. package/skills/fui-skill/design-md/miro/README.md +5 -0
  75. package/skills/fui-skill/design-md/mistral.ai/DESIGN.md +773 -0
  76. package/skills/fui-skill/design-md/mistral.ai/README.md +5 -0
  77. package/skills/fui-skill/design-md/mongodb/DESIGN.md +767 -0
  78. package/skills/fui-skill/design-md/mongodb/README.md +5 -0
  79. package/skills/fui-skill/design-md/nike/DESIGN.md +575 -0
  80. package/skills/fui-skill/design-md/nike/README.md +5 -0
  81. package/skills/fui-skill/design-md/notion/DESIGN.md +821 -0
  82. package/skills/fui-skill/design-md/notion/README.md +5 -0
  83. package/skills/fui-skill/design-md/nvidia/DESIGN.md +640 -0
  84. package/skills/fui-skill/design-md/nvidia/README.md +5 -0
  85. package/skills/fui-skill/design-md/ollama/DESIGN.md +539 -0
  86. package/skills/fui-skill/design-md/ollama/README.md +5 -0
  87. package/skills/fui-skill/design-md/opencode.ai/DESIGN.md +521 -0
  88. package/skills/fui-skill/design-md/opencode.ai/README.md +5 -0
  89. package/skills/fui-skill/design-md/pinterest/DESIGN.md +597 -0
  90. package/skills/fui-skill/design-md/pinterest/README.md +5 -0
  91. package/skills/fui-skill/design-md/playstation/DESIGN.md +661 -0
  92. package/skills/fui-skill/design-md/playstation/README.md +5 -0
  93. package/skills/fui-skill/design-md/posthog/DESIGN.md +690 -0
  94. package/skills/fui-skill/design-md/posthog/README.md +5 -0
  95. package/skills/fui-skill/design-md/raycast/DESIGN.md +669 -0
  96. package/skills/fui-skill/design-md/raycast/README.md +5 -0
  97. package/skills/fui-skill/design-md/renault/DESIGN.md +589 -0
  98. package/skills/fui-skill/design-md/renault/README.md +5 -0
  99. package/skills/fui-skill/design-md/replicate/DESIGN.md +616 -0
  100. package/skills/fui-skill/design-md/replicate/README.md +5 -0
  101. package/skills/fui-skill/design-md/resend/DESIGN.md +585 -0
  102. package/skills/fui-skill/design-md/resend/README.md +5 -0
  103. package/skills/fui-skill/design-md/revolut/DESIGN.md +636 -0
  104. package/skills/fui-skill/design-md/revolut/README.md +5 -0
  105. package/skills/fui-skill/design-md/runwayml/DESIGN.md +244 -0
  106. package/skills/fui-skill/design-md/runwayml/README.md +5 -0
  107. package/skills/fui-skill/design-md/sanity/DESIGN.md +357 -0
  108. package/skills/fui-skill/design-md/sanity/README.md +5 -0
  109. package/skills/fui-skill/design-md/sentry/DESIGN.md +551 -0
  110. package/skills/fui-skill/design-md/sentry/README.md +5 -0
  111. package/skills/fui-skill/design-md/shopify/DESIGN.md +516 -0
  112. package/skills/fui-skill/design-md/shopify/README.md +5 -0
  113. package/skills/fui-skill/design-md/slack/DESIGN.md +482 -0
  114. package/skills/fui-skill/design-md/spacex/DESIGN.md +363 -0
  115. package/skills/fui-skill/design-md/spacex/README.md +5 -0
  116. package/skills/fui-skill/design-md/spotify/DESIGN.md +246 -0
  117. package/skills/fui-skill/design-md/spotify/README.md +5 -0
  118. package/skills/fui-skill/design-md/starbucks/DESIGN.md +580 -0
  119. package/skills/fui-skill/design-md/starbucks/README.md +5 -0
  120. package/skills/fui-skill/design-md/stripe/DESIGN.md +487 -0
  121. package/skills/fui-skill/design-md/stripe/README.md +5 -0
  122. package/skills/fui-skill/design-md/supabase/DESIGN.md +462 -0
  123. package/skills/fui-skill/design-md/supabase/README.md +5 -0
  124. package/skills/fui-skill/design-md/superhuman/DESIGN.md +448 -0
  125. package/skills/fui-skill/design-md/superhuman/README.md +5 -0
  126. package/skills/fui-skill/design-md/tesla/DESIGN.md +286 -0
  127. package/skills/fui-skill/design-md/tesla/README.md +5 -0
  128. package/skills/fui-skill/design-md/theverge/DESIGN.md +339 -0
  129. package/skills/fui-skill/design-md/theverge/README.md +5 -0
  130. package/skills/fui-skill/design-md/together.ai/DESIGN.md +633 -0
  131. package/skills/fui-skill/design-md/together.ai/README.md +5 -0
  132. package/skills/fui-skill/design-md/uber/DESIGN.md +636 -0
  133. package/skills/fui-skill/design-md/uber/README.md +5 -0
  134. package/skills/fui-skill/design-md/vercel/DESIGN.md +736 -0
  135. package/skills/fui-skill/design-md/vercel/README.md +5 -0
  136. package/skills/fui-skill/design-md/vodafone/DESIGN.md +538 -0
  137. package/skills/fui-skill/design-md/vodafone/README.md +5 -0
  138. package/skills/fui-skill/design-md/voltagent/DESIGN.md +521 -0
  139. package/skills/fui-skill/design-md/voltagent/README.md +5 -0
  140. package/skills/fui-skill/design-md/warp/DESIGN.md +526 -0
  141. package/skills/fui-skill/design-md/warp/README.md +5 -0
  142. package/skills/fui-skill/design-md/webflow/DESIGN.md +588 -0
  143. package/skills/fui-skill/design-md/webflow/README.md +5 -0
  144. package/skills/fui-skill/design-md/wired/DESIGN.md +497 -0
  145. package/skills/fui-skill/design-md/wired/README.md +5 -0
  146. package/skills/fui-skill/design-md/wise/DESIGN.md +544 -0
  147. package/skills/fui-skill/design-md/wise/README.md +5 -0
  148. package/skills/fui-skill/design-md/x.ai/DESIGN.md +465 -0
  149. package/skills/fui-skill/design-md/x.ai/README.md +5 -0
  150. package/skills/fui-skill/design-md/zapier/DESIGN.md +537 -0
  151. package/skills/fui-skill/design-md/zapier/README.md +5 -0
  152. package/skills/fui-skill/examples/component.vue +162 -0
  153. package/skills/fui-skill/examples/f-table-patterns.json +331 -0
  154. package/skills/fui-skill/examples/module-patterns.json +973 -0
  155. package/skills/fui-skill/examples/project-patterns.json +222 -0
  156. package/skills/fui-skill/metadata.json +75 -0
  157. package/skills/fui-skill/references/INDEX.md +144 -0
  158. package/skills/fui-skill/references/advanced-techniques.md +160 -0
  159. package/skills/fui-skill/references/coding-standards.md +112 -0
  160. package/skills/fui-skill/references/component-design.md +455 -0
  161. package/skills/fui-skill/references/component-quickref.md +77 -0
  162. package/skills/fui-skill/references/component-table.md +276 -0
  163. package/skills/fui-skill/references/components-dialog.md +192 -0
  164. package/skills/fui-skill/references/components-display.md +147 -0
  165. package/skills/fui-skill/references/components-echart.md +391 -0
  166. package/skills/fui-skill/references/components-input.md +359 -0
  167. package/skills/fui-skill/references/controls-patterns.md +847 -0
  168. package/skills/fui-skill/references/controls-styling-vocabulary.md +140 -0
  169. package/skills/fui-skill/references/db-table-design.md +77 -0
  170. package/skills/fui-skill/references/db-workflow.md +504 -0
  171. package/skills/fui-skill/references/default-function.md +415 -0
  172. package/skills/fui-skill/references/design-modes.md +85 -0
  173. package/skills/fui-skill/references/echart-templates.md +481 -0
  174. package/skills/fui-skill/references/fastproject.md +97 -0
  175. package/skills/fui-skill/references/fsheet.md +218 -0
  176. package/skills/fui-skill/references/fullstack-workflow.md +351 -0
  177. package/skills/fui-skill/references/module-data-patterns.md +126 -0
  178. package/skills/fui-skill/references/module-json-anatomy.md +137 -0
  179. package/skills/fui-skill/references/module-structure.md +260 -0
  180. package/skills/fui-skill/references/new-session.md +108 -0
  181. package/skills/fui-skill/references/pdfmake.md +60 -0
  182. package/skills/fui-skill/references/permission-system.md +169 -0
  183. package/skills/fui-skill/references/platform-architecture.md +294 -0
  184. package/skills/fui-skill/references/project-config.md +335 -0
  185. package/skills/fui-skill/references/project-provisioning.md +383 -0
  186. package/skills/fui-skill/references/script-map.md +296 -0
  187. package/skills/fui-skill/references/sql-clr-functions.md +224 -0
  188. package/skills/fui-skill/references/system-design.md +116 -0
  189. package/skills/fui-skill/references/tapi-file-api.md +191 -0
  190. package/skills/fui-skill/references/tapi-permission-patterns.md +158 -0
  191. package/skills/fui-skill/references/tapi-reference.md +549 -0
  192. package/skills/fui-skill/references/tools-registry.md +460 -0
  193. package/skills/fui-skill/references/ui-crosswindow-patterns.md +317 -0
  194. package/skills/fui-skill/references/ui-dialog-patterns.md +229 -0
  195. package/skills/fui-skill/references/ui-layout-patterns.md +176 -0
  196. package/skills/fui-skill/references/ui-patterns.md +315 -0
  197. package/skills/fui-skill/references/ui-screenshot-review.md +94 -0
  198. package/skills/fui-skill/references/ui-table-cell-patterns.md +316 -0
  199. package/skills/fui-skill/references/ui-templates.md +29 -0
  200. package/skills/fui-skill/references/verification.md +246 -0
  201. package/skills/fui-skill/references/watcher-patterns.md +196 -0
  202. package/skills/fui-skill/references/websocket-realtime.md +254 -0
  203. package/skills/fui-skill/scripts/component-3.0.js +2549 -0
  204. package/skills/fui-skill/scripts/component.js +3142 -0
  205. package/skills/fui-skill/scripts/componentTable-3.0.js +909 -0
  206. package/skills/fui-skill/scripts/componentTable.js +769 -0
  207. package/skills/fui-skill/scripts/defaultfunction-3.0.js +781 -0
  208. package/skills/fui-skill/scripts/defaultfunction.js +966 -0
  209. package/skills/fui-skill/scripts/fastproject-3.0.js +870 -0
  210. package/skills/fui-skill/scripts/fastproject.js +828 -0
  211. package/skills/fui-skill/scripts/fechart.js +890 -0
  212. package/skills/fui-skill/scripts/fsheet.js +1330 -0
@@ -0,0 +1,3142 @@
1
+ var defaultControlAttr = {
2
+ "v-text-field": {
3
+ attr: {
4
+ ":tile": true,
5
+ ":outlined": true,
6
+ ":dense": true,
7
+ ":required": true,
8
+ ":hide-details": true,
9
+ ":rules": "[v => (!!v || !isNaN(parseFloat(v)))]"
10
+ }
11
+ },
12
+ "v-textarea": {
13
+ attr: {
14
+ ":outlined": true,
15
+ ":dense": true,
16
+ ":required": false,
17
+ ":rules": "[v => (!!v || !isNaN(parseFloat(v)))]"
18
+ }
19
+ },
20
+ "v-combobox": {
21
+ attr: {
22
+ ":tile": true,
23
+ ":outlined": true,
24
+ ":dense": true,
25
+ ":hide-details": true,
26
+ ":required": true,
27
+ ":rules": "[v => (!!v || !isNaN(parseFloat(v)))]",
28
+ ":menu-props": "{ offsetY: true }"
29
+ }
30
+ },
31
+ "v-select": {
32
+ attr: {
33
+ ":tile": true,
34
+ ":outlined": true,
35
+ ":dense": true,
36
+ ":hide-details": true,
37
+ ":required": true,
38
+ ":rules": "[v => (!!v || !isNaN(parseFloat(v)))]",
39
+ ":menu-props": "{ offsetY: true }"
40
+ }
41
+ },
42
+ "v-autocomplete": {
43
+ attr: {
44
+ ":outlined": true,
45
+ ":dense": true,
46
+ ":hide-details": true,
47
+ ":required": true,
48
+ ":rules": "[v => (!!v || !isNaN(parseFloat(v)))]",
49
+ ":auto-select-first": true
50
+ }
51
+ },
52
+ "v-checkbox": {
53
+ attr: {
54
+ "class": "mt-1",
55
+ ":hide-details": true
56
+ }
57
+ },
58
+ "v-switch": {
59
+ attr: {
60
+ ":dense": true,
61
+ ":hide-details": true,
62
+ "class": "my-1 mx-0"
63
+ }
64
+ },
65
+ "v-card": {
66
+ attr: {
67
+ ":outlined": true
68
+ }
69
+ },
70
+ "v-alert": {
71
+ attr: {
72
+ ":dense": true,
73
+ ":hide-details": true,
74
+ }
75
+ },
76
+ "v-jsoneditor": {
77
+ import: ["/include/jsonEditor/v-jsoneditor.min.js"]
78
+ },
79
+
80
+ "v-btn": {
81
+ attr: {
82
+ "elevation": 0
83
+ }
84
+ },
85
+ //----------------------------------------------
86
+
87
+ "f-button": {
88
+ col: {
89
+ align: ($isPhone ? "end" : null)
90
+ },
91
+ attr: {
92
+ "elevation": 0,
93
+ "color": "primary",
94
+ }
95
+ },
96
+
97
+ "f-table": {
98
+ attr: {
99
+ ":excel": false,
100
+ ":show-search": true,
101
+ ":return-object": true,
102
+ ":allow-hover": false,
103
+ ":fixed-header": true,
104
+ ":disable-sort": false,
105
+ ":dense": true,
106
+ ":hide-default-footer": true,
107
+ ":items-per-page": 50,
108
+ ":footer-props": "{'items-per-page-options':[50, 100, 150, 200, -1]}",
109
+ "mobile-breakpoint": "0",
110
+ },
111
+ },
112
+ "f-dialog": {
113
+ attr: {
114
+ ":outlined": true,
115
+ ":dense": true,
116
+ },
117
+ },
118
+ "f-date": {
119
+ attr: {
120
+ ":picker": {
121
+ "locale": "vi"
122
+ },
123
+ ":hide-details": true,
124
+ ":outlined": true,
125
+ ":dense": true,
126
+ }
127
+ },
128
+
129
+ "f-time": {
130
+ attr: {
131
+ ":hide-details": true,
132
+ ":outlined": true,
133
+ ":dense": true,
134
+ }
135
+ },
136
+ "f-radiobox": {
137
+ attr: {
138
+ "class": "mt-1",
139
+ ":hide-details": true,
140
+ ":required": true,
141
+ ":rules": "[v => (!!v || !isNaN(parseFloat(v)))]"
142
+ }
143
+ },
144
+
145
+ "f-menu": {
146
+ col: {
147
+ align: ($isPhone ? "end" : null)
148
+ },
149
+ attr: {
150
+ "elevation": "1",
151
+ ":depressed": true,
152
+ "color": "primary"
153
+ }
154
+ },
155
+ "f-sheet": {
156
+ import: ["/include/ag-grid/ag-grid-community.min.js", "/include/ag-grid/fsheet.js"]
157
+ },
158
+ "f-pdfmake": {
159
+ attr: {
160
+ "width": "100%",
161
+ "height": "500px"
162
+ },
163
+ import: ['/include/pdfmake/pdfmake.min.js', '/include/pdfmake/print.min.js', '/include/pdfmake/vfs_fonts.js']
164
+ },
165
+ "f-account-manager": {
166
+ import: ["/include/client.min.js", "/component_login.js"]
167
+ },
168
+ "f-chart": {
169
+ import: ["/include/chart/chart.min.js"]
170
+ },
171
+ "f-echart": {
172
+ import: ["/include/chart/echarts.min.js", "/include/chart/fechart.js"]
173
+ },
174
+ "f-json-tree": {
175
+ import: ["/include/jsontree/jsontreeview.css", "/include/jsontree/jsontreeview.js"]
176
+ },
177
+ "f-code-editor": {
178
+ import: [
179
+ "/include/monaco-editor/min/vs/loader.js",
180
+ ]
181
+ },
182
+ "f-box": {
183
+ attr: {
184
+ ":outlined": true,
185
+ }
186
+ },
187
+ "f-label": {
188
+ attr: {
189
+ ":outlined": true
190
+ }
191
+ },
192
+ "f-header": {
193
+ attr: {
194
+ }
195
+ },
196
+ "f-title": {
197
+ attr: {
198
+ }
199
+ },
200
+ "f-file-upload": {
201
+ attr: {
202
+ "elevation": 1,
203
+ ":depressed": true,
204
+ "color": "primary"
205
+ },
206
+ import: [
207
+ "/include/plupload-2.3.7/plupload.full.min.js",
208
+ ],
209
+ },
210
+ "f-image-update": {
211
+ attr: {
212
+ "elevation": 1,
213
+ "color": "primary",
214
+ ":text": true,
215
+ },
216
+ import: [
217
+ "/include/vue-advanced-cropper/index.umd.js",
218
+ "/include/vue-advanced-cropper/style.css",
219
+ ],
220
+ },
221
+ "f-qrcode": {
222
+ attr: {
223
+ },
224
+ import: [
225
+ "/include/qrcode/qrcode.min.js",
226
+ ],
227
+ },
228
+ "f-qrcode-reader": {
229
+ attr: {
230
+ },
231
+ import: [
232
+ "/include/qrcode/qrcode-reader.min.js",
233
+ ],
234
+ },
235
+ "f-search": {
236
+ attr: {
237
+ ":cache-items": true,
238
+ ":outlined": true,
239
+ ":dense": true,
240
+ ":hide-no-data": true,
241
+ ":hide-details": true,
242
+ ":auto-select-first": true
243
+ }
244
+ },
245
+ "f-q-and-a": {
246
+ import: [
247
+ "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js",
248
+ "/include/QandA/q-and-a-ckedit.css",
249
+ "/include/vue-context-menu.js",
250
+ "/include/ckeditor5/build/ckeditor.js",
251
+ "/include/plupload-2.3.7/plupload.full.min.js",
252
+ "/include/audio/vueaudio.js",
253
+ "/include/QandA/QandA.js",
254
+ ],
255
+ },
256
+ "f-etest-review": {
257
+ import: [
258
+ "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js",
259
+ "/include/QandA/q-and-a-ckedit.css",
260
+ "/include/vue-context-menu.js",
261
+ "/include/audio/vueaudio.js",
262
+ "/include/QandA/QandA.js",
263
+ ],
264
+ },
265
+ "f-audio": {
266
+ import: [
267
+ "/include/audio/vueaudio.js",
268
+ ],
269
+ },
270
+ "f-audio-recorder": {
271
+ import: [
272
+ "/include/audio/vueaudio.js",
273
+ ],
274
+ },
275
+ "f-editor": {
276
+ import: [
277
+ "/include/ckeditor5/build/ckeditor.js"
278
+ ],
279
+ },
280
+ "f-editor-dialog": {
281
+ import: [
282
+ "/include/ckeditor5/build/ckeditor.js"
283
+ ],
284
+ },
285
+ "f-excel-reader": {
286
+ col: {
287
+ align: ($isPhone ? "end" : null),
288
+ },
289
+ attr: {
290
+ "elevation": "1",
291
+ ":depressed": true,
292
+ "color": "primary",
293
+ },
294
+ },
295
+ "f-google-login": {
296
+ import: [
297
+ "/component_login.js",
298
+ "https://apis.google.com/js/platform.js"
299
+ ],
300
+ }
301
+ };
302
+
303
+ //===========================================================================================
304
+ Vue.component('f-label', {
305
+ props: ['items', "text", "color", 'iconText'],
306
+ template: `
307
+ <div>
308
+ <v-chip v-bind="$attrs" class="mr-2" v-if="text">
309
+ <v-icon v-if="iconText" left>{{iconText}}</v-icon>{{text}}
310
+ </v-chip>
311
+ <v-chip v-bind="$attrs" v-for="(chip, k) in (_.isString(items)?JSON.parse(items):items)" :key="k" :color="(color ? color : chip.color)" class="mr-2" >
312
+ <v-icon v-if="chip['icon-text']" left>{{chip['icon-text']}}</v-icon>{{chip.text}}
313
+ </v-chip>
314
+ </div>
315
+ `
316
+ });
317
+
318
+ //===========================================================================================
319
+ Vue.component('f-box', {
320
+ props: ['rowFormat', 'items', 'label', 'edit', 'saveclick'],
321
+ template: `
322
+ <v-card v-bind="$attrs">
323
+ <v-list-item>
324
+ <v-list-item-content>
325
+ <v-list-item-title v-if="label" class="headline mb-2">{{label}}</v-list-item-title><br>
326
+ <table style="flex: initial; width:100%;" v-for="R in itemsObj">
327
+ <tr v-for="c in rowFormatArray">
328
+ <td class="py-1 text-no-wrap text-left">{{c.text}}</td>
329
+ <td class="py-1 px-2 text-center"><span v-if="!(edit && !c.noEdit)" >:</span></td>
330
+ <td v-if="!(edit && !c.noEdit)" class="py-1 text-left" style="width: 100%"><b>{{R[c.value]}}</b></td>
331
+ <td v-if="edit && !c.noEdit" class="py-1 text-left" style="width: 100%">
332
+ <v-text-field dense outlined hide-details v-model="R[c.value]"></v-text-field>
333
+ </td>
334
+ </tr>
335
+ </table>
336
+ </v-list-item-content>
337
+ </v-list-item>
338
+ </v-card>
339
+ `,
340
+ data: function () {
341
+ return {
342
+ rowFormatArray: [],
343
+ itemsObj: [],
344
+ itemsIsArray: _.isArray(this.items)
345
+ }
346
+ },
347
+ mounted: function () {
348
+ this.buildDataItems();
349
+ },
350
+ watch: {
351
+ items: function (newVal, oldVal) {
352
+ this.buildDataItems();
353
+ },
354
+ rowFormat: function (newVal, oldVal) {
355
+ this.buildDataItems();
356
+ },
357
+ itemsObj: function (newVal, oldVal) {
358
+ if (this.itemsIsArray) this.$emit('input', this.itemsObj);
359
+ else if (this.itemsObj.length > 0) this.$emit('input', this.itemsObj[0]);
360
+ else this.$emit('input', null);
361
+ }
362
+ },
363
+ methods: {
364
+ buildDataItems: function () {
365
+ if (this.items) {
366
+ if (!_.isArray(this.items)) this.itemsObj = _.castArray(this.items);
367
+ else this.itemsObj = this.items;
368
+ }
369
+
370
+ if (this.rowFormat) this.rowFormatArray = this.rowFormat;
371
+ else this.rowFormatArray = buildHeader(this.itemsObj);
372
+ }
373
+ }
374
+ });
375
+ //===========================================================================================
376
+
377
+ Vue.component('f-header', {
378
+ props: ['label'],
379
+ template: `
380
+ <div class="display-1 text-center primary--text text--darken-2" v-html="label"></div>
381
+ `
382
+ });
383
+
384
+ //===========================================================================================
385
+
386
+ Vue.component('f-title', {
387
+ props: ['label'],
388
+ template: `
389
+ <div class="title primary--text text--darken-1" v-html="label"></div>
390
+ `
391
+ });
392
+
393
+ //===========================================================================================
394
+
395
+ Vue.component('f-radiobox', {
396
+ props: {
397
+ label: {
398
+ type: String,
399
+ default: undefined
400
+ },
401
+ items: {
402
+ type: Array,
403
+ default: []
404
+ },
405
+ itemValue: {
406
+ type: String,
407
+ default: 'value'
408
+ },
409
+ itemText: {
410
+ type: String,
411
+ default: 'text'
412
+ }
413
+ },
414
+ template: `
415
+ <v-radio-group v-bind="$attrs" v-on:change="$emit('input', $event)">
416
+ <template v-slot:label>
417
+ <div class="text-body-1">{{label}}</div>
418
+ </template>
419
+ <v-radio v-for="n in items" :key="n[itemValue]" :label="n[itemText]" :value="n[itemValue]"></v-radio>
420
+ </v-radio-group>
421
+ `,
422
+ });
423
+ //===========================================================================================
424
+
425
+ Vue.component('f-menu', {
426
+ props: ['items', 'label', 'iconText', 'menuAttr'],
427
+ template: `
428
+ <v-menu offset-y v-bind="$attrs">
429
+ <template v-slot:activator="{ on, attrs }">
430
+ <v-btn v-on="on" :icon="label=='' || label==undefined" color="primary" :outlined="label" :text="label"><v-icon v-if="iconText" :left="label">{{iconText}}</v-icon>{{label}}</v-btn>
431
+ </template>
432
+ <v-list>
433
+ <v-list-item v-for="(item, index) in dataList" v-if="item.visible" :key="index" v-on:click="buttonclick(item)" :href="item.href" :target="item.target">
434
+ <v-list-item-icon v-if="item['icon-text']" class="mr-3">
435
+ <v-icon :color="item.color">{{item['icon-text']}}</v-icon>
436
+ </v-list-item-icon>
437
+ <v-list-item-content class="mr-2">
438
+ <v-list-item-title v-text="item.text"></v-list-item-title>
439
+ </v-list-item-content>
440
+ </v-list-item>
441
+ </v-list>
442
+ </v-menu>
443
+ `,
444
+ data: function () {
445
+ return {
446
+ dataList: this.dataFix(this.items)
447
+ }
448
+ },
449
+ watch: {
450
+ items: function (newVal) {
451
+ this.dataList = this.dataFix(newVal)
452
+ }
453
+ },
454
+ methods: {
455
+ buttonclick: function (e) {
456
+ if (e.action) runAction(e.action);
457
+ },
458
+ dataFix: function (d) {
459
+ _.forEach(d, function (o) {
460
+ if (_.isUndefined(o.visible)) {
461
+ o.visible = true;
462
+ }
463
+ mapData(o, o, vueData);
464
+ });
465
+ return d;
466
+ }
467
+
468
+ }
469
+ });
470
+
471
+ //===========================================================================================
472
+ Vue.component('f-search', {
473
+ props: {
474
+ api: {
475
+ type: String,
476
+ default: undefined
477
+ },
478
+ apiData: {
479
+ type: Object,
480
+ default: {}
481
+ },
482
+ items: {
483
+ type: Array,
484
+ default: []
485
+ },
486
+ },
487
+ template: `
488
+ <v-autocomplete v-bind="$attrs" :loading="loading" :items="dataItems" :search-input.sync="search" @change="onChange"></v-autocomplete>
489
+ `,
490
+ data: function () {
491
+ return {
492
+ loading: false,
493
+ dataItems: this.items,
494
+ search: null,
495
+ getDataFormAPI: _.debounce((str) => {
496
+ if (this.search && this.search.length < 2) return;
497
+ var self = this;
498
+ this.loading = true
499
+
500
+ if (typeof this.apiData != 'object') this.apiData = JSON.parse(this.apiData);
501
+
502
+ mapData(this.apiData, this.apiData, _.assignIn({}, vueData, { TEXT: this.search }));
503
+
504
+ ajaxCALL(this.api, this.apiData, function (d) {
505
+ if (_.isArray(d.data)) self.dataItems = d.data;
506
+ else self.dataItems = d;
507
+
508
+ self.loading = false
509
+ }, function (e) {
510
+ console.log("API Error", e);
511
+ })
512
+
513
+ }, 500)
514
+ }
515
+ },
516
+ watch: {
517
+ search(val) {
518
+ val && val !== this.select && this.querySelections(val)
519
+ },
520
+ items(val) {
521
+ this.dataItems = val
522
+ },
523
+ },
524
+ methods: {
525
+ querySelections(val) {
526
+ this.getDataFormAPI(val);
527
+ },
528
+ onChange(e) {
529
+ this.$emit('input', e);
530
+ this.search = "";
531
+ },
532
+ },
533
+ });
534
+ //===========================================================================================
535
+
536
+ Vue.component('f-button', {
537
+ props: {
538
+ label: {
539
+ type: String,
540
+ default: 'OK'
541
+ },
542
+ checkvalid: {
543
+ type: Boolean,
544
+ default: true
545
+ },
546
+ ctrlhotkey: {
547
+ type: Number,
548
+ default: 0
549
+ },
550
+ iconText: {
551
+ default: undefined
552
+ },
553
+ action: {
554
+ default: undefined
555
+ },
556
+ includeData: {
557
+ default: undefined
558
+ }
559
+ },
560
+ template: `
561
+ <v-btn v-on:click="buttonclick" :icon="(iconText && !label?true:false)" v-bind="$attrs" :style="{'min-width': (label?'100px':'auto')}">
562
+ <v-icon v-if="iconText" :left="(label?true:false)">{{iconText}}</v-icon>{{label}}
563
+ </v-btn>
564
+ `,
565
+ mounted: function () {
566
+ if (this.ctrlhotkey > 0) {
567
+ document.addEventListener("keydown", this.keydownHotKey, false);
568
+ }
569
+ },
570
+ destroyed() {
571
+ document.removeEventListener("keydown", this.keydownHotKey, false);
572
+ },
573
+ methods: {
574
+ buttonclick: _.debounce(function (e) {
575
+ var attr = this.$attrs;
576
+ if (attr.target == 'dialog') {
577
+ console.log("dialog onclose", attr.onclose)
578
+ openWindow({
579
+ id: attr.wid,
580
+ title: attr.title,
581
+ url: attr.url,
582
+ onclose: attr.onclose
583
+ });
584
+ return
585
+ }
586
+
587
+ if (this.checkvalid && !vm.formvalidate()) {
588
+ if (attr.type != 'submit') showMessage({ icon: 'mdi mdi-alert-outline', type: "red", title: "Hãy điền đầy đủ dữ liệu bắt buộc !" })
589
+ return;
590
+ }
591
+
592
+ if (this.action) runAction(this.action, this.includeData);
593
+ else console.log("action:", this.action);
594
+
595
+ }, 500, { leading: true, trailing: false }),
596
+ keydownHotKey: (e) => {
597
+ if ((window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == this.ctrlhotkey) {
598
+ e.preventDefault();
599
+ this.buttonclick();
600
+ console.log("Button hotkey :", this.label);
601
+ }
602
+ }
603
+ }
604
+ });
605
+ //===========================================================================================
606
+
607
+ Vue.component('f-slider', {
608
+ props: ['items', 'itemAttr'],
609
+ template: `
610
+ <v-carousel v-bind="$attrs">
611
+ <v-carousel-item v-for="(item,i) in items" :key="i" :src="item.src" v-bind="itemAttr"></v-carousel-item>
612
+ </v-carousel>
613
+ `,
614
+ });
615
+
616
+ //===========================================================================================
617
+
618
+ Vue.component('f-file-upload', {
619
+ props: {
620
+ label: {
621
+ type: String,
622
+ default: 'file upload'
623
+ },
624
+ url: {
625
+ type: String,
626
+ default: ''
627
+ },
628
+ autoclose: {
629
+ type: Boolean,
630
+ default: true
631
+ },
632
+ iconText: {
633
+ type: String,
634
+ default: "mdi-cloud-upload-outline"
635
+ },
636
+ filters: {
637
+ type: Object,
638
+ default: {
639
+ max_file_size: '5mb',
640
+ mime_types: [{ title: "All files", extensions: "*" }]
641
+ }
642
+ },
643
+ resize: {
644
+ type: Object,
645
+ default: {
646
+ width: 1900,
647
+ height: 1600,
648
+ quality: 80
649
+ }
650
+ },
651
+ onclose: {
652
+ default: undefined
653
+ },
654
+
655
+ },
656
+ template: `
657
+ <v-btn :id="elID" v-bind="$attrs">
658
+ <v-icon v-if="iconText" :left="label != ''">{{iconText}}</v-icon>{{label}}
659
+ <v-dialog v-model="dialog" persistent width="450">
660
+ <v-card>
661
+ <v-card-actions>
662
+ Upload file
663
+ <v-spacer></v-spacer>
664
+ <v-btn :loading="loading" icon @click="dialog = false; UploadingError = []; UploadingFile = [];" ><v-icon>mdi-close</v-icon></v-btn>
665
+ </v-card-actions>
666
+ <v-sheet v-if="UploadingError.length>0" color="error" dark class="py-2">
667
+ <v-card-text class="py-1 px-4" v-for="(t, i) in UploadingError" :key="i">
668
+ <div><b>{{t.file.name}}</b></div>
669
+ <div>{{t.message}}</div>
670
+ </v-card-text>
671
+ </v-sheet>
672
+ <v-sheet v-if="UploadingFile.length>0" class="pb-2">
673
+ <v-card-text class="py-1 px-4" v-for="(t, i) in UploadingFile" :key="i">
674
+ <v-card-subtitle class="pa-0" >{{t.percent}}% - {{t.name}}</v-card-subtitle>
675
+ <v-progress-linear :value="t.percent" class="mb-2 mt-1"></v-progress-linear>
676
+ </v-card-text>
677
+ </v-sheet>
678
+ </v-card>
679
+ </v-dialog>
680
+ </v-btn>
681
+ `,
682
+ data: function () {
683
+ return {
684
+ dialog: false,
685
+ loading: true,
686
+ UploadingFile: [],
687
+ UploadingError: [],
688
+ elID: generateID(),
689
+ uploader: null,
690
+ filesUploaded: []
691
+ }
692
+ },
693
+ mounted: function () {
694
+ this.uploaderInit();
695
+ },
696
+ methods: {
697
+ uploaderInit: function () {
698
+ var thisObj = this;
699
+
700
+ var uploader = new plupload.Uploader({
701
+ runtimes: 'html5,html4',
702
+ browse_button: thisObj.elID,
703
+ headers: {
704
+ authorization: 'Bearer ' + getCookie('awt'),
705
+ uploaddata: Date.now()
706
+ },
707
+ resize: thisObj.resize,
708
+ filters: thisObj.filters,
709
+ init: {
710
+ PostInit: function () { },
711
+ OptionChanged: function (up, name, value, oldValue) { },
712
+ BeforeUpload: function (up, file) {
713
+ uploader.settings.url = thisObj.url;
714
+ },
715
+ FilesAdded: function (up, files) {
716
+ thisObj.dialog = true;
717
+ thisObj.UploadingFile = files;
718
+ thisObj.filesUploaded = [];
719
+ uploader.start();
720
+ },
721
+ FileUploaded: function (up, file, result) {
722
+ result = JSON.parse(result.response);
723
+ json_data_parse(result, 0);
724
+ thisObj.filesUploaded = result;
725
+ },
726
+ UploadComplete: function (up, files) {
727
+ if (thisObj.filesUploaded) thisObj.$emit('input', thisObj.filesUploaded);
728
+ else thisObj.$emit('input', files);
729
+
730
+ if (thisObj.UploadingError.length == 0 && thisObj.autoclose) thisObj.dialog = false;
731
+ thisObj.loading = false;
732
+ },
733
+ UploadProgress: function (up, file) { },
734
+ Error: function (up, err) {
735
+ thisObj.loading = false;
736
+ if (err.status == 500) {
737
+ err.message = JSON.parse(err.response).message;
738
+ }
739
+ console.log(err);
740
+ thisObj.dialog = true;
741
+ thisObj.UploadingError.push(err);
742
+ }
743
+ }
744
+ });
745
+ uploader.init();
746
+ thisObj.uploader = uploader;
747
+ }
748
+ },
749
+ watch: {
750
+ dialog: function (newVal, oldVal) {
751
+ if (!newVal && this.onclose) {
752
+ runAction(this.onclose);
753
+ }
754
+ },
755
+ filters: {
756
+ deep: true,
757
+ handler(newVal) {
758
+ this.uploader.setOption("filters", newVal)
759
+ var ext = newVal.mime_types[0].extensions.replace(/([ ]*)/gm, "")
760
+ ext = "." + ext.replace(/,+/gm, ",.")
761
+ $('#' + this.elID).parent().find('.moxie-shim > input[type="file"]').attr("accept", ext);
762
+ }
763
+ }
764
+ }
765
+ });
766
+ //===========================================================================================
767
+
768
+ Vue.component('f-qrcode', {
769
+ template: `<div v-bind="$attrs"><img :src="img"></img></div>`,
770
+ props: {
771
+ value: {
772
+ type: String,
773
+ default: null
774
+ },
775
+ logo: {
776
+ type: String,
777
+ default: null
778
+ },
779
+ color: {
780
+ type: String,
781
+ default: "#000"
782
+ },
783
+ size: {
784
+ type: Number,
785
+ default: 300
786
+ }
787
+ },
788
+ data: () => {
789
+ return {
790
+ options: null,
791
+ qrCode: null,
792
+ img: [],
793
+ }
794
+ },
795
+ watch: {
796
+ value: function (newVal) {
797
+ this.options.data = newVal;
798
+ this.qrCode.update(this.options);
799
+ this.qrBuild();
800
+ }
801
+ },
802
+ mounted() {
803
+ this.options = {
804
+ width: this.size,
805
+ height: this.size,
806
+ type: 'png',
807
+ data: this.value,
808
+ image: this.logo,
809
+ dotsOptions: {
810
+ color: this.color,
811
+ type: "square"
812
+ },
813
+ cornersSquareOptions: {
814
+ type: "square",
815
+ color: this.color
816
+ },
817
+ cornersDotOptions: {
818
+ type: "square",
819
+ color: this.color
820
+ },
821
+ backgroundOptions: {
822
+ color: "#fff",
823
+ },
824
+ imageOptions: {
825
+ crossOrigin: "anonymous",
826
+ margin: 2
827
+ }
828
+ };
829
+
830
+ this.qrCode = new QRCodeStyling(this.options)
831
+ this.qrBuild();
832
+ },
833
+ methods: {
834
+ async qrBuild() {
835
+ const _img = await this.qrCode.getRawData('png')
836
+ var objectURL = URL.createObjectURL(_img);
837
+ this.img = objectURL
838
+ }
839
+ }
840
+ });
841
+
842
+ //=========================================================================================================
843
+ Vue.component('f-qrcode-reader', {
844
+ props: {
845
+ value: {
846
+ type: Boolean,
847
+ default: true
848
+ },
849
+ label: {
850
+ type: String,
851
+ default: "Đọc QRCode"
852
+ },
853
+ width: {
854
+ type: Number,
855
+ default: ($(window).width() < 400 ? $(window).width() - 50 : 350)
856
+ }
857
+ },
858
+ template: `
859
+ <v-dialog v-model="value" :width="width" persistent dark overlay-opacity="0.8">
860
+ <v-card>
861
+ <v-card-title v-if="noSupportCamera" class="red--text justify-center">Thiết bị không hỗ trợ camera</v-card-title>
862
+ <qrcode-stream v-if="!noSupportCamera" :camera="camera" @decode="onDecode" @init="onInit" class="black" :style="{width: width + 'px', height: width + 'px'}"></qrcode-stream>
863
+ <v-card-actions dense>
864
+ <h4 class="">{{label}}</h4>
865
+ <v-spacer></v-spacer>
866
+ <v-btn icon @click="$emit('input', false)"><v-icon>mdi-close</v-icon></v-btn>
867
+ </v-card-actions>
868
+ </v-card>
869
+ </v-dialog>
870
+ `,
871
+ data: function () {
872
+ return {
873
+ camera: 'auto',
874
+ noSupportCamera: false,
875
+ }
876
+ },
877
+ methods: {
878
+ async onInit(promise) {
879
+ try {
880
+ await promise
881
+ } catch (e) {
882
+ const cameraMissingError = this.camera === 'OverconstrainedError'
883
+ if (cameraMissingError) {
884
+ this.noSupportCamera = true
885
+ }
886
+ } finally {
887
+ this.showScanConfirmation = this.camera === "off"
888
+ }
889
+ },
890
+ async onDecode(content) {
891
+ this.$emit("input", false)
892
+
893
+ this.$emit("update", content)
894
+
895
+ this.pause()
896
+ await this.timeout(500)
897
+ this.unpause()
898
+ },
899
+ unpause() {
900
+ this.camera = 'auto'
901
+ },
902
+ pause() {
903
+ this.camera = 'off'
904
+ },
905
+ timeout(ms) {
906
+ return new Promise(resolve => {
907
+ window.setTimeout(resolve, ms)
908
+ })
909
+ }
910
+ },
911
+ });
912
+
913
+ //=========================================================================================================
914
+ Vue.component('f-image-update', {
915
+ props: {
916
+ value: {
917
+ default: null
918
+ },
919
+ label: {
920
+ type: String,
921
+ default: 'image upload'
922
+ },
923
+ title: {
924
+ type: String,
925
+ default: 'Cập nhật hình'
926
+ },
927
+ apiUpload: {
928
+ type: String,
929
+ default: ''
930
+ },
931
+ iconText: {
932
+ type: String,
933
+ default: "mdi-camera"
934
+ },
935
+ imageType: {
936
+ type: String,
937
+ default: null
938
+ },
939
+ size: {
940
+ type: Object,
941
+ default: true
942
+ },
943
+ quality: {
944
+ default: 0.9
945
+ },
946
+ dialogWidth: {
947
+ type: Number,
948
+ default: 500
949
+ },
950
+ dialogHeight: {
951
+ type: Number,
952
+ default: 350
953
+ },
954
+ imageBoxAttr: {
955
+ type: Object,
956
+ default: {
957
+ "class": "d-flex flex-column align-center",
958
+ "style": "position: relative; line-height: 0; width: 100%; height: 300px; outline: 1px solid #c0c0c0; overflow: hidden;"
959
+ },
960
+ },
961
+ imageAttr: {
962
+ type: Object,
963
+ default: {
964
+ "style": "max-width: 100%; max-height: 100%;"
965
+ },
966
+ },
967
+ buttonAttr: {
968
+ type: Object,
969
+ default: {
970
+ "depressed": "",
971
+ "tile": "",
972
+ "style": "position: absolute; outline: 1px solid #c0c0c0; bottom: 0px; right: 0px; margin: 0 auto; left: 0; border-radius: 0;"
973
+ },
974
+ },
975
+ croperAttr: {
976
+ type: Object,
977
+ default: {
978
+ "stencil-props": { aspectRatio: 1 / 1 },// thiet lam cung ti le khung co dinh
979
+ "default-size": {// thiet lam khung cat ban dau la bang tam hinh
980
+ "width": 3333,
981
+ "height": 3333
982
+ },
983
+ "transitions": true,
984
+ "image-restriction": "fit-area"
985
+ },
986
+ },
987
+ },
988
+ template: `
989
+ <v-dialog v-model="dialog" persistent :width="dialogWidth" :key="cropperRerender">
990
+ <template v-slot:activator="{ on, attrs }">
991
+ <div v-bind="imageBoxAttr">
992
+ <div v-bind="imageAttr" :style="{'background-image': 'url(' + value + ')', 'background-size': 'contain', 'background-position': 'center center', 'width':'100%', 'height':'calc(100% - 37px)'}"></div>
993
+ <v-btn v-on="on" v-bind="buttonAttr"><v-icon v-if="iconText" :left="label != ''">{{iconText}}</v-icon>{{label}}</v-btn>
994
+ </div>
995
+ </template>
996
+ <v-card style="overflow-x: hidden;">
997
+ <v-toolbar dense elevation="0">
998
+ <v-toolbar-title>{{title}}</v-toolbar-title><v-spacer></v-spacer><v-btn icon @click="dialog = false"><v-icon>mdi-close</v-icon></v-btn>
999
+ </v-toolbar>
1000
+
1001
+ <v-card-text class="px-1 py-0">
1002
+ <cropper ref="cropper" :src="image.src" :canvas="size" v-bind="croperAttr" :style="{'width': dialogWidth + 'px', 'height': dialogHeight + 'px'}"/>
1003
+ </v-card-text>
1004
+
1005
+ <v-card-actions class="d-flex justify-space-around align-center">
1006
+ <v-btn icon @click="reset"><v-icon>mdi-delete</v-icon></v-btn>
1007
+ <v-btn icon @click="flip(true, false)"><v-icon>mdi-flip-horizontal</v-icon></v-btn>
1008
+ <v-btn icon @click="flip(false, true)"><v-icon>mdi-flip-vertical</v-icon></v-btn>
1009
+ <v-btn icon @click="rotate(90)"><v-icon>mdi-rotate-right</v-icon></v-btn>
1010
+ <v-btn icon @click="rotate(-90)"><v-icon>mdi-rotate-left</v-icon></v-btn>
1011
+ <v-btn icon @click="onClickFileUpload"><v-icon>mdi-camera</v-icon></v-btn><input type="file" ref="fileUpload" @change="readFile" style="display: none;" accept="image/*" />
1012
+ <v-divider vertical />
1013
+ <v-btn @click="onSaved" color="primary" depressed><v-icon>mdi-cloud-upload-outline</v-icon></v-btn>
1014
+ </v-card-actions>
1015
+ </v-card>
1016
+ <v-overlay :value="overlay"><v-progress-circular indeterminate size="64"></v-progress-circular></v-overlay>
1017
+ </v-dialog>
1018
+ `,
1019
+ data: function () {
1020
+ return {
1021
+ dialog: false,
1022
+ overlay: false,
1023
+ cropperRerender: 0,
1024
+ image: {
1025
+ src: null,
1026
+ type: null
1027
+ }
1028
+ }
1029
+ },
1030
+ watch: {
1031
+ value: function (newVal, oldVal) {
1032
+ this.image.src = newVal;
1033
+ },
1034
+ dialog: function (newVal, oldVal) {
1035
+ if (newVal) {
1036
+ if ($isPhone && this.dialogWidth < 300) this.dialogWidth = 300;
1037
+ else if (this.dialogWidth < 450) this.dialogWidth = 450;
1038
+ this.image.src = this.value;
1039
+
1040
+ if (this.imageType) {
1041
+ this.image.type = this.imageType;
1042
+ }
1043
+ else {
1044
+ (async () => {
1045
+ const reponse = await fetch(this.image.src, {
1046
+ method: 'HEAD'
1047
+ });
1048
+ this.image.type = reponse.headers.get('Content-Type');
1049
+ })();
1050
+ }
1051
+
1052
+ this.cropperRerender = Math.floor(Math.random() * 1000);
1053
+ }
1054
+ }
1055
+ },
1056
+ methods: {
1057
+ flip(x, y) {
1058
+ if (this.$refs.cropper.imageTransforms.rotate % 180 !== 0) {
1059
+ this.$refs.cropper.flip(!x, !y);
1060
+ } else {
1061
+ this.$refs.cropper.flip(x, y);
1062
+ }
1063
+ },
1064
+ rotate(angle) {
1065
+ this.$refs.cropper.rotate(angle);
1066
+ },
1067
+ onClickFileUpload() {
1068
+ this.$refs.fileUpload.click();
1069
+ },
1070
+ readFile(event) {
1071
+ //--------------------------
1072
+ const { files } = event.target;
1073
+ if (files && files[0]) {
1074
+ if (this.image.src) {
1075
+ URL.revokeObjectURL(this.image.src)
1076
+ }
1077
+ const blob = URL.createObjectURL(files[0]);
1078
+
1079
+ const reader = new FileReader();
1080
+ reader.onload = (e) => {
1081
+ this.image = {
1082
+ src: blob,
1083
+ type: this.getMimeType(e.target.result, files[0].type),
1084
+ };
1085
+ };
1086
+ reader.readAsArrayBuffer(files[0]);
1087
+ } else {
1088
+ console.log("Sorry - you're device doesn't support the FileReader API");
1089
+ }
1090
+ },
1091
+ onSaved(e) {
1092
+ var self = this;
1093
+ const { canvas } = this.$refs.cropper.getResult();
1094
+ if (canvas) {
1095
+ this.value = canvas.toDataURL("image/jpeg");
1096
+ if (self.apiUpload) {
1097
+ self.overlay = true;
1098
+ canvas.toBlob(blobImage => {
1099
+ var fd = new FormData();
1100
+ fd.append('fname', 'image.jpg');
1101
+ fd.append('data', blobImage);
1102
+
1103
+ $.ajax({
1104
+ type: 'POST',
1105
+ headers: {
1106
+ authorization: $awt
1107
+ },
1108
+ url: self.apiUpload,
1109
+ data: fd,
1110
+ processData: false,
1111
+ contentType: false,
1112
+ success: function (data) {
1113
+ self.$emit('update', { returnData: data, imageData: canvas.toDataURL("image/jpeg") });
1114
+ self.overlay = false;
1115
+ self.dialog = false;
1116
+ },
1117
+ error: function (xhr) {
1118
+ self.overlay = false;
1119
+ console.log("Upload error: ", xhr.responseJSON.Message)
1120
+ }
1121
+ });
1122
+ }, (this.imageType ? this.imageType : this.image.type), this.quality * 1.0);
1123
+ } else {
1124
+ self.dialog = false;
1125
+ }
1126
+ } else {
1127
+ console.log("Canvas error: ", canvas)
1128
+ }
1129
+ },
1130
+ reset() {
1131
+ this.image = {
1132
+ src: null,
1133
+ type: null
1134
+ }
1135
+ },
1136
+ getMimeType(file, fallback = null) {
1137
+ const byteArray = (new Uint8Array(file)).subarray(0, 4);
1138
+ let header = '';
1139
+ for (let i = 0; i < byteArray.length; i++) {
1140
+ header += byteArray[i].toString(16);
1141
+ }
1142
+ switch (header) {
1143
+ case "89504e47":
1144
+ return "image/png";
1145
+ case "47494638":
1146
+ return "image/gif";
1147
+ case "ffd8ffe0":
1148
+ case "ffd8ffe1":
1149
+ case "ffd8ffe2":
1150
+ case "ffd8ffe3":
1151
+ case "ffd8ffe8":
1152
+ return "image/jpeg";
1153
+ default:
1154
+ return fallback;
1155
+ }
1156
+ }
1157
+ }
1158
+ });
1159
+
1160
+ //===========================================================================================
1161
+ Vue.component("f-date", {
1162
+ template: `<v-menu v-model="showDatePicker" :close-on-content-click="false" offset-y max-width="290px" min-width="290px">
1163
+ <template v-slot:activator="{ on, attrs }">
1164
+ <v-text-field ref="dateInput" v-model="displayValue" :label="label" v-bind="$attrs" :required="required" :rules="dateRules" @keydown="handleKeydown"
1165
+ @keypress="restrictToNumbers" @paste="handlePaste" @click="handleClick" @focus="handleFocus" maxlength="10" append-icon="mdi-calendar-text" @click:append="on.click"></v-text-field>
1166
+ </template>
1167
+ <v-date-picker v-model="pickerDate" no-title scrollable @input="handleDatePickerInput"></v-date-picker>
1168
+ </v-menu>`,
1169
+ props: {
1170
+ value: {
1171
+ type: String,
1172
+ default: null,
1173
+ },
1174
+ label: {
1175
+ type: String,
1176
+ default: null,
1177
+ },
1178
+ dateAdd: {
1179
+ type: Number,
1180
+ default: null,
1181
+ },
1182
+ required: {
1183
+ type: Boolean,
1184
+ default: true,
1185
+ },
1186
+ },
1187
+ data() {
1188
+ return {
1189
+ internalValue: ['', '', ''], // [day, month, year]
1190
+ displayValue: 'dd/mm/yyyy',
1191
+ cursorPosition: 0,
1192
+ selectedField: null,
1193
+ isEditingField: false,
1194
+ showDatePicker: false,
1195
+ pickerDate: null,
1196
+ dateRules: [
1197
+ (v) => !v || this.isValidDate(v) || 'Ngày không hợp lệ (dd/mm/yyyy)',
1198
+ ],
1199
+ };
1200
+ },
1201
+ computed: {
1202
+ formattedDate() {
1203
+ const [day, month, year] = this.internalValue;
1204
+ return `${day.padStart(2, '0')}/${month.padStart(2, '0')}/${year.padStart(4, '0')}`;
1205
+ },
1206
+ },
1207
+ methods: {
1208
+ restrictToNumbers(event) {
1209
+ // Chỉ cho phép nhập số (0-9)
1210
+ const charCode = event.which ? event.which : event.keyCode;
1211
+ if (charCode < 48 || charCode > 57) {
1212
+ event.preventDefault();
1213
+ }
1214
+ },
1215
+ handlePaste(event) {
1216
+ // Chặn dán nếu dữ liệu chứa ký tự không phải số
1217
+ const pastedData = event.clipboardData.getData('text');
1218
+ if (!/^[0-9/]*$/.test(pastedData)) {
1219
+ event.preventDefault();
1220
+ }
1221
+ },
1222
+ handleKeydown(event) {
1223
+ const input = this.$refs.dateInput.$el.querySelector('input');
1224
+ this.cursorPosition = input.selectionStart;
1225
+ const key = event.key;
1226
+ const fields = { day: 0, month: 1, year: 2 };
1227
+ const maxLengths = { day: 2, month: 2, year: 4 };
1228
+ const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight'];
1229
+ const isDigit = /^[0-9]$/.test(key);
1230
+ const isAllowedSystemKey = /^F([1-9]|1[0-2])$/.test(key) || key === 'Tab';
1231
+ if (isDigit) {
1232
+ event.preventDefault();
1233
+ const fieldIndex = fields[this.selectedField];
1234
+ let fieldValue = this.internalValue[fieldIndex];
1235
+ if (!this.isEditingField) {
1236
+ fieldValue = '';
1237
+ this.isEditingField = true;
1238
+ }
1239
+ if (fieldValue == '0') fieldValue = key;
1240
+ else fieldValue += key;
1241
+ fieldValue = fieldValue.slice(-maxLengths[this.selectedField]);
1242
+ Vue.set(this.internalValue, fieldIndex, fieldValue);
1243
+ if (fieldValue.length === maxLengths[this.selectedField]) {
1244
+ if (this.selectedField === 'day') this.selectedField = 'month';
1245
+ else if (this.selectedField === 'month') this.selectedField = 'year';
1246
+ this.isEditingField = false;
1247
+ }
1248
+ this.updateDisplayValue();
1249
+ this.$nextTick(() => this.selectField());
1250
+ return;
1251
+ }
1252
+ if (key === 'Backspace' || key === 'Delete') {
1253
+ event.preventDefault();
1254
+ const fieldIndex = fields[this.selectedField];
1255
+ Vue.set(this.internalValue, fieldIndex, '');
1256
+ this.isEditingField = false;
1257
+ this.updateDisplayValue();
1258
+ this.$nextTick(() => this.selectField());
1259
+ return;
1260
+ }
1261
+ if (key === 'ArrowLeft') {
1262
+ event.preventDefault();
1263
+ if (this.selectedField === 'year') this.selectedField = 'month';
1264
+ else if (this.selectedField === 'month') this.selectedField = 'day';
1265
+ this.isEditingField = false;
1266
+ this.$nextTick(() => this.selectField());
1267
+ return;
1268
+ }
1269
+ if (key === 'ArrowRight') {
1270
+ event.preventDefault();
1271
+ if (this.selectedField === 'day') this.selectedField = 'month';
1272
+ else if (this.selectedField === 'month') this.selectedField = 'year';
1273
+ this.isEditingField = false;
1274
+ this.$nextTick(() => this.selectField());
1275
+ return;
1276
+ }
1277
+ if (!isDigit && !allowedKeys.includes(key) && !isAllowedSystemKey) {
1278
+ event.preventDefault();
1279
+ }
1280
+ },
1281
+ handleClick(event) {
1282
+ const input = this.$refs.dateInput.$el.querySelector('input');
1283
+ this.cursorPosition = input.selectionStart;
1284
+ if (this.cursorPosition <= 2) {
1285
+ this.selectedField = 'day';
1286
+ } else if (this.cursorPosition <= 5) {
1287
+ this.selectedField = 'month';
1288
+ } else {
1289
+ this.selectedField = 'year';
1290
+ }
1291
+ this.isEditingField = false;
1292
+ this.$nextTick(() => {
1293
+ this.selectField();
1294
+ });
1295
+ },
1296
+ handleFocus() {
1297
+ if (!this.selectedField) {
1298
+ this.selectedField = 'day';
1299
+ }
1300
+ this.isEditingField = false;
1301
+ this.$nextTick(() => {
1302
+ this.selectField();
1303
+ });
1304
+ },
1305
+ selectField() {
1306
+ const input = this.$refs.dateInput.$el.querySelector('input');
1307
+ if (this.selectedField === 'day') {
1308
+ input.setSelectionRange(0, 2);
1309
+ this.cursorPosition = 2;
1310
+ } else if (this.selectedField === 'month') {
1311
+ input.setSelectionRange(3, 5);
1312
+ this.cursorPosition = 5;
1313
+ } else if (this.selectedField === 'year') {
1314
+ input.setSelectionRange(6, 10);
1315
+ this.cursorPosition = 10;
1316
+ }
1317
+ },
1318
+ updateDisplayValue() {
1319
+ const [day, month, year] = this.internalValue;
1320
+ this.displayValue = `${day.padStart(2, (day ? '0' : '_')) || 'dd'}/${month.padStart(2, (month ? '0' : '_')) || 'mm'}/${year.padStart(4, (year ? '0' : '_')) || 'yyyy'}`;
1321
+ if (this.isValidDate(this.formattedDate)) {
1322
+ this.pickerDate = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
1323
+ this.$emit('input', this.pickerDate);
1324
+ } else
1325
+ this.$emit('input', null);
1326
+ },
1327
+ isValidDate(value) {
1328
+ // Cho phép giá trị rỗng hoặc placeholder
1329
+ if (!this.required && (!value || value === '__/__/____' || value === 'dd/mm/yyyy' || value === 'yyyy/mm/dd' || value === 'dd-mm-yyyy' || value === 'yyyy-mm-dd')) return true;
1330
+ // Regex cho các định dạng
1331
+ const ddmmyyyySlashRegex = /^(\d{2})\/(\d{2})\/(\d{4})$/; // dd/mm/yyyy
1332
+ const yyyymmddSlashRegex = /^(\d{4})\/(\d{2})\/(\d{2})$/; // yyyy/mm/dd
1333
+ const ddmmyyyyDashRegex = /^(\d{2})-(\d{2})-(\d{4})$/; // dd-mm-yyyy
1334
+ const yyyymmddDashRegex = /^(\d{4})-(\d{2})-(\d{2})$/; // yyyy-mm-dd
1335
+ let day, month, year;
1336
+ // Kiểm tra định dạng dd/mm/yyyy
1337
+ if (ddmmyyyySlashRegex.test(value)) {
1338
+ [day, month, year] = value.split('/').map(Number);
1339
+ }
1340
+ // Kiểm tra định dạng yyyy/mm/dd
1341
+ else if (yyyymmddSlashRegex.test(value)) {
1342
+ [year, month, day] = value.split('/').map(Number);
1343
+ }
1344
+ // Kiểm tra định dạng dd-mm-yyyy
1345
+ else if (ddmmyyyyDashRegex.test(value)) {
1346
+ [day, month, year] = value.split('-').map(Number);
1347
+ }
1348
+ // Kiểm tra định dạng yyyy-mm-dd
1349
+ else if (yyyymmddDashRegex.test(value)) {
1350
+ [year, month, day] = value.split('-').map(Number);
1351
+ }
1352
+ // Nếu không khớp định dạng nào
1353
+ else {
1354
+ return false;
1355
+ }
1356
+ // Tạo đối tượng Date và kiểm tra tính hợp lệ
1357
+ const date = new Date(year, month - 1, day);
1358
+ return (
1359
+ date.getDate() === day &&
1360
+ date.getMonth() + 1 === month &&
1361
+ date.getFullYear() === year
1362
+ );
1363
+ },
1364
+ handleDatePickerInput(date) {
1365
+ if (date) {
1366
+ const [year, month, day] = date.split('-');
1367
+ this.internalValue = [day, month, year];
1368
+ this.updateDisplayValue();
1369
+ this.showDatePicker = false;
1370
+ }
1371
+ },
1372
+ initializeDate() {
1373
+ // Regex cho các định dạng
1374
+ const yyyymmddSlashRegex = /^(\d{4})\/(\d{2})\/(\d{2})$/; // yyyy/mm/dd
1375
+ const yyyymmddDashRegex = /^(\d{4})-(\d{2})-(\d{2})$/; // yyyy-mm-dd
1376
+ // Nếu có chuỗi dạng ISO có chứa 'T' (ví dụ: "2024-12-28T12:30:00")
1377
+ if (this.value && typeof this.value === 'string' && this.value.includes('T')) {
1378
+ this.value = this.value.split('T')[0]; // Cắt phần thời gian, giữ lại yyyy-mm-dd
1379
+ }
1380
+
1381
+ // Nếu có giá trị từ prop value
1382
+ if (this.value && this.isValidDate(this.value)) {
1383
+ let day, month, year;
1384
+ // Kiểm tra định dạng yyyy/mm/dd
1385
+ if (yyyymmddSlashRegex.test(this.value)) {
1386
+ [year, month, day] = this.value.split('/').map(Number);
1387
+ }
1388
+ // Kiểm tra định dạng yyyy-mm-dd
1389
+ else if (yyyymmddDashRegex.test(this.value)) {
1390
+ [year, month, day] = this.value.split('-').map(Number);
1391
+ }
1392
+ // Chuyển đổi sang chuỗi với định dạng 2 chữ số
1393
+ day = String(day).padStart(2, '0');
1394
+ month = String(month).padStart(2, '0');
1395
+ year = String(year);
1396
+ this.internalValue = [day, month, year];
1397
+ this.pickerDate = `${year}-${month}-${day}`;
1398
+ this.updateDisplayValue();
1399
+ }
1400
+ // Nếu không có value và dateAdd khác null
1401
+ else if (this.dateAdd !== null) {
1402
+ const date = new Date();
1403
+ date.setDate(date.getDate() + parseInt(this.dateAdd));
1404
+ const day = String(date.getDate()).padStart(2, '0');
1405
+ const month = String(date.getMonth() + 1).padStart(2, '0');
1406
+ const year = String(date.getFullYear());
1407
+ this.internalValue = [day, month, year];
1408
+ this.pickerDate = `${year}-${month}-${day}`;
1409
+ this.updateDisplayValue();
1410
+ }
1411
+ }
1412
+ },
1413
+ watch: {
1414
+ value(newVal) {
1415
+ if (newVal && this.isValidDate(newVal)) {
1416
+ if (newVal != this.pickerDate) {
1417
+ this.initializeDate();
1418
+ }
1419
+ } else {
1420
+ if (this.isValidDate(this.formattedDate)) {
1421
+ this.internalValue = ['', '', ''];
1422
+ this.displayValue = 'dd/mm/yyyy';
1423
+ this.pickerDate = null;
1424
+ }
1425
+ }
1426
+ }
1427
+ },
1428
+ created() {
1429
+ this.initializeDate();
1430
+ }
1431
+ });
1432
+
1433
+ //===========================================================================================
1434
+ Vue.component("f-time", {
1435
+ template: `<v-menu v-model="showTimePicker" :close-on-content-click="false" offset-y max-width="290px" min-width="290px">
1436
+ <template v-slot:activator="{ on, attrs }">
1437
+ <v-text-field ref="timeInput" v-model="displayValue" :label="label" :rules="timeRules" v-bind="$attrs" :required="required" @keydown="handleKeydown" @keypress="restrictToNumbers"
1438
+ @paste="handlePaste" @click="handleClick" @focus="handleFocus" maxlength="5" append-icon="mdi-clock-outline" @click:append="on.click"></v-text-field>
1439
+ </template>
1440
+ <v-time-picker v-if="showTimePicker" v-model="pickerTime" format="24hr" scrollable @click:minute="showTimePicker = false" @input="handleTimePickerInput"></v-time-picker>
1441
+ </v-menu>`,
1442
+ props: {
1443
+ value: {
1444
+ type: String,
1445
+ default: null,
1446
+ },
1447
+ label: {
1448
+ type: String,
1449
+ default: null,
1450
+ },
1451
+ timeAdd: {
1452
+ type: Number,
1453
+ default: null,
1454
+ },
1455
+ required: {
1456
+ type: Boolean,
1457
+ default: true,
1458
+ },
1459
+ },
1460
+ data() {
1461
+ return {
1462
+ internalValue: ['', ''], // [hour, minute]
1463
+ displayValue: 'HH:MM',
1464
+ cursorPosition: 0,
1465
+ selectedField: null,
1466
+ isEditingField: false,
1467
+ showTimePicker: false,
1468
+ pickerTime: null,
1469
+ timeRules: [
1470
+ (v) => !v || this.isValidTime(v) || 'Thời gian không hợp lệ (HH:MM)',
1471
+ ],
1472
+ };
1473
+ },
1474
+ computed: {
1475
+ formattedTime() {
1476
+ const [hour, minute] = this.internalValue;
1477
+ return `${hour.padStart(2, hour ? "0" : "x")}:${minute.padStart(2, minute ? "0" : "X")}`;
1478
+ },
1479
+ },
1480
+ methods: {
1481
+ restrictToNumbers(event) {
1482
+ const charCode = event.which ? event.which : event.keyCode;
1483
+ if (charCode < 48 || charCode > 57) {
1484
+ event.preventDefault();
1485
+ }
1486
+ },
1487
+ handlePaste(event) {
1488
+ const pastedData = event.clipboardData.getData('text');
1489
+ if (!/^[0-9:]*$/.test(pastedData)) {
1490
+ event.preventDefault();
1491
+ }
1492
+ },
1493
+ handleKeydown(event) {
1494
+ const input = this.$refs.timeInput.$el.querySelector('input');
1495
+ this.cursorPosition = input.selectionStart;
1496
+ const key = event.key;
1497
+ const fields = { hour: 0, minute: 1 };
1498
+ const maxLengths = { hour: 2, minute: 2 };
1499
+ const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight'];
1500
+ const isDigit = /^[0-9]$/.test(key);
1501
+ const isAllowedSystemKey = /^F([1-9]|1[0-2])$/.test(key) || key === 'Tab';
1502
+ if (isDigit) {
1503
+ event.preventDefault();
1504
+ const fieldIndex = fields[this.selectedField];
1505
+ let fieldValue = this.internalValue[fieldIndex];
1506
+ if (!this.isEditingField) {
1507
+ fieldValue = '';
1508
+ this.isEditingField = true;
1509
+ }
1510
+ if (fieldValue === '0') fieldValue = key;
1511
+ else fieldValue += key;
1512
+ fieldValue = fieldValue.slice(-maxLengths[this.selectedField]);
1513
+ Vue.set(this.internalValue, fieldIndex, fieldValue);
1514
+ if (fieldValue.length === maxLengths[this.selectedField]) {
1515
+ if (this.selectedField === 'hour') this.selectedField = 'minute';
1516
+ this.isEditingField = false;
1517
+ }
1518
+ this.updateDisplayValue();
1519
+ this.$nextTick(() => this.selectField());
1520
+ return;
1521
+ }
1522
+ if (key === 'Backspace' || key === 'Delete') {
1523
+ event.preventDefault();
1524
+ const fieldIndex = fields[this.selectedField];
1525
+ Vue.set(this.internalValue, fieldIndex, '');
1526
+ this.isEditingField = false;
1527
+ this.updateDisplayValue();
1528
+ this.$nextTick(() => this.selectField());
1529
+ return;
1530
+ }
1531
+ if (key === 'ArrowLeft') {
1532
+ event.preventDefault();
1533
+ if (this.selectedField === 'minute') this.selectedField = 'hour';
1534
+ this.isEditingField = false;
1535
+ this.$nextTick(() => this.selectField());
1536
+ return;
1537
+ }
1538
+ if (key === 'ArrowRight') {
1539
+ event.preventDefault();
1540
+ if (this.selectedField === 'hour') this.selectedField = 'minute';
1541
+ this.isEditingField = false;
1542
+ this.$nextTick(() => this.selectField());
1543
+ return;
1544
+ }
1545
+ if (!isDigit && !allowedKeys.includes(key) && !isAllowedSystemKey) {
1546
+ event.preventDefault();
1547
+ }
1548
+ },
1549
+ handleClick(event) {
1550
+ const input = this.$refs.timeInput.$el.querySelector('input');
1551
+ this.cursorPosition = input.selectionStart;
1552
+ if (this.cursorPosition <= 2) {
1553
+ this.selectedField = 'hour';
1554
+ } else {
1555
+ this.selectedField = 'minute';
1556
+ }
1557
+ this.isEditingField = false;
1558
+ this.$nextTick(() => this.selectField());
1559
+ },
1560
+ handleFocus() {
1561
+ if (!this.selectedField) {
1562
+ this.selectedField = 'hour';
1563
+ }
1564
+ this.isEditingField = false;
1565
+ this.$nextTick(() => this.selectField());
1566
+ },
1567
+ selectField() {
1568
+ const input = this.$refs.timeInput.$el.querySelector('input');
1569
+ if (this.selectedField === 'hour') {
1570
+ input.setSelectionRange(0, 2);
1571
+ this.cursorPosition = 2;
1572
+ } else if (this.selectedField === 'minute') {
1573
+ input.setSelectionRange(3, 5);
1574
+ this.cursorPosition = 5;
1575
+ }
1576
+ },
1577
+ updateDisplayValue() {
1578
+ const [hour, minute] = this.internalValue;
1579
+ this.displayValue = `${hour.padStart(2, (hour ? '0' : '_')) || 'HH'}:${minute.padStart(2, (minute ? '0' : '_')) || 'MM'}`;
1580
+ if (this.isValidTime(this.formattedTime)) {
1581
+ this.pickerTime = `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`;
1582
+ this.$emit('input', this.pickerTime);
1583
+ } else {
1584
+ this.$emit('input', null);
1585
+ }
1586
+ },
1587
+ isValidTime(value) {
1588
+ if (!this.required && (!value || value === '__:__' || value === 'HH:MM')) return true;
1589
+ const timeRegex = /^(\d{2}):(\d{2})$/;
1590
+ if (!timeRegex.test(value)) return false;
1591
+ const [hour, minute] = value.split(':').map(Number);
1592
+ return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59;
1593
+ },
1594
+ handleTimePickerInput(time) {
1595
+ if (time) {
1596
+ const [hour, minute] = time.split(':');
1597
+ this.internalValue = [hour, minute];
1598
+ this.updateDisplayValue();
1599
+ }
1600
+ },
1601
+ initializeTime() {
1602
+ const timeRegex = /^(\d{2}):(\d{2})$/;
1603
+ if (this.value && this.isValidTime(this.value)) {
1604
+ const [hour, minute] = this.value.split(':').map(Number);
1605
+ this.internalValue = [String(hour).padStart(2, '0'), String(minute).padStart(2, '0')];
1606
+ this.pickerTime = `${this.internalValue[0]}:${this.internalValue[1]}`;
1607
+ this.updateDisplayValue();
1608
+ } else if (this.timeAdd !== null) {
1609
+ const date = new Date();
1610
+ date.setMinutes(date.getMinutes() + parseInt(this.timeAdd));
1611
+ const hour = String(date.getHours()).padStart(2, '0');
1612
+ const minute = String(date.getMinutes()).padStart(2, '0');
1613
+ this.internalValue = [hour, minute];
1614
+ this.pickerTime = `${hour}:${minute}`;
1615
+ this.updateDisplayValue();
1616
+ }
1617
+ },
1618
+ },
1619
+ watch: {
1620
+ value(newVal) {
1621
+ if (newVal && this.isValidTime(newVal)) {
1622
+ if (newVal !== this.pickerTime) {
1623
+ this.initializeTime();
1624
+ }
1625
+ } else {
1626
+ if (this.isValidTime(this.formattedTime)) {
1627
+ this.internalValue = ['', ''];
1628
+ this.displayValue = 'HH:MM';
1629
+ this.pickerTime = null;
1630
+ }
1631
+ }
1632
+ },
1633
+ },
1634
+ created() {
1635
+ this.initializeTime();
1636
+ },
1637
+ });
1638
+ //===========================================================================================
1639
+ Vue.component('f-time-counter', {
1640
+ props: {
1641
+ value: {
1642
+ type: Number,
1643
+ default: 0
1644
+ },
1645
+ type: {
1646
+ type: Number,
1647
+ default: 0
1648
+ },
1649
+ labelFormat: {
1650
+ type: Object,
1651
+ default: { class: "text-caption black--text" }
1652
+ },
1653
+ format: {
1654
+ type: Object,
1655
+ default: {
1656
+ label: "",
1657
+ day: " ngày",
1658
+ hour: " giờ",
1659
+ minute: " phút",
1660
+ second: " giây"
1661
+ }
1662
+ },
1663
+ },
1664
+ template: `<span v-bind="$attrs">{{format.label}}<span v-if="type==2 && timeNew">{{timeNew.toLocaleString()}}</span><span v-if="type==1">{{counterDown}}<span v-bind="labelFormat">{{format.second}}</span></span><span v-if="type==0"><span v-if="days">{{days}}<span v-bind="labelFormat">{{format.day}}</span></span> <span v-if="hours">{{hours}}<span v-bind="labelFormat">{{format.hour}}</span></span> <span v-if="minutes">{{minutes}}<span v-bind="labelFormat">{{format.minute}}</span></span> <span>{{seconds}}<span v-bind="labelFormat">{{format.second}}</span></span></span></span>`,
1665
+ data: function () {
1666
+ return {
1667
+ returnVal: false,
1668
+ myCounter: null,
1669
+ timeNew: 0,
1670
+ timeEnd: null,
1671
+ counterDown: 0,
1672
+ days: 0,
1673
+ hours: 0,
1674
+ minutes: 0,
1675
+ seconds: 0,
1676
+ }
1677
+ },
1678
+ mounted: function () {
1679
+ this.startCountDown(this.value);
1680
+ },
1681
+ destroyed: function () {
1682
+ if (this.myCounter) clearInterval(this.myCounter);
1683
+ },
1684
+ methods: {
1685
+ formatDateTime(input) {
1686
+ let date;
1687
+ if (!input) {
1688
+ date = new Date();
1689
+ } else if (typeof input === "string") {
1690
+ const match = input.match(/\/Date\((\d+)\)\//);
1691
+ if (match) {
1692
+ date = new Date(parseInt(match[1], 10));
1693
+ } else {
1694
+ date = new Date(input);
1695
+ }
1696
+ } else if (input instanceof Date) {
1697
+ date = input;
1698
+ } else {
1699
+ date = new Date();
1700
+ }
1701
+
1702
+ const pad = (num) => (num < 10 ? "0" + num : num);
1703
+
1704
+ const yyyy = date.getFullYear();
1705
+ const MM = pad(date.getMonth() + 1);
1706
+ const dd = pad(date.getDate());
1707
+ const HH = pad(date.getHours());
1708
+ const mm = pad(date.getMinutes());
1709
+ const ss = pad(date.getSeconds());
1710
+
1711
+ return `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}`;
1712
+ },
1713
+ startCountDown: function (second) {
1714
+ this.returnVal = false;
1715
+
1716
+ if (_.isString(second)) {
1717
+ var regNum = /^[0-9]*$/gm;
1718
+ if (regNum.test(second)) second = parseInt(second);
1719
+ else {
1720
+ var arr = second.split(/[- :]/);
1721
+ second = new Date(arr[0], arr[1] - 1, arr[2], arr[3], arr[4], arr[5]);
1722
+ }
1723
+ }
1724
+
1725
+ if (!_.isDate(second)) {
1726
+ this.timeEnd = new Date();
1727
+ this.timeEnd.setSeconds(this.timeEnd.getSeconds() + second);
1728
+ this.returnVal = true;
1729
+ } else this.timeEnd = second;
1730
+
1731
+ if (this.myCounter) clearInterval(this.myCounter);
1732
+ //-------------------------------------------------------------
1733
+ if (this.type == 2) { //--- dong ho thoi gian
1734
+ this.counterDown = (Date.parse(this.timeEnd) - Date.parse(new Date()));
1735
+ this.timeNew = new Date(Date.parse(new Date()) + this.counterDown);
1736
+ setInterval(function () {
1737
+ this.timeNew = new Date(Date.parse(new Date()) + this.counterDown);
1738
+ }.bind(this), 1000);
1739
+ return;
1740
+ }
1741
+ //----- Dem lui --------------------------
1742
+ countDown(this);
1743
+ if (this.counterDown <= 0) return;
1744
+
1745
+ this.myCounter = setInterval(function () {
1746
+ countDown(this);
1747
+ }.bind(this), 1000);
1748
+ //-----------------------------------------
1749
+ function countDown(self) {
1750
+ self.counterDown = (Date.parse(self.timeEnd) - Date.parse(new Date())) / 1000;
1751
+
1752
+ if (self.returnVal) self.$emit("input", self.counterDown);
1753
+ if (self.counterDown <= 0) {
1754
+ self.$emit("time-end");
1755
+ clearInterval(self.myCounter);
1756
+ }
1757
+
1758
+ if (self.counterDown < 0) self.counterDown = 0;
1759
+
1760
+ self.days = Math.floor(self.counterDown / (60 * 60 * 24));
1761
+ self.hours = Math.floor((self.counterDown % (60 * 60 * 24)) / (60 * 60));
1762
+ self.minutes = Math.floor((self.counterDown % (60 * 60)) / 60);
1763
+ self.seconds = Math.floor((self.counterDown % (60)));
1764
+ }
1765
+ }
1766
+ },
1767
+ watch: {
1768
+ value(newValue, oldValue) {
1769
+ this.startCountDown(this.formatDateTime(newValue));
1770
+ },
1771
+ }
1772
+ });
1773
+
1774
+ //=======================================================================================================================================
1775
+ Vue.component('fp-profile', {
1776
+ props: {
1777
+ urlAppList: {
1778
+ default: null
1779
+ },
1780
+ urlSignout: {
1781
+ default: null
1782
+ },
1783
+ urlChangePassword: {
1784
+ default: null
1785
+ },
1786
+ urlAccount: {
1787
+ default: null
1788
+ },
1789
+ urlAvatar: {
1790
+ default: null
1791
+ },
1792
+ color: {
1793
+ default: "primary"
1794
+ },
1795
+ openOnHover: {
1796
+ default: false
1797
+ },
1798
+ },
1799
+ template: `
1800
+ <v-menu v-if="userInfo.UserID" v-model="menu" :close-on-content-click="false" :open-on-hover="openOnHover" offset-y slide-y transition="slide-y-transition">
1801
+ <template v-slot:activator="{ on, attrs }">
1802
+ <v-avatar v-bind="attrs" v-on="on" size="36" class="ml-2 cursor-pointer">
1803
+ <img v-if="urlAvatar" :src="avatarImg">
1804
+ <v-icon v-else :color="color">mdi-card-account-details</v-icon>
1805
+ </v-avatar>
1806
+ </template>
1807
+
1808
+ <v-card width=350>
1809
+ <v-list>
1810
+ <v-list-item><h4 class="">Thông tin người dùng</h4></v-list-item>
1811
+ <v-list-item>
1812
+ <v-avatar color="grey lighten-2" size="56">
1813
+ <img v-if="urlAvatar" :src="avatarImg">
1814
+ <v-icon v-else>mdi-account-tie</v-icon>
1815
+ </v-avatar>
1816
+
1817
+ <v-list-item-content v-if="userInfo">
1818
+ <v-list-item-title><v-btn color="primary" text :href="urlAccount">{{userInfo.FirstName}} {{userInfo.LastName}}</v-btn></v-list-item-title>
1819
+ <v-list-item-subtitle class="ml-4"><b style="color: #000; font-size: larger;">{{userInfo.UserName}}</b> - {{userInfo.UserID}} - {{userInfo.DepartmentID}}</v-list-item-subtitle>
1820
+ </v-list-item-content>
1821
+ </v-list-item>
1822
+ </v-list>
1823
+
1824
+ <v-card-actions>
1825
+ <v-spacer></v-spacer>
1826
+ <v-btn v-if="urlChangePassword" color="primary" text @click="clickChangePass"><v-icon left>mdi-key-arrow-right</v-icon>Đổi mật khẩu</v-btn>
1827
+ <v-btn v-if="urlSignout" color="warning" text @click="clickSignout"><v-icon left>mdi-logout</v-icon>Đăng xuất</v-btn>
1828
+ </v-card-actions>
1829
+
1830
+ <v-list-item v-if="appList.length > 0"><h4 class="">Ứng dụng của tôi</h4></v-list-item>
1831
+ <v-list v-if="appList.length > 0" class="fpScrollbar" style="display: grid; grid-template-columns: auto auto auto; max-height: 400px; ">
1832
+ <v-list-item v-for="(item, i) in appList" :key="i" :value="item" :href="item.URL" color="primary" rounded="shaped" class="pa-2" style="display: inline-block; text-align: center; cursor: pointer;">
1833
+ <v-icon v-text="item.Icon" color="primary" style="font-size: 30px;"></v-icon>
1834
+ <v-list-item-title v-text="item.AppName" class="mt-1" style="font-size: 11px; text-wrap: wrap;"></v-list-item-title>
1835
+ </v-list-item>
1836
+ </v-list>
1837
+ </v-card>
1838
+ </v-menu>
1839
+ `,
1840
+ data: function () {
1841
+ return {
1842
+ userInfo: vueData.user,
1843
+ menu: false,
1844
+ appList: [],
1845
+ firstLoad: false
1846
+ }
1847
+ },
1848
+ computed: {
1849
+ avatarImg: function () {
1850
+ return this.urlAvatar + this.userInfo.UserID + ".jpg"
1851
+ }
1852
+ },
1853
+ watch: {
1854
+ menu: function (e) {
1855
+ if (!this.firstLoad && this.urlAppList) {
1856
+ this.firstLoad = true
1857
+ ajaxCALL(this.urlAppList, {}, (d) => {
1858
+ this.appList = d.data;
1859
+ });
1860
+ }
1861
+ },
1862
+ },
1863
+ methods: {
1864
+ clickChangePass: function (e) {
1865
+ this.menu = false
1866
+ location.assign(this.urlChangePassword)
1867
+ },
1868
+ clickSignout: function (item) {
1869
+ this.menu = false
1870
+ location.assign(this.urlSignout)
1871
+ }
1872
+ },
1873
+ });
1874
+ //===========================================================================================
1875
+ // Vue.component('header-bar', {
1876
+ // props: {
1877
+ // menu: {
1878
+ // default: null
1879
+ // },
1880
+ // menuLeft: {
1881
+ // default: null
1882
+ // },
1883
+ // title: {
1884
+ // default: ''
1885
+ // },
1886
+ // logo: {
1887
+ // default: ""
1888
+ // },
1889
+ // expand: {
1890
+ // default: true
1891
+ // },
1892
+ // menuStyle: {
1893
+ // default: {
1894
+ // topmenu: {
1895
+ // bgcolor: "primary",
1896
+ // textcolor: "white"
1897
+ // },
1898
+ // leftmenu: {
1899
+ // bgcolor: "primary",
1900
+ // textcolor: "white"
1901
+ // }
1902
+ // }
1903
+ // },
1904
+ // component: {
1905
+ // default: null
1906
+ // },
1907
+ // userProfile: {
1908
+ // default: null
1909
+ // },
1910
+ // },
1911
+ // template: `<div>
1912
+ // <v-app-bar v-if="isMobile || menu" light app dense flat clipped-left :color="menuStyle.topmenu.bgcolor" :style="{ 'box-shadow': 'rgba(0, 0, 0, 0.5) 0px 0px '+ boxShadow +'px 0px !important'}">
1913
+ // <v-img class="mr-3" v-if="logo" :src="logo" height="26" width="26" max-height="26" max-width="26" contain></v-img>
1914
+ // <v-toolbar-title v-if="title" :class="[menuStyle.topmenu.textcolor + '--text']">{{title}}</v-toolbar-title>
1915
+ // <v-spacer></v-spacer>
1916
+ // <v-toolbar-items class="hidden-sm-and-down" v-if="menu">
1917
+ // <v-menu offset-y v-for="(item, index) in menu.filter(info => checkright(info))" :key="index">
1918
+ // <template v-slot:activator="{ on }">
1919
+ // <v-btn v-on="on" :href="(!item.submenu?item.url:'')" :target="item.target" text :class="[menuStyle.topmenu.textcolor + '--text']">
1920
+ // <v-icon v-if="item.icon" v-text="item.icon" left class="mr-1"></v-icon>
1921
+ // {{ item.name }}<v-icon v-if="!!item.submenu">mdi-chevron-down</v-icon>
1922
+ // </v-btn>
1923
+ // </template>
1924
+ // <v-list v-if="!!item.submenu">
1925
+ // <v-list-item v-for="(i, index) in item.submenu.filter(info => checkright(info))" :key="index" :href="i.url" :target="i.target">
1926
+ // <v-list-item-title>{{ i.name }}</v-list-item-title>
1927
+ // </v-list-item>
1928
+ // </v-list>
1929
+ // </v-menu>
1930
+ // </v-toolbar-items>
1931
+ // <component v-if="!isMobile && component" v-for="(c, index) in component" :key="index" v-bind:is="c.el" v-bind="c.attr"></component>
1932
+ // <fp-profile v-if="!isMobile && userProfile" v-bind="userProfile"></fp-profile>
1933
+ // <v-app-bar-nav-icon v-if="isMobile && leftMenuAndMobile" :color="menuStyle.topmenu.textcolor" @click.stop="openLeftMenu=!openLeftMenu"></v-app-bar-nav-icon>
1934
+ // </v-app-bar>
1935
+
1936
+ // <v-navigation-drawer v-if="leftMenuAndMobile" :class="[menuStyle.leftmenu.bgcolor, menuStyle.leftmenu.textcolor + '--text','fpScrollbar']" v-model="openLeftMenu" app clipped :right="isMobile" :permanent="!isMobile" :mini-variant="pinLeftMenu && !isMobile" :expand-on-hover="pinLeftMenu && !isMobile">
1937
+ // <template v-slot:prepend v-if="!isMobile && !menu">
1938
+ // <v-list-item class="px-0 pt-1 pl-3">
1939
+ // <v-img class="mr-1" v-if="logo" :src="logo" height="30" width="30" max-height="30" max-width="30" contain></v-img>
1940
+ // <v-toolbar-title v-if="title" :class="[menuStyle.leftmenu.textcolor + '--text']">{{title}}</v-toolbar-title>
1941
+ // </v-list-item>
1942
+ // </template>
1943
+ // <v-list class="text-left pa-0" :color="menuStyle.leftmenu.bgcolor" dark v-if="leftMenuAndMobile">
1944
+ // <template v-for="(item, index) in leftMenuAndMobile.filter(info => checkright(info))" :key="index">
1945
+ // <v-list-item v-if="!item.submenu" :href="item.url" :target="item.target">
1946
+ // <v-icon v-if="item.icon" v-text="item.icon" class="mr-2"></v-icon>
1947
+ // <v-list-item-title :class="[menuStyle.leftmenu.textcolor + '--text']">{{ item.name }}</v-list-item-title>
1948
+ // </v-list-item>
1949
+ // <v-list-group v-if="item.submenu" v-model="item.active" :color="menuStyle.leftmenu.textcolor">
1950
+ // <template v-slot:activator>
1951
+ // <v-icon v-if="item.icon" v-text="item.icon" class="mr-2"></v-icon>
1952
+ // <v-list-item-title :class="[menuStyle.leftmenu.textcolor + '--text']">{{ item.name }}</v-list-item-title>
1953
+ // </template>
1954
+ // <v-list-item v-for="(i, index) in item.submenu.filter(info => checkright(info))" :key="index" :href="i.url" :target="i.target">
1955
+ // <v-list-item-subtitle class="ml-6" :class="[menuStyle.leftmenu.textcolor + '--text']">{{ i.name }}</v-list-item-title>
1956
+ // </v-list-item>
1957
+ // </v-list-group>
1958
+ // </template>
1959
+ // </v-list>
1960
+ // <template v-slot:append>
1961
+ // <component v-if="!menu && component || isMobile" v-for="(c, index) in component" :key="index" v-bind:is="c.el" v-bind="c.attr"></component>
1962
+ // <v-list-item class="pa-0">
1963
+ // <fp-profile v-if="!menu && userProfile || isMobile" v-bind="userProfile" open-on-hover></fp-profile>
1964
+ // <v-spacer></v-spacer>
1965
+ // <v-btn @click.stop="on_of_menu" v-if="(menu || leftMenuAndMobile) && !isMobile" :color="menuStyle.topmenu.textcolor" icon><v-icon :color="menuStyle.leftmenu.textcolor">{{ pinLeftMenu?'mdi-pin-outline':'mdi-pin-off-outline'}}</v-icon></v-btn>
1966
+ // </v-list-item>
1967
+ // </template>
1968
+ // </v-navigation-drawer>
1969
+ // </div>`,
1970
+ // data: function () {
1971
+ // var leftMenuAndMobile = []
1972
+ // if (this.$vuetify.breakpoint.smAndDown) {
1973
+ // _.forEach(_.union(this.menu, this.menuLeft), function (value) {
1974
+ // if (rightTest(value.right)) leftMenuAndMobile.push(value);
1975
+ // });
1976
+
1977
+ // if (leftMenuAndMobile.length == 0) leftMenuAndMobile = null;
1978
+ // }
1979
+ // else leftMenuAndMobile = this.menuLeft;
1980
+
1981
+ // return {
1982
+ // leftMenuAndMobile: leftMenuAndMobile,
1983
+ // openLeftMenu: this.isMobile,
1984
+ // pinLeftMenu: (sessionStorage.getItem("fpOpenmenuAppBar") == null ? !this.expand : sessionStorage.getItem("fpOpenmenuAppBar") == 'true'),
1985
+ // boxShadow: 0
1986
+ // }
1987
+ // },
1988
+ // computed: {
1989
+ // isMobile() {
1990
+ // if (this.$vuetify.breakpoint.smAndDown) return true
1991
+ // else return false;
1992
+ // }
1993
+ // },
1994
+ // methods: {
1995
+ // on_of_menu: function (e) {
1996
+ // this.pinLeftMenu = !this.pinLeftMenu
1997
+ // sessionStorage.setItem("fpOpenmenuAppBar", this.pinLeftMenu);
1998
+ // },
1999
+ // checkright: function (item) {
2000
+ // if (!item.right) return true;
2001
+ // if (!vueData.user) return false;
2002
+
2003
+ // return rightTest(item.right);
2004
+ // }
2005
+ // },
2006
+ // created() {
2007
+ // if (!this.$isServer) {
2008
+ // this._scrollListener = () => {
2009
+ // this.boxShadow = Math.round(window.pageYOffset) / 10;
2010
+ // if (this.boxShadow > 10) this.boxShadow = 10;
2011
+ // }
2012
+ // this._scrollListener()
2013
+ // window.addEventListener('scroll', this._scrollListener)
2014
+ // }
2015
+ // },
2016
+ // beforeDestroy() {
2017
+ // window.removeEventListener('scroll', this._scrollListener)
2018
+ // }
2019
+ // });
2020
+ Vue.component('header-bar', {
2021
+ props: {
2022
+ menu: {
2023
+ default: null
2024
+ },
2025
+ menuLeft: {
2026
+ default: null
2027
+ },
2028
+ title: {
2029
+ default: ''
2030
+ },
2031
+ logo: {
2032
+ default: ""
2033
+ },
2034
+ expand: {
2035
+ default: true
2036
+ },
2037
+ menuStyle: {
2038
+ default: {
2039
+ topmenu: {
2040
+ bgcolor: "primary",
2041
+ textcolor: "white"
2042
+ },
2043
+ leftmenu: {
2044
+ bgcolor: "primary",
2045
+ textcolor: "white"
2046
+ }
2047
+ }
2048
+ },
2049
+ component: {
2050
+ default: null
2051
+ },
2052
+ userProfile: {
2053
+ default: null
2054
+ },
2055
+ activeColor: {
2056
+ default: "rgba(255,255,255,0.15)"
2057
+ }
2058
+ },
2059
+ template: "<div>\
2060
+ <v-app-bar v-if=\"isMobile || menu\" light app dense flat clipped-left :color=\"menuStyle.topmenu.bgcolor\" :style=\"{ 'box-shadow': 'rgba(0, 0, 0, 0.5) 0px 0px '+ boxShadow +'px 0px !important'}\">\
2061
+ <v-img class=\"mr-3\" v-if=\"logo\" :src=\"logo\" height=\"26\" width=\"26\" max-height=\"26\" max-width=\"26\" contain></v-img>\
2062
+ <v-toolbar-title v-if=\"title\" :class=\"[menuStyle.topmenu.textcolor + '--text']\">{{title}}</v-toolbar-title>\
2063
+ <v-spacer></v-spacer>\
2064
+ <v-toolbar-items class=\"hidden-sm-and-down\" v-if=\"menu\">\
2065
+ <v-menu offset-y v-for=\"(item, index) in menu.filter(function(info) { return checkright(info); })\" :key=\"index\">\
2066
+ <template v-slot:activator=\"{ on }\">\
2067
+ <v-btn v-on=\"on\" :href=\"(!item.submenu?item.url:'')\" :target=\"item.target\" text :class=\"[menuStyle.topmenu.textcolor + '--text']\">\
2068
+ <v-icon v-if=\"item.icon\" v-text=\"item.icon\" left class=\"mr-1\"></v-icon>\
2069
+ {{ item.name }}<v-icon v-if=\"!!item.submenu\">mdi-chevron-down</v-icon>\
2070
+ </v-btn>\
2071
+ </template>\
2072
+ <v-list v-if=\"!!item.submenu\">\
2073
+ <v-list-item v-for=\"(i, index) in item.submenu.filter(function(info) { return checkright(info); })\" :key=\"index\" :href=\"i.url\" :target=\"i.target\">\
2074
+ <v-list-item-title>{{ i.name }}</v-list-item-title>\
2075
+ </v-list-item>\
2076
+ </v-list>\
2077
+ </v-menu>\
2078
+ </v-toolbar-items>\
2079
+ <component v-if=\"!isMobile && component\" v-for=\"(c, index) in component\" :key=\"index\" v-bind:is=\"c.el\" v-bind=\"c.attr\"></component>\
2080
+ <fp-profile v-if=\"!isMobile && userProfile\" v-bind=\"userProfile\"></fp-profile>\
2081
+ <v-app-bar-nav-icon v-if=\"isMobile && leftMenuAndMobile\" :color=\"menuStyle.topmenu.textcolor\" @click.stop=\"openLeftMenu=!openLeftMenu\"></v-app-bar-nav-icon>\
2082
+ </v-app-bar>\
2083
+ <v-navigation-drawer \
2084
+ v-if=\"leftMenuAndMobile\" \
2085
+ ref=\"navDrawer\"\
2086
+ :class=\"[menuStyle.leftmenu.bgcolor, menuStyle.leftmenu.textcolor + '--text', 'fp-custom-scrollbar']\" \
2087
+ v-model=\"openLeftMenu\" \
2088
+ app \
2089
+ clipped \
2090
+ :right=\"isMobile\" \
2091
+ :permanent=\"!isMobile\" \
2092
+ :mini-variant=\"pinLeftMenu && !isMobile\" \
2093
+ :expand-on-hover=\"pinLeftMenu && !isMobile\">\
2094
+ <template v-slot:prepend v-if=\"!isMobile && !menu\">\
2095
+ <v-list-item class=\"px-0 pt-1 pl-3\">\
2096
+ <v-img class=\"mr-1\" v-if=\"logo\" :src=\"logo\" height=\"30\" width=\"30\" max-height=\"30\" max-width=\"30\" contain></v-img>\
2097
+ <v-toolbar-title v-if=\"title\" :class=\"[menuStyle.leftmenu.textcolor + '--text']\">{{title}}</v-toolbar-title>\
2098
+ </v-list-item>\
2099
+ </template>\
2100
+ <v-list class=\"text-left pa-0\" :color=\"menuStyle.leftmenu.bgcolor\" dark v-if=\"leftMenuAndMobile\">\
2101
+ <template v-for=\"(item, index) in leftMenuAndMobile.filter(function(info) { return checkright(info); })\">\
2102
+ <v-list-item \
2103
+ v-if=\"!item.submenu\" \
2104
+ :key=\"'item-'+index\"\
2105
+ :href=\"item.url\" \
2106
+ :target=\"item.target\"\
2107
+ :data-active=\"isActiveUrl(item.url) ? 'true' : null\"\
2108
+ :style=\"isActiveUrl(item.url) ? {backgroundColor: activeColor} : {}\">\
2109
+ <v-icon v-if=\"item.icon\" v-text=\"item.icon\" class=\"mr-2\"></v-icon>\
2110
+ <v-list-item-title :class=\"[menuStyle.leftmenu.textcolor + '--text']\">{{ item.name }}</v-list-item-title>\
2111
+ </v-list-item>\
2112
+ <v-list-group \
2113
+ v-if=\"item.submenu\" \
2114
+ :key=\"'group-'+index\"\
2115
+ v-model=\"item.active\" \
2116
+ :color=\"menuStyle.leftmenu.textcolor\">\
2117
+ <template v-slot:activator>\
2118
+ <v-icon v-if=\"item.icon\" v-text=\"item.icon\" class=\"mr-2\"></v-icon>\
2119
+ <v-list-item-title :class=\"[menuStyle.leftmenu.textcolor + '--text']\">{{ item.name }}</v-list-item-title>\
2120
+ </template>\
2121
+ <v-list-item \
2122
+ v-for=\"(i, subIndex) in item.submenu.filter(function(info) { return checkright(info); })\" \
2123
+ :key=\"subIndex\" \
2124
+ :href=\"i.url\" \
2125
+ :target=\"i.target\"\
2126
+ :data-active=\"isActiveUrl(i.url) ? 'true' : null\"\
2127
+ :style=\"isActiveUrl(i.url) ? {backgroundColor: activeColor} : {}\">\
2128
+ <v-list-item-subtitle class=\"ml-6\" :class=\"[menuStyle.leftmenu.textcolor + '--text']\">{{ i.name }}</v-list-item-subtitle>\
2129
+ </v-list-item>\
2130
+ </v-list-group>\
2131
+ </template>\
2132
+ </v-list>\
2133
+ <template v-slot:append>\
2134
+ <component v-if=\"!menu && component || isMobile\" v-for=\"(c, index) in component\" :key=\"index\" v-bind:is=\"c.el\" v-bind=\"c.attr\"></component>\
2135
+ <v-list-item class=\"pa-0\">\
2136
+ <fp-profile v-if=\"!menu && userProfile || isMobile\" v-bind=\"userProfile\" open-on-hover></fp-profile>\
2137
+ <v-spacer></v-spacer>\
2138
+ <v-btn @click.stop=\"on_of_menu\" v-if=\"(menu || leftMenuAndMobile) && !isMobile\" :color=\"menuStyle.topmenu.textcolor\" icon><v-icon :color=\"menuStyle.leftmenu.textcolor\">{{ pinLeftMenu?'mdi-pin-outline':'mdi-pin-off-outline'}}</v-icon></v-btn>\
2139
+ </v-list-item>\
2140
+ </template>\
2141
+ </v-navigation-drawer>\
2142
+ </div>",
2143
+ data: function () {
2144
+ var currentFullUrl = window.location.pathname + window.location.search;
2145
+ var leftMenuAndMobile = [];
2146
+ var menuSource = null;
2147
+
2148
+ if (this.$vuetify.breakpoint.smAndDown) {
2149
+ _.forEach(_.union(this.menu, this.menuLeft), function (value) {
2150
+ if (rightTest(value.right)) leftMenuAndMobile.push(value);
2151
+ });
2152
+ if (leftMenuAndMobile.length == 0) leftMenuAndMobile = null;
2153
+ menuSource = leftMenuAndMobile;
2154
+ } else {
2155
+ menuSource = this.menuLeft;
2156
+ leftMenuAndMobile = this.menuLeft;
2157
+ }
2158
+
2159
+ // Chỉ xử lý auto-expand nếu có menu bên trái
2160
+ if (menuSource && menuSource.length > 0) {
2161
+ _.forEach(menuSource, function (item) {
2162
+ if (item.submenu) {
2163
+ var hasActiveChild = _.some(item.submenu, function (subItem) {
2164
+ return subItem.url && currentFullUrl === subItem.url;
2165
+ });
2166
+ if (hasActiveChild) {
2167
+ item.active = true;
2168
+ }
2169
+ }
2170
+ });
2171
+ }
2172
+
2173
+ return {
2174
+ leftMenuAndMobile: leftMenuAndMobile,
2175
+ openLeftMenu: this.isMobile,
2176
+ pinLeftMenu: (sessionStorage.getItem("fpOpenmenuAppBar") == null ? !this.expand : sessionStorage.getItem("fpOpenmenuAppBar") == 'true'),
2177
+ boxShadow: 0,
2178
+ currentFullUrl: currentFullUrl
2179
+ };
2180
+ },
2181
+ computed: {
2182
+ isMobile: function () {
2183
+ if (this.$vuetify.breakpoint.smAndDown) return true;
2184
+ else return false;
2185
+ }
2186
+ },
2187
+ methods: {
2188
+ on_of_menu: function (e) {
2189
+ this.pinLeftMenu = !this.pinLeftMenu;
2190
+ sessionStorage.setItem("fpOpenmenuAppBar", this.pinLeftMenu);
2191
+ },
2192
+ checkright: function (item) {
2193
+ if (!item.right) return true;
2194
+ if (!vueData.user) return false;
2195
+
2196
+ return rightTest(item.right);
2197
+ },
2198
+ isActiveUrl: function (url) {
2199
+ if (!url) return false;
2200
+ return this.currentFullUrl === url;
2201
+ },
2202
+ scrollToActiveMenu: function () {
2203
+ if (!this.leftMenuAndMobile || this.leftMenuAndMobile.length === 0) return;
2204
+
2205
+ var self = this;
2206
+ this.$nextTick(function () {
2207
+ setTimeout(function () {
2208
+ var activeEl = self.$el.querySelector('[data-active="true"]');
2209
+ if (activeEl) {
2210
+ // Fallback cho trình duyệt cũ không hỗ trợ options
2211
+ if ('scrollBehavior' in document.documentElement.style) {
2212
+ activeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
2213
+ } else {
2214
+ activeEl.scrollIntoView(true);
2215
+ }
2216
+ }
2217
+ }, 300);
2218
+ });
2219
+ },
2220
+ injectScrollbarStyles: function () {
2221
+ if (!this.leftMenuAndMobile || this.leftMenuAndMobile.length === 0) return;
2222
+
2223
+ var styleId = 'fp-custom-scrollbar-style';
2224
+ if (document.getElementById(styleId)) return;
2225
+
2226
+ var thumbColor = 'rgba(255,255,255,0.3)';
2227
+ var thumbHoverColor = 'rgba(255,255,255,0.5)';
2228
+ var trackColor = 'rgba(0,0,0,0.1)';
2229
+
2230
+ var style = document.createElement('style');
2231
+ style.id = styleId;
2232
+ style.textContent = [
2233
+ '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar {',
2234
+ ' width: 5px;',
2235
+ '}',
2236
+ '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar-track {',
2237
+ ' background: ' + trackColor + ';',
2238
+ '}',
2239
+ '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar-thumb {',
2240
+ ' background: ' + thumbColor + ';',
2241
+ ' border-radius: 3px;',
2242
+ '}',
2243
+ '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar-thumb:hover {',
2244
+ ' background: ' + thumbHoverColor + ';',
2245
+ '}',
2246
+ '.fp-custom-scrollbar .v-navigation-drawer__content {',
2247
+ ' scrollbar-width: thin;',
2248
+ ' scrollbar-color: ' + thumbColor + ' ' + trackColor + ';',
2249
+ '}'
2250
+ ].join('\n');
2251
+ document.head.appendChild(style);
2252
+ }
2253
+ },
2254
+ created: function () {
2255
+ if (!this.$isServer) {
2256
+ var self = this;
2257
+ this._scrollListener = function () {
2258
+ self.boxShadow = Math.round(window.pageYOffset) / 10;
2259
+ if (self.boxShadow > 10) self.boxShadow = 10;
2260
+ };
2261
+ this._scrollListener();
2262
+ window.addEventListener('scroll', this._scrollListener);
2263
+ }
2264
+ },
2265
+ mounted: function () {
2266
+ if (this.leftMenuAndMobile && this.leftMenuAndMobile.length > 0) {
2267
+ this.injectScrollbarStyles();
2268
+ this.scrollToActiveMenu();
2269
+ }
2270
+ },
2271
+ beforeDestroy: function () {
2272
+ window.removeEventListener('scroll', this._scrollListener);
2273
+ }
2274
+ });
2275
+
2276
+ //===========================================================================================
2277
+ Vue.component('f-chart', {
2278
+ props: ["data", "type", "options", "reverseData"],
2279
+ template: `<canvas ref="myChart"></canvas>`,
2280
+ data: function () {
2281
+ return {
2282
+ myChart: null,
2283
+ config: {
2284
+ type: this.type,
2285
+ data: this.data,
2286
+ options: this.options
2287
+ },
2288
+ chart_color: [
2289
+ "#1e90ff",
2290
+ "#ffa64d",
2291
+ "#6fdc6f",
2292
+ "#f14aff",
2293
+ "#9966ff",
2294
+ "#02e8e8",
2295
+ "#402b58",
2296
+ "#3b8d74",
2297
+ "#095fa4",
2298
+ "#b63639",
2299
+ "#c84f7a",
2300
+ "#a8b236",
2301
+ "#90692f",
2302
+ "#b42e89",
2303
+ "#ff9966",
2304
+ "#4db8ff",
2305
+ "#1c8540",
2306
+ "#ff6699",
2307
+ "#5666ad",
2308
+ "#b2b266",
2309
+ "#e2976b",
2310
+ "#84c887",
2311
+ "#4d9dd0",
2312
+ "#a187f7",
2313
+ "#d9745b",
2314
+ "#862929",
2315
+ "#ea80fc",
2316
+ "#8c4391",
2317
+ "#ff8c00",
2318
+ "#53915b",
2319
+ "#6dcbab",
2320
+ "#7b7353",
2321
+ "#d4e157",
2322
+ "#cc242f",
2323
+ "#c69e31",
2324
+ "#fdd835",
2325
+ // Tableau20
2326
+ "#4E79A7",
2327
+ "#A0CBE8",
2328
+ "#F28E2B",
2329
+ "#FFBE7D",
2330
+ "#59A14F",
2331
+ "#8CD17D",
2332
+ "#B6992D",
2333
+ "#F1CE63",
2334
+ "#499894",
2335
+ "#86BCB6",
2336
+ "#E15759",
2337
+ "#FF9D9A",
2338
+ "#79706E",
2339
+ "#BAB0AC",
2340
+ "#D37295",
2341
+ "#FABFD2",
2342
+ "#B07AA1",
2343
+ "#D4A6C8",
2344
+ "#9D7660",
2345
+ "#D7B5A6",
2346
+ ]
2347
+ }
2348
+ },
2349
+ mounted() {
2350
+ this.buildChart(this);
2351
+ },
2352
+ watch: {
2353
+ data: {
2354
+ deep: true,
2355
+ handler(newValue) {
2356
+ this.myChart.destroy();
2357
+ this.config.data = newValue;
2358
+ this.buildChart(this);
2359
+ }
2360
+ }
2361
+ },
2362
+ methods: {
2363
+
2364
+ buildChart(self) {
2365
+ if (self.reverseData && self.config.data.labels) self.config.data.labels = _.reverse(self.config.data.labels);
2366
+ let n = parseInt(Math.random() * 45);
2367
+
2368
+ if (self.config.data) {
2369
+ _.forEach(self.config.data.datasets, function (v, k) {
2370
+ if (self.reverseData && v.data) v.data = _.reverse(v.data);
2371
+ if (!v.type) v.type = self.config.type;
2372
+
2373
+ if (v.type == 'pie' || v.type == 'doughnut' || v.type == 'polarArea') {
2374
+ if (!v.backgroundColor) v.backgroundColor = self.chart_color;
2375
+ } else {
2376
+ if (!v.borderColor) v.borderColor = self.chart_color[k + n % self.chart_color.length];
2377
+ if (!v.backgroundColor) v.backgroundColor = transparentize(v.borderColor, 0.6);
2378
+ if (v.type && v.type != 'line') if (!v.borderWidth) v.borderWidth = 1;
2379
+ }
2380
+ });
2381
+
2382
+ self.myChart = new Chart(self.$refs.myChart, self.config);
2383
+ }
2384
+ },
2385
+ }
2386
+ });
2387
+
2388
+ //===========================================================================================
2389
+ Vue.component('f-window', {
2390
+ props: ['id'],
2391
+ template: `
2392
+ <v-dialog v-if="prop.open" v-model="prop.open" :fullscreen="(prop.width?false:true)" :width="prop.width" :max-width="prop.width" scrollable persistent :transition="prop.transition">
2393
+ <v-card class="rounded-0" :height="prop.height">
2394
+ <v-toolbar v-if="prop.headerbar" dense flat :dark="(prop.toolbarcolor?true:false)" :color="prop.toolbarcolor">
2395
+ <v-toolbar-title style="font-size: 18px;" v-html="prop.title"></v-toolbar-title>
2396
+ <v-spacer></v-spacer>
2397
+ <f-button :checkvalid=false v-for="(item,i) in prop.button" :key="i" text v-bind="item"></f-button>
2398
+ <v-btn icon :dark="(prop.toolbarcolor?true:false)" @click="prop.open = false" class="ml-4"><v-icon>mdi-close</v-icon></v-btn>
2399
+ </v-toolbar>
2400
+ <iframe v-bind="$attrs" :id="iframeDialog" name='frameName' height="100%" width="100%" style="border:none;" allow="fullscreen"></iframe>
2401
+ </v-card>
2402
+ </v-dialog>
2403
+ `,
2404
+ data: function () {
2405
+ var propInit = vueData[this.id];
2406
+
2407
+ if (typeof propInit.height == 'undefined') propInit.height = '100%';
2408
+ if (_.isInteger(propInit.height)) propInit.height = propInit.height + 'px';
2409
+ if (typeof propInit.toolbarcolor == 'undefined' && !(propInit.height != '100%' || propInit.width)) propInit.toolbarcolor = 'primary';
2410
+
2411
+ if (typeof propInit.headerbar == 'undefined') propInit.headerbar = true;
2412
+ if (typeof propInit.transition == 'undefined') propInit.transition = 'scale-transition';
2413
+
2414
+ if (propInit.button && _.isPlainObject(propInit.button)) propInit.button = [propInit.button];
2415
+
2416
+ return {
2417
+ prop: propInit,
2418
+ iframeDialog: this.id
2419
+ }
2420
+ },
2421
+ watch: {
2422
+ "prop.open": function (newValue, oldValue) {
2423
+ if (!newValue) {
2424
+ if (this.prop.onclose) runAction(this.prop.onclose, this.prop);
2425
+ setTimeout(() => {
2426
+ if (vueData.v_windowVM[this.id]) {
2427
+ vueData.v_windowVM[this.id].$destroy();
2428
+ delete vueData.v_windowVM[this.id];
2429
+ delete vueData[this.id];
2430
+ if (Object.keys(vueData.v_windowVM).length == 0) $('html').css({ "overflow": "" });
2431
+ }
2432
+ }, 10);
2433
+ }
2434
+ }
2435
+ },
2436
+ mounted: function () {
2437
+ var self = this;
2438
+ setTimeout(function () {
2439
+ console.log(self.prop.url);
2440
+ document.getElementById(self.iframeDialog).src = self.prop.url;
2441
+ }, 250);
2442
+ $('html').css({
2443
+ 'cssText': 'overflow: hidden !important'
2444
+ });
2445
+ },
2446
+ destroyed() {
2447
+ console.log("window destroyed : ", this.id)
2448
+ }
2449
+ });
2450
+
2451
+ //===========================================================================================
2452
+
2453
+ Vue.component('f-editor', {
2454
+ props: {
2455
+ value: {
2456
+ type: String,
2457
+ default: null,
2458
+ },
2459
+ imageapi: {
2460
+ type: String,
2461
+ default: 'https://fap.vn',
2462
+ },
2463
+ height: {
2464
+ type: Number,
2465
+ default: null,
2466
+ },
2467
+ toolbar: {
2468
+ type: String,
2469
+ default: null,
2470
+ },
2471
+ },
2472
+ template: `<div ref="elID" v-bind="$attrs"></div>`,
2473
+ data: function () {
2474
+ return {
2475
+ emitData: true,
2476
+ Editor: null
2477
+ }
2478
+ },
2479
+ watch: {
2480
+ value: function (newVal, oldVal) {
2481
+ this.$nextTick(() => {
2482
+ if (!this.emitData && newVal && this.Editor) this.Editor.setData(newVal);
2483
+ this.emitData = false;
2484
+ });
2485
+ }
2486
+ },
2487
+ mounted: function () {
2488
+ setTimeout(() => {
2489
+ ClassicEditor.create(this.$refs.elID, {
2490
+ toolbar: {
2491
+ shouldNotGroupWhenFull: true,
2492
+ items: (this.toolbar ? this.toolbar : [
2493
+ 'undo',
2494
+ 'redo',
2495
+ 'removeFormat',
2496
+ 'heading',
2497
+ 'fontFamily',
2498
+ 'fontSize',
2499
+ '|',
2500
+ 'bulletedList',
2501
+ 'numberedList',
2502
+ 'outdent',
2503
+ 'indent',
2504
+ '|',
2505
+ 'specialCharacters',
2506
+ 'MathType',
2507
+ 'ChemType',
2508
+ 'horizontalLine',
2509
+ 'subscript',
2510
+ 'superscript',
2511
+ 'blockQuote',
2512
+ '|',
2513
+ 'fontColor',
2514
+ 'fontBackgroundColor',
2515
+ 'highlight',
2516
+ '|',
2517
+ 'alignment',
2518
+ 'bold',
2519
+ 'italic',
2520
+ 'underline',
2521
+ 'strikethrough',
2522
+ '|',
2523
+ 'insertTable',
2524
+ 'mediaEmbed',
2525
+ 'link',
2526
+ 'imageUpload',
2527
+ ])
2528
+ },
2529
+ language: 'en',
2530
+ mediaEmbed: {
2531
+ toolbar: ['mediaEmbed']
2532
+ },
2533
+ image: {
2534
+ toolbar: [
2535
+ 'imageStyle:alignLeft', 'imageStyle:alignCenter', 'imageStyle:alignRight',
2536
+ '|',
2537
+ 'resizeImage',
2538
+ '|',
2539
+ 'imageTextAlternative',
2540
+ 'linkImage'
2541
+ ],
2542
+ styles: [
2543
+ 'alignLeft', 'alignCenter', 'alignRight'
2544
+ ],
2545
+ },
2546
+ table: {
2547
+ contentToolbar: [
2548
+ 'tableColumn',
2549
+ 'tableRow',
2550
+ 'mergeTableCells',
2551
+ 'tableCellProperties',
2552
+ 'tableProperties'
2553
+ ]
2554
+ },
2555
+ licenseKey: '',
2556
+ simpleUpload: {
2557
+ // The URL that the images are uploaded to.
2558
+ uploadUrl: this.imageapi,
2559
+ // Enable the XMLHttpRequest.withCredentials property.
2560
+ withCredentials: false,
2561
+ // Headers sent along with the XMLHttpRequest to the upload server.
2562
+ headers: {
2563
+ Authorization: 'Bearer ' + getCookie('awt')
2564
+ }
2565
+ },
2566
+
2567
+ }).then(editor => {
2568
+ this.Editor = editor;
2569
+ if (this.value) editor.setData(this.value);
2570
+ editor.editing.view.change(writer => {
2571
+ if (!this.height) this.height = 200;
2572
+ writer.setStyle({ 'height': this.height + 'px' }, editor.editing.view.document.getRoot());
2573
+ });
2574
+
2575
+ editor.model.document.on('change:data', () => {
2576
+ if (this.value != editor.getData()) {
2577
+ this.emitData = true;
2578
+ this.$emit('input', editor.getData());
2579
+ }
2580
+ });
2581
+
2582
+ this.$emit('input', editor.getData());
2583
+ }).catch(error => {
2584
+ console.log(error);
2585
+ });
2586
+ }, 100);
2587
+ },
2588
+ methods: {}
2589
+ });
2590
+ //===========================================================================================
2591
+ Vue.component('f-editor-dialog', {
2592
+ props: {
2593
+ value: {
2594
+ type: String,
2595
+ default: null,
2596
+ },
2597
+ imageapi: {
2598
+ type: String,
2599
+ default: null,
2600
+ },
2601
+ label: {
2602
+ type: String,
2603
+ default: '',
2604
+ },
2605
+ width: {
2606
+ type: Number,
2607
+ default: 800,
2608
+ },
2609
+ toolbar: {
2610
+ type: String,
2611
+ default: null,
2612
+ },
2613
+ },
2614
+ template: `
2615
+ <v-overlay v-model="value.open" style="width: 100vw; height: 100vh; left: 0px; top: 0px; position: fixed;">
2616
+ <v-card light elevation="10" :width="width" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); -webkit-transform: translate(-50%, -50%); ">
2617
+ <v-toolbar dense flat>
2618
+ <v-toolbar-title>{{label}}</v-toolbar-title>
2619
+ <v-spacer></v-spacer>
2620
+ <v-btn text color="orange darken-2" @click="closeDialog(0)" class="mr-6">
2621
+ <v-icon left>mdi-close</v-icon>Cancel
2622
+ </v-btn>
2623
+ <v-btn text color="primary" @click="closeDialog(1)">
2624
+ <v-icon left>mdi-check</v-icon>OK
2625
+ </v-btn>
2626
+ </v-toolbar>
2627
+ <v-container text-left fluid class="pt-0">
2628
+ <v-layout>
2629
+ <v-flex xs12 :style="{height: windowHeight + 'px'}">
2630
+ <f-editor v-model="textData" :imageapi="imageapi" :height="windowHeight - 76"></f-editor>
2631
+ </v-flex>
2632
+ </v-layout>
2633
+ <v-layout><v-flex xs12></v-flex></v-layout>
2634
+ </v-container>
2635
+ </v-card>
2636
+ </v-overlay>
2637
+ `,
2638
+ data: function () {
2639
+ return {
2640
+ textData: this.getData(this.value),
2641
+ windowHeight: $(window).height() * 0.9 - 70,
2642
+ }
2643
+ },
2644
+ watch: {
2645
+ 'value.text': {
2646
+ handler(newVal, oldVal) {
2647
+ this.textData = this.getData(this.value);
2648
+ }
2649
+ },
2650
+ 'value.object': {
2651
+ deep: true,
2652
+ handler(newVal, oldVal) {
2653
+ this.textData = this.getData(this.value);
2654
+ }
2655
+ },
2656
+ },
2657
+ methods: {
2658
+ cleanWhitespace(html) {
2659
+ // Loại bỏ khoảng trắng sau <p> và trước </p>
2660
+ let cleanedContent = html.replace(/<p>\s*(?:&nbsp;|\s)*|(?:&nbsp;|\s)*<\/p>/g, function (match) {
2661
+ return match.startsWith('<p>') ? '<p>' : '</p>';
2662
+ }).trim();
2663
+
2664
+ // Loại bỏ các thẻ <p></p> rỗng hoặc chỉ chứa khoảng trắng ở đầu và cuối chuỗi
2665
+ cleanedContent = cleanedContent.replace(/^(<p>\s*(?: |\s)*<\/p>\s*)+/, '');
2666
+ cleanedContent = cleanedContent.replace(/(<p>\s*(?: |\s)*<\/p>\s*)+$/, '');
2667
+
2668
+ return cleanedContent.replace(/<p>\s*(?: |\s)*<\/p>/g, '<p>&nbsp;</p>');
2669
+ },
2670
+ getData(d) {
2671
+ if (!d) return "";
2672
+ if (d.object) return d.object[d.text];
2673
+ else return d.text;
2674
+ },
2675
+ closeDialog(e) {
2676
+ if (e == 1) {
2677
+ if (this.value.object) this.value.object[this.value.text] = this.cleanWhitespace(this.textData);
2678
+ else this.value.text = this.cleanWhitespace(this.textData);
2679
+
2680
+ this.value.ok = true;
2681
+ } else this.value.ok = false;
2682
+ this.value.open = false;
2683
+ },
2684
+ },
2685
+ });
2686
+ //===========================================================================================
2687
+ Vue.component('f-dialog', {
2688
+ props: {
2689
+ value: {
2690
+ type: Boolean,
2691
+ default: false
2692
+ },
2693
+ data: {
2694
+ type: Object,
2695
+ default: {}
2696
+ },
2697
+ dataOut: {
2698
+ type: Object,
2699
+ default: {}
2700
+ },
2701
+ title: {
2702
+ type: String,
2703
+ default: ''
2704
+ },
2705
+ controls: {
2706
+ type: Array,
2707
+ default: []
2708
+ },
2709
+ watch: {
2710
+ type: Object,
2711
+ default: {}
2712
+ },
2713
+ button: {
2714
+ default: []
2715
+ },
2716
+ disableValidate: {
2717
+ type: Boolean,
2718
+ default: false
2719
+ },
2720
+ width: {
2721
+ type: Number,
2722
+ default: 800
2723
+ },
2724
+ openAction: {
2725
+ type: Object,
2726
+ default: null
2727
+ },
2728
+ },
2729
+ data: function () {
2730
+ var controlsForm = [{
2731
+ prop: "fluid text-center grid-list-lg py-0",
2732
+ rows: []
2733
+ }];
2734
+
2735
+ var row = {
2736
+ prop: "row",
2737
+ cols: []
2738
+ };
2739
+
2740
+ var updateUI = bindData(this.controls);
2741
+
2742
+ if (_.isArray(updateUI) && updateUI.length > 0) {
2743
+ if (updateUI[0].rows) controlsForm = updateUI;
2744
+ else {
2745
+ _.forEach(updateUI, function (c) {
2746
+ if (!c.w) c.w = 12
2747
+ row.cols.push(c);
2748
+ if (c.br) {
2749
+ controlsForm[0].rows.push(_.cloneDeep(row));
2750
+ row = {
2751
+ prop: "row",
2752
+ cols: []
2753
+ };
2754
+ }
2755
+ });
2756
+ if (row.cols.length > 0) controlsForm[0].rows.push(_.cloneDeep(row));
2757
+ }
2758
+ };
2759
+
2760
+ var comData = { v_outData: {}, v_watch: {} };
2761
+ var controlsFormDOM = buildModuleUI(controlsForm, comData);
2762
+
2763
+ //---------build v-model data object -------------------------------------
2764
+ _.forEach(controlsForm, function (C) {
2765
+ _.forEach(C.rows, function (R) {
2766
+ _.forEach(R.cols, function (e) {
2767
+ if (e.attr['v-model'] && e.attr['v-model'].indexOf('vueData.') < 0) {
2768
+ comData.v_outData[e.attr['v-model']] = null;
2769
+ comData[e.attr['v-model']] = null;
2770
+ }
2771
+ });
2772
+ });
2773
+ });
2774
+
2775
+ this.setDefaultData(comData);
2776
+ //---------------------------------------------------------------------------------------------------------
2777
+ this.$options.template = `<div>
2778
+ <v-overlay v-if="value" absolute style="position: fixed;"></v-overlay>
2779
+ <v-scale-transition origin="center center">
2780
+ <v-overlay v-if="value" opacity=0 absolute style="position: fixed;">
2781
+ <v-form ref="formTable" lazy-validation @submit.prevent="return true">
2782
+ <v-card light elevation="10" :width="width" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); -webkit-transform: translate(-50%, -50%); ">
2783
+ <v-toolbar dense flat>
2784
+ <v-toolbar-title>{{ title }}</v-toolbar-title>
2785
+ <v-spacer></v-spacer>
2786
+ <v-btn icon @click="$emit('input', false)">
2787
+ <v-icon>mdi-close</v-icon>
2788
+ </v-btn>
2789
+ </v-toolbar>
2790
+ <v-card-text class="py-0 px-0">${controlsFormDOM[0].outerHTML}</v-card-text>
2791
+ <v-card-actions class="mr-1 mb-1">
2792
+ <v-spacer></v-spacer>
2793
+ ${this.buildButton(this.button)}
2794
+ </v-card-actions>
2795
+ </v-card>
2796
+ </v-form>
2797
+ </v-overlay>
2798
+ </v-scale-transition>
2799
+ </div>
2800
+ `;
2801
+ //------------------------------------------------------------------------------------------------------------
2802
+ return comData;
2803
+ },
2804
+ mounted: function () {
2805
+ //this.watchDialog(this.watch);
2806
+ if (typeof this.value == "undefined") this.value = true;
2807
+ },
2808
+ watch: {
2809
+ value(val) {
2810
+ if (val) {// Open Dialog
2811
+ this.setDefaultData(this);
2812
+ this.watchDialog(this.watch);
2813
+ if (this.openAction) runAction(this.openAction, this);
2814
+ console.log("Dialog Data:", this)
2815
+ }
2816
+ else {// Close Dialog
2817
+ _.forEach(this.v_watch, (item) => {
2818
+ item();
2819
+ });
2820
+ }
2821
+ }
2822
+ },
2823
+ methods: {
2824
+ setDefaultData(obj) {
2825
+ _.assignIn(obj.v_outData, _.assignIn({}, this.data));
2826
+ _.assignIn(obj, _.assignIn({}, obj.v_outData));
2827
+ this.$emit('update:data-out', this);
2828
+ },
2829
+ formvalidate() {
2830
+ if (this.disableValidate) return true;
2831
+ return this.$refs.formTable.validate(true);
2832
+ },
2833
+ watchDialog(obj) {
2834
+ if (_.isPlainObject(obj)) {
2835
+ _.forEach(obj, (value, key) => {
2836
+ console.log("ADD dialog watch : " + key);
2837
+ if (!this.hasOwnProperty(key)) this[key] = undefined;
2838
+
2839
+ this.v_watch["v_watch_" + key] = this.$watch(key, (newValue, oldValue) => {
2840
+ runAction(value, this);
2841
+ });
2842
+ });
2843
+ }
2844
+ },
2845
+ buildButton(buttonArray) {
2846
+ var buttonContainer = $("<div>");
2847
+ _.forEach(buttonArray, function (C, index) {
2848
+ C = _.assignIn({
2849
+ elevation: 0,
2850
+ color: "primary",
2851
+ text: true,
2852
+ }, C);
2853
+
2854
+ delete C.action;
2855
+
2856
+ var btn = $("<v-btn>").attr(C);
2857
+ btn.attr({ "v-on:click": "buttonclick(button[" + index + "])" });
2858
+
2859
+ if (C['icon-text']) btn.append("<v-icon left>" + C['icon-text'] + "</v-icon>" + C['label']);
2860
+ else btn.append(C['label']);
2861
+
2862
+ buttonContainer.append(btn);
2863
+ });
2864
+
2865
+ return buttonContainer[0].innerHTML;
2866
+ },
2867
+ buttonclick: _.debounce(function (e) {
2868
+ if (e.getoutdata || e.checkvalid) {
2869
+ if (!this.formvalidate()) {
2870
+ showMessage({ icon: 'mdi mdi-alert-outline', type: "red", title: "Hãy điền đầy đủ dữ liệu bắt buộc !" })
2871
+ return;
2872
+ }
2873
+ }
2874
+
2875
+ _.forEach(this.v_outData, (v, k) => {
2876
+ this.v_outData[k] = this[k];
2877
+ });
2878
+
2879
+ if (e.getoutdata) {
2880
+ this.$emit('update:data', this.v_outData);
2881
+ }
2882
+
2883
+ console.log("action", e.action)
2884
+
2885
+ if (_.isFunction(e.action)) e.action();
2886
+ else
2887
+ if (e.action) {
2888
+ let dData = _.assignIn({}, this.v_outData, { dData: this.v_outData });
2889
+ console.log("Dialog Data: ", dData);
2890
+ runAction(e.action, dData);
2891
+ }
2892
+
2893
+ }, 500, { leading: true, trailing: false }),
2894
+
2895
+ }
2896
+ });
2897
+
2898
+ //===========================================================================================
2899
+ Vue.component('f-pdfmake', {
2900
+ props: ['data'],
2901
+ template: `
2902
+ <v-card elevation="0" style="text-align: right">
2903
+ <v-overlay absolute :value="overlay"><v-progress-circular indeterminate color="white"></v-progress-circular></v-overlay>
2904
+ <iframe id="pdfmakeViewer" v-bind="$attrs" style="border: thin solid rgba(0,0,0,.12); margin:0;"></iframe>
2905
+ </v-card>
2906
+ `,
2907
+ data: function () {
2908
+ return {
2909
+ overlay: true
2910
+ }
2911
+ },
2912
+ mounted: function () {
2913
+ var obj = this;
2914
+ printPDF({
2915
+ data: _.cloneDeep(obj.data),
2916
+ viewer: '#pdfmakeViewer',
2917
+ callBack: function () {
2918
+ obj.overlay = false;
2919
+ }
2920
+ });
2921
+ },
2922
+ watch: {
2923
+ data: {
2924
+ deep: true,
2925
+ handler(newVal) {
2926
+ printPDF({ data: _.cloneDeep(newVal), viewer: '#pdfmakeViewer' });
2927
+ }
2928
+ }
2929
+ },
2930
+
2931
+ });
2932
+
2933
+ //===========================================================================================
2934
+ Vue.component('f-excel-reader', {
2935
+ model: { prop: 'value', event: 'input' },
2936
+ props: {
2937
+ value: { type: Array, default: function () { return []; } },
2938
+
2939
+ header: { type: [String, Number, Array], default: null },
2940
+ raw: { type: [Boolean, String], default: false },
2941
+ dateFormat: { type: String, default: 'dd/mm/yyyy' },
2942
+ defval: { type: [String, Number], default: '' },
2943
+
2944
+ sheet: { type: [String, Number], default: 0 },
2945
+ skipRows: { type: [String, Number], default: 0 },
2946
+
2947
+ label: { type: String, default: 'Load Excel' },
2948
+ iconText: { type: String, default: 'mdi-grid' },
2949
+ accept: { type: String, default: '.xlsx,.xlsb,.xlsm,.xls,.csv,.txt,.ods,.dbf,.prn,.html,.htm' },
2950
+ maxSize: { type: [String, Number], default: 20 },
2951
+ loadTimeout: { type: [String, Number], default: 15000 },
2952
+
2953
+ action: { type: Object, default: null }
2954
+ },
2955
+ template: `
2956
+ <div>
2957
+ <v-btn v-bind="$attrs" v-on="$listeners"
2958
+ :disabled="parsing" :loading="busy" @click="onButtonClick">
2959
+ <v-icon v-if="iconText" left>{{ iconText }}</v-icon>
2960
+ {{ label }}
2961
+ </v-btn>
2962
+ <input ref="uploader" style="display:none" type="file" :multiple="false"
2963
+ :accept="accept" @change="onchange" onclick="this.value=null;">
2964
+ </div>
2965
+ `,
2966
+ data: function () {
2967
+ return { busy: false, parsing: false, reqId: 0 };
2968
+ },
2969
+ computed: {
2970
+ optHeader: function () {
2971
+ var h = this.header;
2972
+ if (h === null || h === undefined || h === '') return null;
2973
+ if (Array.isArray(h)) return h;
2974
+ var s = String(h).trim();
2975
+ if (s === '1') return 1;
2976
+ if (s.toUpperCase() === 'A') return 'A';
2977
+ return h;
2978
+ },
2979
+ optRaw: function () {
2980
+ if (typeof this.raw === 'boolean') return this.raw;
2981
+ var s = String(this.raw).trim().toLowerCase();
2982
+ return s === 'true' || s === '1' || s === 'yes';
2983
+ },
2984
+ optRange: function () {
2985
+ var n = parseInt(this.skipRows, 10);
2986
+ return n > 0 ? n : undefined;
2987
+ },
2988
+ limitMB: function () {
2989
+ var mb = parseFloat(this.maxSize);
2990
+ return isNaN(mb) ? 20 : mb;
2991
+ }
2992
+ },
2993
+ methods: {
2994
+ onButtonClick: function () {
2995
+ var ctrl = this;
2996
+ if (ctrl.parsing) return; // (2)
2997
+ ctrl.busy = true;
2998
+ window.addEventListener('focus', function () {
2999
+ setTimeout(function () { if (!ctrl.parsing) ctrl.busy = false; }, 300);
3000
+ }, { once: true });
3001
+ ctrl.$refs.uploader.click();
3002
+ },
3003
+
3004
+ onchange: function (evt) {
3005
+ var ctrl = this;
3006
+ var file = evt.target.files && evt.target.files[0];
3007
+ evt.target.value = '';
3008
+
3009
+ if (ctrl.parsing) return; // (2)
3010
+ if (!file) { ctrl.busy = false; return; }
3011
+
3012
+ var id = ++ctrl.reqId; // (1)
3013
+ var parsed = false; // (3) cờ chốt: đã đọc xong Excel
3014
+ ctrl.parsing = true;
3015
+ ctrl.busy = true;
3016
+
3017
+ Promise.resolve()
3018
+ .then(function () {
3019
+ if (ctrl.limitMB > 0 && file.size > ctrl.limitMB * 1024 * 1024)
3020
+ throw new Error('File vượt quá ' + ctrl.limitMB + 'MB');
3021
+ return ctrl.ensureXLSX();
3022
+ })
3023
+ .then(function () { return ctrl.parseFile(file); })
3024
+ .then(function (res) {
3025
+ if (id !== ctrl.reqId) return; // (1) lượt cũ -> vứt
3026
+ ctrl.parsing = false;
3027
+ ctrl.busy = false;
3028
+ parsed = true;
3029
+
3030
+ ctrl.$emit('input', res.rows);
3031
+ ctrl.$emit('load', res.meta);
3032
+ ctrl.$nextTick(function () { ctrl.runAction(file); }); // (3)
3033
+ })
3034
+ .catch(function (err) {
3035
+ if (id !== ctrl.reqId || parsed) { // (1)(3)
3036
+ console.warn('[FExcelReader] bỏ qua lỗi ngoài phạm vi đọc Excel:', err);
3037
+ return;
3038
+ }
3039
+ ctrl.fail(err, file);
3040
+ });
3041
+ },
3042
+
3043
+ ensureXLSX: function () {
3044
+ var ctrl = this;
3045
+ return new Promise(function (resolve, reject) {
3046
+ if (typeof XLSX !== 'undefined') return resolve();
3047
+ if (typeof Loader === 'undefined')
3048
+ return reject(new Error('Không tìm thấy Loader để nạp thư viện XLSX'));
3049
+
3050
+ var settled = false;
3051
+ var ms = parseInt(ctrl.loadTimeout, 10) || 15000;
3052
+ var timer = setTimeout(function () {
3053
+ if (settled) return;
3054
+ settled = true;
3055
+ reject(new Error('Quá thời gian nạp thư viện XLSX'));
3056
+ }, ms);
3057
+
3058
+ new Loader().require(['/include/excel/xlsx.full.min.js'], function () {
3059
+ if (settled) return;
3060
+ settled = true;
3061
+ clearTimeout(timer);
3062
+ if (typeof XLSX !== 'undefined') resolve();
3063
+ else reject(new Error('Nạp thư viện XLSX thất bại'));
3064
+ });
3065
+ });
3066
+ },
3067
+
3068
+ readBuffer: function (file) {
3069
+ return new Promise(function (resolve, reject) {
3070
+ var reader = new FileReader();
3071
+ reader.onload = function (e) { resolve(e.target.result); };
3072
+ reader.onerror = function () { reject(new Error('Không đọc được file: ' + file.name)); };
3073
+ reader.onabort = function () { reject(new Error('Việc đọc file bị hủy')); };
3074
+ reader.readAsArrayBuffer(file);
3075
+ });
3076
+ },
3077
+
3078
+ parseFile: function (file) {
3079
+ var ctrl = this;
3080
+ return ctrl.readBuffer(file).then(function (buf) {
3081
+ var wb = XLSX.read(new Uint8Array(buf), { type: 'array', cellDates: true });
3082
+
3083
+ var isIndex = typeof ctrl.sheet === 'number' || /^\d+$/.test(String(ctrl.sheet));
3084
+ var wsname = isIndex
3085
+ ? wb.SheetNames[parseInt(ctrl.sheet, 10)]
3086
+ : (wb.SheetNames.indexOf(ctrl.sheet) !== -1 ? ctrl.sheet : wb.SheetNames[0]);
3087
+
3088
+ var ws = wb.Sheets[wsname];
3089
+ if (!ws) throw new Error('Không tìm thấy sheet: ' + ctrl.sheet);
3090
+
3091
+ var rows = XLSX.utils.sheet_to_json(ws, {
3092
+ header: ctrl.optHeader,
3093
+ raw: ctrl.optRaw,
3094
+ dateNF: ctrl.dateFormat,
3095
+ defval: ctrl.defval,
3096
+ range: ctrl.optRange,
3097
+ blankrows: false
3098
+ });
3099
+
3100
+ return {
3101
+ rows: rows,
3102
+ meta: {
3103
+ rows: rows,
3104
+ rowCount: rows.length,
3105
+ headers: (ctrl.optHeader === 1 && rows.length) ? rows[0] : Object.keys(rows[0] || {}),
3106
+ sheetName: wsname,
3107
+ sheetNames: wb.SheetNames,
3108
+ fileName: file.name
3109
+ }
3110
+ };
3111
+ });
3112
+ },
3113
+
3114
+ runAction: function (file) {
3115
+ var ctrl = this;
3116
+ if (!ctrl.action || typeof CALL !== 'function') return;
3117
+ var onErr = function (err) {
3118
+ console.error('[FExcelReader] action lỗi (dữ liệu Excel vẫn hợp lệ):', err);
3119
+ ctrl.$emit('action-error', {
3120
+ message: (err && err.message) || String(err),
3121
+ fileName: file.name
3122
+ });
3123
+ };
3124
+ try {
3125
+ var r = CALL(ctrl.action);
3126
+ if (r && typeof r.then === 'function') r.then(null, onErr);
3127
+ } catch (err) {
3128
+ onErr(err);
3129
+ }
3130
+ },
3131
+
3132
+ fail: function (err, file) {
3133
+ console.error('[FExcelReader]', err);
3134
+ this.parsing = false;
3135
+ this.busy = false;
3136
+ this.$emit('error', {
3137
+ message: (err && err.message) || String(err),
3138
+ fileName: file ? file.name : null
3139
+ });
3140
+ }
3141
+ }
3142
+ });