@airalogy/airalogy-bd 1.3.0 → 1.3.2

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.3.2] - 2026-10-27
8
+
9
+ - 业务总览默认展示按事件发生日期倒序排列的最近 6 条进展,包含普通和重要事件;重要事件显示标签,并提供“仅看关键”切换,保留事项权限、筛选范围及完整时间线。
10
+ - 修复 Markdown 无序列表圆点、有序列表编号和多级列表标记被基础样式清除的问题,保留任务清单复选框及折叠记录中的列表。
11
+ - 在身份验证器首次绑定步骤增加三步指南,提供 Microsoft Authenticator(微软)和 Google Authenticator(谷歌)的操作入口、官方说明和验证码排错提示;补充日常登录时获取动态验证码的说明。
12
+
13
+ ## [1.3.1] - 2026-10-27
14
+
15
+ - 将「待确认」移到右上角头像旁,有草稿时显示数量,没有草稿时使用弱化样式,保持主导航聚焦于工作台和管理中心。
16
+ - 数量仅统计当前账号有权查看的有效待确认草稿,与草稿列表共用权限和有效期检查;处理草稿后立即刷新,页面可见时定期更新。
17
+ - 调整手机端顶部操作布局,并补充数量接口的账号隔离、权限撤销、过期及处理状态测试。
18
+
7
19
  ## [1.3.0] - 2026-10-27
8
20
 
9
21
  - 统一界面字体、字号与行高,改善正文、表格和说明文字的可读性,手机端通过布局适配而非缩小正文。
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import Link from "next/link";
4
+ import PendingChangesLink from "./PendingChangesLink";
4
5
  import { usePathname, useRouter } from "next/navigation";
5
6
  import { BrandIdentity, BrandLogo } from "./Branding";
6
7
  import { teamRequest, type TeamUser } from "./lib/team-client";
