polish: 集成 review 反馈 + 用户新 surface 的 Windows 视觉修复

review 阶段标的 4 个 High 全部处理,加上用户在 v1.3.1-4 装机后新报的 2 个
Windows 视觉问题,本 commit 一次性收:

[review-capsule High]
- Capsule.tsx 抽 EXIT_ANIM_MS / INITIAL_VISIBLE_STATE 两个 const,去掉
  `240` magic number + `isTauri ? 'idle' : 'recording'` 字面量重复
- 修正退出动画调度的 race 描述注释(cleanup clearTimeout 阻止 fire,
  不是"定时器仍会触发")

[review-qwen3 High]
- LocalAsr.tsx Windows Qwen3 disabled wrapper 加 `inert` HTML5 属性,
  让键盘 Tab 也跳过 disabled 区域(仅靠 pointerEvents:none 不挡键盘 +
  aria-disabled 不传给后代)。两个 wrapper 都加。@ts-expect-error
  注释解释为何这里需要绕过 TS lib.dom 旧版未收录 inert 属性。

[review-icons 2 High]
- Icon.tsx `overview`: 在三个 dashboard tile 顶部加 sparkline 数据线
  区分 layout icon(之前几乎一样,14px nav 视觉无法分辨)
- Icon.tsx `selectionAsk`: 气泡尾巴 y=24 → y≈23,避免 strokeLinecap
  round + strokeWidth 1.5 出 viewBox 底边被裁

