@fui-org/fui-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/fui.js +160 -43
- package/package.json +4 -2
- package/skills/fui/SKILL.md +131 -66
- package/skills/fui-skill/README.md +112 -0
- package/skills/fui-skill/SKILL.md +269 -0
- package/skills/fui-skill/assets/projectdefaultstyle.css +518 -0
- package/skills/fui-skill/design-md/airbnb/DESIGN.md +545 -0
- package/skills/fui-skill/design-md/airbnb/README.md +5 -0
- package/skills/fui-skill/design-md/airtable/DESIGN.md +554 -0
- package/skills/fui-skill/design-md/airtable/README.md +5 -0
- package/skills/fui-skill/design-md/apple/DESIGN.md +562 -0
- package/skills/fui-skill/design-md/apple/README.md +5 -0
- package/skills/fui-skill/design-md/asu/DESIGN.md +179 -0
- package/skills/fui-skill/design-md/asu/README.md +107 -0
- package/skills/fui-skill/design-md/binance/DESIGN.md +634 -0
- package/skills/fui-skill/design-md/binance/README.md +5 -0
- package/skills/fui-skill/design-md/bmw/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/bmw/README.md +5 -0
- package/skills/fui-skill/design-md/bmw-m/DESIGN.md +503 -0
- package/skills/fui-skill/design-md/bmw-m/README.md +5 -0
- package/skills/fui-skill/design-md/bugatti/DESIGN.md +454 -0
- package/skills/fui-skill/design-md/bugatti/README.md +5 -0
- package/skills/fui-skill/design-md/cal/DESIGN.md +542 -0
- package/skills/fui-skill/design-md/cal/README.md +5 -0
- package/skills/fui-skill/design-md/claude/DESIGN.md +589 -0
- package/skills/fui-skill/design-md/claude/README.md +5 -0
- package/skills/fui-skill/design-md/clay/DESIGN.md +541 -0
- package/skills/fui-skill/design-md/clay/README.md +5 -0
- package/skills/fui-skill/design-md/clickhouse/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/clickhouse/README.md +5 -0
- package/skills/fui-skill/design-md/cohere/DESIGN.md +451 -0
- package/skills/fui-skill/design-md/cohere/README.md +5 -0
- package/skills/fui-skill/design-md/coinbase/DESIGN.md +570 -0
- package/skills/fui-skill/design-md/coinbase/README.md +5 -0
- package/skills/fui-skill/design-md/composio/DESIGN.md +506 -0
- package/skills/fui-skill/design-md/composio/README.md +5 -0
- package/skills/fui-skill/design-md/cursor/DESIGN.md +537 -0
- package/skills/fui-skill/design-md/cursor/README.md +5 -0
- package/skills/fui-skill/design-md/elevenlabs/DESIGN.md +504 -0
- package/skills/fui-skill/design-md/elevenlabs/README.md +5 -0
- package/skills/fui-skill/design-md/expo/DESIGN.md +526 -0
- package/skills/fui-skill/design-md/expo/README.md +5 -0
- package/skills/fui-skill/design-md/ferrari/DESIGN.md +531 -0
- package/skills/fui-skill/design-md/ferrari/README.md +5 -0
- package/skills/fui-skill/design-md/figma/DESIGN.md +578 -0
- package/skills/fui-skill/design-md/figma/README.md +5 -0
- package/skills/fui-skill/design-md/framer/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/framer/README.md +5 -0
- package/skills/fui-skill/design-md/fui/DESIGN.md +532 -0
- package/skills/fui-skill/design-md/hashicorp/DESIGN.md +575 -0
- package/skills/fui-skill/design-md/hashicorp/README.md +5 -0
- package/skills/fui-skill/design-md/ibm/DESIGN.md +550 -0
- package/skills/fui-skill/design-md/ibm/README.md +5 -0
- package/skills/fui-skill/design-md/intercom/DESIGN.md +546 -0
- package/skills/fui-skill/design-md/intercom/README.md +5 -0
- package/skills/fui-skill/design-md/kraken/DESIGN.md +125 -0
- package/skills/fui-skill/design-md/kraken/README.md +5 -0
- package/skills/fui-skill/design-md/lamborghini/DESIGN.md +288 -0
- package/skills/fui-skill/design-md/lamborghini/README.md +5 -0
- package/skills/fui-skill/design-md/linear.app/DESIGN.md +548 -0
- package/skills/fui-skill/design-md/linear.app/README.md +5 -0
- package/skills/fui-skill/design-md/lovable/DESIGN.md +298 -0
- package/skills/fui-skill/design-md/lovable/README.md +5 -0
- package/skills/fui-skill/design-md/mastercard/DESIGN.md +365 -0
- package/skills/fui-skill/design-md/mastercard/README.md +5 -0
- package/skills/fui-skill/design-md/meta/DESIGN.md +683 -0
- package/skills/fui-skill/design-md/meta/README.md +5 -0
- package/skills/fui-skill/design-md/minimax/DESIGN.md +746 -0
- package/skills/fui-skill/design-md/minimax/README.md +5 -0
- package/skills/fui-skill/design-md/mintlify/DESIGN.md +852 -0
- package/skills/fui-skill/design-md/mintlify/README.md +5 -0
- package/skills/fui-skill/design-md/miro/DESIGN.md +825 -0
- package/skills/fui-skill/design-md/miro/README.md +5 -0
- package/skills/fui-skill/design-md/mistral.ai/DESIGN.md +773 -0
- package/skills/fui-skill/design-md/mistral.ai/README.md +5 -0
- package/skills/fui-skill/design-md/mongodb/DESIGN.md +767 -0
- package/skills/fui-skill/design-md/mongodb/README.md +5 -0
- package/skills/fui-skill/design-md/nike/DESIGN.md +575 -0
- package/skills/fui-skill/design-md/nike/README.md +5 -0
- package/skills/fui-skill/design-md/notion/DESIGN.md +821 -0
- package/skills/fui-skill/design-md/notion/README.md +5 -0
- package/skills/fui-skill/design-md/nvidia/DESIGN.md +640 -0
- package/skills/fui-skill/design-md/nvidia/README.md +5 -0
- package/skills/fui-skill/design-md/ollama/DESIGN.md +539 -0
- package/skills/fui-skill/design-md/ollama/README.md +5 -0
- package/skills/fui-skill/design-md/opencode.ai/DESIGN.md +521 -0
- package/skills/fui-skill/design-md/opencode.ai/README.md +5 -0
- package/skills/fui-skill/design-md/pinterest/DESIGN.md +597 -0
- package/skills/fui-skill/design-md/pinterest/README.md +5 -0
- package/skills/fui-skill/design-md/playstation/DESIGN.md +661 -0
- package/skills/fui-skill/design-md/playstation/README.md +5 -0
- package/skills/fui-skill/design-md/posthog/DESIGN.md +690 -0
- package/skills/fui-skill/design-md/posthog/README.md +5 -0
- package/skills/fui-skill/design-md/raycast/DESIGN.md +669 -0
- package/skills/fui-skill/design-md/raycast/README.md +5 -0
- package/skills/fui-skill/design-md/renault/DESIGN.md +589 -0
- package/skills/fui-skill/design-md/renault/README.md +5 -0
- package/skills/fui-skill/design-md/replicate/DESIGN.md +616 -0
- package/skills/fui-skill/design-md/replicate/README.md +5 -0
- package/skills/fui-skill/design-md/resend/DESIGN.md +585 -0
- package/skills/fui-skill/design-md/resend/README.md +5 -0
- package/skills/fui-skill/design-md/revolut/DESIGN.md +636 -0
- package/skills/fui-skill/design-md/revolut/README.md +5 -0
- package/skills/fui-skill/design-md/runwayml/DESIGN.md +244 -0
- package/skills/fui-skill/design-md/runwayml/README.md +5 -0
- package/skills/fui-skill/design-md/sanity/DESIGN.md +357 -0
- package/skills/fui-skill/design-md/sanity/README.md +5 -0
- package/skills/fui-skill/design-md/sentry/DESIGN.md +551 -0
- package/skills/fui-skill/design-md/sentry/README.md +5 -0
- package/skills/fui-skill/design-md/shopify/DESIGN.md +516 -0
- package/skills/fui-skill/design-md/shopify/README.md +5 -0
- package/skills/fui-skill/design-md/slack/DESIGN.md +482 -0
- package/skills/fui-skill/design-md/spacex/DESIGN.md +363 -0
- package/skills/fui-skill/design-md/spacex/README.md +5 -0
- package/skills/fui-skill/design-md/spotify/DESIGN.md +246 -0
- package/skills/fui-skill/design-md/spotify/README.md +5 -0
- package/skills/fui-skill/design-md/starbucks/DESIGN.md +580 -0
- package/skills/fui-skill/design-md/starbucks/README.md +5 -0
- package/skills/fui-skill/design-md/stripe/DESIGN.md +487 -0
- package/skills/fui-skill/design-md/stripe/README.md +5 -0
- package/skills/fui-skill/design-md/supabase/DESIGN.md +462 -0
- package/skills/fui-skill/design-md/supabase/README.md +5 -0
- package/skills/fui-skill/design-md/superhuman/DESIGN.md +448 -0
- package/skills/fui-skill/design-md/superhuman/README.md +5 -0
- package/skills/fui-skill/design-md/tesla/DESIGN.md +286 -0
- package/skills/fui-skill/design-md/tesla/README.md +5 -0
- package/skills/fui-skill/design-md/theverge/DESIGN.md +339 -0
- package/skills/fui-skill/design-md/theverge/README.md +5 -0
- package/skills/fui-skill/design-md/together.ai/DESIGN.md +633 -0
- package/skills/fui-skill/design-md/together.ai/README.md +5 -0
- package/skills/fui-skill/design-md/uber/DESIGN.md +636 -0
- package/skills/fui-skill/design-md/uber/README.md +5 -0
- package/skills/fui-skill/design-md/vercel/DESIGN.md +736 -0
- package/skills/fui-skill/design-md/vercel/README.md +5 -0
- package/skills/fui-skill/design-md/vodafone/DESIGN.md +538 -0
- package/skills/fui-skill/design-md/vodafone/README.md +5 -0
- package/skills/fui-skill/design-md/voltagent/DESIGN.md +521 -0
- package/skills/fui-skill/design-md/voltagent/README.md +5 -0
- package/skills/fui-skill/design-md/warp/DESIGN.md +526 -0
- package/skills/fui-skill/design-md/warp/README.md +5 -0
- package/skills/fui-skill/design-md/webflow/DESIGN.md +588 -0
- package/skills/fui-skill/design-md/webflow/README.md +5 -0
- package/skills/fui-skill/design-md/wired/DESIGN.md +497 -0
- package/skills/fui-skill/design-md/wired/README.md +5 -0
- package/skills/fui-skill/design-md/wise/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/wise/README.md +5 -0
- package/skills/fui-skill/design-md/x.ai/DESIGN.md +465 -0
- package/skills/fui-skill/design-md/x.ai/README.md +5 -0
- package/skills/fui-skill/design-md/zapier/DESIGN.md +537 -0
- package/skills/fui-skill/design-md/zapier/README.md +5 -0
- package/skills/fui-skill/examples/component.vue +162 -0
- package/skills/fui-skill/examples/f-table-patterns.json +331 -0
- package/skills/fui-skill/examples/module-patterns.json +973 -0
- package/skills/fui-skill/examples/project-patterns.json +222 -0
- package/skills/fui-skill/metadata.json +75 -0
- package/skills/fui-skill/references/INDEX.md +144 -0
- package/skills/fui-skill/references/advanced-techniques.md +160 -0
- package/skills/fui-skill/references/coding-standards.md +112 -0
- package/skills/fui-skill/references/component-design.md +455 -0
- package/skills/fui-skill/references/component-quickref.md +77 -0
- package/skills/fui-skill/references/component-table.md +276 -0
- package/skills/fui-skill/references/components-dialog.md +192 -0
- package/skills/fui-skill/references/components-display.md +147 -0
- package/skills/fui-skill/references/components-echart.md +391 -0
- package/skills/fui-skill/references/components-input.md +359 -0
- package/skills/fui-skill/references/controls-patterns.md +847 -0
- package/skills/fui-skill/references/controls-styling-vocabulary.md +140 -0
- package/skills/fui-skill/references/db-table-design.md +77 -0
- package/skills/fui-skill/references/db-workflow.md +504 -0
- package/skills/fui-skill/references/default-function.md +415 -0
- package/skills/fui-skill/references/design-modes.md +85 -0
- package/skills/fui-skill/references/echart-templates.md +481 -0
- package/skills/fui-skill/references/fastproject.md +97 -0
- package/skills/fui-skill/references/fsheet.md +218 -0
- package/skills/fui-skill/references/fullstack-workflow.md +351 -0
- package/skills/fui-skill/references/module-data-patterns.md +126 -0
- package/skills/fui-skill/references/module-json-anatomy.md +137 -0
- package/skills/fui-skill/references/module-structure.md +260 -0
- package/skills/fui-skill/references/new-session.md +108 -0
- package/skills/fui-skill/references/pdfmake.md +60 -0
- package/skills/fui-skill/references/permission-system.md +169 -0
- package/skills/fui-skill/references/platform-architecture.md +294 -0
- package/skills/fui-skill/references/project-config.md +335 -0
- package/skills/fui-skill/references/project-provisioning.md +383 -0
- package/skills/fui-skill/references/script-map.md +296 -0
- package/skills/fui-skill/references/sql-clr-functions.md +224 -0
- package/skills/fui-skill/references/system-design.md +116 -0
- package/skills/fui-skill/references/tapi-file-api.md +191 -0
- package/skills/fui-skill/references/tapi-permission-patterns.md +158 -0
- package/skills/fui-skill/references/tapi-reference.md +549 -0
- package/skills/fui-skill/references/tools-registry.md +460 -0
- package/skills/fui-skill/references/ui-crosswindow-patterns.md +317 -0
- package/skills/fui-skill/references/ui-dialog-patterns.md +229 -0
- package/skills/fui-skill/references/ui-layout-patterns.md +176 -0
- package/skills/fui-skill/references/ui-patterns.md +315 -0
- package/skills/fui-skill/references/ui-screenshot-review.md +94 -0
- package/skills/fui-skill/references/ui-table-cell-patterns.md +316 -0
- package/skills/fui-skill/references/ui-templates.md +29 -0
- package/skills/fui-skill/references/verification.md +246 -0
- package/skills/fui-skill/references/watcher-patterns.md +196 -0
- package/skills/fui-skill/references/websocket-realtime.md +254 -0
- package/skills/fui-skill/scripts/component-3.0.js +2549 -0
- package/skills/fui-skill/scripts/component.js +3142 -0
- package/skills/fui-skill/scripts/componentTable-3.0.js +909 -0
- package/skills/fui-skill/scripts/componentTable.js +769 -0
- package/skills/fui-skill/scripts/defaultfunction-3.0.js +781 -0
- package/skills/fui-skill/scripts/defaultfunction.js +966 -0
- package/skills/fui-skill/scripts/fastproject-3.0.js +870 -0
- package/skills/fui-skill/scripts/fastproject.js +828 -0
- package/skills/fui-skill/scripts/fechart.js +890 -0
- package/skills/fui-skill/scripts/fsheet.js +1330 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# f-sheet — Bảng tính nhập liệu (dựa trên AG Grid)
|
|
2
|
+
|
|
3
|
+
> File này sở hữu: **`f-sheet` (AG Grid): import, props, cấu hình cột, cell renderer dựng sẵn, chọn vùng/fill/clipboard, wiring API**.
|
|
4
|
+
|
|
5
|
+
`f-sheet` là **data grid nhập liệu kiểu Excel**, xây dựng **hoàn toàn trên AG Grid Community**. Mọi cấu hình cột/lưới đều là cấu hình AG Grid — **khi cần tuỳ biến, dựa vào kiến thức AG Grid** (https://www.ag-grid.com/, phiên bản Community). FUI chỉ bọc thêm: toolbar (thêm/xoá/lưu/xuất Excel), theo dõi dòng thay đổi, chọn vùng nhiều ô kiểu Excel + fill handle + copy/paste TSV, và wiring API.
|
|
6
|
+
|
|
7
|
+
> **Phân biệt với `f-table`**: `f-table` hiển thị dữ liệu dạng datatable, sửa dòng qua dialog form riêng. `f-sheet` sửa **ngay trên ô của bảng** (không qua dialog), phù hợp nhập liệu hàng loạt kiểu Excel (dán nhiều dòng, sửa nhanh nhiều ô). Cùng là component có sẵn — không tự viết bảng nhập liệu bằng `uc-*.vue`.
|
|
8
|
+
|
|
9
|
+
> Nền tảng (nguồn sự thật): `scripts/fsheet.js` — **một file DUY NHẤT dùng chung cho cả V2 và V3** (không còn khái niệm 2 bản phải giữ đồng bộ). Component khai cả `beforeDestroy` (Vue 2) lẫn `beforeUnmount` (Vue 3) để hoạt động đúng trên cả hai runtime. Template dùng thuần class/prop **Vuetify 3** (`variant="text"`, `density="comfortable"`, `text-primary`, `text-subtitle-1`, `text-caption`) — trên module V2 (Vuetify 2) toolbar vẫn bấm được nhưng có thể không lên đúng màu/kiểu vì Vuetify 2 bỏ qua các prop/class lạ này. Dùng `ajaxCALL`/`confirm`/`jsonToExcel` bên trong — khớp [component-design.md](component-design.md) §12.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Import BẮT BUỘC (thiếu → không dùng được f-sheet)
|
|
14
|
+
|
|
15
|
+
Thêm đúng 2 file, **đúng thứ tự**, qua `file_import_new`:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
/include/ag-grid/ag-grid-community.min.js
|
|
19
|
+
/include/ag-grid/fsheet.js
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Quy trình khi thêm/bỏ f-sheet: `file_import_list` (projectId + moduleId) → chưa có thì `file_import_new` từng file → bỏ f-sheet mà không module/component khác dùng thì `file_import_delete`. Xem [component-quickref.md](component-quickref.md).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Props
|
|
27
|
+
|
|
28
|
+
| Prop (kebab-case trong `attr`) | Kiểu | Ý nghĩa |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `label` | string | Tiêu đề + hiện trên thanh công cụ |
|
|
31
|
+
| `:columns` | array | **colDefs của AG Grid** (xem mục Cột). Bỏ trống → tự suy cột từ `items[0]` (mỗi field → `{ field, headerName: field }`) |
|
|
32
|
+
| `:items` | array | Dữ liệu dòng (rowData) |
|
|
33
|
+
| `row-key` | string | Tên field khóa chính (mặc định `id`) — dùng để nhận diện dòng đổi/mới/xoá |
|
|
34
|
+
| `:height` | number | Bỏ trống = tự giãn theo số dòng; `<=0` = fill tới đáy màn hình chừa `abs(height)` px; `>0` = px cố định (xem mục Chiều cao) |
|
|
35
|
+
| `:allow-add` | bool | Hiện nút Thêm dòng |
|
|
36
|
+
| `:allow-delete` | bool | Hiện cột checkbox chọn + nút Xoá |
|
|
37
|
+
| `readonly` | bool | Khoá toàn bộ chỉnh sửa (đồng thời tắt luôn fill handle) |
|
|
38
|
+
| `update-api` | string | Endpoint tAPI **lưu từng dòng đã đổi** (gọi qua `ajaxCALL`, tự prepend `apiDomain`). Không khai thì nút Lưu bị `disabled` |
|
|
39
|
+
| `delete-api` | string | Endpoint tAPI xoá một dòng |
|
|
40
|
+
| `:save-extra` | object | Tham số phụ **trộn vào payload** mỗi lần lưu/xoá (vd `{ LopID: vueData.lopID }`) — đi qua `mapData`, hỗ trợ tham chiếu động `vueData.x` |
|
|
41
|
+
| `:default-item` | object | Mẫu giá trị cho dòng mới khi bấm Thêm — cũng đi qua `mapData`, hỗ trợ tham chiếu động `vueData.x`, không chỉ literal tĩnh |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Cột (`columns`) = colDef của AG Grid
|
|
46
|
+
|
|
47
|
+
Mỗi phần tử `columns` là một **AG Grid colDef** — dùng trực tiếp kiến thức AG Grid:
|
|
48
|
+
|
|
49
|
+
| Key colDef hay dùng | Ý nghĩa |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `field` | Tên field trong dòng dữ liệu |
|
|
52
|
+
| `headerName` | Nhãn cột |
|
|
53
|
+
| `editable` | Cho sửa ô — **mặc định bật** (f-sheet đặt `editable: !readonly` trong `defaultColDef`); cột chỉ đọc khai `"editable": false` |
|
|
54
|
+
| `width` / `minWidth` / `flex` | Bề rộng cột (mặc định `flex:1`, `minWidth:80`) |
|
|
55
|
+
| `pinned` | `'left'`/`'right'` cố định cột |
|
|
56
|
+
| `sortable` / `filter` / `resizable` | Mặc định đều bật |
|
|
57
|
+
| `valueFormatter` / `valueGetter` | Định dạng/hiển thị (hàm AG Grid) |
|
|
58
|
+
| `cellEditor` | Editor tuỳ chỉnh — tên có sẵn của AG Grid (vd `agDateStringCellEditor`) hoặc tên hàm global tự viết |
|
|
59
|
+
| `cellRenderer` | Renderer tuỳ chỉnh — 4 renderer dựng sẵn của f-sheet (xem bên dưới) hoặc tên hàm global tự viết |
|
|
60
|
+
| `type` | **Tên columnType đã đăng ký** — chỉ có `"number"` và `"date"`. **KHÔNG phải** chỗ khai kiểu dữ liệu tự do (xem mục columnType ngay dưới) |
|
|
61
|
+
|
|
62
|
+
> Các tính năng cột nâng cao khác (cellClassRules, aggregation...) tra tài liệu AG Grid Community — f-sheet truyền colDef thẳng vào grid. Riêng cột `type:"number"`/`type:"date"` được f-sheet **tự gắn** `cellStyle`/`valueFormatter`/`valueParser`/`filter` (xem bên dưới) — chỉ khai tay khi cần logic khác mặc định.
|
|
63
|
+
|
|
64
|
+
### columnType — f-sheet đăng ký ĐÚNG 2 tên: `number` và `date`
|
|
65
|
+
|
|
66
|
+
`type` **không phải** key gốc của AG Grid dùng để khai kiểu dữ liệu. Nó là **tên tham chiếu tới một columnType đã đăng ký** trong `gridOptions.columnTypes`. f-sheet đăng ký đúng hai tên — bảng `FS_COLUMN_TYPES` ở đầu `scripts/fsheet.js` là nguồn duy nhất.
|
|
67
|
+
|
|
68
|
+
Đặt một tên **chưa đăng ký** (`"date_time"`, `"currency"`, `"text"`, `"string"`, `"boolean"`…) thì:
|
|
69
|
+
|
|
70
|
+
- AG Grid **bỏ qua hoàn toàn** và chỉ log `warning #36 colDef.type 'x' does not correspond to defined gridOptions.columnTypes` — cột chạy y hệt cột text thô, **không** format, **không** parse, **không** filter. Cấu hình trông như có tác dụng nhưng không có.
|
|
71
|
+
- f-sheet phát thêm một `console.error` nêu đích danh tên cột + danh sách tên hợp lệ (quét cả colDef nằm trong `children` của column group). Dòng này **hiện lên trong `module_simulate({ renderUI: true })`** — warning gốc của AG Grid thì không.
|
|
72
|
+
|
|
73
|
+
Cần một kiểu khác hai kiểu trên (tiền tệ, phần trăm, ngày-giờ…) thì **đừng đặt tên mới cho `type`** — khai thẳng `valueFormatter`/`valueParser`/`filter` trên colDef đó.
|
|
74
|
+
|
|
75
|
+
### Cell renderer dựng sẵn (`cellRenderer`)
|
|
76
|
+
|
|
77
|
+
f-sheet đăng ký sẵn 4 renderer — dùng ngay qua `"cellRenderer": "tênRenderer"` + `cellRendererParams`, không cần khai gì thêm trong `script.js`:
|
|
78
|
+
|
|
79
|
+
| Renderer | Mục đích | `cellRendererParams` |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `fsHtmlCellRenderer` | In `value` ra dạng HTML thô (giống `t-html` của f-table) | *(không cần)* |
|
|
82
|
+
| `fsLinkCellRenderer` | Ô dạng link | `urlField`/`textField` (field khác trong dòng làm URL/nhãn, mặc định dùng `value`), `icon` (mdi, tuỳ chọn), `target` — `'dialog'` mở qua `openWindow({id: wid, title, url, onclose})`, còn lại là target của thẻ `<a>` (mặc định `_blank`) |
|
|
83
|
+
| `fsButtonCellRenderer` | Ô dạng nút bấm nhỏ | `label`, `icon` (mdi, tuỳ chọn), `color` (mặc định `#1976d2`); bấm vào: có `url` → `openWindow({id: wid, title, url, onclose})`, không có `url` → `tableActionEvent({action, item: data})` (action đặt tên trong `action`) |
|
|
84
|
+
| `fsMenuCellRenderer` | Nút `⋮` mở menu thả xuống theo dòng | `items` — mảng `{icon, text, color, action}` hoặc hàm `function(params)` trả về mảng đó (để tính động theo dòng); `icon` cho nút mở menu (mặc định `mdi-dots-vertical`). Bấm một item → `tableActionEvent({action: item.action, item: data})` |
|
|
85
|
+
|
|
86
|
+
`fsButtonCellRenderer`/`fsMenuCellRenderer` dùng lại `openWindow`/`tableActionEvent` — 2 hàm global gốc của FUI (giống hệt cách `t-link`/`t-button`/`t-menu` của `f-table` hoạt động), không phải cơ chế riêng của f-sheet.
|
|
87
|
+
|
|
88
|
+
**Tên hàm global tự viết** cũng dùng được trực tiếp cho cả `cellRenderer`/`cellEditor` lẫn `cellEditorParams`/`cellRendererParams` — khai hàm đó trong `script.js` của module rồi tham chiếu bằng tên (string). f-sheet tự nhận diện và đăng ký (kể cả khi colDef nằm trong `children` của một column group), không cần đăng ký thủ công.
|
|
89
|
+
|
|
90
|
+
### Cột số — `"type": "number"`
|
|
91
|
+
|
|
92
|
+
Khai `"type": "number"` là đủ, f-sheet tự gắn 4 thứ (mỗi thứ chỉ gắn khi colDef **chưa** tự khai, nên muốn khác mặc định thì cứ khai đè):
|
|
93
|
+
|
|
94
|
+
| Tự gắn | Hành vi |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `cellStyle` | Canh **phải** |
|
|
97
|
+
| `valueFormatter` | Phân cách nghìn `toLocaleString('en-US', { maximumFractionDigits: 10 })` → `1,234,567.89`. Chỉ áp lúc **hiển thị** — click vào ô sửa vẫn thấy số thô |
|
|
98
|
+
| `valueParser` | Parse khi rời ô sửa: rỗng/`null`/`undefined` → `null` thật (không phải `""`), chuỗi số → `Number`, chuỗi hỏng → `null`. Áp cho cả paste-từ-Excel |
|
|
99
|
+
| `filter` | `agNumberColumnFilter` (lọc lớn hơn/nhỏ hơn/bằng thay vì lọc text) |
|
|
100
|
+
|
|
101
|
+
### Cột ngày — `"type": "date"`
|
|
102
|
+
|
|
103
|
+
Khai `"type": "date"` là đủ, f-sheet tự gắn 4 thứ (mỗi thứ chỉ gắn khi colDef **chưa** tự khai):
|
|
104
|
+
|
|
105
|
+
| Tự gắn | Hành vi |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `cellStyle` | Canh **giữa** |
|
|
108
|
+
| `valueFormatter` | Hiển thị `dd/mm/yyyy`. Giá trị không phải ngày → in **chuỗi thô** (để dữ liệu rác hiện ra, không biến ô thành trống) |
|
|
109
|
+
| `valueParser` | Nhận `dd/mm/yyyy`, `dd-mm-yyyy`, `yyyy-mm-dd` và ISO datetime `2026-08-15T00:00:00` → lưu `yyyy-mm-dd`. Rỗng → `null`. Sai → `null`. Áp cho cả paste-từ-Excel |
|
|
110
|
+
| `filter` | `agDateColumnFilter` (lọc trước/sau/bằng ngày) |
|
|
111
|
+
|
|
112
|
+
**Hợp đồng giá trị — đây là thứ quyết định SP nhận được gì:** ô ngày luôn giữ chuỗi **`yyyy-mm-dd`** (hoặc `null`). Khớp thẳng với `f-date` (cũng emit `yyyy-mm-dd`) và với `OPENJSON ... NgayX date '$.NgayX'` phía tAPI — không phải convert gì thêm.
|
|
113
|
+
|
|
114
|
+
- **Ngày không hợp lệ về `null`, ô thành rỗng** — giống hệt cột số. `31/02/2026` và `15/13/2026` bị từ chối (không âm thầm cuộn sang `03/03/2026` / `15/01/2027`).
|
|
115
|
+
- **Copy và Xuất Excel lấy giá trị thô** ⇒ ra `2026-08-15`, không phải `15/08/2026`. Cố ý: dán ngược vào lưới thì parse lại được, round-trip không mất mát.
|
|
116
|
+
|
|
117
|
+
#### Muốn có date picker
|
|
118
|
+
|
|
119
|
+
`type: "date"` lo hiển thị/parse/lọc, **không tự gắn editor** — sửa ô vẫn là gõ text. Cần picker thì khai thêm:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{ "field": "NgayThi", "headerName": "Ngày thi", "type": "date", "cellEditor": "agDateStringCellEditor" }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`agDateStringCellEditor` là editor **có sẵn của AG Grid**, không cần đăng ký gì. Nhưng nó đọc **giá trị thô** chứ không qua `valueFormatter`, nên ô đang giữ ISO datetime (`2026-08-15T00:00:00`) sẽ mở ra **rỗng** cho tới khi được sửa một lần. Cho SP trả `CONVERT(date, NgayThi)` là hết — cũng là lý do f-sheet không gắn editor này mặc định (gắn ngầm thì bấm Enter là ghi `null` đè lên ngày đang có).
|
|
126
|
+
|
|
127
|
+
> **Không có editor nào tên `fsheetDate`.** Tên đó **từng tồn tại** trong `componentTable*.js` rồi `fsheet.js` và **đã bị gỡ**. Module cũ còn khai `"cellEditor": "fsheetDate"` thì đổi sang `"type": "date"` (thêm `agDateStringCellEditor` nếu cần picker). Luật nền cần nhớ: `cellEditor`/`cellRenderer` dạng string chỉ resolve được khi `window[tên]` là một function — tên không tồn tại thì f-sheet **im lặng** bỏ qua và AG Grid rơi về editor text, không lỗi, không cảnh báo.
|
|
128
|
+
|
|
129
|
+
### Chiều cao — `:height`
|
|
130
|
+
|
|
131
|
+
| Giá trị | Hành vi |
|
|
132
|
+
|---|---|
|
|
133
|
+
| *(bỏ trống)* | **Tự giãn theo số dòng** — AG Grid `domLayout: 'autoHeight'`, lưới cao bằng đúng nội dung, không có thanh cuộn riêng |
|
|
134
|
+
| `<= 0` | **Fill tới đáy màn hình, chừa `abs(height)` px** |
|
|
135
|
+
| `> 0` | px cố định (vd `":height": 300`) |
|
|
136
|
+
|
|
137
|
+
Chế độ `<=0` **đo `getBoundingClientRect().top` thật** của phần tử lưới sau khi layout xong rồi tính `window.innerHeight - top - abs(height)` (sàn tối thiểu 200px), và đăng ký `window.resize` để tự cập nhật. Nghĩa là **không cần biết** header/toolbar/breadcrumb phía trên cao bao nhiêu — chỉ khai khoảng hở muốn chừa ở đáy.
|
|
138
|
+
|
|
139
|
+
- **Bảng chính của module**: khai `":height": -16` — cuộn data chỉ cuộn trong lưới, trang không cuộn (lint D9). Thiếu `:height` sẽ bị D9 cảnh báo.
|
|
140
|
+
- **Trong dialog**: dùng số **dương** (px cố định), vì lưới không nằm sát đáy viewport.
|
|
141
|
+
- **Bảng ngắn/lookup ít dòng**: bỏ trống `:height` để tự giãn — hợp lệ, bỏ qua D9.
|
|
142
|
+
- ⚠ Chế độ tự giãn **render toàn bộ dòng** (AG Grid không ảo hoá được khi không có chiều cao cố định). Vài trăm dòng trở lên thì phải khai `:height` để lấy lại virtual scrolling.
|
|
143
|
+
- ⚠ Số âm **không** có nghĩa "viewport trừ đi N". Module cũ khai `":height": -220` phải sửa thành `-16`, nếu không lưới hụt mất ~200px.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Wiring API — hợp đồng response
|
|
148
|
+
|
|
149
|
+
`update-api` và `delete-api` được gọi bằng `ajaxCALL(api, { ...saveExtra, ...row })`. **SP nên trả về một dòng** dạng:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
SELECT Success = 1, Message = N'Đã lưu' -- hoặc Success = 0 khi lỗi nghiệp vụ
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
f-sheet đọc `res.data[0]` → hiện snackbar `Message`; `Success` quyết định đánh dấu thành công. Xem quy ước SP: [tapi-reference.md](tapi-reference.md).
|
|
156
|
+
|
|
157
|
+
- **Dòng mới**: nhận khóa **âm** tạm (−1, −2…) và tô nền **xanh nhạt**; khi lưu, khóa âm được đặt `0` trong payload → SP hiểu là INSERT.
|
|
158
|
+
- **Ô đã sửa**: tô nền **vàng nhạt**; "N dòng thay đổi" hiển thị trên toolbar.
|
|
159
|
+
- Lưu chạy **từng dòng song song**; xong hết & không lỗi → làm mới baseline, xoá đánh dấu, emit `saved`.
|
|
160
|
+
- Xoá nhiều dòng đã tồn tại (đã khai `delete-api`) → hỏi `confirm` trước rồi mới gọi API cho từng dòng; **chưa khai `delete-api`** thì xoá cục bộ ngay, không hỏi. Dòng mới (khóa âm) luôn xoá cục bộ ngay, không hỏi, không gọi API.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Tính năng có sẵn (không cần tự code)
|
|
165
|
+
|
|
166
|
+
- **Sửa inline** + theo dõi thay đổi (vàng nhạt = ô đã đổi, xanh nhạt = dòng mới).
|
|
167
|
+
- **Chọn nhiều ô kiểu Excel**: kéo chuột hoặc Shift+click để chọn một khối ô liên tục.
|
|
168
|
+
- **Fill handle**: kéo núm vuông ở góc dưới-phải vùng đang chọn để điền lặp lại khối nguồn sang các ô lân cận (theo chiều kéo dài hơn — ngang hoặc dọc).
|
|
169
|
+
- **Copy/Cut/Paste dạng TSV** (tương thích Excel/Google Sheets): phím tắt Ctrl+C/Ctrl+X/Ctrl+V khi đang focus trong lưới, hoặc menu chuột phải (Cut/Copy/Copy with Headers/Paste).
|
|
170
|
+
- **Phím Escape** bỏ vùng đang chọn.
|
|
171
|
+
- Toolbar: **Xuất Excel** (luôn có, dùng `jsonToExcel`) · **Thêm** (`allow-add`) · **Lưu** (hiện khi có dòng đổi, `disabled` nếu chưa khai `update-api`) · **Huỷ thay đổi** (hiện khi có dòng đổi) · **Xoá đã chọn** (`allow-delete`, multi-select, chỉ hiện khi có dòng đang chọn).
|
|
172
|
+
|
|
173
|
+
## Events & method (`$refs`)
|
|
174
|
+
|
|
175
|
+
- Emit: `change(data, field, value)` (mỗi lần một ô đổi giá trị) · `save(rows)` (bấm Lưu, trước khi gọi API) · `saved` (lưu thành công toàn bộ).
|
|
176
|
+
- Method: `getChangedRows()` · `addRow()` · `deleteRow(row)` · `deleteSelected()` · `discardAll()` · `exportExcel()`.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Ví dụ trong module.json (controls)
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"el": "f-sheet",
|
|
185
|
+
"w": 12,
|
|
186
|
+
"attr": {
|
|
187
|
+
"label": "Bảng điểm",
|
|
188
|
+
":items": "dsDiem",
|
|
189
|
+
"row-key": "DiemID",
|
|
190
|
+
":columns": [
|
|
191
|
+
{ "field": "MaSV", "headerName": "Mã SV", "editable": false, "width": 120, "pinned": "left" },
|
|
192
|
+
{ "field": "HoTen", "headerName": "Họ tên", "editable": false },
|
|
193
|
+
{ "field": "Diem", "headerName": "Điểm", "type": "number" },
|
|
194
|
+
{ "field": "NgayThi","headerName": "Ngày thi", "type": "date" },
|
|
195
|
+
{ "field": "GhiChu","headerName": "Ghi chú" }
|
|
196
|
+
],
|
|
197
|
+
":allow-add": true,
|
|
198
|
+
":allow-delete": true,
|
|
199
|
+
"update-api": "Diem_Save",
|
|
200
|
+
"delete-api": "Diem_Delete",
|
|
201
|
+
":save-extra": { "LopID": "vueData.lopID" },
|
|
202
|
+
":default-item": { "Diem": 0 },
|
|
203
|
+
":height": -16
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- `dsDiem` được nạp trước bằng một action trong `data[]` (`API`/`OUT`).
|
|
209
|
+
- `Diem_Save`/`Diem_Delete` là SP tAPI trả `{ Success, Message }` mỗi dòng.
|
|
210
|
+
- Kết hợp mở cửa sổ chi tiết: nút/menu trong toolbar hoặc cột gọi `openWindow` — xem [ui-crosswindow-patterns.md](ui-crosswindow-patterns.md) §5.
|
|
211
|
+
|
|
212
|
+
## Checklist
|
|
213
|
+
|
|
214
|
+
1. Đã `file_import_new` đủ **2 file AG Grid**, đúng thứ tự (`ag-grid-community.min.js` trước, `fsheet.js` sau).
|
|
215
|
+
2. `row-key` trỏ đúng khóa chính; SP `update-api`/`delete-api` trả `{ Success, Message }`.
|
|
216
|
+
3. Cần tuỳ biến cột → dùng colDef AG Grid; ô dạng link/nút/menu → 4 cell renderer dựng sẵn (`fsHtmlCellRenderer`/`fsLinkCellRenderer`/`fsButtonCellRenderer`/`fsMenuCellRenderer`); số → `type: "number"`, ngày → `type: "date"` (không tự khai `cellStyle`/`valueFormatter`/`valueParser` tay nữa). `type` **chỉ** nhận đúng 2 tên đó — kiểu khác thì khai thẳng `valueFormatter`/`valueParser`, đừng đặt tên `type` mới.
|
|
217
|
+
4. `:height` — bảng chính khai `-16`; trong dialog dùng số **dương**; bỏ trống = tự giãn (chỉ hợp cho bảng ngắn). Số âm = khoảng chừa ở đáy, **không phải** "viewport trừ N".
|
|
218
|
+
5. Đừng tự dựng lại grid/CSS — mọi cấu hình lưới đi qua `columns` (AG Grid); chọn vùng/fill/copy-paste đã có sẵn, không tự viết thêm.
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# Full-Stack Module Workflow
|
|
2
|
+
|
|
3
|
+
> File này sở hữu: **quy trình 6 bước tạo một module fullstack, UI-first (phân tích → dựng UI data cứng → chốt contract → SP → wiring API → kiểm chứng tĩnh)**, và **quy trình Design Review Gate** (SKILL.md §3 chỉ giữ *luật* "phải qua gate", các bước nằm ở đây). Quy ước tAPI xem [tapi-reference.md](tapi-reference.md); thiết kế nhiều module xem [system-design.md](system-design.md).
|
|
4
|
+
|
|
5
|
+
Quy trình 6 bước để thiết kế một FUI module hoàn chỉnh: có cả UI (FUI) lẫn backend API (tAPI).
|
|
6
|
+
|
|
7
|
+
**Nguyên tắc UI-first:** dựng UI bằng dữ liệu cứng trước, xác nhận hình ảnh đúng bằng `module_simulate({ renderUI: true })`, RỒI mới chốt contract API và viết SP. Tách pha "làm UI" khỏi pha "nối API" giảm hẳn lỗi so với viết cả hai cùng lúc — sửa cấu trúc data khi còn là literal trong `data[]` rẻ hơn nhiều so với sửa sau khi đã có `IN`/`OUT`/`watch` phụ thuộc vào nó. Không có bước nào trong quy trình này gọi API/SP thật để "test" — kiểm chứng luôn là đọc + suy luận + mock (xem [verification.md](verification.md)).
|
|
8
|
+
|
|
9
|
+
Đọc [tapi-reference.md](tapi-reference.md) trước khi bắt đầu bước 3.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Bước 1 — Phân tích yêu cầu
|
|
14
|
+
|
|
15
|
+
Trước khi viết bất kỳ dòng code nào, xác định rõ:
|
|
16
|
+
|
|
17
|
+
**1.1 Nghiệp vụ cần làm gì?**
|
|
18
|
+
- Danh sách / tìm kiếm / lọc dữ liệu?
|
|
19
|
+
- Thêm / sửa / xoá bản ghi?
|
|
20
|
+
- Báo cáo / thống kê?
|
|
21
|
+
- Workflow (duyệt, từ chối, trạng thái)?
|
|
22
|
+
|
|
23
|
+
**1.2 Dữ liệu liên quan (ước lượng ban đầu — sẽ chốt lại chính xác ở bước 3):**
|
|
24
|
+
- Entity chính là gì? Các entity liên kết?
|
|
25
|
+
- Trường nào hiển thị, trường nào nhập liệu?
|
|
26
|
+
|
|
27
|
+
**1.3 Quyền và xác thực:**
|
|
28
|
+
- Module có cần login không? (nếu không → dùng `AUTH_` trong SP)
|
|
29
|
+
- Có phân quyền theo `sys_SystemRight` không? → xử lý trong thân SP
|
|
30
|
+
|
|
31
|
+
**Output bước 1:** Danh sách tính năng + tên các entity chính (chưa cần chốt kiểu dữ liệu/tên cột).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Bước 1b — Design Review Gate (bắt buộc, trước khi viết dòng code đầu tiên)
|
|
36
|
+
|
|
37
|
+
SKILL.md §3 quy định **luật**: mọi hệ thống/trang/chức năng mới đều phải qua gate này. Đây là
|
|
38
|
+
**quy trình** của nó. Áp dụng cho cả module fullstack lẫn module chỉ có UI.
|
|
39
|
+
|
|
40
|
+
1. **Phân tích hiệu quả nghiệp vụ** — bao nhiêu bước để user hoàn thành tác vụ, dữ liệu nào thực sự
|
|
41
|
+
cần hiển thị/nhập, có bước nào thừa không.
|
|
42
|
+
2. **Tham khảo pattern đã có trong project** — `module_list` + `module_get` xem module tương tự
|
|
43
|
+
(layout, cách đặt tên control, vị trí action, cách trình bày filter/table/dialog). Thiết kế mới
|
|
44
|
+
phải đồng bộ với trang hiện có, **không tự sáng tạo kiểu khác nếu project đã có convention**.
|
|
45
|
+
3. **Phác thảo UI trước** bằng lời hoặc khung layout sơ bộ — control nào, ở đâu, luồng dialog/form
|
|
46
|
+
ra sao.
|
|
47
|
+
4. **Tự đánh giá lại phác thảo** — thân thiện không? số bước hợp lý không? vị trí nút Thêm/Sửa/Xóa,
|
|
48
|
+
filter, phân trang có đúng chỗ user mong đợi không? nhất quán với trang khác không? Phát hiện bất
|
|
49
|
+
hợp lý → **sửa phác thảo trước**, không mang bất hợp lý vào code.
|
|
50
|
+
5. Chỉ khi phác thảo đã hợp lý mới viết code thật (bước 2 trở đi).
|
|
51
|
+
|
|
52
|
+
**Chọn phương án dựng UI:**
|
|
53
|
+
|
|
54
|
+
| Phương án | Khi nào dùng |
|
|
55
|
+
|---|---|
|
|
56
|
+
| **A — module.json thuần** | Trang đơn giản: hiển thị dữ liệu, CRUD một bảng, form nhập liệu cơ bản. Ít file, dễ maintain. |
|
|
57
|
+
| **B — Vue component (`uc-*.vue`)** | Giao diện phức tạp: dashboard nhiều vùng, layout tùy chỉnh, logic UI lặp lại nhiều nơi, hoặc `controls` vượt ~200 dòng ([component-design.md](component-design.md)). |
|
|
58
|
+
|
|
59
|
+
> User không chỉ định → **tự đề xuất** dựa trên mô tả, giải thích ngắn lý do và **chờ xác nhận** —
|
|
60
|
+
> đừng tự chọn im lặng.
|
|
61
|
+
|
|
62
|
+
**Ưu tiên component**: dùng `f-*` trước (`f-button` thay `v-btn`, `f-table` thay `v-data-table` cho
|
|
63
|
+
MỌI bảng, `f-echart` cho mọi biểu đồ), chỉ dùng `v-*` khi không có FUI equivalent.
|
|
64
|
+
→ [component-quickref.md](component-quickref.md)
|
|
65
|
+
|
|
66
|
+
**Cổng Design System** chạy song song ở đây: xác định App-mode hay Web-mode và kiểm tra
|
|
67
|
+
`{projectId}/DESIGN.md` **trước khi** phác thảo — hai chế độ có bộ quy tắc thẩm mỹ riêng, không vay
|
|
68
|
+
mượn qua lại. → [design-modes.md](design-modes.md)
|
|
69
|
+
|
|
70
|
+
**Output bước 1b:** phác thảo UI đã tự review, phương án A/B đã chốt (có xác nhận của user nếu do ta
|
|
71
|
+
đề xuất), chế độ thẩm mỹ đã xác định.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Bước 2 — Dựng UI với dữ liệu cứng
|
|
76
|
+
|
|
77
|
+
Thiết kế toàn bộ UI của module bằng dữ liệu **literal** khai báo thẳng trong `data[]` — chưa có API nào cả. Xem [module-json-anatomy.md](module-json-anatomy.md) và [controls-patterns.md](controls-patterns.md) để biết cấu trúc control/grid.
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
"data": [
|
|
81
|
+
{ "keyword": "" },
|
|
82
|
+
{ "classId": "" },
|
|
83
|
+
{ "studentList": [
|
|
84
|
+
{ "StudentID": "SV001", "FullName": "Nguyễn Văn A", "ClassID": "C01", "ClassName": "Lớp 10A", "GPA": 8.5 },
|
|
85
|
+
{ "StudentID": "SV002", "FullName": "Trần Thị B", "ClassID": "C02", "ClassName": "Lớp 10B", "GPA": 7.2 }
|
|
86
|
+
] },
|
|
87
|
+
{ "form": {} },
|
|
88
|
+
{ "dialogOpen": false }
|
|
89
|
+
]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Dựng `f-table`/`v-dialog`/form theo dữ liệu cứng này — component-table/dialog patterns xem [component-table.md](component-table.md), [ui-templates.md](ui-templates.md).
|
|
93
|
+
|
|
94
|
+
**QA hình ảnh ngay ở bước này:** `module_simulate({ renderUI: true, vueData: {...dữ liệu cứng nếu muốn override} })`, đọc screenshot + DOM audit, tự sửa lỗi hiển thị (tràn ngang, dialog vỡ, thiếu label...) trước khi đi tiếp — không mang lỗi UI sang các bước sau.
|
|
95
|
+
|
|
96
|
+
**Output bước 2:** `module.json` có layout hoàn chỉnh, chạy được với data cứng, đã qua `renderUI` QA sạch.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Bước 3 — Chốt cấu trúc data + thiết kế API endpoints
|
|
101
|
+
|
|
102
|
+
Từ chính các field đã dùng ở data cứng bước 2 (đây là lý do bước 2 đi trước — cấu trúc field đã được xác nhận đúng bằng mắt trước khi biến thành contract SP), chốt:
|
|
103
|
+
- Tên field chính xác, kiểu dữ liệu (dùng để suy ra kiểu cột SQL)
|
|
104
|
+
- Entity/table nào sinh ra field nào
|
|
105
|
+
|
|
106
|
+
Rồi map sang các SP tAPI cần tạo. Quy ước đặt tên đầy đủ (`spAPI_*` / `spAPI_AUTH_*` / `spAPIFILE_*`, params `@url1_`, response contract) là của [tapi-reference.md](tapi-reference.md) — ở đây chỉ áp dụng vào việc map tính năng sang SP theo pattern `spAPI_{Entity}{Action}`.
|
|
107
|
+
|
|
108
|
+
**Bảng mapping thường gặp:**
|
|
109
|
+
|
|
110
|
+
| Tính năng | SP name | Params chính |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| Lấy danh sách | `spAPI_{Entity}List` | Filter params (custom) |
|
|
113
|
+
| Lấy chi tiết | `spAPI_{Entity}Get` | `@url1_{EntityID}` |
|
|
114
|
+
| Thêm mới | `spAPI_{Entity}Insert` | Các field của entity |
|
|
115
|
+
| Cập nhật | `spAPI_{Entity}Update` | ID + các field cần cập nhật |
|
|
116
|
+
| Xoá | `spAPI_{Entity}Delete` | `@url1_{EntityID}` |
|
|
117
|
+
| Lookup / dropdown | `spAPI_AUTH_{Entity}Options` | Không cần auth, trả danh sách |
|
|
118
|
+
|
|
119
|
+
**Ví dụ cho module "Quản lý Sinh viên"** (khớp field đã dùng ở data cứng bước 2: `StudentID`, `FullName`, `ClassID`, `ClassName`, `GPA`):
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
spAPI_StudentList -- danh sách, lọc theo ClassID + Keyword — trả đúng các cột đã hiển thị ở bước 2
|
|
123
|
+
spAPI_StudentGet -- chi tiết theo url1=StudentID
|
|
124
|
+
spAPI_StudentInsert -- thêm mới
|
|
125
|
+
spAPI_StudentUpdate -- cập nhật
|
|
126
|
+
spAPI_StudentDelete -- xoá theo url1=StudentID
|
|
127
|
+
spAPI_AUTH_ClassOptions -- dropdown danh sách lớp (không cần auth)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Output bước 3:** Danh sách đầy đủ SP, tên + params + response shape khớp field đã dùng trong UI.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Bước 4 — Viết SQL Stored Procedures
|
|
135
|
+
|
|
136
|
+
Viết SP theo đúng quy tắc tAPI và đúng contract đã chốt ở bước 3. Xem chi tiết tại [tapi-reference.md](tapi-reference.md).
|
|
137
|
+
|
|
138
|
+
**Template SP SELECT (danh sách):**
|
|
139
|
+
|
|
140
|
+
```sql
|
|
141
|
+
CREATE PROCEDURE spAPI_StudentList
|
|
142
|
+
@Keyword nvarchar(200) = NULL,
|
|
143
|
+
@ClassID varchar(20) = NULL,
|
|
144
|
+
@sys_UserID varchar(9)
|
|
145
|
+
AS BEGIN
|
|
146
|
+
SELECT s.StudentID, s.FullName, s.ClassID, c.ClassName, s.GPA
|
|
147
|
+
FROM tblStudent s
|
|
148
|
+
JOIN tblClass c ON s.ClassID = c.ClassID
|
|
149
|
+
WHERE (@Keyword IS NULL OR s.FullName LIKE '%' + @Keyword + '%')
|
|
150
|
+
AND (@ClassID IS NULL OR s.ClassID = @ClassID)
|
|
151
|
+
ORDER BY s.FullName
|
|
152
|
+
END
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Template SP INSERT:**
|
|
156
|
+
|
|
157
|
+
```sql
|
|
158
|
+
CREATE PROCEDURE spAPI_StudentInsert
|
|
159
|
+
@StudentID varchar(9),
|
|
160
|
+
@FullName nvarchar(100),
|
|
161
|
+
@ClassID varchar(20),
|
|
162
|
+
@sys_UserID varchar(9)
|
|
163
|
+
AS BEGIN
|
|
164
|
+
IF EXISTS (SELECT 1 FROM tblStudent WHERE StudentID = @StudentID)
|
|
165
|
+
BEGIN
|
|
166
|
+
RAISERROR(N'Mã sinh viên đã tồn tại', 16, 1)
|
|
167
|
+
RETURN
|
|
168
|
+
END
|
|
169
|
+
INSERT INTO tblStudent (StudentID, FullName, ClassID, CreateUser, CreateTime)
|
|
170
|
+
VALUES (@StudentID, @FullName, @ClassID, @sys_UserID, GETDATE())
|
|
171
|
+
SELECT mess = N'Thêm thành công', id = @StudentID
|
|
172
|
+
END
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Template SP DELETE:**
|
|
176
|
+
|
|
177
|
+
```sql
|
|
178
|
+
CREATE PROCEDURE spAPI_StudentDelete
|
|
179
|
+
@url1_StudentID varchar(9),
|
|
180
|
+
@sys_UserID varchar(9)
|
|
181
|
+
AS BEGIN
|
|
182
|
+
DELETE FROM tblStudent WHERE StudentID = @url1_StudentID
|
|
183
|
+
SELECT mess = N'Đã xoá', id = @url1_StudentID
|
|
184
|
+
END
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Template SP GET (chi tiết, dùng convert_to_object):**
|
|
188
|
+
|
|
189
|
+
```sql
|
|
190
|
+
CREATE PROCEDURE spAPI_StudentGet
|
|
191
|
+
@url1_StudentID varchar(9),
|
|
192
|
+
@sys_UserID varchar(9)
|
|
193
|
+
AS BEGIN
|
|
194
|
+
SELECT convert_to_object = '',
|
|
195
|
+
s.StudentID, s.FullName, s.ClassID, c.ClassName, s.GPA, s.Email
|
|
196
|
+
FROM tblStudent s
|
|
197
|
+
JOIN tblClass c ON s.ClassID = c.ClassID
|
|
198
|
+
WHERE s.StudentID = @url1_StudentID
|
|
199
|
+
END
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
> Dùng `convert_to_object = ''` khi SP trả về đúng một record → response dạng object thay vì `data[0][0]`.
|
|
203
|
+
|
|
204
|
+
Deploy bằng `db_sp_save` + `db_sp_deploy` (không tự gọi endpoint để "chạy thử" — xem bước 6).
|
|
205
|
+
|
|
206
|
+
**KHÔNG viết `GRANT EXECUTE` vào file .sql.** Thân `CREATE PROCEDURE` kéo dài **tới hết batch**, mà
|
|
207
|
+
`_sql_execute_NonQuery` gửi đúng một batch và **không hỗ trợ `GO`** (`GO` là chỉ thị của client, không
|
|
208
|
+
phải lệnh T-SQL). Nên câu `GRANT` đặt sau `END` bị **nuốt vào trong thân SP** — nó thành một lệnh chạy
|
|
209
|
+
mỗi lần gọi SP, chứ không cấp quyền lúc deploy. Không lỗi nào nổi lên; chỉ là endpoint tAPI trả lỗi
|
|
210
|
+
quyền trong khi SP nhìn vào thì hoàn toàn đúng.
|
|
211
|
+
|
|
212
|
+
`db_sp_deploy` **tự cấp quyền** cho procedure mới (không áp dụng cho FUNCTION — function không cần
|
|
213
|
+
EXECUTE cho `[public]`), rồi **hỏi lại `sys.database_permissions` xem quyền có thật hay không** và báo
|
|
214
|
+
trong kết quả:
|
|
215
|
+
|
|
216
|
+
- `✓ [public] đã có quyền EXECUTE trên "..."` — xong.
|
|
217
|
+
- `❗ [public] CHƯA có quyền EXECUTE ...` — kết quả in sẵn câu chạy được, copy nguyên văn:
|
|
218
|
+
`db_sql_execute_nonquery({ sqlcmd: "GRANT EXECUTE ON [dbo].[spAPI_StudentList] TO [public] AS [dbo]", db: "..." })`
|
|
219
|
+
- Bước cấp quyền hỏng nhưng deploy thành công thì **SP vẫn đã lên server** — chạy câu GRANT ở trên,
|
|
220
|
+
**đừng deploy lại**.
|
|
221
|
+
|
|
222
|
+
Deploy SP bằng đường khác (`db_sql_execute_nonquery` với `CREATE PROCEDURE`) thì **không có auto-grant
|
|
223
|
+
và không xoá cache tham số tAPI** — tool sẽ cảnh báo và nêu đúng hai lệnh cần chạy tiếp.
|
|
224
|
+
|
|
225
|
+
**Output bước 4:** Toàn bộ SQL script đã deploy cho các SP thiết kế ở bước 3.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Bước 5 — Thay dữ liệu cứng bằng binding API
|
|
230
|
+
|
|
231
|
+
Sửa lại `module.json` đã dựng ở bước 2: giữ nguyên layout, thay giá trị literal trong `data[]` bằng named action `IN`/`OUT`/`CALLBACK` trỏ vào các SP vừa deploy.
|
|
232
|
+
|
|
233
|
+
**5.1 Khai báo action thay cho data cứng**
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
"data": [
|
|
237
|
+
{ "keyword": "" },
|
|
238
|
+
{ "classId": "" },
|
|
239
|
+
{ "studentList": [] },
|
|
240
|
+
|
|
241
|
+
{
|
|
242
|
+
"fetchStudents": {
|
|
243
|
+
"API": "/{apiName}/StudentList",
|
|
244
|
+
"IN": { "Keyword": "vueData.keyword", "ClassID": "vueData.classId" },
|
|
245
|
+
"OUT": "studentList"
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"saveStudent": {
|
|
250
|
+
"API": "/{apiName}/StudentInsert",
|
|
251
|
+
"IN": {
|
|
252
|
+
"StudentID": "vueData.form.StudentID",
|
|
253
|
+
"FullName": "vueData.form.FullName",
|
|
254
|
+
"ClassID": "vueData.form.ClassID"
|
|
255
|
+
},
|
|
256
|
+
"CALLBACK": [
|
|
257
|
+
{ "CALL": "vueData.fetchStudents" },
|
|
258
|
+
{ "MESS": "Thêm thành công" },
|
|
259
|
+
{ "EXE": "vueData.dialogOpen = false" }
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"deleteStudent": {
|
|
265
|
+
"API": "/{apiName}/StudentDelete/`{{vueData.selectedID}}",
|
|
266
|
+
"CONFIRM": "Bạn có chắc muốn xoá sinh viên này?",
|
|
267
|
+
"CALLBACK": { "CALL": "vueData.fetchStudents" }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**5.2 Khai báo watch để tự load khi filter thay đổi**
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
"watch": {
|
|
277
|
+
"keyword": { "CALL": "vueData.fetchStudents" },
|
|
278
|
+
"classId": { "CALL": "vueData.fetchStudents" }
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**5.3 Thêm `{ "CALL": "vueData.fetchStudents" }` cuối `data[]`** để auto-load khi module mở.
|
|
283
|
+
|
|
284
|
+
**Output bước 5:** `module.json` đã wiring API đầy đủ, layout không đổi so với bước 2.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Bước 6 — Kiểm chứng tĩnh và hoàn thiện
|
|
289
|
+
|
|
290
|
+
Checklist trước khi publish — **không bước nào gọi API/SP/DML thật**:
|
|
291
|
+
|
|
292
|
+
**API/SP** — checklist đầy đủ (naming, params, security, `GRANT EXECUTE`, deploy workflow) ở
|
|
293
|
+
[verification.md](verification.md#deploy-workflow). Ở bước này:
|
|
294
|
+
- [ ] SP có `AUTH_` cho các endpoint không cần login
|
|
295
|
+
- [ ] `db_sp_verify` cho từng SP mới deploy — đọc thân SP thật, suy luận response shape/contract lỗi, đối chiếu params, sinh sẵn snippet `apiMocks` (không gọi endpoint thật)
|
|
296
|
+
- [ ] API **ghi**: đọc thân SP suy luận (WHERE, transaction, RAISERROR) — không có "chạy thử" nào khác ngoài đọc + suy luận
|
|
297
|
+
|
|
298
|
+
**FUI module.json:**
|
|
299
|
+
- [ ] `API` path trong mỗi action khớp đúng `/{apiName}/{FunctionName}`
|
|
300
|
+
- [ ] `IN` params khớp đúng tên param trong SP (phân biệt hoa thường)
|
|
301
|
+
- [ ] Route params (url1, url2) được truyền qua URL, không phải qua `IN`
|
|
302
|
+
- [ ] `OUT` nhận đúng kiểu dữ liệu (array hay object tuỳ SP) — đối chiếu với response shape suy luận từ `db_sp_verify`
|
|
303
|
+
- [ ] `watch` đã khai báo cho các filter tự động reload
|
|
304
|
+
- [ ] `data[]` khai báo named action fetch ban đầu, kết thúc bằng `{ "CALL": ... }` auto-startup
|
|
305
|
+
|
|
306
|
+
**Kiểm chứng cuối cùng theo thứ tự:** `module_validate` (lỗi cú pháp/quy tắc) → `module_outline` (review cấu trúc UI dạng text) → `module_simulate` với `apiMocks` lấy từ snippet của `db_sp_verify` (verify luồng action/watch/dialog) → nếu còn nghi ngờ hiển thị, `module_simulate({ renderUI: true })` lần cuối với cùng `apiMocks`.
|
|
307
|
+
|
|
308
|
+
**Output bước 6:** Module hoạt động end-to-end, UI ↔ API thông suốt, chưa từng có lời gọi thật nào trong suốt quá trình kiểm chứng.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Ví dụ đầu ra hoàn chỉnh
|
|
313
|
+
|
|
314
|
+
### SP cần tạo (bước 4 output)
|
|
315
|
+
|
|
316
|
+
Mỗi SP là **một file riêng** qua `db_sp_save` → `db_sp_deploy`. Không GRANT trong file, không `GO`
|
|
317
|
+
(xem bước 4); tham số tAPI **không được có default value** — `db_sp_deploy` chặn cứng.
|
|
318
|
+
|
|
319
|
+
```sql
|
|
320
|
+
-- 1. Danh sách (file spAPI_StudentList.sql)
|
|
321
|
+
CREATE PROCEDURE spAPI_StudentList @Keyword nvarchar(200), @ClassID varchar(20), @sys_UserID varchar(9)
|
|
322
|
+
AS BEGIN
|
|
323
|
+
SELECT s.StudentID, s.FullName, s.ClassID, s.GPA
|
|
324
|
+
FROM tblStudent s WHERE (@Keyword = '' OR s.FullName LIKE '%'+@Keyword+'%') AND (@ClassID = '' OR s.ClassID = @ClassID)
|
|
325
|
+
END
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
```sql
|
|
329
|
+
-- 2. Thêm mới (file spAPI_StudentInsert.sql)
|
|
330
|
+
CREATE PROCEDURE spAPI_StudentInsert @StudentID varchar(9), @FullName nvarchar(100), @ClassID varchar(20), @sys_UserID varchar(9)
|
|
331
|
+
AS BEGIN
|
|
332
|
+
INSERT INTO tblStudent(StudentID, FullName, ClassID, CreateUser, CreateTime) VALUES(@StudentID, @FullName, @ClassID, @sys_UserID, GETDATE())
|
|
333
|
+
SELECT mess = N'Thêm thành công'
|
|
334
|
+
END
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### module.json actions (bước 5 output)
|
|
338
|
+
|
|
339
|
+
```json
|
|
340
|
+
"fetchStudents": { "API": "/me/StudentList", "IN": { "Keyword": "vueData.keyword" }, "OUT": "studentList" },
|
|
341
|
+
"addStudent": { "API": "/me/StudentInsert", "IN": { "StudentID": "vueData.form.StudentID", "FullName": "vueData.form.FullName", "ClassID": "vueData.form.ClassID" }, "CALLBACK": [ { "CALL": "vueData.fetchStudents" }, { "MESS": "Đã thêm" } ] }
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Lưu ý quan trọng
|
|
347
|
+
|
|
348
|
+
- **`{apiName}`** trong API path là tên apiName của app trong tAPI (ví dụ `me`, `lms`, `hr`). Hỏi người dùng nếu chưa biết. **Một project có thể dùng nhiều apiName** (mỗi cái một database) — mỗi alias cần một lần `db_connect` riêng, và alias trong path chính là tham số `db` của các DB tool ([db-workflow.md](db-workflow.md) §4).
|
|
349
|
+
- **Route params** (`@url1_`) truyền qua URL, không qua `IN`: dùng backtick prefix trong API path: `"API": "/me/StudentDelete/\`{{vueData.selectedID}}"`
|
|
350
|
+
- **Response từ SP có nhiều SELECT** → `data[0]`, `data[1]`... — khai báo rõ trong `OUT` hoặc dùng `EXE` để tách.
|
|
351
|
+
- **`convert_to_object = ''`** → response là object phẳng, đọc trực tiếp `vueData.studentDetail.FullName` thay vì `vueData.studentDetail[0].FullName`.
|