feat(knowledge): 分类可改名/配色/删除,选中态与加载态更清楚
CI / backend (push) Canceled after 0s

分类之前只能新增,建错了只能干看着。补上三件事:

- knowledge_category 加 color 列(迁移 0009,CHECK 卡 #RRGGBB),
  PATCH 支持改名与改色,空串表示取消上色;知识列表带出分类色,前端不用再查。
- 分类行悬停出「⋯」:改名、选色板/手填色值、建子分类、删除(有知识时确认后 force)。
- 选中分类改成主色浅底 + 描边 + 色点,列表头显示当前筛选;切分类时列表淡出并转圈,
  不再一动不动等请求回来。

顺带给卡片画分类色上边线:扫一眼就能分出哪几条属于同一类。
This commit is contained in:
2026-11-02 19:24:44 +08:00
parent d7c9670880
commit 71982f9fa4
6 changed files with 632 additions and 32 deletions
@@ -329,3 +329,86 @@ async def test_categories_come_back_as_a_tree(client: AsyncClient) -> None:
"技术开发 / 技术 / 部署",
"餐饮业务",
]
@pytest.mark.asyncio
async def test_category_color_roundtrip(client: AsyncClient) -> None:
"""分类色:建时能给、能改、能取消,非法值当场拒绝。
颜色是直接拼进 CSS 的(卡片上边线),所以格式必须在入口挡住:
一个 `red` 或 `#4f46e5cc` 进了库,前端只会静默退化成灰边框,没人知道为什么。
"""
user, ws_id = await _ws(client)
base = f"{V1}/workspaces/{ws_id}/knowledge/categories"
created = unwrap(
await client.post(json={"name": "部署", "color": "#4f46e5"}, url=base, headers=user["headers"])
)
# 统一成大写,前端不用再 casefold
assert created["color"] == "#4F46E5"
listed = unwrap(await client.get(base, headers=user["headers"]))["items"]
assert [c["color"] for c in listed] == ["#4F46E5"]
recolored = unwrap(
await client.patch(
f"{base}/{created['id']}",
json={"color": "#0EA5E9", "version": created["version"]},
headers=user["headers"],
)
)
assert recolored["color"] == "#0EA5E9"
assert recolored["name"] == "部署" # 只改色不动名字
cleared = unwrap(
await client.patch(
f"{base}/{created['id']}",
json={"color": "", "version": recolored["version"]},
headers=user["headers"],
)
)
assert cleared["color"] is None
# 不传 color 的普通改名不该顺手把颜色清掉
colored = unwrap(
await client.patch(
f"{base}/{created['id']}",
json={"color": "#10B981", "version": cleared["version"]},
headers=user["headers"],
)
)
renamed = unwrap(
await client.patch(
f"{base}/{created['id']}",
json={"name": "上线", "version": colored["version"]},
headers=user["headers"],
)
)
assert renamed["name"] == "上线"
assert renamed["color"] == "#10B981"
bad = await client.post(json={"name": "业务", "color": "red"}, url=base, headers=user["headers"])
assert bad.status_code == 400, bad.text
assert _code(bad) == 1002
too_long = await client.post(
json={"name": "业务2", "color": "#4f46e5cc"}, url=base, headers=user["headers"]
)
assert too_long.status_code == 400, too_long.text
@pytest.mark.asyncio
async def test_knowledge_card_carries_category_color(client: AsyncClient) -> None:
"""知识列表里带上分类(含颜色),前端画卡片上边线不用再查一次。"""
user, ws_id = await _ws(client)
cat = unwrap(
await client.post(
f"{V1}/workspaces/{ws_id}/knowledge/categories",
json={"name": "复盘", "color": "#EF4444"},
headers=user["headers"],
)
)
await _note(client, user, ws_id, "一次事故复盘", category_id=cat["id"])
page = unwrap(await client.get(f"{V1}/workspaces/{ws_id}/knowledge", headers=user["headers"]))
assert page["items"][0]["category"]["color"] == "#EF4444"