@fui-org/fui-cli 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fui-x7kph78r.js +420 -0
- package/dist/fui.js +1 -429
- package/package.json +3 -3
- package/skills/fui/SKILL.md +3 -3
- package/skills/fui-skill-cli/SKILL.md +139 -0
- package/skills/fui-skill-cli/references/INDEX.md +110 -0
- package/skills/fui-skill-cli/references/advanced-techniques.md +168 -0
- package/skills/fui-skill-cli/references/coding-standards.md +112 -0
- package/skills/fui-skill-cli/references/component-design.md +448 -0
- package/skills/fui-skill-cli/references/component-quickref.md +78 -0
- package/skills/fui-skill-cli/references/component-table.md +248 -0
- package/skills/fui-skill-cli/references/components-dialog.md +191 -0
- package/skills/fui-skill-cli/references/components-display.md +141 -0
- package/skills/fui-skill-cli/references/components-echart.md +316 -0
- package/skills/fui-skill-cli/references/components-input.md +335 -0
- package/skills/fui-skill-cli/references/controls-patterns.md +701 -0
- package/skills/fui-skill-cli/references/controls-styling-vocabulary.md +137 -0
- package/skills/fui-skill-cli/references/db-table-design.md +73 -0
- package/skills/fui-skill-cli/references/db-workflow.md +288 -0
- package/skills/fui-skill-cli/references/default-function.md +425 -0
- package/skills/fui-skill-cli/references/design-modes.md +57 -0
- package/skills/fui-skill-cli/references/echart-templates.md +489 -0
- package/skills/fui-skill-cli/references/fastproject.md +99 -0
- package/skills/fui-skill-cli/references/fsheet.md +203 -0
- package/skills/fui-skill-cli/references/fullstack-workflow.md +313 -0
- package/skills/fui-skill-cli/references/module-data-patterns.md +117 -0
- package/skills/fui-skill-cli/references/module-json-anatomy.md +132 -0
- package/skills/fui-skill-cli/references/module-structure.md +141 -0
- package/skills/fui-skill-cli/references/new-session.md +85 -0
- package/skills/fui-skill-cli/references/pdfmake.md +60 -0
- package/skills/fui-skill-cli/references/permission-system.md +150 -0
- package/skills/fui-skill-cli/references/platform-architecture.md +269 -0
- package/skills/fui-skill-cli/references/project-config.md +303 -0
- package/skills/fui-skill-cli/references/project-provisioning.md +278 -0
- package/skills/fui-skill-cli/references/script-map.md +262 -0
- package/skills/fui-skill-cli/references/sql-clr-functions.md +225 -0
- package/skills/fui-skill-cli/references/system-design.md +89 -0
- package/skills/fui-skill-cli/references/tapi-file-api.md +185 -0
- package/skills/fui-skill-cli/references/tapi-permission-patterns.md +156 -0
- package/skills/fui-skill-cli/references/tapi-reference.md +474 -0
- package/skills/fui-skill-cli/references/tools-registry.md +84 -0
- package/skills/fui-skill-cli/references/ui-crosswindow-patterns.md +321 -0
- package/skills/fui-skill-cli/references/ui-dialog-patterns.md +255 -0
- package/skills/fui-skill-cli/references/ui-layout-patterns.md +176 -0
- package/skills/fui-skill-cli/references/ui-patterns.md +303 -0
- package/skills/fui-skill-cli/references/ui-screenshot-review.md +95 -0
- package/skills/fui-skill-cli/references/ui-table-cell-patterns.md +318 -0
- package/skills/fui-skill-cli/references/ui-templates.md +22 -0
- package/skills/fui-skill-cli/references/verification.md +236 -0
- package/skills/fui-skill-cli/references/watcher-patterns.md +163 -0
- package/skills/fui-skill-cli/references/websocket-realtime.md +271 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
# Controls Patterns & Logic
|
|
2
|
+
|
|
3
|
+
> Owns: value resolution (backtick / `{{ }}` / JS eval), action engine (API/IN/OUT/CALLBACK/CONFIRM/MESS/CALL/IF/EXE), key notation, grid wrapper `container > rows > cols`, control object, layout rules. Classes/styles: [controls-styling-vocabulary.md](controls-styling-vocabulary.md); module.json overview: [module-json-anatomy.md](module-json-anatomy.md). _(Tier A — loaded with `fui skill get`.)_
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Prefer `f-*` over `v-*`
|
|
8
|
+
|
|
9
|
+
Use `v-*` only when no `f-*` equivalent exists (check [component-quickref.md](component-quickref.md)); `f-*` has built-in features and runtime integration.
|
|
10
|
+
|
|
11
|
+
| Use | Instead of | Why |
|
|
12
|
+
| ------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `f-table` | `v-data-table` (CRUD and readonly) | built-in sort/search/inline action/CRUD; readonly = omit `:update-api`; auto headers when `:headers` omitted; totals via `sum-format` |
|
|
14
|
+
| `f-search` | `v-autocomplete` | API search, debounce, loading |
|
|
15
|
+
| `f-button` | `v-btn` (FUI action) | CALL, hotkey, form validation |
|
|
16
|
+
| `f-date` / `f-time` | `v-text-field` + parsing | picker + typed mask |
|
|
17
|
+
| `f-menu` | `v-menu + v-btn` | simpler, icon/action/link |
|
|
18
|
+
| `f-echart` | `<canvas>`, `v-chart`, other libs | ECharts V5, all chart types |
|
|
19
|
+
|
|
20
|
+
Defaults auto-injected (`defaultControlAttr` in `scripts/component.js`) — declare a prop only to override:
|
|
21
|
+
|
|
22
|
+
| Component | Defaults |
|
|
23
|
+
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `f-table` | `:dense true` · `:show-search true` · `:fixed-header true` · `:items-per-page 50` · `:hide-default-footer true` · `mobile-breakpoint 0` |
|
|
25
|
+
| `f-button` | `color primary` · `elevation 0` |
|
|
26
|
+
| `f-date` / `f-time` | `:dense true` · `:outlined true` · `:hide-details true` |
|
|
27
|
+
| `f-search` | `:dense true` · `:outlined true` · `:hide-details true` · `:auto-select-first true` |
|
|
28
|
+
| `f-menu` | `color primary` · `:depressed true` |
|
|
29
|
+
|
|
30
|
+
> **CRUD:** never `f-button` + `f-dialog` for an add form (unstable in V2). Use `f-table` `ctrl-update` (`:update-form` + `:update-api`). Special dialogs (custom logic, dynamic controls, multi-step) → hand-built `v-dialog` in f-dialog anatomy (`fui design read fui` §4, `ui-dialog-patterns.md`).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 0. Value Resolution Rules (Fundamental)
|
|
35
|
+
|
|
36
|
+
`getVueData(value, src)` resolves every string in `data[]` and `IN`:
|
|
37
|
+
|
|
38
|
+
| Value | Result |
|
|
39
|
+
| ------------------------------------------ | ----------------------------------- |
|
|
40
|
+
| Primitive (number, boolean, object, array) | as-is |
|
|
41
|
+
| String starting with `` ` `` | lodash template (literal + `{{ }}`) |
|
|
42
|
+
| String containing `{{ }}` | lodash template |
|
|
43
|
+
| String with spaces, no backtick/`{{ }}` | literal |
|
|
44
|
+
| String without spaces, backtick, `{{ }}` | **evaluated as JS** |
|
|
45
|
+
| `""` | `""` |
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
"data": [
|
|
49
|
+
{
|
|
50
|
+
"a": "Hang So", // has space → literal string "Hang So"
|
|
51
|
+
"b": 3, // number → 3
|
|
52
|
+
"c": true, // boolean → true
|
|
53
|
+
"d": null, // null → null
|
|
54
|
+
"e": "val1", // no space → JS eval: value of variable val1
|
|
55
|
+
"f": "val4.prop2", // JS eval: vueData.val4.prop2
|
|
56
|
+
"g": "val5[1]", // JS eval: 2nd element of array val5
|
|
57
|
+
"h": "[val1]", // JS eval: array [value_of_val1]
|
|
58
|
+
"i": "[1,2,3].map(x=>x*2)", // no space → JS eval → [2,4,6]
|
|
59
|
+
"j": "`Xin chào {{user.UserName}}", // backtick → lodash template → "Xin chào Tuấn"
|
|
60
|
+
"k": "tổng là {{a+b}} item", // {{ }} → lodash template → "tổng là 3 item"
|
|
61
|
+
"l": "tuimuonghep{{val4.prop3}}chuoi" // {{ }} → "tuimuonghep10chuoi"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`"val1"` = value of `val1`, not the string; literal needs ``"`val1"``.
|
|
67
|
+
|
|
68
|
+
> ⚠️ **Not for `:attr`.** Bindings (`:option`, `:config`, any `v-bind:`) go straight to Vue's compiler: one pure expression; no `function(){}`/`var`/`return`/`;`. Violation ⇒ blank page, 0 JS error, 0 `[Vue warn]`. Rules + moving logic to `script.js` + `EXE`: [advanced-techniques.md](advanced-techniques.md) §1.
|
|
69
|
+
|
|
70
|
+
`{{ }}` takes any JS expression:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
"label": "Còn {{items.length}} bản ghi",
|
|
74
|
+
"title": "Ngày {{moment().format('DD/MM/YYYY')}}",
|
|
75
|
+
"flag": "Là array: {{Array.isArray(myList)}}"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 1. `data[]` keys and assignment
|
|
81
|
+
|
|
82
|
+
### Key notation
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
"data": [
|
|
86
|
+
{
|
|
87
|
+
"val": "Hang So", // simple variable
|
|
88
|
+
"obj.prop": "val1", // dot notation → vueData.obj.prop
|
|
89
|
+
"obj.nested.deep": true, // multi-level dot
|
|
90
|
+
"arr[0].name": "Tuan", // bracket, literal index
|
|
91
|
+
"arr[val2].name": "abc", // bracket, variable index (val2 = 3 → arr[3])
|
|
92
|
+
"arr[0][val1].x": "y", // bracket, variable key (val1 = "Hang So")
|
|
93
|
+
"#PARENT.field": "val1", // write to parent window
|
|
94
|
+
"#Win1.field": "val1" // write to iframe Win1
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Missing intermediate objects/arrays are auto-created (also for `OUT` targets and assignment fragments) — no need to reassign the whole object. `#...` keys use `setWindowData` ([ui-patterns.md](ui-patterns.md) §Cross-window).
|
|
100
|
+
|
|
101
|
+
### Events/slots in `attr` — full form only
|
|
102
|
+
|
|
103
|
+
Every event/slot of every component, incl. custom `f-*` and `$emit()` events:
|
|
104
|
+
|
|
105
|
+
| Kind | Shorthand (`.vue` only) | `module.json` |
|
|
106
|
+
| ----- | -------------------------------- | -------------------------------------------- |
|
|
107
|
+
| Event | `@click`, `@saved`, `@any-event` | `v-on:click`, `v-on:saved`, `v-on:any-event` |
|
|
108
|
+
| Slot | `#default`, `#item.name` | `v-slot:default`, `v-slot:item.name` |
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
// ❌ WRONG — shorthand, fails to load
|
|
112
|
+
{ "el": "f-contest-list", "attr": { "@saved": "CALL(apiLoadContests)" } }
|
|
113
|
+
|
|
114
|
+
// ✅ RIGHT
|
|
115
|
+
{ "el": "f-contest-list", "attr": { "v-on:saved": "CALL(apiLoadContests)" } }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`#PARENT.field` / `#Win1.field` in `data[]` is FUI cross-window syntax, unrelated to Vue slot `#`.
|
|
119
|
+
|
|
120
|
+
### Alias and JS init
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
"GanLienGiaTri": "val1" // GanLienGiaTri = current value of val1
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Snapshot at `data[]` run, not reactive — use `watch` for reactivity.
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
"data": [
|
|
130
|
+
{
|
|
131
|
+
"today": "moment().format('YYYY-MM-DD')",
|
|
132
|
+
"ids": "[1,2,3,4].map(x=>x*2)",
|
|
133
|
+
"firstItem": "dsUser[0]",
|
|
134
|
+
"total": "dsItems.length"
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 2. Actions (`data`)
|
|
142
|
+
|
|
143
|
+
Declare named action objects in `data`; invoke with `CALL`.
|
|
144
|
+
|
|
145
|
+
### CORE RULE — one `{}` = ONE command; sequence = ARRAY `[]`
|
|
146
|
+
|
|
147
|
+
An object may hold parameters and nested sub-actions (`IN`, `OUT`, `CALLBACK`, `THEN`/`ELSE`…) but runs one command. An array is the ONLY way to sequence.
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
// ❌ WRONG — only CALL runs. EXE and MESS NEVER run.
|
|
151
|
+
"CALLBACK": {
|
|
152
|
+
"EXE": "vueData.previewRows = []; vueData.excelRaw = [];",
|
|
153
|
+
"CALL": "getList",
|
|
154
|
+
"MESS": "Đã cập nhật dữ liệu tháng"
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
// ✅ RIGHT — three commands, three objects, run in written order.
|
|
160
|
+
"CALLBACK": [
|
|
161
|
+
{ "EXE": "vueData.previewRows = []; vueData.excelRaw = [];" },
|
|
162
|
+
{ "CALL": "getList" },
|
|
163
|
+
{ "MESS": "Đã cập nhật dữ liệu tháng" }
|
|
164
|
+
]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`vueAction()` (`fastproject.js:407`, `fastproject-3.0.js:448`, same in V2/V3) checks keys in fixed order, each branch returns:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
IN (tham số, không return) → API ⏎ → CALL ⏎ → EXE ⏎ → FUN ⏎ → IF ⏎ → MESS ⏎ → MESSBOX ⏎ → (fallthrough) mapData
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Highest priority wins, not first written; reordering changes nothing. `runAction()` runs arrays in order (`_.forEach`). Silent failure (no error, no `[Vue warn]`); symptoms: "list reloads but no toast", "toast shows but form not reset".
|
|
174
|
+
|
|
175
|
+
Applies to `CALLBACK`, `ERRORCALLBACK`, `THEN`/`ELSE`, `CANCEL`, `:action`, `:update-api.new/edit/delete`, `watch`, `onclose`, named actions. When in doubt wrap: `[{…}]` is always valid.
|
|
176
|
+
|
|
177
|
+
| Group | Keys |
|
|
178
|
+
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
179
|
+
| **Commands** (one per object, priority above) | `API` · `CALL` · `EXE` · `FUN` · `IF` · `MESS` · `MESSBOX` |
|
|
180
|
+
| **Parameters** (same object as command) | `IN` · `OUT` · `HEADER` · `METHOD` · `CALLBACK` · `ERROROUT` · `ERRORCALLBACK` · `THEN` · `ELSE` · `CONFIRM` · `CANCEL` (`CONFIRM`/`CANCEL` handled by `runAction` before `vueAction`) |
|
|
181
|
+
|
|
182
|
+
**Sole exception:** `MESS` beside `API` is a parameter — `callAPI` toasts on success (`fastproject-3.0.js:755`, after `OUT`, before `CALLBACK`). `{ "API": …, "IN": …, "MESS": …, "CALLBACK": … }` is valid. Not `MESSBOX`; not with `FUN` (`runFunction` ignores `MESS`).
|
|
183
|
+
|
|
184
|
+
Assignment fragments (`{ "dlgOpen": false, "form.Name": "" }`) run only in the fallthrough branch; any command in the same object silently drops them:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
// ❌ WRONG — CALL runs, the three variables are NOT assigned
|
|
188
|
+
{ "PageNum": 1, "Description": "", "IsPaper": false, "CALL": "reload" }
|
|
189
|
+
|
|
190
|
+
// ✅ RIGHT
|
|
191
|
+
[
|
|
192
|
+
{ "PageNum": 1, "Description": "", "IsPaper": false },
|
|
193
|
+
{ "CALL": "reload" }
|
|
194
|
+
]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Keys
|
|
198
|
+
|
|
199
|
+
| Key | Meaning |
|
|
200
|
+
| ---------------------------- | ------------------------------------------------------------ |
|
|
201
|
+
| `API` | endpoint |
|
|
202
|
+
| `IN` | input params (`vueData.` or `item.`) |
|
|
203
|
+
| `OUT` | store response (e.g. `"myList"`); object form splits SELECTs |
|
|
204
|
+
| `CALLBACK` | on success |
|
|
205
|
+
| `ERROROUT` / `ERRORCALLBACK` | manual error handling |
|
|
206
|
+
| `CONFIRM` / `CANCEL` | confirm first / on Cancel |
|
|
207
|
+
| `MESS` / `MESSBOX` | toast / modal |
|
|
208
|
+
| `CALL` | run named action |
|
|
209
|
+
| `IF` / `THEN` / `ELSE` | branch |
|
|
210
|
+
| `FUN` | FUI function (`openWindow`, `showMessage`, ...) |
|
|
211
|
+
| `ARRAY` + `COL` | column of object array → flat array |
|
|
212
|
+
| `EXE` | raw JS, sparingly |
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
"fetchUsers": {
|
|
216
|
+
"API": "/api/users",
|
|
217
|
+
"IN": { "GroupID": "vueData.selectedGroup" },
|
|
218
|
+
"OUT": "userList",
|
|
219
|
+
"CALLBACK": { "MESS": "Loaded!" }
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
"apiLoadData": {
|
|
225
|
+
"API": "/api/controller/action",
|
|
226
|
+
"IN": {
|
|
227
|
+
"Page": 1,
|
|
228
|
+
"Search": "vueData.searchText",
|
|
229
|
+
"ID": "item.ID"
|
|
230
|
+
},
|
|
231
|
+
"OUT": "items",
|
|
232
|
+
"CALLBACK": [
|
|
233
|
+
{ "MESS": "Loaded!" },
|
|
234
|
+
{ "CALL": "anotherAction" }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- Two commands ⇒ array; merged, only `CALL` runs.
|
|
240
|
+
- No nested object assignment: `{ "form": { "field": "item.field" } }` fails; use `{ "form.field": "item.field" }`. `ctrl-update` needs no manual form handling.
|
|
241
|
+
|
|
242
|
+
### URL interpolation
|
|
243
|
+
|
|
244
|
+
`{{ }}` in `API` reads vueData:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
"getDetail": {
|
|
248
|
+
"API": "/ts/Student/{{StudentID}}",
|
|
249
|
+
"OUT": "studentDetail"
|
|
250
|
+
},
|
|
251
|
+
"getReport": {
|
|
252
|
+
"API": "/ts/Report/{{sYear}}/{{sMonth}}",
|
|
253
|
+
"OUT": "reportData"
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### ERROROUT / ERRORCALLBACK
|
|
258
|
+
|
|
259
|
+
Default: full-screen error dialog. With `ERROROUT` nothing shows automatically — you handle it.
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
"saveData": {
|
|
263
|
+
"API": "/ts/Student_Insert",
|
|
264
|
+
"IN": { "Name": "sName" },
|
|
265
|
+
"ERROROUT": {
|
|
266
|
+
"errStatus": "status",
|
|
267
|
+
"errMessage": "responseJSON"
|
|
268
|
+
},
|
|
269
|
+
"ERRORCALLBACK": {
|
|
270
|
+
"MESS": "Lưu thất bại: {{errMessage.Message}}"
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`ERROROUT` maps the XHR error (`status` = HTTP code, `responseJSON` = parsed body); `ERRORCALLBACK` runs after.
|
|
276
|
+
|
|
277
|
+
### METHOD / HEADER
|
|
278
|
+
|
|
279
|
+
Defaults `METHOD = "POST"`, `HEADER = { authorization: token }`; override for GET or external APIs.
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
"getPublicData": {
|
|
283
|
+
"API": "/ts/PublicData",
|
|
284
|
+
"METHOD": "GET",
|
|
285
|
+
"HEADER": { "x-api-key": "`my-api-key" },
|
|
286
|
+
"OUT": "publicData"
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Literal strings in `IN`
|
|
291
|
+
|
|
292
|
+
Only for `IN`: no-space strings may be evaluated. Force literal with backtick ``"`myValue"`` (or ``"`myValue`"``) or inner single quotes `"'myValue'"`. Always do this for `id`, `url`, and any key that must stay text.
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{
|
|
296
|
+
"FUN": "openWindow",
|
|
297
|
+
"IN": {
|
|
298
|
+
"id": "`winUser",
|
|
299
|
+
"url": "'/fp/module?mid=123'",
|
|
300
|
+
"title": "`Tiêu đề cố định"
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`attr` props without `:` are already plain strings (`"url": "/fp/module?mid=123"` needs no backtick).
|
|
306
|
+
|
|
307
|
+
### CONFIRM + CANCEL
|
|
308
|
+
|
|
309
|
+
`CONFIRM` (supports `{{ }}`) asks first; optional `CANCEL` runs on Cancel.
|
|
310
|
+
|
|
311
|
+
```json
|
|
312
|
+
"deleteItem": {
|
|
313
|
+
"CONFIRM": "Bạn có chắc muốn xóa {{dongChon.Name}}?",
|
|
314
|
+
"API": "/api/delete",
|
|
315
|
+
"IN": { "id": "dongChon.ID" },
|
|
316
|
+
"CALLBACK": { "CALL": "reloadData" },
|
|
317
|
+
"CANCEL": { "MESS": "Đã hủy thao tác" }
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### IF / THEN / ELSE
|
|
322
|
+
|
|
323
|
+
```json
|
|
324
|
+
"checkStatus": {
|
|
325
|
+
"IF": "status === 1",
|
|
326
|
+
"THEN": { "MESS": "Active" },
|
|
327
|
+
"ELSE": {
|
|
328
|
+
"IF": "status === 2",
|
|
329
|
+
"THEN": { "MESS": "Pending" },
|
|
330
|
+
"ELSE": { "MESS": "Inactive" }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Condition is JS reading vueData directly (no `vueData.` prefix). THEN/ELSE may be a JS string: `"THEN": "vueData.x = 1"`.
|
|
336
|
+
|
|
337
|
+
### MESS vs MESSBOX
|
|
338
|
+
|
|
339
|
+
`MESS` = auto-closing top toast (success/failure). `MESSBOX` = modal the user must close (must be read/acknowledged). Both support `{{ }}`.
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
{ "MESS": "Lưu thành công!" }
|
|
343
|
+
{ "MESSBOX": "Phiên của bạn đã hết hạn. Vui lòng đăng nhập lại." }
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### FUN: showMessage
|
|
347
|
+
|
|
348
|
+
`title`; `message` (optional); `onclose` = action after closing (use instead of `MESS` when something must follow).
|
|
349
|
+
|
|
350
|
+
```json
|
|
351
|
+
{
|
|
352
|
+
"FUN": "showMessage",
|
|
353
|
+
"IN": {
|
|
354
|
+
"title": "`Xác nhận thành công",
|
|
355
|
+
"message": "`Dữ liệu đã được lưu. Tiếp tục?",
|
|
356
|
+
"onclose": {
|
|
357
|
+
"CALL": "reloadData"
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### CALL
|
|
364
|
+
|
|
365
|
+
Actions stored in `data[]` are callable anywhere (CALLBACK, IF/THEN/ELSE, watch):
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
"data": [
|
|
369
|
+
{ "apiGetLogs": { "API": "/nema/Wifi_Log_today", "OUT": "dataTableLogs" } }
|
|
370
|
+
]
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
```json
|
|
374
|
+
{ "CALL": "apiGetLogs" }
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### ARRAY + COL
|
|
378
|
+
|
|
379
|
+
```json
|
|
380
|
+
{
|
|
381
|
+
"danhSachID": {
|
|
382
|
+
"ARRAY": "dsHoaDon",
|
|
383
|
+
"COL": "DanhMucThuID"
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// danhSachID = [32423, 4, 23, 4, 234, 2]
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### OUT object — multiple SELECTs
|
|
390
|
+
|
|
391
|
+
For SPs returning `data[0]`, `data[1]`...; prefer this over CALLBACK + EXE.
|
|
392
|
+
|
|
393
|
+
```json
|
|
394
|
+
"getReport": {
|
|
395
|
+
"API": "/ts/TS_Report",
|
|
396
|
+
"OUT": {
|
|
397
|
+
"tongSoLuong": "data[0][0].Total",
|
|
398
|
+
"danhSachChiTiet": "data[1]",
|
|
399
|
+
"thongKe": "data[2]"
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### MAP — `IN` + `OUT` only, no API
|
|
405
|
+
|
|
406
|
+
```json
|
|
407
|
+
{
|
|
408
|
+
"IN": {
|
|
409
|
+
"HoTen": "formData.FullName",
|
|
410
|
+
"MaSV": "formData.StudentID"
|
|
411
|
+
},
|
|
412
|
+
"OUT": "mappedData"
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
```json
|
|
417
|
+
{
|
|
418
|
+
"IN": { "HoTen": "formData.FullName" },
|
|
419
|
+
"OUT": { "displayName": "HoTen" }
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### EXE
|
|
424
|
+
|
|
425
|
+
Only for logic action JSON can't express; `vueData` accessible; end with `;`.
|
|
426
|
+
|
|
427
|
+
```json
|
|
428
|
+
{ "EXE": "vueData.items = vueData.items.filter(x => x.active); " }
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 3. Watchers (`watch`)
|
|
434
|
+
|
|
435
|
+
Cascading/filter design: [watcher-patterns.md](watcher-patterns.md).
|
|
436
|
+
|
|
437
|
+
```json
|
|
438
|
+
"watch": {
|
|
439
|
+
"searchText": { "CALL": "apiLoadData" },
|
|
440
|
+
"selectedGroup": { "CALL": "apiLoadUsers" }
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
`v_old` holds the previous value in watch actions/CALLBACK:
|
|
445
|
+
|
|
446
|
+
```json
|
|
447
|
+
"watch": {
|
|
448
|
+
"taoID": {
|
|
449
|
+
"backupID": "v_old.taoID",
|
|
450
|
+
"BAC": 12345
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Watch is shallow (reference only). `deep-watch` detects inner changes, runs before normal watches, costs more — use only when needed:
|
|
456
|
+
|
|
457
|
+
```json
|
|
458
|
+
"watch": {
|
|
459
|
+
"deep-watch": {
|
|
460
|
+
"formData": { "CALL": "autoSave" },
|
|
461
|
+
"selectedItems": { "CALL": "recalcTotal" }
|
|
462
|
+
},
|
|
463
|
+
"sModuleID": { "CALL": "reload" }
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## 4. Table cell action context
|
|
470
|
+
|
|
471
|
+
Actions from `f-table` cells (t-button, t-check, t-select, t-menu, v-on:click) get `item` (row), `index` (row index), and `$row` (settable: `{ "$row": "item" }`). Update one row via `danhSach[index].field` — the standard pattern:
|
|
472
|
+
|
|
473
|
+
```json
|
|
474
|
+
{
|
|
475
|
+
"el": "t-button",
|
|
476
|
+
"text": "Tên",
|
|
477
|
+
"value": "ten",
|
|
478
|
+
"attr": {
|
|
479
|
+
":label": "`${item.ten}-${vueData.val1}`",
|
|
480
|
+
":action": [
|
|
481
|
+
{ "$row": "item" },
|
|
482
|
+
{
|
|
483
|
+
"API": "/api/getDetail",
|
|
484
|
+
"OUT": "DETAIL",
|
|
485
|
+
"CALLBACK": {
|
|
486
|
+
"danhSach[index].kohienthi": true
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
{ "MESS": "Row {{danhSach[index].ten}}" }
|
|
490
|
+
]
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## 5. URL parameters → vueData
|
|
498
|
+
|
|
499
|
+
On load: `p_router` (path, no query), `p_routers` (segment array), each query param → same-named string variable.
|
|
500
|
+
|
|
501
|
+
```
|
|
502
|
+
URL: https://fp.example.vn/ts/newhocsinh?id=12345&mod=abcdef
|
|
503
|
+
→ vueData.p_router = "/ts/newhocsinh"
|
|
504
|
+
→ vueData.p_routers = ["ts", "newhocsinh"]
|
|
505
|
+
→ vueData.id = "12345"
|
|
506
|
+
→ vueData.mod = "abcdef"
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
```json
|
|
510
|
+
"data": [
|
|
511
|
+
{
|
|
512
|
+
"IF": "typeof menu != 'undefined' && menu=='0'",
|
|
513
|
+
"THEN": { "v_Set.menu": false }
|
|
514
|
+
}
|
|
515
|
+
]
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## 6. Layout (`controls`)
|
|
521
|
+
|
|
522
|
+
> V2 = Vuetify 1.5 (`v-container / v-layout / v-flex`); V3 (fastproject-3.0.js) = Vuetify 3 (`v-container / v-row / v-col`). Examples use V2 syntax (most projects); for V3 see the `w` thresholds below. V2 ref: https://v15.vuetifyjs.com/en/framework/grid/. New project default is V3 — [project-provisioning.md](project-provisioning.md) §Step 1.
|
|
523
|
+
|
|
524
|
+
**MANDATORY**: Container > Row > Col > Element. Nothing at root. No `children` key — nest via array `innerHTML`. `attr` = element attributes; `col` = grid-column attributes. Events `v-on:click`, never `@click`.
|
|
525
|
+
|
|
526
|
+
Defaults — use first, deviate only with reason: container `prop` `"fluid grid-list-md"`; row `prop` `"row wrap"` or `""`, mixed control types/heights `"row wrap align-center"`. Put spacing/alignment on `col` or `attr`, not container/row.
|
|
527
|
+
|
|
528
|
+
### Vertical centering
|
|
529
|
+
|
|
530
|
+
Verify each row's controls share a vertical center (esp. mixing input/select, button, icon, checkbox, label, differing heights).
|
|
531
|
+
|
|
532
|
+
- Mixed row: add `align-center` to row `prop`.
|
|
533
|
+
- One stray control: `"col": { "class": "align-self-center" }` on it.
|
|
534
|
+
- Never `mt-*`, `mb-*`, or inline `style` to fake centering.
|
|
535
|
+
- Recheck desktop and mobile; wrapped controls keep natural spacing.
|
|
536
|
+
|
|
537
|
+
```json
|
|
538
|
+
{
|
|
539
|
+
"prop": "row wrap align-center",
|
|
540
|
+
"cols": [
|
|
541
|
+
{ "el": "v-text-field", "w": 6, "attr": { "label": "Từ khóa", "v-model": "vueData.keyword" } },
|
|
542
|
+
{ "el": "f-button", "w": "", "col": { "class": "shrink" }, "attr": { "label": "Tìm", "icon-text": "mdi-magnify" } }
|
|
543
|
+
]
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Exception — page is one custom component
|
|
548
|
+
|
|
549
|
+
If the page renders only one self-laid-out component (custom dashboard, `uc-*` app), container `prop` = `"fluid pa-0"` to avoid outer padding. Any other controls on the page (filter, header, sections) → keep `"fluid grid-list-md"`.
|
|
550
|
+
|
|
551
|
+
```json
|
|
552
|
+
[
|
|
553
|
+
{
|
|
554
|
+
"prop": "fluid pa-0",
|
|
555
|
+
"rows": [
|
|
556
|
+
{
|
|
557
|
+
"prop": "",
|
|
558
|
+
"cols": [
|
|
559
|
+
{
|
|
560
|
+
"el": "uc-main-ui",
|
|
561
|
+
"w": "12",
|
|
562
|
+
"attr": {}
|
|
563
|
+
}
|
|
564
|
+
]
|
|
565
|
+
}
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
]
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Grid and control object
|
|
572
|
+
|
|
573
|
+
```json
|
|
574
|
+
{
|
|
575
|
+
"prop": "fluid grid-list-md",
|
|
576
|
+
"rows": [
|
|
577
|
+
{
|
|
578
|
+
"prop": "row wrap",
|
|
579
|
+
"cols": [
|
|
580
|
+
{ "w": "6", "el": "v-text-field", "attr": { "label": "Tên", "v-model": "tenBien" } },
|
|
581
|
+
{ "w": "6", "el": "v-btn", "innerHTML": "Submit" }
|
|
582
|
+
]
|
|
583
|
+
}
|
|
584
|
+
]
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
```json
|
|
589
|
+
{
|
|
590
|
+
"w": "6",
|
|
591
|
+
"el": "v-text-field",
|
|
592
|
+
"col": { "class": "py-0" },
|
|
593
|
+
"attr": { "label": "Tên", "v-model": "tenBien" },
|
|
594
|
+
"innerHTML": "..."
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
| Field | To | Meaning |
|
|
599
|
+
| ----------- | ------------ | ------------------------------------------------------------------------------------------------------------- |
|
|
600
|
+
| `w` | `v-flex` | **Required on every `cols` item.** `1`–`12` grid or `>= 25` px (px **requires `col: { "class": "shrink" }`**) |
|
|
601
|
+
| `col` | `v-flex` | class, style, offset, align-self... |
|
|
602
|
+
| `el` | tag | `div`, `span`, Vuetify/FUI (`v-text-field`, `f-button`, `uc-*`...) |
|
|
603
|
+
| `attr` | element | props/attrs/directives (`label`, `v-model`, `:disabled`, `v-on:click`); **no `@`** |
|
|
604
|
+
| `innerHTML` | default slot | HTML string (supports `{{ }}`) or recursive array of Control Objects |
|
|
605
|
+
|
|
606
|
+
```json
|
|
607
|
+
{
|
|
608
|
+
"el": "v-btn",
|
|
609
|
+
"w": 6,
|
|
610
|
+
"col": { "class": "text-center" },
|
|
611
|
+
"attr": {
|
|
612
|
+
":disabled": "true",
|
|
613
|
+
"color": "primary",
|
|
614
|
+
"v-on:click": "CALL(vueData.submit)"
|
|
615
|
+
},
|
|
616
|
+
"innerHTML": "Submit"
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
```json
|
|
621
|
+
{ "el": "div", "w": "12", "innerHTML": "Xin chào {{formData.fullName}}" }
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Pixel `w` — REQUIRES `col: { "class": "shrink" }`
|
|
625
|
+
|
|
626
|
+
Small `w` → grid class `sm{w}` (V2) / `v-col-{w}` (V3). Large `w` → inline `flex-basis: {w}px`. But grid columns have `flex-grow: 1` (`.flex`/`.v-col`), which beats `flex-basis`: without `shrink` the px value silently does nothing.
|
|
627
|
+
|
|
628
|
+
```json
|
|
629
|
+
// ❌ WRONG — 300 has no effect, column still stretches full
|
|
630
|
+
{ "el": "v-text-field", "w": 300, "attr": { "v-model": "tuKhoa" } }
|
|
631
|
+
|
|
632
|
+
// ✅ RIGHT — shrink disables flex-grow, flex-basis 300px applies
|
|
633
|
+
{ "el": "v-text-field", "w": 300, "col": { "class": "shrink" },
|
|
634
|
+
"attr": { "v-model": "tuKhoa" } }
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Measured (chromium, 1200px, neighbour takes rest):
|
|
638
|
+
|
|
639
|
+
| Declared | V2 | V3 |
|
|
640
|
+
| ------------------------------ | -------- | -------- |
|
|
641
|
+
| `w: 300` | 687px ❌ | 738px ❌ |
|
|
642
|
+
| + `col: { "shrink": true }` | 300px ✅ | 738px ❌ |
|
|
643
|
+
| + `col: { "class": "shrink" }` | 300px ✅ | 300px ✅ |
|
|
644
|
+
|
|
645
|
+
Only `col: { "class": "shrink" }` works in both: `shrink: true` is a V2 `v-flex` prop, inert in Vuetify 3; the `shrink` class is defined for V3 in FUI's `projectdefaultstyle.css` (`flex-grow:0; flex-shrink:1; flex-basis:content`). `"flex-grow-0"` also works; use `shrink` for consistency.
|
|
646
|
+
|
|
647
|
+
**Never `w` 13–24.** V2 (`fastproject.js`) switches to px at `w > 12`; V3 (`fastproject-3.0.js`) at `w > 24`, so V3 `w: 20` emits `v-col-20`, which doesn't exist (only `.v-col-1`…`.v-col-12`) → no width, equal split (measured `flex-basis: 0px`, 600px). Rule: `w` is `1`–`12` or `>= 25` + shrink. `fui module validate` warns on both (including `cols` nested in `innerHTML`) — run it before publishing.
|
|
648
|
+
|
|
649
|
+
### Toolbar row — every column `shrink` except `v-spacer`
|
|
650
|
+
|
|
651
|
+
Canonical template (filters + buttons left, group pushed right):
|
|
652
|
+
|
|
653
|
+
```json
|
|
654
|
+
{
|
|
655
|
+
"prop": "row",
|
|
656
|
+
"cols": [
|
|
657
|
+
{ "el": "v-select", "w": "150", "col": "shrink", "attr": {} },
|
|
658
|
+
{ "el": "v-select", "w": "180", "col": "shrink", "attr": {} },
|
|
659
|
+
{ "el": "f-button", "w": "", "col": "shrink", "attr": {} },
|
|
660
|
+
{ "el": "v-spacer", "w": "" },
|
|
661
|
+
{ "el": "f-excel-reader", "col": "shrink", "attr": {} },
|
|
662
|
+
{ "el": "f-button", "w": "", "col": "shrink", "attr": {} }
|
|
663
|
+
]
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Rule:** every control column gets `"col": "shrink"`, including empty-`w` ones. Sole exception: `v-spacer` (shrinking it disables it). Empty `w` ≠ fit content: width logic is inside `if (c.w)`, so `w: ""` emits nothing and the column (`flex-grow: 1`) takes an equal share of free space. Any non-`shrink` column gets a share; in a toolbar only `v-spacer` should.
|
|
668
|
+
|
|
669
|
+
Measured (1200px; 2 selects `w:150`/`w:180` + "Tìm" | `v-spacer` | 2 buttons; V3 / V2):
|
|
670
|
+
|
|
671
|
+
| | select `w:150` | "Tìm" | `v-spacer` | Result |
|
|
672
|
+
| -------------------------------- | -------------- | ---------------- | ---------------- | ---------------------------------------------- |
|
|
673
|
+
| no `shrink` | 271 / 240 ❌ | 145 / 154 | 145 / 90 | filters bloat, evenly spread |
|
|
674
|
+
| **`v-spacer` only, no `shrink`** | 150 / 150 | **218 / 198** ❌ | 218 / 134 | buttons bloat, gaps, end group not flush right |
|
|
675
|
+
| `shrink` incl. `v-spacer` | 150 / 150 | 88 / 64 | **24 / 0** ❌ | row packs left (Σ 780/662 of 1200px) |
|
|
676
|
+
| **`shrink` all but `v-spacer`** | 150 / 150 ✅ | 88 / 64 ✅ | **445 / 537** ✅ | exact px, buttons fit, end group flush right |
|
|
677
|
+
|
|
678
|
+
Row 2 is the common bug ("right-align these buttons" → spacer added, `shrink` forgotten).
|
|
679
|
+
|
|
680
|
+
- `"col": "shrink"` ≡ `"col": { "class": "shrink" }`; use the object when adding `v-if`, `style`…, keeping `class: "shrink"`.
|
|
681
|
+
- Never `"col": { "shrink": true }` (V2 only).
|
|
682
|
+
- Right-align a whole row without spacer: row `prop` `"row justify-end"`.
|
|
683
|
+
- Left | center | right: two `v-spacer`s, neither `shrink`.
|
|
684
|
+
- Data-entry form rows are the opposite: fields use `w` 1–12, no `shrink`. `shrink` is for control rows only.
|
|
685
|
+
|
|
686
|
+
### Responsive
|
|
687
|
+
|
|
688
|
+
```json
|
|
689
|
+
"attr": {
|
|
690
|
+
":width": "$vuetify.breakpoint.mdAndUp ? '500px' : '100%'"
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## 7. Common element patterns
|
|
697
|
+
|
|
698
|
+
- Text/simple HTML: `innerHTML`.
|
|
699
|
+
- Events: `v-on:click`, `v-on:change`; wrap logic in `CALL()`.
|
|
700
|
+
- Visibility: `v-if` (pre-render) or `v-show` (CSS toggle).
|
|
701
|
+
- Lists: avoid raw `v-for`; use `f-table` or recursive partials.
|