# 文章草稿功能（status：草稿/已發布）

> 狀態：實作完成，待 jyg 跑 migration + 實測
> 與「編輯器上傳臨時檔回收」(`article-draft-binding.md`) 正交、不衝突。
> SQL：`article-status-migrate.sql`

---

## 1. 需求
後台能把文章存成**草稿**（未完成、之後續編），跟**正式（已發布）**分開；列表看得到草稿狀態並可篩選。

## 2. 資料模型
`*_bind` 加 `status`（int + 常數 `App\Models\Articles\Status`）：
- `PUBLISHED=1` 已發布（正式）
- `DRAFT=0` 草稿

**與 `enable`(Y/N 上下架) 完全獨立**：status 只是後台「草稿/正式」標籤；**顯不顯示完全由 enable 決定**（前台只撈 `enable=Y`），兩者互不牽動。
**backfill**：`ALTER ... DEFAULT 1` → 既有資料自動全部「已發布」（保留各自 enable），免 UPDATE。

## 3. 涵蓋模組（全部共用 Articles\Article）
articles / products / services / bulletins / videos / tags —— 這些模組的 controller 都 `extends Articles\ArticleController`、save 都走 `Articles\Article`（靠 Base init 換表名），所以 model 改一處全通。6 個 `_bind` 表都加 status。
（downloads/menus 用別的模板、不顯示草稿按鈕 → 不加。）

## 4. 實作
| 項目 | 位置 |
|---|---|
| 狀態常數 | `App\Models\Articles\Status`（PUBLISHED=1 / DRAFT=0） |
| 存 status | `Articles\Article::_check_params` 把 `basic[status]` 寫入（共用，全模組通）。**不牽動 enable**。 |
| 編輯頁按鈕 | `articles/article-edit.html`：`hidden basic[status]` + 「存草稿」(0,formnovalidate 略過必填) /「發布」(1)；JS 點擊寫入 hidden、存草稿跳過自訂驗證 |
| 列表標籤+篩選 | `articles/article-list.html`：row 加 `data-status`、草稿顯示 `.art-badge`、上方「**正式 / 草稿**」前端篩選（預設正式；依 data-status 顯示隱藏） |
| 列上轉狀態 | 每列依 status 顯示「轉草稿(0)/轉正式(1)」`.act-status` → `api.{article}.status`（後端 `Article::setStatus`/`IndexsAPI::setStatus`/API `status()`，全模組共用）。只改 status、不動 enable |
| 各狀態獨立排序 | 拖曳 `onUpdate` 只送「目前篩選可見」的列 → `api.article.sorting`(`/api/articles/sorting`,cat_id走body→`IndexController@sorting`) 只重寫該狀態 sortID。轉狀態後把列移到 tbody 最上+存目標狀態順序＝**排到最前**（重用拖曳機制，不依賴 sortID 方向） |

## 5. 模板結構（重要）
本次把編輯頁母模板統一成 **`articles/article-edit.html`**（dash，新命名）：
- `article-edit.html` = 唯一內容本體（含 project=article 預設、表單、草稿按鈕）。
- `article.create.html` / `article.edit.html`（dot）→ 各 1 行 `{% extends "articles/article-edit.html" %}` 轉發殼。**因 controller 用 `{模組}.create.html`/`.edit.html` 慣例 render，這兩個檔名必須留**（跟其他每個模組都有 create+edit 殼一樣）。
- 其他模組 `{module}.create.html`/`.edit.html`（products/services/bulletins/videos/tags）的 extends 全改指 `article-edit.html`。
- 草稿按鈕在共用母模板，**全模組顯示**（因 6 個 _bind 都有 status 欄位）。

## 6. 選單入口（新版列表/分類）
舊選單路由導向新版（articles-only，純 route redirect，不動 DB 選單）：
- `article.cat.list`（文章分類）→ redirect `article.categories.list`（`/articles/categories/view/0` → category-list.html）
- `article.list` / `article.list.operator`（文章列表）→ redirect `article.posts.list`（`/articles/posts/{root_id}` → article-list.html）
- 貼文排序路由原本在 `if($dev)` 內（且指向不存在的 `ArticleController@sorting`）→ 已移除 dev 區塊、改永久 `api.article.sorting`→`IndexController@sorting`。

## 7. 待測（jyg）
1. 跑 `article-status-migrate.sql`（6 個 ALTER + 索引）。
2. 文章按「存草稿」→ status=0（必填可不填）；「發布」→ status=1。enable 不被牽動。
3. 列表：草稿顯示「草稿」標籤；「正式/草稿」篩選正常（預設正式）。
4. **列上轉狀態**：正式列點「轉草稿」→ 從正式消失、切草稿在最上；反之亦然。
5. **各狀態獨立排序**：正式拖排序不影響草稿順序；反之。⚠ 若拖曳方向相反＝底層 `IndexController@sorting` 的 reverse 邏輯問題。
6. products/bulletins… 編輯頁也有存草稿/發布、存檔不報錯。
7. **確認列表查詢有回傳 `status`**（article-list 的 `articles` 資料要含 status，否則標籤/篩選/轉狀態失效）— 若無，於該 bind 查詢 select 補上。

## 8. 新增流程修正（2026-06-22，純前端，全 6 模組共用 `article-edit.html`）
- **成功後返回列表**：表單 `data-redirect` 設為該系列文章列表（`{module}.list` 帶 root_id）。neko.submit 成功彈窗按 OK → 導回列表，**不再重載新增頁**（空白表單會被誤判沒存、導致重複建立）。編輯時 `redirect` 欄位 `back()` 不為空會優先回來源列表。
- **至少一個語系要有標題**（存草稿/發布皆擋）：列表是「**依當前語系 inner join 內容表 `articles`**」，完全沒標題的文章不會產生任何內容列 → 任何語系列表都看不到、卻回報成功（幽靈紀錄）。提交時檢查 `[name^="i18n["][name$="[title]"]` 至少一個非空才放行，否則 `_p.alert('請至少填寫一個語系的標題')` 擋下。**不綁當前語系**（業主常只維護單一語系，只填繁中也放行）；模組若無標題欄則略過不誤擋。
- **跨語系列表**：維持「依語系」現狀——只填繁中就在繁中列表管理，**不做** fallback（單語系文章不在其他語系列表出現）。
- 後端 interim（`Article::create()` i18n 空也補 `Match.update` 綁分類，commit a31354bc）仍在但因 opcache 未生效、且已改用前端擋空白，**不依賴它**；無害留作安全網。
- 相關大重構（snowflake/bigint/去 compID/match 葉子化/append 集中）→ **延到大改版**，設計見 `station-migration-runbook.md` §10。
