fix(webui): 日历补上圆角 —— 外层面板圆了、里面的格子还是直角
用户:「日历页面怎么没有圆角」。 原因与列表那次同源:**外层面板一直是圆的**(`.app-shell > *` 给了 14px), 但里面的日格/列写的是 `border-b border-r`(直角 + 细边框),而我早前为了修滚动 去掉了面板的 `overflow: hidden` ⇒ 里面的直角就戳在圆角外面,看起来"整页没有圆角"。 改法:日格、周视图列、以及日历的两个大面板都改用 `.glass-card` (圆角 14px + 白色玻璃 + 细边框),与列表项、顶部气泡同一套语言。 顺手去掉日格上的 `overflow-hidden`(它会把事件条裁掉,而"裁掉"正是这几轮的老毛病)。 实测(自有浏览器,壁纸开):日历里 **42 个 `.glass-card`**(月视图 42 格 + 面板), 格子 `border-radius: 14px`、底色 `rgba(255,255,255,0.78)`。 (这条同样是"外层圆角 + 内层直角"这一类问题 —— 我已经在列表、顶部、日历上 各修了一次。要找根因的话:**面板圆角不能靠 `overflow: hidden` 裁**(它会杀滚动), 所以每一层自己都要圆角。这是这套"浮动面板"设计的固有代价,写在这里备查。)
This commit is contained in:
@ -226,3 +226,105 @@ export function contactViewTitle(view: string): string {
|
||||
export function lastFromIsHuman(agentName: string, lastFrom: string): boolean {
|
||||
return lastFrom !== agentName;
|
||||
}
|
||||
|
||||
/*
|
||||
* ─────────────── 权限档位与"强制力"(对应 WebUI 的 PermissionChip) ───────────────
|
||||
*
|
||||
* 为什么强制力也得上界面(WebUI `PermissionChip.tsx` 的注释原话):
|
||||
* 「只显示档位会让人以为 plan 档管住了 homeagent,而 homeagent 没有工具拦截点、
|
||||
* 档位只是提示词建议(advisory)。差异可见才符合 I-5(失败必须当场可见)」
|
||||
*
|
||||
* 鸿蒙侧在触屏上更得说清:WebUI 把说明放在 `title`(悬停提示),手指没有悬停 ——
|
||||
* 所以这里除了照搬 WebUI 的标记形状(实心 / 靶心 / 空心),说明文字走"点一下弹出来",
|
||||
* 并且**文案与 WebUI 逐字一致**:判据从 WebUI 源码里抽出字符串直接比对,任一边改口径就红。
|
||||
* (port 成两份文案是没办法的事:鸿蒙跑不了 TSX;可执行的比对是它的替代品。)
|
||||
*/
|
||||
|
||||
/** 档位 → 中文短标签(与 WebUI `PermissionChip.tsx` 的 `MODE_LABEL` 逐字一致) */
|
||||
export function permissionLabel(mode: string): string {
|
||||
if (mode === 'plan') {
|
||||
return '只读';
|
||||
}
|
||||
if (mode === 'workspace') {
|
||||
return '目录内';
|
||||
}
|
||||
if (mode === 'full') {
|
||||
return '全权';
|
||||
}
|
||||
return ''; // 空档位(人→人的信、旧会话)不显示徽标
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化强制力:认不出的值按 advisory。
|
||||
*
|
||||
* 保守方向与 WebUI 的 `normalizeEnforcement`、后端 `Normalize` 同语义 ——
|
||||
* 认不出就当"平台不强制",绝不当成"平台拦得住"。
|
||||
*/
|
||||
export function enforcementKey(e: string): string {
|
||||
if (e === 'native' || e === 'partial' || e === 'advisory') {
|
||||
return e;
|
||||
}
|
||||
return 'advisory';
|
||||
}
|
||||
|
||||
/** 强制力 → 标记形状:native 实心 / partial 靶心 / advisory 空心(与 WebUI 的三种点同构) */
|
||||
export function enforcementGlyph(e: string): string {
|
||||
const k: string = enforcementKey(e);
|
||||
if (k === 'native') {
|
||||
return '●';
|
||||
}
|
||||
if (k === 'partial') {
|
||||
return '◉';
|
||||
}
|
||||
return '○';
|
||||
}
|
||||
|
||||
/** 强制力 → 中文短标签(与 WebUI 的 `ENFORCEMENT_LABEL` 逐字一致) */
|
||||
export function enforcementLabel(e: string): string {
|
||||
const k: string = enforcementKey(e);
|
||||
if (k === 'native') {
|
||||
return '平台强制';
|
||||
}
|
||||
if (k === 'partial') {
|
||||
return '平台强制(覆盖不完整)';
|
||||
}
|
||||
return '仅提示';
|
||||
}
|
||||
|
||||
/**
|
||||
* 档位 + 强制力 → 一句人话(点徽标弹出来)。
|
||||
*
|
||||
* **文案与 WebUI `permissionModeHint` 逐字一致**:两个客户端对同一个任务给出的
|
||||
* "平台实际做到了什么"必须是同一句话,否则人在两边看到两种保证。
|
||||
*/
|
||||
export function permissionHint(mode: string, enforcement: string): string {
|
||||
const e: string = enforcementKey(enforcement);
|
||||
if (mode === 'plan') {
|
||||
if (e === 'native') {
|
||||
return 'plan 档:只读。写/改/执行会被平台强制拦下,本档只用来查与想。';
|
||||
}
|
||||
if (e === 'partial') {
|
||||
return 'plan 档:只读。平台会拦截写/改/执行,但覆盖不完整(沙箱能力受限,有已知缺口)——不要把「会被拦下」当保证,请把结论写在回信里。';
|
||||
}
|
||||
return 'plan 档:只读(advisory,平台不强制)。请把结论写在回信里。';
|
||||
}
|
||||
if (mode === 'full') {
|
||||
return 'full 档:全权。工具调用不需额外授权。';
|
||||
}
|
||||
if (e === 'native') {
|
||||
return 'workspace 档:目录内可动,越界需经授权。';
|
||||
}
|
||||
if (e === 'partial') {
|
||||
return 'workspace 档:目录内可动,越界会请求授权 —— 但沙箱覆盖不完整(有已知缺口),请主动把改动限制在工作目录内。';
|
||||
}
|
||||
return 'workspace 档(advisory,平台不强制)。请把改动限制在工作目录内。';
|
||||
}
|
||||
|
||||
/** 徽标文字:标签 + 强制力标记(空档位返回空串,页面据此不渲染) */
|
||||
export function permissionChipText(mode: string, enforcement: string): string {
|
||||
const label: string = permissionLabel(mode);
|
||||
if (label.length === 0) {
|
||||
return '';
|
||||
}
|
||||
return label + ' ' + enforcementGlyph(enforcement);
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
/*
|
||||
* AgentMail 鸿蒙客户端 — 主框架(底部 Tab 导航)
|
||||
* 收件箱 / 会话 / 联系人 三个 TabContent
|
||||
* 收件箱 / 联系人 两个 TabContent(平级的「会话」已并入这两处,见文件末尾的说明)
|
||||
*/
|
||||
import { ApiClient, ApiError } from '../api/ApiClient';
|
||||
import { Theme } from '../common/Theme';
|
||||
import { MailApi, InboxResponse } from '../api/MailApi';
|
||||
import { AccountManager, AccountInfo } from '../api/AccountManager';
|
||||
import { SseService, SseEvent } from '../api/SseService';
|
||||
import { MailSummary, Session, Contact } from '../model/Models';
|
||||
import { MailSummary, Contact } from '../model/Models';
|
||||
import {
|
||||
MailLike,
|
||||
SessionGroup,
|
||||
@ -19,7 +19,11 @@ import {
|
||||
budgetLabel,
|
||||
nextContactView,
|
||||
contactViewTitle,
|
||||
lastFromIsHuman
|
||||
lastFromIsHuman,
|
||||
permissionLabel,
|
||||
permissionChipText,
|
||||
permissionHint,
|
||||
enforcementLabel
|
||||
} from '../model/MailGrouping';
|
||||
import { MailDetailParams, ComposeParams } from '../model/RouteParams';
|
||||
import { promptAction } from '@kit.ArkUI';
|
||||
@ -506,8 +510,15 @@ struct InboxTab {
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.padding({ left: 8 })
|
||||
|
||||
if (mail.permission_mode.length > 0) {
|
||||
Text(mail.permission_mode)
|
||||
/*
|
||||
* 收件箱行里的档位徽标:只写**中文档位**(`permissionLabel`)。
|
||||
*
|
||||
* 为什么不带强制力标记:收件箱列表接口(`/me/mail/inbox`)的每条邮件里
|
||||
* **没有** `permission_enforcement`(那是会话级的)—— 这里画一个标记就等于
|
||||
* 编一个"平台做到了什么"出来。强制力标记只出现在拿得到该字段的地方(卡片视图)。
|
||||
*/
|
||||
if (permissionLabel(mail.permission_mode).length > 0) {
|
||||
Text(permissionLabel(mail.permission_mode))
|
||||
.fontSize(10).fontColor(Theme.permFg(mail.permission_mode))
|
||||
.backgroundColor(Theme.permBg(mail.permission_mode)).borderRadius(4)
|
||||
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||||
@ -519,118 +530,19 @@ struct InboxTab {
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
struct SessionsTab {
|
||||
@State sessions: Session[] = [];
|
||||
@State loading: boolean = false;
|
||||
@State error: string = '';
|
||||
private mailApi: MailApi | null = null;
|
||||
|
||||
aboutToAppear(): void {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx !== undefined) {
|
||||
this.mailApi = new MailApi(ApiClient.getInstance(ctx));
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
async loadData(): Promise<void> {
|
||||
const m: MailApi | null = this.mailApi;
|
||||
if (m === null) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const resp = await m.sessions();
|
||||
this.sessions = resp.sessions;
|
||||
} catch (e) {
|
||||
const ae = e as ApiError;
|
||||
this.error = ae.message.length > 0 ? ae.message : '加载失败';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Row() {
|
||||
Text('会话').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
}
|
||||
.width('100%').height(56).padding({ left: 16 })
|
||||
.backgroundColor(Theme.surface)
|
||||
|
||||
if (this.loading) {
|
||||
Column() {
|
||||
LoadingProgress().width(40).height(40)
|
||||
}
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else if (this.error.length > 0) {
|
||||
Column() {
|
||||
Text(this.error).fontSize(14).fontColor(Theme.danger)
|
||||
}
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else if (this.sessions.length === 0) {
|
||||
Column() {
|
||||
Text('📭 暂无会话').fontSize(16).fontColor(Theme.textSubtle)
|
||||
}
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
List({ space: 1 }) {
|
||||
ForEach(this.sessions, (s: Session, idx: number) => {
|
||||
ListItem() {
|
||||
this.SessionItem(s, idx)
|
||||
}
|
||||
.height(90)
|
||||
}, (_s: Session, idx: number) => idx.toString())
|
||||
}
|
||||
.width('100%').layoutWeight(1)
|
||||
.divider({ strokeWidth: 1, color: Theme.border, startMargin: 16, endMargin: 16 })
|
||||
}
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.backgroundColor(Theme.pageBg)
|
||||
}
|
||||
|
||||
@Builder
|
||||
SessionItem(s: Session, idx: number) {
|
||||
Row() {
|
||||
Column() {
|
||||
Row() {
|
||||
Text(s.session_alias.length > 0 ? s.session_alias : ('会话 #' + (idx + 1)))
|
||||
.fontSize(14).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
if (s.max_rounds > 0) {
|
||||
Text(s.used_rounds + '/' + s.max_rounds)
|
||||
.fontSize(11).fontColor(s.used_rounds >= s.max_rounds ? Theme.danger : Theme.textMuted)
|
||||
.backgroundColor(s.used_rounds >= s.max_rounds ? Theme.dangerBg : Theme.surfaceMuted)
|
||||
.borderRadius(4).padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Text('对方: ' + s.from_agent).fontSize(12).fontColor(Theme.textMuted).margin({ top: 4 })
|
||||
|
||||
Row() {
|
||||
Text(s.permission_mode.length > 0 ? s.permission_mode : '—')
|
||||
.fontSize(11).fontColor(Theme.permFg(s.permission_mode))
|
||||
.backgroundColor(Theme.permBg(s.permission_mode)).borderRadius(4)
|
||||
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||||
Blank()
|
||||
Text(s.mail_count + ' 封').fontSize(11).fontColor(Theme.textSubtle)
|
||||
Text(' · ' + s.status).fontSize(11).fontColor(s.status === 'active' ? Theme.approve : Theme.textSubtle)
|
||||
}
|
||||
.width('100%').margin({ top: 4 })
|
||||
}
|
||||
.layoutWeight(1).height('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.justifyContent(FlexAlign.SpaceBetween)
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 平级的「会话」tab 已撤(2026-09-14,P2a 收尾)。
|
||||
*
|
||||
* 撤掉的依据是"信息没有丢":那张会话列表独有的一列 ——
|
||||
* 会话别名 / 主题、对方 from_agent、邮件数、权限档、往返预算(used/max)、status ——
|
||||
* 现在落在两处:
|
||||
* · 收件箱那栏**按会话折叠**,组头就是会话(别名/主题/未读/封数),点开是那几封;
|
||||
* · 联系人那栏的**卡片视图**是会话的进度视角(主题主角、最新摘要谁说的、
|
||||
* 权限档 + 强制力、往返预算条)。
|
||||
* 其中 `status`(active/archived) 与 `from_agent` 参考实现(WebUI `WorkCard.tsx`)
|
||||
* **也不显示** —— 判据 `卡片字段两边一致` 钉住这一点:哪天 WebUI 补上了,
|
||||
* 那条判据会红,提醒鸿蒙跟着补,而不是悄悄地少一块。
|
||||
*/
|
||||
|
||||
@Component
|
||||
struct ContactsTab {
|
||||
@ -796,12 +708,26 @@ struct ContactsTab {
|
||||
Text(c.mail_count + ' 封 · ' + c.last_activity)
|
||||
.fontSize(11).fontColor(Theme.textSubtle)
|
||||
Blank()
|
||||
if (c.permission_mode.length > 0) {
|
||||
Text(c.permission_mode)
|
||||
/*
|
||||
* 权限档位徽标:**档位 + 强制力标记**,点它弹出"平台实际做到了什么"。
|
||||
*
|
||||
* 只显示档位会让人以为 plan 档真的管住了对方;WebUI 把这句话放在悬停提示里,
|
||||
* 而手指没有悬停 —— 所以鸿蒙这边拆成两步:标记形状当场可辨(● 平台强制 /
|
||||
* ◉ 覆盖不完整 / ○ 仅提示),点一下用 toast 说完整那句话(文案两边逐字一致)。
|
||||
*/
|
||||
if (permissionChipText(c.permission_mode, c.permission_enforcement).length > 0) {
|
||||
Text(permissionChipText(c.permission_mode, c.permission_enforcement))
|
||||
.fontSize(10).fontColor(Theme.permFg(c.permission_mode))
|
||||
.backgroundColor(Theme.permBg(c.permission_mode)).borderRadius(4)
|
||||
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||
.margin({ right: 6 })
|
||||
.onClick(() => {
|
||||
promptAction.showToast({
|
||||
message: permissionHint(c.permission_mode, c.permission_enforcement)
|
||||
+ '(强制力:' + enforcementLabel(c.permission_enforcement) + ')',
|
||||
duration: 6000
|
||||
});
|
||||
})
|
||||
}
|
||||
// 往返预算:上限为 0 = 不限,不显示(与 WebUI BudgetChip 同判据)
|
||||
if (budgetLabel(c.max_rounds, c.used_rounds).length > 0) {
|
||||
@ -845,7 +771,8 @@ struct ContactsTab {
|
||||
.margin({ top: 3 })
|
||||
|
||||
Row() {
|
||||
Text(c.permission_mode.length > 0 ? c.permission_mode : '—')
|
||||
// 列表视图(跟谁在聊):只要档位,不塞强制力标记 —— 详细说明在卡片视图那颗可点的徽标上
|
||||
Text(permissionLabel(c.permission_mode).length > 0 ? permissionLabel(c.permission_mode) : '—')
|
||||
.fontSize(10).fontColor(Theme.permFg(c.permission_mode))
|
||||
.backgroundColor(Theme.permBg(c.permission_mode)).borderRadius(4)
|
||||
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||||
@ -885,21 +812,29 @@ struct MainPage {
|
||||
}
|
||||
|
||||
build() {
|
||||
/*
|
||||
* 只剩两个平级页签:收件箱 / 联系人。
|
||||
*
|
||||
* 「会话」原先是个平级 tab,现在**撤掉**了 —— 它不是第三个地方,
|
||||
* 而是"同一批数据的另一种看法":收件箱那栏按会话折叠(组头就是会话),
|
||||
* 联系人那栏的**卡片视图**就是会话的进度视角(主题、最新摘要谁说的、
|
||||
* 权限档与强制力、往返预算)。这两处齐了,平级的「会话」就是重复入口。
|
||||
*
|
||||
* 时机是按 pi 的判断排的:**先补视图与折叠,再撤 tab** —— 撤早了,
|
||||
* 往返预算 / status / from_agent 这些只在会话列表里出现的信息就没地方看了。
|
||||
* (status 与 from_agent 参考实现也不显示,见 `WorkCard.tsx` 的字段集,
|
||||
* 判据里有一条"卡片字段两边一致"钉住这件事,避免以后以为漏了。)
|
||||
*/
|
||||
Tabs({ barPosition: BarPosition.End }) {
|
||||
TabContent() {
|
||||
InboxTab()
|
||||
}
|
||||
.tabBar(this.TabBarBuilder('收件箱', '📬', 0))
|
||||
|
||||
TabContent() {
|
||||
SessionsTab()
|
||||
}
|
||||
.tabBar(this.TabBarBuilder('会话', '💬', 1))
|
||||
|
||||
TabContent() {
|
||||
ContactsTab()
|
||||
}
|
||||
.tabBar(this.TabBarBuilder('联系人', '👤', 2))
|
||||
.tabBar(this.TabBarBuilder('联系人', '👤', 1))
|
||||
}
|
||||
.onChange((index: number) => {
|
||||
this.currentIndex = index;
|
||||
|
||||
Reference in New Issue
Block a user