@@ -25,13 +26,12 @@ export default function AccountBar({ user, workspace }: { user: TeamUser | null;
25
26
  <Link className="app-brand" href="/" aria-label="返回工作台">{workspace?<><BrandLogo/><span><strong>{workspace.short_name}</strong><small>{workspace.workspace_title}</small></span></>:<BrandIdentity/>}</Link>
26
27
  <nav className="app-navigation" aria-label="全局导航">
27
28
  <Link href="/" aria-current={pathname==="/"?"page":undefined}>工作台</Link>
28
- {user&&<Link href="/remote" aria-current={pathname==="/remote"?"page":undefined}>待确认修改</Link>}
29
29
  {(!user||user.role==="admin")&&<Link href="/admin" aria-current={pathname==="/admin"?"page":undefined}>管理中心</Link>}
30
30
  </nav>
31
- {user?<details className="app-account" ref={menu} onKeyDown={event=>{if(event.key==="Escape"&&menu.current){menu.current.open=false;menu.current.querySelector("summary")?.focus();}}}>
31
+ {user?<div className="app-account-actions"><PendingChangesLink key={user.id}/><details className="app-account" ref={menu} onKeyDown={event=>{if(event.key==="Escape"&&menu.current){menu.current.open=false;menu.current.querySelector("summary")?.focus();}}}>
32
32
  <summary aria-label={`${user.name},账号菜单`} className={pathname==="/account"?"is-current":undefined}><span className="app-avatar" aria-hidden="true">{Array.from(user.name)[0]}</span><span className="app-account-name">{user.name}<small>{{admin:"管理员",editor:"编辑成员",viewer:"只读成员"}[user.role]}</small></span><span className="app-account-chevron" aria-hidden="true">⌄</span></summary>
33
33
  <div className="app-account-menu"><Link href="/account" onClick={()=>{if(menu.current)menu.current.open=false;}}>我的账号</Link><Link href="/account#ai-tools" onClick={()=>{if(menu.current)menu.current.open=false;}}>AI 工具连接</Link><button type="button" onClick={()=>void logout()}>退出登录</button></div>
34
- </details>:<span className="app-local-label">本机工作区</span>}
34
+ </details></div>:<span className="app-local-label">本机工作区</span>}
35
35
  </div>
36
36
  </header>
37
37
  {error&&<p className="admin-alert" role="alert">{error}</p>}
@@ -0,0 +1,21 @@
1
+ export default function AuthenticatorSetup({ secret, otp_uri }: { secret: string; otp_uri: string }) {
2
+ return <section className="auth-setup" aria-labelledby="auth-setup-title">
3
+ <h2 id="auth-setup-title">绑定身份验证器</h2>
4
+ <p>身份验证器是生成六位动态验证码的手机 App,登录时用它确认身份。</p>
5
+ <ol className="auth-setup-steps">
6
+ <li>安装 <a href="https://support.microsoft.com/en-us/authenticator/how-to-add-your-accounts-to-microsoft-authenticator" target="_blank" rel="noopener noreferrer">Microsoft Authenticator(微软)</a>或 <a href="https://support.google.com/accounts/answer/1066447?hl=zh-Hans" target="_blank" rel="noopener noreferrer">Google Authenticator(谷歌)</a>,任选一个。</li>
7
+ <li>在 App 中手动添加账号。名称可填“Airalogy BD + 你的用户名”,密钥填写下方内容;类型选“基于时间”(如有)。</li>
8
+ <li>输入 App 当前显示的六位验证码,点击“完成绑定”。以后登录也从这个 App 获取验证码。</li>
9
+ </ol>
10
+ <details className="auth-setup-guide">
11
+ <summary>App 操作入口与常见问题</summary>
12
+ <p><strong>微软:</strong>点击“+”添加账号 → “其他账户” → “手动输入代码”。</p>
13
+ <p><strong>谷歌:</strong>点击“+”→“输入设置密钥”。不同版本的按钮名称可能略有差异。</p>
14
+ <p>验证码不可用时,等下一组数字刷新后再试,并检查手机已开启自动设置时间。</p>
15
+ </details>
16
+ <h3>设置密钥</h3>
17
+ <code aria-label="身份验证器设置密钥">{secret}</code>
18
+ <p className="admin-help">此密钥仅用于你自己的身份验证器,请勿分享。</p>
19
+ <p><a href={otp_uri}>在此设备的身份验证器中打开</a></p>
20
+ </section>;
21
+ }
@@ -519,6 +519,7 @@ function EventItem({
519
519
  <div className="event-meta">
520
520
  <time dateTime={event.date}>{formatDate(event.date, workspace)}</time>
521
521
  {sourceLabel && <span>{sourceLabel}</span>}
522
+ {event.importance === "key" && <span className="event-importance">重要</span>}
522
523
  <span className={`event-type event-${event.type}`}>
523
524
  {eventTypeLabels[event.type] ?? event.type}
524
525
  </span>
@@ -1027,6 +1028,7 @@ export default function Dashboard({ data, workspaceStatus, workspaceError, onWor
1027
1028
  const [selectedProjectValue, setSelectedProject] = useState<Project | null>(null);
1028
1029
  const selectedProject = data.projects.find(project => project.id === selectedProjectValue?.id) || null;
1029
1030
  const [selectedRecord, setSelectedRecord] = useState<RecordSelection | null>(null);
1031
+ const [keyEventsOnly, setKeyEventsOnly] = useState(false);
1030
1032
  const [keyPeopleOnly, setKeyPeopleOnly] = useState(true);
1031
1033
  const [peopleSort, setPeopleSort] = useState<PeopleSort>("recent");
1032
1034
 
@@ -1044,6 +1046,7 @@ export default function Dashboard({ data, workspaceStatus, workspaceError, onWor
1044
1046
  clearFacetFilters();
1045
1047
  setProjectSort("recent");
1046
1048
  setPeopleSort("recent");
1049
+ setKeyEventsOnly(false);
1047
1050
  setSelectedProject(null);
1048
1051
  };
1049
1052
 
@@ -1228,7 +1231,9 @@ export default function Dashboard({ data, workspaceStatus, workspaceError, onWor
1228
1231
  : [];
1229
1232
  const allEvents = [...projectEvents, ...companyEvents].sort(eventSort);
1230
1233
 
1231
- const keyEvents = allEvents.filter((event) => event.importance === "key").slice(0, 6);
1234
+ const recentEvents = (keyEventsOnly
1235
+ ? allEvents.filter((event) => event.importance === "key")
1236
+ : allEvents).slice(0, 6);
1232
1237
 
1233
1238
  const scopedProjectIds = new Set(filteredProjects.map((project) => project.id));
1234
1239
  const scopedPeople = data.people
@@ -1497,10 +1502,19 @@ export default function Dashboard({ data, workspaceStatus, workspaceError, onWor
1497
1502
  <div className="section-heading">
1498
1503
  <div>
1499
1504
  <div className="eyebrow">最近事件</div>
1500
- <h2 id="events-title">关键进展</h2>
1505
+ <h2 id="events-title">最近进展</h2>
1501
1506
  </div>
1502
1507
  <div className="event-panel-actions">
1503
- <span>最近 {keyEvents.length} 条</span>
1508
+ <span role="status">最近 {recentEvents.length} 条</span>
1509
+ <button
1510
+ type="button"
1511
+ className="history-trigger event-key-filter"
1512
+ aria-pressed={keyEventsOnly}
1513
+ aria-controls="recent-progress-list"
1514
+ onClick={() => setKeyEventsOnly((current) => !current)}
1515
+ >
1516
+ 仅看关键
1517
+ </button>
1504
1518
  <EventHistoryDialog
1505
1519
  events={allEvents}
1506
1520
  workspace={data.workspace}
@@ -1512,9 +1526,9 @@ export default function Dashboard({ data, workspaceStatus, workspaceError, onWor
1512
1526
  />
1513
1527
  </div>
1514
1528
  </div>
1515
- <div className="event-list">
1516
- {keyEvents.length > 0 ? (
1517
- keyEvents.map((event) => (
1529
+ <div className="event-list" id="recent-progress-list">
1530
+ {recentEvents.length > 0 ? (
1531
+ recentEvents.map((event) => (
1518
1532
  <EventItem
1519
1533
  key={`${event.scopeType}-${event.sourceLabel}-${event.id}`}
1520
1534
  event={event}
@@ -1526,7 +1540,7 @@ export default function Dashboard({ data, workspaceStatus, workspaceError, onWor
1526
1540
  />
1527
1541
  ))
1528
1542
  ) : (
1529
- <div className="panel-empty-state">当前筛选下暂无关键事件。</div>
1543
+ <div className="panel-empty-state">{keyEventsOnly ? "当前筛选下暂无关键进展,可关闭“仅看关键”查看普通进展。" : "当前筛选下暂无进展。"}</div>
1530
1544
  )}
1531
1545
  </div>
1532
1546
  </section>
@@ -3,6 +3,7 @@ import {useEffect,useRef,useState} from "react";
3
3
  import {teamRequest} from "./lib/team-client";
4
4
  import {useRouter} from "next/navigation";
5
5
  import {BrandIdentity} from "./Branding";
6
+ import AuthenticatorSetup from "./AuthenticatorSetup";
6
7
  import {usernamePattern,usernameHelp} from "./lib/username-policy";
7
8
 
8
9
  type Setup={secret:string;otp_uri:string};
@@ -70,8 +71,8 @@ export default function LoginForm({join=false}:{join?:boolean}) {
70
71
  {join&&<label>用户名<input required autoComplete="username" autoCapitalize="none" spellCheck={false} pattern={usernamePattern} minLength={3} maxLength={30} value={username} onChange={e=>setUsername(e.target.value)} readOnly={!!invitation?.username}/><span className="admin-help">{invitation?.username?"账号恢复保留原用户名、事项权限和历史记录。":usernameHelp}</span></label>}</>}
71
72
  {(step==="password"||step==="setup")&&<label>{join&&!accepted?"设置密码(至少 12 个字符)":"密码"}<input required type="password" minLength={12} maxLength={256} autoComplete={join&&!accepted?"new-password":"current-password"} value={password} onChange={e=>setPassword(e.target.value)}/></label>}
72
73
  {step==="email"&&<><h2>验证账号邮箱</h2><p role="status">{emailSent?"验证码已发送至账号邮箱,10 分钟内有效。这是邮箱收到的验证码,身份验证器的动态验证码将在下一步输入。":"账号已保留,但验证邮件暂未发送成功。请稍后重新发送,或联系管理员检查发信配置。"}</p><label>邮箱验证码<input required inputMode="numeric" pattern="[0-9]{6}" maxLength={6} autoComplete="one-time-code" value={emailCode} onChange={e=>setEmailCode(e.target.value)}/></label><button className="admin-button" type="button" onClick={resendEmail} disabled={busy}>重新发送验证码</button><p className="admin-help">每分钟最多发送一次;重新发送后请使用最新验证码。也可以返回登录页稍后继续。</p><a href="/login">返回登录</a></>}
73
- {setup && <div className="auth-setup"><h2>绑定身份验证器</h2><p>在你的身份验证器中添加账号,选择“输入设置密钥”,填入以下密钥。请勿分享此密钥。</p><code>{setup.secret}</code><p><a href={setup.otp_uri}>在支持的身份验证器中打开</a></p></div>}
74
- {step==="verify"&&<label>六位动态验证码<input required inputMode="numeric" pattern="[0-9]{6}" maxLength={6} autoComplete="one-time-code" value={code} onChange={e=>setCode(e.target.value)}/></label>}
74
+ {setup && <AuthenticatorSetup {...setup} />}
75
+ {step==="verify"&&<label>六位动态验证码<input required inputMode="numeric" pattern="[0-9]{6}" maxLength={6} autoComplete="one-time-code" value={code} onChange={e=>setCode(e.target.value)}/>{!setup&&<span className="admin-help">打开你绑定的身份验证器 App,输入本账号当前显示的验证码。</span>}</label>}
75
76
  <button className="admin-button admin-primary" type="submit" disabled={busy}>{busy?"正在验证…":step==="email"?"验证邮箱并继续":step==="verify"?(setup?"完成绑定":"登录"):step==="setup"?"继续绑定":join&&!accepted?"设置密码并继续":"下一步"}</button>
76
77
  </form>}
77
78
  <p className="admin-help">仅限受邀成员。忘记密码或无法取得验证码时,请联系管理员重新签发邀请。设置中断后,可从登录页面继续绑定。</p>
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+ import { SettingsIcon } from "./SettingsUI";
6
+ import { REMOTE_DRAFTS_CHANGED } from "./lib/remote-events";
7
+
8
+ export default function PendingChangesLink() {
9
+ const pathname = usePathname();
10
+ const [count, setCount] = useState<number | null>(null);
11
+ const [unavailable, setUnavailable] = useState(false);
12
+ useEffect(() => {
13
+ let controller: AbortController | null = null;
14
+ let disposed = false;
15
+ async function refresh(force = false) {
16
+ if (document.visibilityState === "hidden" || (controller && !force)) return;
17
+ controller?.abort();
18
+ const current = new AbortController();
19
+ controller = current;
20
+ try {
21
+ const response = await fetch("/api/remote?action=pending", { cache: "no-store", signal: current.signal });
22
+ if (!response.ok) throw new Error("unavailable");
23
+ const body = await response.json();
24
+ if (!body.ok || !Number.isSafeInteger(body.result?.count) || body.result.count < 0) throw new Error("invalid count");
25
+ if (!disposed && !current.signal.aborted) { setCount(body.result.count); setUnavailable(false); }
26
+ } catch {
27
+ if (!disposed && !current.signal.aborted) { setCount(null); setUnavailable(true); }
28
+ } finally {
29
+ if (controller === current) controller = null;
30
+ }
31
+ }
32
+ const onVisible = () => void refresh();
33
+ const onChanged = () => void refresh(true);
34
+ void refresh();
35
+ const timer = window.setInterval(onVisible, 60_000);
36
+ window.addEventListener("focus", onVisible);
37
+ document.addEventListener("visibilitychange", onVisible);
38
+ window.addEventListener(REMOTE_DRAFTS_CHANGED, onChanged);
39
+ return () => {
40
+ disposed = true;
41
+ controller?.abort();
42
+ window.clearInterval(timer);
43
+ window.removeEventListener("focus", onVisible);
44
+ document.removeEventListener("visibilitychange", onVisible);
45
+ window.removeEventListener(REMOTE_DRAFTS_CHANGED, onChanged);
46
+ };
47
+ }, [pathname]);
48
+ const hasPending = count !== null && count > 0;
49
+ const label = hasPending ? `待确认修改,${count} 条草稿` : count === 0 ? "待确认修改,暂无草稿" : "待确认修改";
50
+ return <Link href="/remote" className={`app-pending${hasPending ? " has-pending" : " is-muted"}`}
51
+ aria-current={pathname === "/remote" ? "page" : undefined} aria-label={label}
52
+ title={unavailable ? "数量暂时无法获取,点击查看待确认修改" : label}>
53
+ <SettingsIcon name="review"/><span>待确认</span>
54
+ {hasPending && <span className="app-pending-count" aria-hidden="true">{count > 99 ? "99+" : count}</span>}
55
+ </Link>;
56
+ }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { useCallback, useEffect, useState } from "react";
3
3
  import Link from "next/link";
4
+ import { notifyRemoteDraftsChanged } from "./lib/remote-events";
4
5
  import {SettingsPanelHeader,SettingsStatus} from "./SettingsUI";
5
6
  import type { TeamUser } from "./lib/team-client";
6
7
  import { stageLabels, followUpStatusLabels } from "./lib/labels";
@@ -53,6 +54,7 @@ export default function RemoteAccess({ user, mode, endpoint = "" }: { user: Team
53
54
  setBusy(true); setError(""); setNotice("");
54
55
  try {
55
56
  const result = await request<{ token?: string }>(action, input);
57
+ notifyRemoteDraftsChanged();
56
58
  if (result.token) setSecret(result.token);
57
59
  setNotice(message); await reload();
58
60
  } catch (err) { setError((err as Error).message); }
@@ -1,6 +1,6 @@
1
1
  import type {ReactNode} from "react";
2
2
 
3
- export type SettingsIconName = "members" | "ai" | "usage" | "remote" | "backup" | "plus" | "search" | "lock";
3
+ export type SettingsIconName = "members" | "ai" | "usage" | "remote" | "backup" | "plus" | "search" | "lock" | "review";
4
4
  const paths:Record<SettingsIconName,ReactNode> = {
5
5
  members:<><circle cx="9" cy="8" r="3"/><path d="M3 20v-2a6 6 0 0 1 12 0v2M16 5a3 3 0 0 1 0 6m2 3a5 5 0 0 1 3 4v2"/></>,
6
6
  ai:<><path d="m12 3 2.7 6.3L21 12l-6.3 2.7L12 21l-2.7-6.3L3 12l6.3-2.7L12 3Z"/></>,
@@ -9,6 +9,7 @@ const paths:Record<SettingsIconName,ReactNode> = {
9
9
  backup:<><path d="M12 3v12m-4-4 4 4 4-4M4 15v5h16v-5"/></>,
10
10
  plus:<path d="M12 5v14M5 12h14"/>,
11
11
  search:<><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 4 4"/></>,
12
+ review:<><path d="M9 4H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4"/><rect x="9" y="2" width="6" height="4" rx="1"/><path d="m8 13 3 3 5-6"/></>,
12
13
  lock:<><rect x="5" y="10" width="14" height="11" rx="2"/><path d="M8 10V6a4 4 0 0 1 8 0v4m-4 5v2"/></>,
13
14
  };
14
15
  export function SettingsIcon({name}:{name:SettingsIconName}) {
@@ -8,7 +8,7 @@ export async function GET(request: Request) {
8
8
  const access = await authorizedRequest(request);
9
9
  if (access instanceof Response) return access;
10
10
  const operation = new URL(request.url).searchParams.get("action") || "status";
11
- if (!["status", "settings", "drafts"].includes(operation)) return denied();
11
+ if (!["status", "settings", "drafts", "pending"].includes(operation)) return denied();
12
12
  return serviceResponse(await invokeWorkspace({ action: "remote_browser", operation }, access.session));
13
13
  }
14
14
  export async function POST(request: Request) {
@@ -840,9 +840,15 @@ select:focus-visible {
840
840
  scroll-margin-top: 20px;
841
841
  }
842
842
 
843
+ .events-panel .section-heading {
844
+ flex-wrap: wrap;
845
+ gap: 12px;
846
+ }
847
+
843
848
  .event-panel-actions {
844
849
  align-items: center;
845
850
  display: flex;
851
+ flex-wrap: wrap;
846
852
  gap: 9px;
847
853
  }
848
854
 
@@ -869,6 +875,12 @@ select:focus-visible {
869
875
  color: var(--navy);
870
876
  }
871
877
 
878
+ .event-key-filter[aria-pressed="true"] {
879
+ background: var(--navy);
880
+ border-color: var(--navy);
881
+ color: #fff;
882
+ }
883
+
872
884
  .event-list {
873
885
  display: grid;
874
886
  margin-top: 14px;
@@ -939,6 +951,15 @@ select:focus-visible {
939
951
  padding: 2px 7px;
940
952
  }
941
953
 
954
+ .event-importance {
955
+ background: #fff3d6;
956
+ border: 1px solid #eed49b;
957
+ border-radius: 999px;
958
+ color: #82510d;
959
+ font-weight: 700;
960
+ padding: 1px 7px;
961
+ }
962
+
942
963
  .event-agreement {
943
964
  background: #f9efe1;
944
965
  color: #8f5b1b;
@@ -1299,9 +1320,16 @@ select:focus-visible {
1299
1320
 
1300
1321
  .markdown-record ul,
1301
1322
  .markdown-record ol {
1323
+ list-style-position: outside;
1302
1324
  padding-left: 1.7em;
1303
1325
  }
1304
1326
 
1327
+ .markdown-record ul { list-style-type: disc; }
1328
+ .markdown-record ol { list-style-type: decimal; }
1329
+ .markdown-record ul ul { list-style-type: circle; }
1330
+ .markdown-record ul ul ul { list-style-type: square; }
1331
+ .markdown-record li.task-list-item { list-style-type: none; }
1332
+
1305
1333
  .markdown-record li + li {
1306
1334
  margin-top: 5px;
1307
1335
  }
@@ -0,0 +1,5 @@
1
+ export const REMOTE_DRAFTS_CHANGED = "bd:remote-drafts-changed";
2
+
3
+ export function notifyRemoteDraftsChanged() {
4
+ window.dispatchEvent(new Event(REMOTE_DRAFTS_CHANGED));
5
+ }
@@ -235,6 +235,15 @@
235
235
  .auth-setup { padding:16px; background:#f3f6fa; border:1px solid var(--line); border-radius:9px; }
236
236
  .auth-setup p { margin:10px 0; font-size:var(--text-small); line-height:var(--leading-reading); }
237
237
  .auth-setup code { display:block; overflow-wrap:anywhere; user-select:all; font-size:var(--text-body); padding:10px; background:white; border-radius:6px; }
238
+ .auth-setup-steps { list-style:decimal outside; padding-left:1.5em; margin:12px 0; font-size:var(--text-small); line-height:var(--leading-reading); }
239
+ .auth-setup-steps li + li { margin-top:8px; }
240
+ .auth-setup-guide { margin:14px 0; font-size:var(--text-small); }
241
+ .auth-setup-guide summary { color:var(--blue); cursor:pointer; }
242
+ .auth-setup-guide p { margin:8px 0; }
243
+ .auth-setup h3 { margin:16px 0 8px; font-size:var(--text-small); font-weight:600; }
244
+ .auth-setup > .admin-help { color:var(--muted); font-size:var(--text-caption); }
245
+ .auth-setup > :last-child { margin-bottom:0; }
246
+
238
247
  .auth-card a { color:var(--blue); font-size:var(--text-small); }
239
248
 
240
249
  @media(max-width:1050px) {
@@ -321,3 +330,27 @@
321
330
  .auth-page { padding:24px 16px; }
322
331
  .auth-card { padding:26px 22px; }
323
332
  }
333
+
334
+ /* Pending reviews are account actions, separate from the main navigation. */
335
+ .app-account-actions { display:flex; align-items:center; gap:14px; margin-left:auto; flex-shrink:0; }
336
+ .app-account-actions .app-account { margin-left:0; }
337
+ .app-pending { display:inline-flex; align-items:center; gap:6px; min-height:38px; padding:8px 10px; border:1px solid transparent; border-radius:8px; color:var(--muted); font-size:var(--text-small); line-height:var(--leading-compact); text-decoration:none; white-space:nowrap; }
338
+ .app-pending .settings-icon { width:17px; height:17px; }
339
+ .app-pending:hover,.app-pending[aria-current=page] { background:var(--canvas); color:var(--navy); }
340
+ .app-pending.has-pending { color:var(--blue); background:var(--blue-soft); }
341
+ .app-pending.has-pending:hover,.app-pending.has-pending[aria-current=page] { border-color:#cbddeb; }
342
+ .app-pending-count { display:inline-flex; align-items:center; justify-content:center; min-width:20px; height:20px; padding:0 5px; border-radius:10px; background:var(--blue); color:white; font-size:var(--text-caption); font-weight:600; font-variant-numeric:tabular-nums; }
343
+ @media(max-width:760px) {
344
+ .app-account-actions { order:1; gap:8px; }
345
+ .app-account-actions .app-account { order:0; }
346
+ }
347
+ @media(max-width:520px) {
348
+ .app-header-inner { padding-left:14px; padding-right:14px; gap:10px; }
349
+ .app-account-actions { gap:4px; }
350
+ .app-account summary { padding:5px; gap:4px; }
351
+ .app-account-name,.app-account-chevron { display:none; }
352
+ .app-pending { padding:7px; gap:5px; }
353
+ .app-pending .settings-icon { display:none; }
354
+ .app-brand > span:not(.site-brand-identity) { min-width:0; }
355
+ .app-brand strong,.app-brand small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
356
+ }
@@ -18,6 +18,8 @@ Airalogy BD 使用系统无衬线字体:优先采用操作系统界面字体
18
18
 
19
19
  同类元素在不同页面共用字号,不通过不断增加字号区分信息层级;辅助信息优先用字重、颜色和间距区分。手机端通过换行、单列布局和表格横向滚动适配,不缩小正文。主要表单的手机端输入字号为 16px,避免聚焦输入时触发浏览器自动缩放。
20
20
 
21
- 公司标识位于应用最上方,工作区导航和账号操作在同一栏;页面标题、页面操作和局部导航位于下方。管理中心负责全局配置和成员权限;个人连接由成员在“我的账号”管理。
21
+ 公司标识位于应用最上方,工作区导航和账号操作在同一栏;「待确认」作为头像旁的操作入口,有草稿时显示数量,没有时弱化显示;页面标题、页面操作和局部导航位于下方。管理中心负责全局配置和成员权限;个人连接由成员在“我的账号”管理。
22
22
 
23
23
  导出报告有独立的纸张与排版要求,使用报告组件的专用字号,不随界面字号批量调整。
24
+
25
+ 业务总览的“最近进展”默认按事件发生日期倒序展示当前权限和筛选范围内的最近 6 条进展,包含普通和重要事件;重要事件显示“重要”标签。“仅看关键”仅筛选该预览列表,“查看全部”仍展示当前范围的完整时间线。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airalogy/airalogy-bd",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Business development workspace for local use and team collaboration.",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -108,6 +108,10 @@ class RemoteAccess:
108
108
  def browser(self, request, actor):
109
109
  op = request.get("operation")
110
110
  if op == "status": return self.status(actor)
111
+ if op == "pending":
112
+ if not self.settings()["enabled"] or not self.accounts.remote_allowed(actor["id"]) or actor["role"] == "viewer":
113
+ return {"count": 0}
114
+ return {"count": sum(1 for _ in self.pending_drafts(actor))}
111
115
  if op in {"settings", "configure"}:
112
116
  self.accounts.require_admin(actor)
113
117
  if op == "configure":
@@ -152,13 +156,8 @@ class RemoteAccess:
152
156
  return {"revoked": True}
153
157
  if op == "drafts":
154
158
  self.grant(actor)
155
- policy, drafts = Policy(self.workspace, actor), []
156
- for row in self.db.execute("SELECT * FROM remote_drafts WHERE owner=? AND state='pending' ORDER BY created DESC", (actor["id"],)):
157
- try:
158
- self.check_draft(row, actor, policy)
159
- drafts.append({"id": row["id"], "created": row["created"], "expires": row["expires"], **json.loads(row["value"])["review"]})
160
- except AuthError: continue
161
- return {"drafts": drafts}
159
+ return {"drafts": [{"id": row["id"], "created": row["created"], "expires": row["expires"],
160
+ **json.loads(row["value"])["review"]} for row in self.pending_drafts(actor)]}
162
161
  if op in {"confirm", "reject"}:
163
162
  self.grant(actor)
164
163
  row = self.db.execute("SELECT * FROM remote_drafts WHERE id=?", (text(request.get("draftId")),)).fetchone()
@@ -189,6 +188,16 @@ class RemoteAccess:
189
188
  return result
190
189
  raise AuthError("未知的远程管理操作。", "invalid")
191
190
 
191
+ def pending_drafts(self, actor):
192
+ # The badge and review page use the same ownership, expiry and permission checks.
193
+ policy = Policy(self.workspace, actor)
194
+ for row in self.db.execute("SELECT * FROM remote_drafts WHERE owner=? AND state='pending' ORDER BY created DESC", (actor["id"],)):
195
+ try:
196
+ self.check_draft(row, actor, policy)
197
+ except AuthError:
198
+ continue
199
+ yield row
200
+
192
201
  def check_draft(self, row, actor, policy):
193
202
  if not row or row["owner"] != actor["id"] or row["binding"] != actor["revision"] or row["expires"] <= int(time.time()) or row["state"] != "pending":
194
203
  raise AuthError("草稿已处理或失效;请检查最新记录,必要时重新提交。", "conflict")