合并 feat/org-directory:成员目录(Keycloak 读写面穿透)+ 治理台成员/组织/权限三页 + 广播桥回弹修复 + 稳定版脚本 + W05 目标件
CI / ci (push) Failing after 11s

分支四条(2026-08-26 做的,一直留在工作树没入库,今天补提交):
① feat(org) 成员目录 —— KeycloakDirectory 端口与适配器、org-directory 契约/路由、sync-roster CLI、
   personName 姓在前、Staff.kind 按账号名现判;前端员工屏重做成成员 / 组织 / 权限三页签;
② fix(app) 广播桥「转发进来的不再 NOTIFY 回去」—— 两台后端互桥回弹 19 万条广播那次的修复,代码此前没入库;
③ chore stable.sh + .stable/ 入 ignore + vite preview 宿主白名单;
④ docs(w05) 目标件五份 HTML 与三份说明、实施计划、三张图。

冲突六处,全在 org 后端「姓在前」上 —— 主支(0c029b6,shanqin)与分支同日各做了一版:
主支拼「姓 名」留空格,分支 personName 汉字直拼(「柴浩」)、非汉字留空格(「Admin Amy」),
且分支那版是超集(服务账号识别、名册同步走同一把尺)。取分支版:port.ts / claims.ts /
keycloak-token-verifier.ts / keycloak-admin-roster.ts 及其测试按分支;主支那条 claims 测试
(「亲 删」→ 姓在前)保留,期望从「删 亲」改为「删亲」。api-report 重生成。

五道门禁绿:2096 测试通过、23 跳过(无模型密钥 / 无真信箱的惯常项)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
This commit is contained in:
xcs
2026-08-27 11:49:05 +08:00
co-authored by Claude Fable 5
67 changed files with 10890 additions and 418 deletions
+6 -1
View File
@@ -135,6 +135,7 @@ export function GovShell({ surface }: { surface: Surface }) {
const domains = sortDomains([...new Set((objects.data?.objects ?? []).map((o) => o.domain))]);
// 进台账没带域(左轨点进来)时落到首域 —— 页签与内容同一判定
const effectiveTab = zone === 'ledgers' && tab === '' ? (domains[0] ?? '') : tab;
const tabHint = TABS[zone].find((t) => t.key === tab)?.hint;
/* 面包屑:台面 › 区 › 页签 › 下钻各级 */
const crumbs: Array<{ label: string; go?: () => void }> = [
@@ -249,7 +250,8 @@ export function GovShell({ surface }: { surface: Surface }) {
* 摆一个只有一颗按钮的导航条,是在给人做无谓的选择。
* 台账例外 —— 它的页签是随发布生长的数据域,数量本来就不定。
*/}
<div className={'g-tabs' + (zone !== 'ledgers' && TABS[zone].length <= 1 ? ' hidden' : '')}>
<div className={'g-tabsrow' + (zone !== 'ledgers' && TABS[zone].length <= 1 ? ' hidden' : '')}>
<div className="g-tabs">
{zone === 'ledgers'
? // 台账的页签是**数据域**(短名 2-3 字),随发布生长 —— 设计产生运营的形状,落在页签上
domains.map((d) => (
@@ -273,6 +275,9 @@ export function GovShell({ surface }: { surface: Surface }) {
{t.label}
</button>
))}
</div>
{/* 右端一句话:这一格管什么、写下去会发生什么 —— 界面自己说清楚,不靠人记 */}
{tabHint !== undefined && <span className="g-tabs-hint">{tabHint}</span>}
</div>
<ZoneHome zone={zone} tab={effectiveTab} nav={nav} />
</>
+17
View File
@@ -84,6 +84,10 @@
.g-head-t p { margin: 5px 0 0; font-size: 12.5px; line-height: 1.7; color: var(--ink-3); max-width: 108ch; }
/* 区内页签:一个页签 = 一个维度 */
/* 页签栏一行两件:左边那颗药丸是页签,右端一句话说清这一格管什么(可缺席) */
.g-tabsrow { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.g-tabsrow.hidden { display: none; }
.g-tabs-hint { font-size: 12px; color: var(--ink-4); text-align: right; }
.g-tabs.hidden { display: none; }
.g-tabs { display: flex; flex-wrap: wrap; gap: 3px; padding: 4px; border-radius: 12px; background: #e3e8ef; align-self: flex-start; }
.g-tab { display: inline-flex; align-items: center; gap: 7px; height: 32px; padding: 0 14px; border: 0; border-radius: 9px; background: transparent; color: var(--ink-3); font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; transition: background var(--t-fast), color var(--t-fast); }
@@ -862,6 +866,19 @@
/* 供应商:一行小签 —— 挑一个即预填它的默认端点。
从前是 146px 宽的大牌子,一个人吃掉 118px 高,把下面的输入框全顶出屏幕。 */
.g-chiprow { display: flex; flex-wrap: wrap; gap: 6px; }
/* 组织那一屏:左树右详情;窄屏叠成一列(树在上,详情在下) */
.g-split { display: grid; grid-template-columns: minmax(240px, 340px) 1fr; gap: 14px; align-items: start; }
@media (max-width: 900px) { .g-split { grid-template-columns: 1fr; } }
.g-tree { display: flex; flex-direction: column; gap: 2px; }
.g-tree-r { display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 10px; border: 0; border-radius: 8px; background: transparent; color: var(--ink-2); font: inherit; font-size: 12.5px; text-align: left; cursor: pointer; transition: background var(--t-fast), color var(--t-fast); }
.g-tree-r:hover { background: #f1f4f8; color: var(--ink); }
.g-tree-r.on { background: var(--brand-soft, #e8f0ff); color: var(--ink); font-weight: 700; }
.g-tree-r b { flex: 1; font-weight: inherit; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.g-tree-r em { font-style: normal; font-size: 11px; color: var(--ink-4); }
/* 一枚勾选 + 一句话 —— `.g-swrow` 会把行内**所有** input 压成 14px,
而这一行里还站着检索框,所以单起一个,只管勾选框自己 */
.g-tick { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--ink-2); cursor: pointer; white-space: nowrap; }
.g-tick input { width: 14px; height: 14px; accent-color: var(--brand); cursor: pointer; }
.g-chip { padding: 6px 11px; border: 1px solid var(--cline); border-radius: 15px; background: #fff; font: 500 12px/1.2 var(--sans); color: var(--ink-3); cursor: pointer; transition: border-color var(--t-fast), background var(--t-fast), color var(--t-fast); }
.g-chip:hover { border-color: #aebdcd; color: var(--ink); }
.g-chip.on { border-color: var(--brand); background: var(--brand-wash); color: var(--brand-deep); font-weight: 600; }