draftgo-cli 2.0.9 → 3.0.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.
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ DraftGo Delete Script
4
+ 删除云端资源,并同步移除本地 index.json 条目和文件。
5
+
6
+ 用法:
7
+ python draftgo_delete.py pages <id>
8
+ python draftgo_delete.py nav <id>
9
+ python draftgo_delete.py db_meta <id>
10
+ python draftgo_delete.py custom_scripts <id>
11
+ python draftgo_delete.py docs <id>
12
+ python draftgo_delete.py doc_categories <id>
13
+ python draftgo_delete.py external_apis <id>
14
+ python draftgo_delete.py aihub <id>
15
+ """
16
+ import json, sys
17
+ from pathlib import Path
18
+ import urllib.request, urllib.error
19
+
20
+ SCRIPT_DIR = Path(__file__).resolve().parent
21
+
22
+
23
+ def find_project_root(start: Path) -> Path:
24
+ cur = start
25
+ for _ in range(10):
26
+ if (cur / ".draftgo").is_dir():
27
+ return cur
28
+ if cur.parent == cur:
29
+ break
30
+ cur = cur.parent
31
+ cwd = Path.cwd()
32
+ if (cwd / ".draftgo").is_dir():
33
+ return cwd
34
+ return start.parents[3] if len(start.parents) >= 4 else start
35
+
36
+
37
+ DEFAULT_ROOT = find_project_root(SCRIPT_DIR)
38
+
39
+
40
+ def load_config():
41
+ cfg_path = DEFAULT_ROOT / ".draftgo/config.json"
42
+ if not cfg_path.exists():
43
+ print("ERR: .draftgo/config.json not found, run /draftgo init first", file=sys.stderr)
44
+ sys.exit(1)
45
+ cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
46
+ return cfg["server"].rstrip("/"), cfg["token"]
47
+
48
+
49
+ def api_delete(server, token, path):
50
+ headers = {
51
+ "Authorization": f"Bearer {token}",
52
+ "Accept": "application/json",
53
+ }
54
+ req = urllib.request.Request(
55
+ f"{server}{path}",
56
+ method="DELETE",
57
+ headers=headers,
58
+ )
59
+ try:
60
+ with urllib.request.urlopen(req, timeout=15) as r:
61
+ return True, r.status, r.read().decode("utf-8", errors="replace")
62
+ except urllib.error.HTTPError as e:
63
+ return False, e.code, e.read().decode(errors="replace")
64
+ except Exception as e:
65
+ return False, None, str(e)
66
+
67
+
68
+ def _load_index(rel_index):
69
+ new_path = DEFAULT_ROOT / ".draftgo" / rel_index
70
+ legacy_path = DEFAULT_ROOT / rel_index
71
+ path = new_path if new_path.exists() else legacy_path
72
+ if not path.exists():
73
+ print(f"ERR: {rel_index} not found", file=sys.stderr)
74
+ sys.exit(1)
75
+ return path, json.loads(path.read_text(encoding="utf-8"))
76
+
77
+
78
+ def _remove_local_file(rel_path):
79
+ if not rel_path:
80
+ return
81
+ full = DEFAULT_ROOT / rel_path
82
+ if full.exists():
83
+ full.unlink()
84
+ print(f" deleted local file: {rel_path}")
85
+
86
+
87
+ # type → (api_prefix, index_rel, id_field, file_field)
88
+ TYPE_MAP = {
89
+ "pages": ("/api/pages", "pages/index.json", "id", "html_file"),
90
+ "nav": ("/api/navigations", "navigations/index.json", "id", "html_file"),
91
+ "db_meta": ("/api/db-meta", "db_meta/index.json", "id", None),
92
+ "custom_scripts": ("/api/scripts", "custom_scripts/index.json", "id", "code_file"),
93
+ "docs": ("/api/docs/articles", "docs/articles/index.json", "id", "content_file"),
94
+ "doc_categories": ("/api/docs/categories", "doc_categories/index.json", "id", None),
95
+ "external_apis": ("/api/external-apis", "external_apis/index.json", "id", None),
96
+ "aihub": ("/api/aihub", "aihub/index.json", "id", None),
97
+ }
98
+
99
+
100
+ def delete_resource(type_name, resource_id):
101
+ if type_name not in TYPE_MAP:
102
+ print(f"ERR: unknown type '{type_name}'. Supported: {', '.join(TYPE_MAP)}", file=sys.stderr)
103
+ sys.exit(1)
104
+
105
+ api_prefix, index_rel, id_field, file_field = TYPE_MAP[type_name]
106
+ server, token = load_config()
107
+
108
+ # 1. 找到 index 条目
109
+ index_path, items = _load_index(index_rel)
110
+ item = next((it for it in items if str(it.get(id_field)) == str(resource_id)), None)
111
+ label = (
112
+ item.get("title") or item.get("name") or item.get("type") or item.get("slug") or resource_id
113
+ if item else resource_id
114
+ )
115
+
116
+ # 2. 调用 DELETE API
117
+ ok, status, body = api_delete(server, token, f"{api_prefix}/{resource_id}")
118
+ if not ok and status != 404:
119
+ print(f" ERR [{label}] 删除失败: HTTP {status}: {body}")
120
+ sys.exit(1)
121
+
122
+ if status == 404:
123
+ print(f" WARN [{label}] 云端不存在(id={resource_id}),仅清理本地")
124
+ else:
125
+ print(f" OK [{label}] 已从云端删除(id={resource_id})")
126
+
127
+ # 3. 删除本地文件
128
+ if item and file_field:
129
+ _remove_local_file(item.get(file_field))
130
+
131
+ # 4. 从 index 移除条目
132
+ new_items = [it for it in items if str(it.get(id_field)) != str(resource_id)]
133
+ if len(new_items) < len(items):
134
+ index_path.write_text(json.dumps(new_items, ensure_ascii=False, indent=2), encoding="utf-8")
135
+ print(f" OK [{label}] 已从本地 index 移除")
136
+ else:
137
+ print(f" WARN [{label}] 本地 index 中未找到 id={resource_id}")
138
+
139
+
140
+ def main():
141
+ if len(sys.argv) < 3:
142
+ print(f"usage: draftgo_delete.py <{'|'.join(TYPE_MAP)}> <id>", file=sys.stderr)
143
+ sys.exit(1)
144
+
145
+ type_name = sys.argv[1]
146
+ resource_id = sys.argv[2]
147
+ delete_resource(type_name, resource_id)
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()
@@ -43,6 +43,45 @@ def find_project_root(start: Path) -> Path:
43
43
 
44
44
  DEFAULT_ROOT = find_project_root(SCRIPT_DIR)
45
45
 
46
+ # ── 冲突检测 ──
47
+ FORCE = False
48
+
49
+
50
+ def fetch_get(server, token, path):
51
+ """GET 请求,返回解析后的 JSON 或 None。"""
52
+ ok, _status, body = api_call("GET", server, token, path)
53
+ if not ok:
54
+ return None
55
+ try:
56
+ return json.loads(body)
57
+ except Exception:
58
+ return None
59
+
60
+
61
+ def check_conflict(server, token, path, local_updated_at, label):
62
+ """检查云端 updated_at 是否与本地基线一致。返回 True 表示可以继续推送。"""
63
+ if FORCE:
64
+ return True
65
+ if not local_updated_at:
66
+ return True
67
+ raw = fetch_get(server, token, path)
68
+ if raw is None:
69
+ print(f" WARN [{label}] 无法获取云端状态,跳过冲突检测")
70
+ return True
71
+ data = raw.get("data") if isinstance(raw, dict) else None
72
+ if not isinstance(data, dict):
73
+ return True
74
+ remote_updated = data.get("updated_at")
75
+ if not remote_updated:
76
+ return True
77
+ if str(remote_updated) != str(local_updated_at):
78
+ print(f" SKIP [{label}] 云端已被他人修改")
79
+ print(f" 本地基线: {local_updated_at}")
80
+ print(f" 云端时间: {remote_updated}")
81
+ print(f" 请先 pull 同步后再 push,或使用 --force 强制覆盖")
82
+ return False
83
+ return True
84
+
46
85
 
47
86
  def load_config():
48
87
  cfg_path = DEFAULT_ROOT / ".draftgo/config.json"
@@ -182,6 +221,8 @@ def sync_pages(server, token, ids=None):
182
221
  "value": {"html": html},
183
222
  }
184
223
  if pid:
224
+ if not check_conflict(server, token, f"/api/pages/{pid}", page.get("updated_at"), title):
225
+ continue
185
226
  ok, status, body = api_call("PUT", server, token, f"/api/pages/{pid}", payload)
186
227
  # PUT 404:本地 id 与云端不一致(删了重建 / 跨环境),按 route 自动创建
187
228
  if not ok and status == 404:
@@ -225,6 +266,8 @@ def sync_db_meta(server, token, ids=None):
225
266
  "extra": meta.get("extra"),
226
267
  }
227
268
  if mid:
269
+ if not check_conflict(server, token, f"/api/db-meta/{mid}", meta.get("updated_at"), label):
270
+ continue
228
271
  ok, status, body = api_call("PUT", server, token, f"/api/db-meta/{mid}", payload)
229
272
  # 本地 index 的 id 与云端不一致(删了重建 / 跨环境同步)时 PUT 404,按 type 创建
230
273
  if not ok and status == 404:
@@ -261,6 +304,8 @@ def sync_nav(server, token, ids=None):
261
304
  continue
262
305
  html = html_file.read_text(encoding="utf-8")
263
306
  if nid:
307
+ if not check_conflict(server, token, f"/api/navigations/{nid}", nav.get("updated_at"), name):
308
+ continue
264
309
  ok, status, body = api_call("PUT", server, token, f"/api/navigations/{nid}", {"html": html})
265
310
  if not ok and status == 404:
266
311
  print(f" WARN [{name}] nav_id={nid} 不存在,尝试创建")
@@ -308,6 +353,8 @@ def sync_aihub(server, token, ids=None):
308
353
  "tags", "describe", "permission", "status",
309
354
  ) if it.get(k) is not None}
310
355
  if iid:
356
+ if not check_conflict(server, token, f"/api/aihub/{iid}", it.get("updated_at"), name):
357
+ continue
311
358
  ok, status, body = api_call("PUT", server, token, f"/api/aihub/{iid}", payload)
312
359
  if not ok and status == 404:
313
360
  print(f" WARN [{name}] aihub_id={iid} 不存在,尝试创建")
@@ -347,6 +394,8 @@ def sync_external_apis(server, token, ids=None):
347
394
  "param_schema", "tags", "description", "status",
348
395
  ) if it.get(k) is not None}
349
396
  if iid:
397
+ if not check_conflict(server, token, f"/api/external-apis/{iid}", it.get("updated_at"), code):
398
+ continue
350
399
  ok, status, body = api_call("PUT", server, token, f"/api/external-apis/{iid}", payload)
351
400
  if not ok and status == 404:
352
401
  print(f" WARN [{code}] api_id={iid} 不存在,尝试创建")
@@ -390,6 +439,8 @@ def sync_system_config(server, token, keys=None):
390
439
  "status": it.get("status"),
391
440
  }
392
441
  payload = {k: v for k, v in payload.items() if v is not None}
442
+ if not check_conflict(server, token, f"/api/system/{ck}", it.get("updated_at"), ck):
443
+ continue
393
444
  ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", payload)
394
445
  if not ok and status == 404:
395
446
  print(f" WARN [{ck}] system_config 不存在,尝试创建")
@@ -411,6 +462,8 @@ def sync_roles(server, token, ids=None):
411
462
  payload = {k: it.get(k) for k in (
412
463
  "name", "description", "status", "sort_order", "user_visible",
413
464
  ) if it.get(k) is not None}
465
+ if not check_conflict(server, token, f"/api/roles/{rid}", it.get("updated_at"), code):
466
+ continue
414
467
  ok, status, body = api_call("PUT", server, token, f"/api/roles/{rid}", payload)
415
468
  print(f" {'OK' if ok else 'ERR'} [{code}] role_id={rid}{'' if ok else ' -> ' + _info(ok, status, body)}")
416
469
 
@@ -427,6 +480,8 @@ def sync_users(server, token, ids=None):
427
480
  "username", "email", "phone_number", "nickname",
428
481
  "avatar", "status", "notes",
429
482
  ) if it.get(k) is not None}
483
+ if not check_conflict(server, token, f"/api/users/{uid}", it.get("updated_at"), uname):
484
+ continue
430
485
  ok, status, body = api_call("PUT", server, token, f"/api/users/{uid}", payload)
431
486
  print(f" {'OK' if ok else 'ERR'} [{uname}] user_id={uid}{'' if ok else ' -> ' + _info(ok, status, body)}")
432
487
 
@@ -455,6 +510,8 @@ def sync_docs(server, token, ids=None):
455
510
  ) if it.get(k) is not None}
456
511
  payload["content"] = content
457
512
  if aid:
513
+ if not check_conflict(server, token, f"/api/docs/articles/{aid}", it.get("updated_at"), title):
514
+ continue
458
515
  ok, status, body = api_call("PUT", server, token, f"/api/docs/articles/{aid}", payload)
459
516
  if not ok and status == 404:
460
517
  print(f" WARN [{title}] article_id={aid} 不存在,尝试创建")
@@ -492,6 +549,8 @@ def sync_doc_categories(server, token, ids=None):
492
549
  "name", "slug", "description", "icon", "parent_id", "sort_order", "status",
493
550
  ) if it.get(k) is not None}
494
551
  if cid:
552
+ if not check_conflict(server, token, f"/api/docs/categories/{cid}", it.get("updated_at"), name):
553
+ continue
495
554
  ok, status, body = api_call("PUT", server, token, f"/api/docs/categories/{cid}", payload)
496
555
  if not ok and status == 404:
497
556
  print(f" WARN [{name}] category_id={cid} 不存在,尝试创建")
@@ -541,6 +600,8 @@ def sync_custom_scripts(server, token, ids=None):
541
600
  code = code_path.read_text(encoding="utf-8")
542
601
  normalized_triggers = _normalize_script_triggers(it.get("mode"), it.get("triggers"))
543
602
  if sid:
603
+ if not check_conflict(server, token, f"/api/scripts/{sid}", it.get("updated_at"), name):
604
+ continue
544
605
  # ScriptUpdate 接受字段(不含 slug/mode,避免误改启停/路由)
545
606
  payload = {k: it.get(k) for k in (
546
607
  "name", "description", "config", "permission",
@@ -666,22 +727,35 @@ def run_batch(server, token, args):
666
727
 
667
728
 
668
729
  def main():
730
+ global FORCE
731
+
669
732
  if len(sys.argv) < 2:
670
733
  print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]\n"
671
- f" draftgo_push.py --batch <mode> <ids> [<mode> <ids> ...]")
734
+ f" draftgo_push.py --batch <mode> <ids> [<mode> <ids> ...]\n"
735
+ f" --force 跳过冲突检测,强制覆盖")
736
+ sys.exit(1)
737
+
738
+ # 解析 --force(可出现在任意位置)
739
+ args = [a for a in sys.argv[1:] if a != "--force"]
740
+ if len(args) < len(sys.argv) - 1:
741
+ FORCE = True
742
+ print("[force] 已跳过冲突检测,将强制覆盖云端数据")
743
+
744
+ if not args:
745
+ print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]")
672
746
  sys.exit(1)
673
747
 
674
- if sys.argv[1] == "--batch":
748
+ if args[0] == "--batch":
675
749
  server, token, cfg = load_config()
676
- run_batch(server, token, sys.argv[2:])
750
+ run_batch(server, token, args[1:])
677
751
  _lessons_reminder(cfg)
678
752
  return
679
753
 
680
- if sys.argv[1] not in HANDLERS:
754
+ if args[0] not in HANDLERS:
681
755
  print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]")
682
756
  sys.exit(1)
683
- mode = sys.argv[1]
684
- ids = sys.argv[2:] or None
757
+ mode = args[0]
758
+ ids = args[1:] or None
685
759
  server, token, cfg = load_config()
686
760
  HANDLERS[mode](server, token, ids)
687
761
  _lessons_reminder(cfg)
@@ -0,0 +1,108 @@
1
+ ---
2
+ read_when: 操作动态 DB 之前 · 设计数据结构时 · 使用 filters 检索时
3
+ ---
4
+
5
+ # 动态 DB & 数据层
6
+
7
+ ## DB Meta 结构
8
+
9
+ ```json
10
+ {
11
+ "type": "order",
12
+ "label": "订单",
13
+ "schema": {
14
+ "type": "object",
15
+ "properties": {
16
+ "name": { "type": "string", "title": "姓名", "required": true, "searchable": "fuzzy" },
17
+ "status": { "type": "string", "title": "状态", "required": false, "searchable": "exact" },
18
+ "amount": { "type": "number", "title": "金额", "required": false, "searchable": "range" },
19
+ "tags": { "type": "array", "title": "标签", "required": false, "searchable": "contains" },
20
+ "note": { "type": "string", "title": "备注", "required": false, "searchable": false }
21
+ }
22
+ },
23
+ "permission": {
24
+ "public": { "read": "none", "create": "none", "update": "none", "delete": "none" },
25
+ "login": { "read": "all", "create": "all", "update": "owner", "delete": "owner" },
26
+ "admin": { "read": "all", "create": "all", "update": "all", "delete": "all" }
27
+ }
28
+ }
29
+ ```
30
+
31
+ **searchable 模式**:`false`(不可检索)/ `"exact"`(精确)/ `"fuzzy"`(模糊)/ `"range"`(数值范围)/ `"contains"`(数组包含)
32
+
33
+ ---
34
+
35
+ ## CRUD 操作范式
36
+
37
+ ```javascript
38
+ // 查询(分页 + 结构化检索)
39
+ const res = await App.get(`db/order`, {
40
+ page: 1, page_size: 20,
41
+ filters: ['name:like:张', 'status:eq:paid', 'amount:gte:100'],
42
+ order_by: 'amount', order: 'desc',
43
+ });
44
+ const { items, total } = res.data;
45
+
46
+ // 创建(业务字段必须放在 data 包裹里)
47
+ await App.post(`db/order`, { data: { name: '张三', amount: 200 }, status: 1 });
48
+
49
+ // 更新
50
+ await App.put(`db/order/${id}`, { data: { amount: 250 } });
51
+
52
+ // 删除
53
+ await App.delete(`db/order/${id}`);
54
+
55
+ // 批量更新(原子事务,任一失败全批回滚)
56
+ await App.patch(`db/order/batch`, [
57
+ { id: 1, data: { status: 'paid' } },
58
+ { id: 2, data: { status: 'paid' } },
59
+ ]);
60
+ ```
61
+
62
+ ---
63
+
64
+ ## filters 操作符
65
+
66
+ | 操作符 | 含义 | 适用 searchable 模式 |
67
+ |---|---|---|
68
+ | `eq` | 精确等于 | exact / fuzzy / range |
69
+ | `like` | 模糊包含 | fuzzy |
70
+ | `gte` / `lte` / `gt` / `lt` | 数值范围 | range |
71
+ | `in` | 枚举命中(值逗号分隔:`status:in:paid,pending`) | exact / fuzzy |
72
+ | `contains` | 数组字段包含某值 | contains |
73
+
74
+ - 省略操作符(`filters: ['name:张三']`)默认 `like`
75
+ - 多个 filters 为 AND
76
+ - 字段未标 searchable 或操作符不匹配 → 后端返回 400
77
+
78
+ ---
79
+
80
+ ## db_meta 本地文件
81
+
82
+ 开发前先读 `.draftgo/db_meta/index.json` 了解可用 type 和字段:
83
+
84
+ ```json
85
+ [
86
+ {
87
+ "id": 1,
88
+ "type": "order",
89
+ "label": "订单",
90
+ "schema": { ... }
91
+ }
92
+ ]
93
+ ```
94
+
95
+ ⚠️ **GET `/api/db-meta/{type}` 用 type(如 `order`),不是 id。PUT/DELETE 才用 id。**
96
+
97
+ ---
98
+
99
+ ## 通用筛选参数(非动态 DB)
100
+
101
+ 用于 users / roles / pages / navigations / feedback 等标准资源:
102
+
103
+ | 参数 | 说明 |
104
+ |---|---|
105
+ | `page` / `page_size` | 分页(不传返回全量,超 10000 条后端拒绝) |
106
+ | `search` | 全文搜索(动态 DB 不用此参数) |
107
+ | `status` | 状态过滤 |
108
+ | `type` / `tag` | 类型/标签过滤 |
@@ -0,0 +1,98 @@
1
+ ---
2
+ read_when: 需要理解运行时机制时 · 处理 token/路由/事件相关问题时
3
+ ---
4
+
5
+ # 运行时机制(Runtime)
6
+
7
+ ## 壳层启动流程
8
+
9
+ 1. `reloadSystemConfig()` — 拉取 `/api/system/config`,填充 `state.config`
10
+ 2. `validateToken()` — 验证 `localStorage.dg_access_token`,成功后设置 `currentUser`
11
+ 3. `loadSetupStatus()` — 检查系统是否已初始化
12
+ 4. `loadPage()` — 根据当前路由拉取页面 HTML,注入 iframe
13
+
14
+ ---
15
+
16
+ ## iframe 注入机制
17
+
18
+ 壳层通过 `decorateFrameHtml(html, routeContext, theme)` 处理数据库页面 HTML,注入:
19
+
20
+ - `window.__DG_ROUTE_CONTEXT__` — 当前路由上下文(含 `query` 参数),冻结对象
21
+ - `window.__DG_QUERY__` — `routeContext.query` 快捷方式
22
+ - `window.__DG_GET_ROUTE_CONTEXT__()` — 函数形式兜底读取
23
+ - 主题 CSS 变量(写入 `<head>` 最顶部,DOM 解析时即生效)
24
+ - 静态资源(Tailwind runtime、FontAwesome、GSAP 等)
25
+
26
+ 页面以 `iframe.srcdoc` 渲染,**不是独立 URL**,因此:
27
+ - `window.location` 指向壳层地址,**不可用于读取路由参数**
28
+ - `window.parent.App` 是壳层暴露的能力对象
29
+ - `window.location.href = '/path'` 只跳 iframe 自身!导航必须用 `window.parent.location.href`
30
+
31
+ ---
32
+
33
+ ## App 对象来源
34
+
35
+ 壳层将 `app` 对象赋值给 `window.App`,页面通过 `window.parent.App` 访问。
36
+
37
+ `app` 组成:`api.js`(请求)+ `feedback.js`(弹窗/Toast)+ `runtime.js`(路由/主题)+ `i18n.js`(国际化)+ state(currentUser/isAdmin/config/theme)
38
+
39
+ 完整 API 见 `{{SKILL_DIR}}/quickref/app-api.md`
40
+
41
+ ---
42
+
43
+ ## Token 存储
44
+
45
+ | key | 说明 |
46
+ |---|---|
47
+ | `localStorage.dg_access_token` | 访问 token |
48
+ | `localStorage.dg_refresh_token` | 刷新 token |
49
+
50
+ 登录后调用 `App.setAuthTokens({ access_token, refresh_token })` 写入并触发验证。
51
+ 401 时壳层自动用 refresh token 刷新,无需页面处理。
52
+
53
+ ---
54
+
55
+ ## URL 参数读取(标准三阶回落)
56
+
57
+ ```javascript
58
+ const routeContext =
59
+ window.__DG_ROUTE_CONTEXT__
60
+ || window.__DG_GET_ROUTE_CONTEXT__?.()
61
+ || window.parent?.App?.getCurrentRouteContext?.()
62
+ || { query: {} };
63
+ const query = routeContext.query || {};
64
+
65
+ // 路由 /orders?orderId=42
66
+ const orderId = query.orderId; // "42"
67
+ ```
68
+
69
+ **禁止**:`new URLSearchParams(window.location.search)` / 直接读 `window.location.search`
70
+
71
+ ---
72
+
73
+ ## 全局事件
74
+
75
+ | 事件名 | 触发时机 |
76
+ |---|---|
77
+ | `dg:auth-ready` | token 验证完成(成功或失败) |
78
+ | `dg:auth-changed` | token 变更(登录/登出) |
79
+
80
+ 监听方式:`window.parent.addEventListener('dg:auth-ready', handler)`
81
+
82
+ ---
83
+
84
+ ## 前端全局层
85
+
86
+ 全局浮窗、客服入口、统计脚本等属于全局层,不属于单个业务页面。
87
+
88
+ 存储:`sys_config.category = frontend_global`
89
+
90
+ | config_key | 用途 |
91
+ |---|---|
92
+ | `frontend_global_head_html` | 注入壳层 head |
93
+ | `frontend_global_body_html` | 注入壳层 body 末尾 |
94
+ | `frontend_global_css` / `_js` | 壳层全局 CSS/JS |
95
+ | `frontend_global_iframe_head_html` | 注入每个业务页面 iframe head |
96
+ | `frontend_global_widget_html/css/js` | 全局挂件层 |
97
+
98
+ 规则:**不能在业务页面内重复实现全局层能力;不允许新增槽位,只能编辑内置槽位。**
@@ -0,0 +1,74 @@
1
+ ---
2
+ read_when: 开发前检查合规性时 · Code Review 时
3
+ ---
4
+
5
+ # 开发禁区(违反必报错)
6
+
7
+ ## 网络资源
8
+
9
+ | ❌ 禁止 | ✅ 替代 |
10
+ |---|---|
11
+ | 任何境外 CDN(googleapis / jsdelivr / cdnjs / unpkg) | 国内镜像(npmmirror.com / staticfile.net)或本地资源 |
12
+ | 外部 CDN 引入图标 | `/assets/icons/{name}.svg` 内置图标库 |
13
+
14
+ ---
15
+
16
+ ## App 对象使用
17
+
18
+ | ❌ 禁止 | ✅ 替代 |
19
+ |---|---|
20
+ | `App()` 写法(App 是对象不是函数) | `const App = window.parent?.App` |
21
+ | `const App = () => window.parent?.App` | 去掉箭头函数 |
22
+ | `App?.user?.role` 判断权限 | `App.isAdmin` 或 `App.currentUser?.role_code` |
23
+
24
+ ---
25
+
26
+ ## 路由 & 导航
27
+
28
+ | ❌ 禁止 | ✅ 替代 |
29
+ |---|---|
30
+ | `window.location.search` 读参数 | `window.__DG_ROUTE_CONTEXT__.query` |
31
+ | `navigate('/login')` 退出 | `window.parent.location.href = '/login'` |
32
+ | 页面内 `window.location.href = ...` 跳转 | `window.parent.location.href = ...`(页面在 iframe 中) |
33
+
34
+ ---
35
+
36
+ ## 弹窗 & 交互
37
+
38
+ | ❌ 禁止 | ✅ 替代 |
39
+ |---|---|
40
+ | `window.alert(msg)` | `App.showModal(msg, title?)` |
41
+ | `window.confirm(msg)` | `await App.confirm(msg, title?)` |
42
+ | `window.prompt(msg)` | 自建 modal + input |
43
+
44
+ ---
45
+
46
+ ## UI 组件
47
+
48
+ | ❌ 禁止 | ✅ 替代 |
49
+ |---|---|
50
+ | 把 React/TSX 版 shadcn 组件写进数据库 HTML | 用对应 `dg-*` 标签 |
51
+ | 把 `dg-*` 当 daisyUI / Bootstrap / Ant Design | `dg-*` 只能是 shadcn 的 HTML 协议形态 |
52
+ | 硬编码 hex / rgb / rgba 颜色 | 用 `var(--dg-*)` 语义 token |
53
+ | 在页面内读 `localStorage.dg_theme` | `App.theme` |
54
+
55
+ ---
56
+
57
+ ## 全局层
58
+
59
+ | ❌ 禁止 | ✅ 替代 |
60
+ |---|---|
61
+ | 在业务页面内实现全局浮窗 / 客服 / 统计脚本 | 使用 `frontend_global_*` 固定槽位 |
62
+ | 在页面内渲染系统 Header / Logo / 用户头像下拉 | 这些属于壳层,不属于业务页面 |
63
+ | 新增全局层槽位 | 只能编辑系统内置固定槽位 |
64
+
65
+ ---
66
+
67
+ ## 颜色 Token 参考
68
+
69
+ | Token | 用途 |
70
+ |---|---|
71
+ | `--dg-bg-base` / `--dg-bg-page` / `--dg-bg-surface` | 背景层级 |
72
+ | `--dg-text-primary` / `--dg-text-secondary` / `--dg-text-muted` | 文字层级 |
73
+ | `--dg-accent` / `--dg-accent-hover` / `--dg-accent-subtle` | 主题色 |
74
+ | `--dg-border` / `--dg-success` / `--dg-error` / `--dg-warning` | 功能色 |
@@ -0,0 +1,68 @@
1
+ ---
2
+ read_when: 开发数据库页面时需要 dg-* 组件 · 了解 shadcn 映射关系时
3
+ ---
4
+
5
+ # dg-* 完整映射表
6
+
7
+ ## 核心认知
8
+
9
+ ```
10
+ dg-* ≠ 自研组件库
11
+ dg-* = shadcn/ui 在数据库 HTML 页面里的协议化表达
12
+ ```
13
+
14
+ 数据库页面不进入 Vite/React 编译链,不能写 TSX;改用 `dg-*` 标签,runtime 解析并渲染对应 shadcn 语义。
15
+
16
+ ---
17
+
18
+ ## shadcn → dg-* 完整映射
19
+
20
+ | shadcn 组件 | dg-* 标签 | 说明 |
21
+ |---|---|---|
22
+ | Button | `dg-button` | variant/size/disabled 属性对齐 |
23
+ | Card | `dg-card` + `dg-card-header/title/description/content/footer` | |
24
+ | Badge | `dg-badge` | variant: default/secondary/destructive/outline |
25
+ | Skeleton | `dg-skeleton` | 骨架屏 |
26
+ | Input | `dg-input` | type/placeholder/value/disabled |
27
+ | Textarea | `dg-textarea` | |
28
+ | Select | `dg-select` + `dg-select-item` | |
29
+ | Checkbox | `dg-checkbox` | |
30
+ | Switch | `dg-switch` | |
31
+ | RadioGroup | `dg-radio-group` + `dg-radio-item` | |
32
+ | Form | `dg-form` + `dg-form-item` + `dg-form-message` | |
33
+ | Table | `dg-table` | source 属性拉取数据,columns via data-dg-props |
34
+ | Tabs | `dg-tabs` + `dg-tabs-list/trigger/content` | default-value 属性 |
35
+ | Dialog | `dg-dialog` + `dg-dialog-trigger/content/header/title/footer` | |
36
+ | Sheet | `dg-sheet` + `dg-sheet-trigger/content/header/title` | side: right/left/top/bottom |
37
+ | DropdownMenu | `dg-dropdown-menu` + `dg-dropdown-menu-trigger/content/item/separator` | |
38
+ | Tooltip | `dg-tooltip` | content 属性 |
39
+ | Popover | `dg-popover` + `dg-popover-trigger/content` | |
40
+ | Alert | `dg-alert` + `dg-alert-title/description` | variant: default/destructive |
41
+ | Progress | `dg-progress` | value/max 属性 |
42
+ | Separator | `dg-separator` | orientation: horizontal/vertical |
43
+ | ScrollArea | `dg-scroll-area` | |
44
+ | Avatar | `dg-avatar` + `dg-avatar-image/fallback` | |
45
+
46
+ ---
47
+
48
+ ## 属性约定
49
+
50
+ `dg-*` 属性命名跟随 shadcn 语义:
51
+ - `variant`、`size`、`disabled`、`default-value`、`data-state`、`aria-*` 保持原意
52
+ - 复杂组件使用 `data-dg-props='{"key":"value"}'` 传递结构化配置
53
+
54
+ ---
55
+
56
+ ## 禁止混淆
57
+
58
+ - ❌ `dg-*` 不是 daisyUI
59
+ - ❌ `dg-*` 不是 Bootstrap
60
+ - ❌ `dg-*` 不是 Ant Design / Element Plus
61
+ - ❌ `dg-*` 不是任意相似样式的泛称
62
+ - ✅ `dg-*` 只能是 shadcn 的 DraftGo HTML 协议形态
63
+
64
+ ---
65
+
66
+ ## 使用示例速查
67
+
68
+ 完整带代码的示例见 `{{SKILL_DIR}}/quickref/dg-components.md`