[用户新 surface — Vocab 词条大椭圆]
- Vocab.tsx VocabChip 加 `alignSelf: 'flex-start'`。真根因(之前 PR
  #422 误判为 button user-agent appearance):父 flex 容器 minHeight: 80
  让 flex item 默认 align-self: stretch 把单个 chip 拉伸到 80px 高,
  borderRadius:999 渲染成超大椭圆。alignSelf:flex-start 阻止拉伸。

[用户新 surface — 顶栏 vs 玻璃色差]
- lib.rs DWMWA_CAPTION_COLOR 从纯白 0xFFFFFF → 0x00F7F5F5
  (rgb 245,245,247) 跟 WindowChrome glass linear-gradient 起始色一致
- WindowChrome.tsx glass alpha 0.78 → 0.92(PR #422 为加强玻璃感降的,
  现在为压缩色差升回去,玻璃感由 backdrop-filter blur+saturate 提供)

后续:用户希望调研"完全取消 Windows 原生顶栏(像微信那样自绘)"——
属于 v1.3.1-6 级别 large rewrite,单独追。
This commit is contained in:
baiqing committed 2026-05-12 17:31:41 +08:00
1 parent f44eb5958c
commit 35d53b8be2
6 files changed
+39 -17

No files matched your search

+6 -4
View File
@@ -627,14 +627,16 @@ fn apply_windows_caption_color<R: Runtime>(window: &tauri::WebviewWindow<R>) {
};
let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void);
// COLORREF 0x00BBGGRR 编码——白色就是 0x00FFFFFF。
let white: u32 = 0x00FFFFFF;
// COLORREF 0x00BBGGRR 编码——选用 rgb(245,245,247) 跟 WindowChrome 的 glass linear-gradient
// 起始色一致,减小原生 caption bar 跟应用磨砂玻璃的色差(用户反馈:纯白 caption + 半透灰 glass
// 色差很丑)。R=0xF5 G=0xF5 B=0xF7 → COLORREF = 0x00F7F5F5。
let glass_match: u32 = 0x00F7F5F5;
unsafe {
if let Err(e) = DwmSetWindowAttribute(
hwnd,
DWMWA_CAPTION_COLOR,
&white as *const _ as *const core::ffi::c_void,
std::mem::size_of_val(&white) as u32,
&glass_match as *const _ as *const core::ffi::c_void,
std::mem::size_of_val(&glass_match) as u32,
) {
log::warn!("[main] set caption color failed (likely pre-22H2 Win): {e}");
}
+14 -7
View File
@@ -285,11 +285,18 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }:
);
}
// 与 @keyframes capsule-out 的 0.24s 时长一致——必须同步,否则定时器先于
// 动画结束就 unmount → 用户看到半截动画被截断。
const EXIT_ANIM_MS = 240;
// 初始可见 state:Tauri 内运行从 idle 开始(等后端 capsule:state 事件),
// 浏览器 dev 模式从 recording 开始以便直接看到胶囊。
const INITIAL_VISIBLE_STATE: CapsuleState = isTauri ? 'idle' : 'recording';
export function Capsule() {
const { t } = useTranslation();
const os = detectOS();
const metrics = getCapsulePillMetrics(os);
const [state, setState] = useState<CapsuleState>(isTauri ? 'idle' : 'recording');
const [state, setState] = useState<CapsuleState>(INITIAL_VISIBLE_STATE);
const [level, setLevel] = useState<number>(isTauri ? 0 : 0.6);
const [insertedChars, setInsertedChars] = useState<number>(0);
const [message, setMessage] = useState<string | undefined>();
@@ -297,10 +304,10 @@ export function Capsule() {
// `leaving` 与 `lastVisibleState` 协同实现「退出动画」:
// - 当 state 从非 idle 变成 idle 时,不立即卸载,而是把 leaving 置为 true 并保留
// 最后一帧的可见 state(lastVisibleState),让胶囊用 capsule-out 动画收缩淡出。
// - 动画结束(~240ms)后再把 leaving 置回 false,组件回到「真正未挂载」分支。
// - 动画结束(EXIT_ANIM_MS)后再把 leaving 置回 false,组件回到「真正未挂载」分支。
// - 若期间 state 又切回非 idle(例如用户连按热键),立刻中止 leaving 并恢复显示。
const [leaving, setLeaving] = useState<boolean>(false);
const [lastVisibleState, setLastVisibleState] = useState<CapsuleState>(isTauri ? 'idle' : 'recording');
const [lastVisibleState, setLastVisibleState] = useState<CapsuleState>(INITIAL_VISIBLE_STATE);
// Windows 端 host 在翻译模式从 84 长到 118;macOS / Linux 上 capsuleLayout 已固定 42 忽略此参数。
const hostMetrics = getCapsuleHostMetrics(os, translation);
@@ -327,12 +334,12 @@ export function Capsule() {
};
}, []);
// 退出动画调度:在 state 真正进入 idle 时,先用 capsule-out 播放 ~240ms,再卸载。
// 退出动画调度:在 state 真正进入 idle 时,先用 capsule-out 播放 EXIT_ANIM_MS,再卸载。
// 设计要点:
// 1. 进入非 idle:清掉 leaving,记录最新可见 state;
// 2. 进入 idle 且之前可见:开启 leaving 并启动定时器;
// 3. 期间又被打回非 idle:定时器仍会触发,但 effect 内会读到 state !== idle,
// 所以 cleanup 时直接 clearTimeout,避免错误地把可见状态切到 idle。
// 3. 期间又被打回非 idle:cleanup 直接 clearTimeout,定时器不会触发,
// 新一轮 effect 会立即恢复可见态,避免错误地把可见状态切到 idle。
useEffect(() => {
if (state !== 'idle') {
// 立即恢复可见,并取消上一轮可能挂着的离场。
@@ -346,7 +353,7 @@ export function Capsule() {
const timer = setTimeout(() => {
setLeaving(false);
setLastVisibleState('idle');
}, 240);
}, EXIT_ANIM_MS);
return () => clearTimeout(timer);
// 故意只依赖 state —— lastVisibleState / leaving 是内部派生量,
// 把它们加进依赖会让定时器被反复重建。
+4 -4
View File
@@ -4,8 +4,8 @@
import type { CSSProperties } from 'react';
export const ICONS: Record<string, string> = {
// overview — 仪表盘式分屏布局(顶部摘要条 + 左右两块小卡),传达"概览/总览"
overview: 'M4 5h16v5H4zM4 13h7v6H4zM13 13h7v6h-7z',
// overview — 仪表盘三卡 + 顶卡内 sparkline 数据线,区分纯三块拼图(layout)
overview: 'M4 5h16v5H4zM4 13h7v6H4zM13 13h7v6h-7zM6 7.8l2 1 2-1.5 2 1',
// history — 时钟表盘 + 左上角逆时针回拨箭头,强调"过去/回看"
history: 'M12 8v4l3 2M3.5 12a8.5 8.5 0 1 0 2.8-6.3L3 8M3 4v4h4',
// vocab — Feather 风格 open-book(书脊居中 + 左右两页),相比旧的"带书签的合上书"在 14px 下更易辨识
@@ -13,8 +13,8 @@ export const ICONS: Record<string, string> = {
style: 'M12 3a9 9 0 1 0 0 18 3 3 0 0 0 3-3v-1a2 2 0 0 1 2-2h1a3 3 0 0 0 3-3 9 9 0 0 0-9-9z',
// translate — 地球仪(圆 + 赤道 + 经线椭圆),通用的"语言/国际化"符号,比旧版"A+文+三角"在 14px 下更清晰
translate:'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c2.5 2.5 4 5.5 4 9s-1.5 6.5-4 9c-2.5-2.5-4-5.5-4-9s1.5-6.5 4-9',
// selectionAsk — 三行文本("被选中的内容")+ 右下角对话气泡("追问"),直白表达"对选中文字提问"
selectionAsk:'M3 5h12M3 9h12M3 13h7M14 14h6a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3l-3 3v-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2z',
// selectionAsk — 三行文本 + 右下角对话气泡(尾巴拉到 y≈23 防 viewBox 24 底边 stroke-cap 裁切)
selectionAsk:'M3 5h12M3 9h12M3 13h7M14 14h6a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3.5l-2.5 2v-2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2z',
settings:'M12 9.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1A2 2 0 1 1 7 4.9l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z',
help: 'M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z',
mic: 'M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3zM19 11a7 7 0 0 1-14 0M12 18v3M8 21h8',
@@ -39,11 +39,12 @@ export function WindowChrome({
// 两个平台用同一份半透明玻璃 background + backdropFilter,让 sidebar 透明地坐在
// 磨砂底板上时有可见的玻璃感。
// Windows: Tauri transparent:true + lib.rs apply_mica 提供 Win11 Mica 透出来;
// macOS: NSVisualEffectView 提供材质。alpha 0.78 比之前的 0.92 更明显但不过透。
// macOS: NSVisualEffectView 提供材质。
// alpha 0.92:和原生 Win11 caption(lib.rs 设为 rgb(245,245,247))色差最小,玻璃感仍可见。
const background = `
radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 60%),
radial-gradient(100% 70% at 100% 100%, rgba(37,99,235,0.07) 0%, rgba(37,99,235,0) 55%),
linear-gradient(180deg, rgba(245,245,247,0.78) 0%, rgba(232,232,236,0.78) 100%)
linear-gradient(180deg, rgba(245,245,247,0.92) 0%, rgba(232,232,236,0.92) 100%)
`;
return (
+8
View File
@@ -772,6 +772,10 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
)}
<div
aria-disabled={IS_WINDOWS || undefined}
// @ts-expect-error — `inert` 是 HTML5 标准属性(React 19+ 一类型,TS lib.dom 旧版未收录)。
// 跟 pointerEvents:none 配合让键盘 Tab 也跳过区域内所有 focusable 控件,避免 AT 用户
// 激活无效后端的 IPC(review-qwen3 High)。
inert={IS_WINDOWS || undefined}
style={
IS_WINDOWS
? { opacity: 0.5, filter: 'grayscale(0.4)', pointerEvents: 'none' as const }
@@ -877,6 +881,10 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
{(IS_MAC || IS_WINDOWS) && (
<div
aria-disabled={IS_WINDOWS || undefined}
// @ts-expect-error — `inert` 是 HTML5 标准属性(React 19+ 一类型,TS lib.dom 旧版未收录)。
// 跟 pointerEvents:none 配合让键盘 Tab 也跳过区域内所有 focusable 控件,避免 AT 用户
// 激活无效后端的 IPC(review-qwen3 High)。
inert={IS_WINDOWS || undefined}
style={
IS_WINDOWS
? { opacity: 0.5, filter: 'grayscale(0.4)', pointerEvents: 'none' as const }
+4
View File
@@ -450,6 +450,10 @@ function VocabChip({ entry, onRemove, onToggle }: VocabChipProps) {
return (
<span
style={{
// 父 flex 容器 minHeight: 80 会让 flex item 在 align-self 默认 stretch 下被拉到
// 80px 高,chip borderRadius: 999 把高度变大渲染成"超大椭圆"。alignSelf:flex-start
// 阻止拉伸,chip 始终保持 content 高度。
alignSelf: 'flex-start',
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '5px 10px 5px 12px',
borderRadius: 999,