访问申请流

Google Drive「请求访问」范式的访问申请流:被拒五件套(资源名 / 原因 / owner / 申请入口 / 切换身份)+ 申请表单(理由可必填、角色与时长、路由明示「请求将发送给:内容 owner 李四」——Confluence 十年路由踩坑的解)+ 九态状态机徽章族(idle → requested → pending_review → granted(expiresAt 倒计时) → expiring → expired → 重交;revoked 审批侧撤销;denied 不做刺眼红色终态——业界静默默契)+ mode="approver" 审批方视角(请求卡 + 批准选权限级 / 拒绝附留言)。createKbAccess(el, { request, mode?, onRequest?, onDecide? }) → handle { setState(next), getState() };到期是一等公民:每条 grant 可带 expiresAt 三态呈现(永不过期 / 倒计时+续期 / 已过期+重交)。

2026 财务规划

你没有访问权限——「不知道」与「不能说」不可区分(GitHub 404 惯例),此处仅提示可申请。

内容负责人:李四(财务部)
未申请

点「推进状态机」:未申请 → 已提交(路由明示给内容负责人)→ 已开通(30 天到期倒计时)→ 已过期(中性「可重新申请」)→ 回到未申请

<div class="toolbar" style="margin-bottom:10px">  <button class="btn btn-sm" type="button" id="kb-access-advance">推进状态机 →</button></div><div id="kb-access-demo">  <div class="kb-access" data-state="idle">    <div class="kb-access-denied">      <div class="kb-access-title">2026 财务规划</div>      <p class="kb-access-why">你没有访问权限——「不知道」与「不能说」不可区分(GitHub 404 惯例),此处仅提示可申请。</p>      <div class="kb-access-owner">内容负责人:李四(财务部)</div>    </div>    <div class="kb-access-state"><span class="kb-chip" data-state="idle">未申请</span></div>  </div></div><p class="demo-label" id="kb-access-log" style="margin-top:8px">点「推进状态机」:未申请 → 已提交(路由明示给内容负责人)→ 已开通(30 天到期倒计时)→ 已过期(中性「可重新申请」)→ 回到未申请</p>

安装

bun add @icen.ai/ui

一行引入该组件(CSS 与 behavior 自动带上,无需关心内部 CSS 文件名):

import '@icen.ai/ui/kit/kb-access';

也可以用脚手架直接打印引入行:

bunx --bun @icen.ai/ui add kb-access

Astro 项目注意:kit 入口里的 CSS import 在页面 <script> 里会被 Astro 的 client bundle 摇掉——请改在布局 frontmatter 里引 CSS(import '@icen.ai/ui/components/kb-perm.css';),JS 行为仍可走 kit。Vite SPA / webpack 项目无此问题。

用法

import { createKbAccess } from '@icen.ai/ui/kit/kb-access';import type { KbAccessRequest } from '@icen.ai/ui/kit/kb-core'; const request: KbAccessRequest = {  id: 'req-2026-1101', requester: '张三', resource: '2026 财务规划', role: 'viewer',  reason: 'Q4 区域预算对齐需要财务口径', state: 'idle',  submittedAt: new Date().toISOString(),  approverNote: '内容负责人:李四(财务部)',   // 路由明示(页面级找 owner / 空间级找 admin)};const access = createKbAccess(el, {  request,  onRequest: (r) => api.submit(r),                        // 与 icen:kb-access-request 双通道  onDecide: (d) => api.decide(d.id, d.decision, d.role),  // 与 icen:kb-access-decide 双通道});access.setState({ ...request, state: 'granted', decidedBy: '李四', decidedAt: new Date().toISOString(),  expiresAt: new Date(Date.now() + 30 * 864e5).toISOString() });   // 到期一等公民access.getState();   // 当前 KbAccessRequest 快照// 事件:icen:kb-access-request {request} / -decide {id, decision, role?} / -expire {id}