Files
classhelper-island-refactor…/out/refactor-sections/07-island-components.md
T
ClassIsland Prompt Kit 22c1784bab feat: ClassIsland / ClassHelper 灵动岛提示词套件
- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构)
- ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本)
- 投喂指南、分卷、逐节文件
- 调研记录、一致性基线、生成与质检脚本
2026-11-12 21:59:58 +08:00

69 KiB
Raw Blame History

7. 岛结构与组件规格(ClassHelper 落地版)

本部分是「重构 ClassHelper 灵动岛」的视觉与组件规范。它不是新建工程的说明,而是对现有 packages/desktop-client 的改造指令:所有改动都必须落到既有文件(src/island/*、src/main/island.ts、packages/shared/src/*、src/renderer/views/settings/*)上,技术栈、构建方式、IPC 白名单、单窗口模型一律不变。

7.0 改造口径与文件级改动清单

总口径(必须遵守):

  1. 目标视觉默认改为 ClassIsland 规格(行高 40、行内边距 12、行距 5、圆角 8、背景不透明度 0.5、字号 14/16/18/20、进度条 3px、描边 1px、阴影 0 4 8 2 #48000000);ClassHelper 的 WinIsland 经典外观只作为 fixed 的兼容外观族保留(islandAppearance.family === 'winisland')。新安装默认 family='classisland';老用户升级保持 winisland,避免观感突变。
  2. family 只影响 fixed:classisland 下 fixed = ClassIsland 风格信息行(1 行 = 日期 + 课程表,行高 40、圆角 8、背景 0.5、底部 3px 进度条;消息在固定 40px 行内展开 ClassIsland 气泡,保留 ClassHelper 既有暂存/紧急/叫人/已读/去重规则);winisland 下 fixed = 旧"胶囊 + 详情卡"(零改动)。custom 始终使用 ClassIsland 规格,不提供 winisland 回退(custom 现有 rows/nodes 结构本就接近 ClassIsland,只做细化与像素对齐,见 7.2、7.4)。
  3. 不新增第二个窗口、不新增框架、不改服务端:LayoutIslandApp.vue 仍是唯一渲染入口,main/island.ts 仍是唯一窗口持有者(packages/desktop-client/src/island/LayoutIslandApp.vue:1-76)。
  4. 消息/提醒/课程提醒的业务语义(优先级、去重、上课暂存、紧急展开、已读回执)归第 8 部分;本部分只规定它们在岛上的容器、尺寸、颜色、动画。
  5. 所有像素值以本文与第 6 部分 tokens 为准;实现时不得再保留"ClassIsland 用 border-radius、ClassHelper 用 squircle"这种分歧——fixed 由 appearance.family 决定走哪套,custom 固定走 ClassIsland 套;代码里两套都在,但同一时刻只启用一套。

必须改动的文件(按改动类型):

文件 改动类型 内容
packages/shared/src/constants.ts 扩展 新增 ISLAND_FAMILIES + ISLAND_FAMILY_LABELS;DEFAULT_ISLAND_APPEARANCE 增加 family: 'classisland';新增 ClassIsland 规格常量(行高/内边距/行距/圆角/进度条/描边/阴影/字号)供渲染与主进程共用
packages/shared/src/island-layout.ts 扩展 组件 settings 增补 ClassIsland 语义字段(见 7.4);appearance 增补 customMargin(可选);normalizeIslandLayouts() 增加 family 迁移(老用户 → winisland)与规范化范围
packages/desktop-client/src/main/island.ts 改造 normalizeAppearance 接受 family;windowBox 的 fixed/custom 包围盒按 7.1.2 调整;updateResidentPointer 的淡出比例沿用 shared 常量;新增 island:report-layout-metrics 承载"内容最大高"
packages/desktop-client/src/island/IslandApp.vue 改造 增加 data-family="classisland" 分支:fixed 改为"信息行 + 行内气泡"(见 7.3.2);winisland 分支保持原样
packages/desktop-client/src/island/CustomIslandRuntime.vue 改造 custom 常驻卡改为 ClassIsland 行容器(背景/圆角/内边距/行距/进度条/淡出);classisland 分支的 border-radius 与行背景
packages/desktop-client/src/island/ComponentNode.vue 改造 组件内边距 6 → 0(改由行 padding 承担);字号 token 改为 ClassIsland 的 14/16/18/20;新增 schedule 的 ClassIsland 折叠规则与进度条 3px;新增 countdown 的 ClassIsland 文案
packages/desktop-client/src/island/WeatherBrief.vue 改造 按 ClassIsland 天气简报的图标/数值排版(见 7.4.3)
packages/desktop-client/src/island/classic-island.css 扩展 新增 [data-family='winisland'] 下的三套 data-style 变量组保持现值;classisland 的行/气泡样式写进 custom-runtime.css
packages/desktop-client/src/island/custom-runtime.css 改造 行/背景/进度条/淡出/编辑器选中框的 ClassIsland 规格
packages/desktop-client/src/island/custom-island.css 改造 .custom-card 的 ClassIsland 背景/圆角/行距变量
packages/desktop-client/src/island/LessonReminderContent.vue 复用 其 5 段斜切遮罩算法必须提取为共享遮罩组件,供通知遮罩复用(见 7.2.6)
packages/desktop-client/src/renderer/views/settings/IslandSettingsView.vue 改造 新增"外观族"下拉(绑定 appearance.family,默认 classisland,仅 layout='fixed' 可见);原"视觉风格"下拉仅在 fixed+winisland 下可见;custom 下两项都隐藏;圆角/行距/行高滑块范围按 ClassIsland 对齐
packages/desktop-client/src/renderer/views/settings/IslandEditorView.vue + island-editor.css + IslandEditorNode.vue 改造 选中框改 1.5px 虚线、行高与块级尺寸按 ClassIsland(见 7.5)
packages/desktop-client/src/renderer/components/IslandComponentSettings.vue 改造 组件设置项按 7.4 的字段表补充(隐藏规则、宽度、对齐、边距、字体覆盖)

不得改动:main.ts 的零依赖策略(packages/desktop-client/src/island/main.ts:6-11)、preload/island.ts 的白名单结构(:14-62)、spring.ts 的弹簧参数(spring.ts:26-30)、squircle.ts 的超椭圆参数(squircle.ts:21-24)。

7.1 岛窗口与几何

7.1.1 窗口创建参数(保持不变,只能增加)

沿用现有 BrowserWindow 配置(packages/desktop-client/src/main/island.ts:480-512),并逐条核对:

  • frame: false、transparent: true、resizable: false、movable: false、minimizable/maximizable/fullscreenable: false(:484-490)。
  • skipTaskbar: true、hasShadow: false(:491-492)——阴影由 CSS/SVG 画,不得用系统窗口阴影。
  • focusable: true 必须保留(:502):Windows 下关掉可激活性会丢 mousedown,岛点不动(:493-501 的实机结论)。"不抢焦点"靠 showInactive() + 收起时 blur() 实现(applyFocusable,:1663-1672)。
  • alwaysOnTop: true + setAlwaysOnTop(appearance.alwaysOnTop, 'screen-saver') + setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })(:503, 514-515)。
  • type: 'toolbar'、setIgnoreMouseEvents(true, { forward: true })(:504, 527)。
  • webPreferences: contextIsolation: true、sandbox: true、backgroundThrottling: false、preload 指向 island.js(:505-511)。
  • 禁止:win.setBackgroundMaterial() / vibrancy / backgroundMaterial(:523-524 的历史结论;逐像素透明是硬约束)。禁止 setContentProtection(岛不是隐私窗口)。
  • 导航与新窗口一律拒绝(:516-522)。

7.1.2 窗口几何:fixed(两种外观族)/ custom

fixed 的两种口径(由 appearance.family 决定,不是同一个形态)

fixed 只表示"岛按经典交互工作(收到消息才出现)",它的外观由 family 决定:

A. family='classisland'(新安装默认):ClassIsland 风格信息行

  • 形态重定义:fixed 不再是小胶囊,而是一条 ClassIsland 风格的信息行——默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、1px 描边、阴影 0 4 8 2 #48000000、底部 3px 进度条(课程进度 / 提醒倒计时)。
  • 隐藏语义不变:无消息时仍按现有 state.mode='hidden' 隐藏窗口(或按 idleSliver 留细缝,main/island.ts:1483-1496);消息到来才显示行。
  • 消息到来:在同一行内、行高 40px 的固定容器里渲染 ClassIsland 风格气泡——气泡不是"把行撑高",而是整块 40px 行换上遮罩层 + 内容层 + 进度层(见 7.2.8)。行高恒为 40,禁止临时增高到 64 等(--ci-island-height: 40px 不得被气泡改写)。保留 ClassHelper 既有业务规则(普通上课暂存、紧急立即展开、普通叫人可点、已读回执、按 id 去重),业务规则归第 8 部分,视觉归 7.2.8。
  • 窗口几何(固定包围盒,开合时不动):
    • 宽 = 行内容自然宽 + 2 × 12(行内边距)+ 2 × ISLAND_SHADOW_PAD(=10,packages/shared/src/constants.ts:351);
    • 高恒为 40 + 2 × ISLAND_SHADOW_PAD(无消息 / 有消息都一样;气泡在行内,不改变窗口高度);
    • 只在"行宽跨档"、显示器/DPI 变化或"窗口不可见"时 setBounds(沿用 applyWindowLayout 的比较逻辑,main/island.ts:1598-1612)。
  • 行宽/内容宽必须由渲染层测量后上报:reportSize({ width, height })(CustomIslandRuntime.vue:255)+ setHitRect(IslandApp.vue:421-454),主进程不得自行猜内容尺寸。
  • 气泡宽度上限 720【推断:按 1080p 下 ClassIsland 岛常用宽度取整】;超宽时课程表按 settings.count 截断(7.4.5),气泡正文最多 2 行(7.2.8)。
  • 鼠标交互:行上可点(展开/收起气泡、按钮);点行外空白 → 收起气泡(不改变 hidden 语义)。

B. family='winisland'(老用户升级默认):胶囊 + 详情卡

  • 保留现有全部几何:windowBox() 普通模式取 max(BBOX_SIZE, 当前形态 + 2*pad)(main/island.ts:1543-1548);触摸模式取"当前形态 + 2*pad"并贴合岛体(:1549-1552)。
  • 胶囊 = appearance.width × height(默认 268×44,constants.ts:266-267),展开/紧急/叫人 = 胶囊 + ISLAND_SIZE_DELTA(:344-348);岛本体尺寸由渲染层弹簧逼近(IslandApp.vue:216-220, 238-264)。
  • 圆角 = winisland 规格(胶囊 h/2、展开卡 min(48×radius/20, w/2, h/2)),背景 = 纯黑/毛玻璃/主题色渐变三选一。

custom(常驻):固定画布 + 内容自然测量(始终 ClassIsland 规格,与 family 无关)

  • 外观不走 family:custom 无论 family 是 classisland 还是 winisland,都按 7.2 的 ClassIsland 行/背景/进度条/遮罩规格渲染;family 只决定 fixed 用哪种外观(7.3.1)。设置页在 layout='custom' 时不得展示/生效 family 与 winisland 色调选项。
  • 现在的窗口画布已经是"固定画布":windowBox() 在 residentEnabled 时返回 min(960, area.width) × min(640, max(60, area.height - marginY))(main/island.ts:1522-1529),这与 ClassIsland 的固定包围盒思路一致,保留。
  • 目标态改动:画布高度上限改为"内容上限"而不是固定 640——渲染进程上报 reportLayoutMetrics({ contentMaxHeight })(新增 IPC),主进程取 min(area.height - marginY, contentMaxHeight + 2*pad);默认 contentMaxHeight = 常态行数 × (40 + 行距) + 24。理由:ClassIsland 的岛高度只由行决定(repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:916),640 会让屏幕上方出现一块"看不见但可能吃掉点击"的余量(虽然命中已按 hitRect 控制,但窗口越大越依赖 hitRect 的准确性)。
  • available(渲染层可用尺寸)保持现值算法:innerWidth/innerHeight - 2*ISLAND_SHADOW_PAD - 4(CustomIslandRuntime.vue:260-266),最小值 40。
  • 内容缩放:fitted() 取 min(scheme.scale, available.width/natural.width, available.height/natural.height)(:72-80),保留;scheme.scale 范围 0.75–2.5(island-layout.ts:370-375)。
  • customSize 初值 424×80(main/island.ts:261)保留,仅作为首帧占位;首次 reportSize 后覆盖。

7.1.3 锚点、停靠与多显示器

必须沿用现有算法(main/island.ts:1331-1351),逐条核对:

  • 停靠位置 6 值:top-center / top-left / top-right / bottom-center / bottom-left / bottom-right(constants.ts:305-312)。
  • 垂直:vMode = position.startsWith('bottom') ? 'bottom' : 'top';vValue = bottom ? area.y + area.height - marginY : area.y + marginY(:1341-1342)。
  • 水平:hMode = endsWith('left') ? 'left' : endsWith('right') ? 'right' : 'center';hValue = left ? area.x + marginX : right ? area.x + area.width - marginX : round(area.x + area.width/2)(:1343-1349)。
  • 居中停靠不受 marginX 影响(docs/dev/winisland-design-tokens.md:26),只有左右停靠用水平边距。
  • 锚点全部取整(Math.round),避免 0.5px 抖动(:1325-1330)。
  • 窗口定位时窗口比岛大 ISLAND_SHADOW_PAD,因此要再偏移一个 pad,让岛的边/中心落在锚点上(windowBounds,:1615-1648):左停靠 x = hValue - pad、右停靠 x = hValue + pad - width、居中 x = hValue - round(width/2);y = bottom ? vValue + pad - height : vValue - pad。
  • 多显示器:targetDisplay() = followCursorDisplay ? getDisplayNearestPoint(cursor) : getPrimaryDisplay()(:1354-1358);换屏检测挂在既有 60ms 光标轮询上(syncAnchorDisplay,:1370-1379),不得新增定时器。
  • DPI:Electron 的 BrowserWindow.setBounds 用 DIP,渲染层 CSS px 与 DIP 一一对应,禁止在渲染层再乘 devicePixelRatio 或 zoomFactor;appearance 里的宽高/边距全部是 DIP(constants.ts:264-290 的注释口径)。
  • 目标态新增:ClassIsland 的"竖直安全区"(Fluent 20px / Classic 0,repos/ClassIsland/ClassIsland/App.Services.xaml.cs:314)在 ClassHelper 里以 P2 / 可选实现——仅当 appearance.family === 'classisland' 且设置项开启时,把锚点向屏幕内侧再推 safe 像素;默认 0,不得改变现有视觉位置。

7.1.4 点击穿透、命中与触摸

这是岛最容易出 regress 的部分,必须逐条保持:

  1. 窗口默认 setIgnoreMouseEvents(true, { forward: true })(main/island.ts:527)。
  2. 渲染层只上报"进入岛体"(setInteractive(true)),不上报关闭;关闭由主进程按真实光标轮询决定(IslandApp.vue:456-487 的整段说明)。理由:两边都下发会让窗口在"接收/穿透"之间每几毫秒翻转,表现为"点了没反应"。
  3. 命中矩形 hitRect:渲染层用 .island-card/.resident-card 的 getBoundingClientRect() 上报(IslandApp.vue:421-454、CustomIslandRuntime.vue:200-213),限流 50ms 且尾随发送一次;主进程按 60ms 兜底校正(main/island.ts:1824-1836)。
  4. 触摸模式:渲染层按 navigator.maxTouchPoints > 0 上报 setTouchMode(true)(IslandApp.vue:546-549),主进程切到"窗口贴合岛体 + 始终接收输入"(main/island.ts:1891-1899, 1795-1807)。触摸模式的窗口只增不减,收小要等窗口隐藏后再做(syncTouchLayout,:1560-1578)。
  5. 失焦宽限:BLUR_GRACE_MS = 500、触摸/编辑态 BLUR_GRACE_TOUCH_MS = 1200(main/island.ts:2287, 2297);blur 时若指针仍在岛上则忽略(:565-578)。
  6. 常驻模式的指针避让淡化:residentFaded 由 updateResidentPointer() 计算(:1851-1877),触发条件包含"非编辑、非课程提醒、非展开、非触摸操作后 1s、未悬停铃铛",淡化比例 = CLASSISLAND_ISLAND_METRICS.fadedOpacity = 0.05(island-layout.ts:35)。
  7. 编辑预览时不创建额外桌面窗口(docs/app/island.md:47),编辑态由 island:set-editing 控制 setInteractive(true)(main/island.ts:1430-1432)。

7.1.5 窗口护栏(不得删减)

  • 渲染进程心跳:渲染层每 5s alive()(LayoutIslandApp.vue:36),主进程 RENDERER_ALIVE_TIMEOUT_MS = 20_000(main/island.ts:2315),超时隐藏并重建窗口,重建限流 3 次 / 10 分钟(:2317-2318)。
  • 淡入淡出:步进 0.16 / 0.2、16ms 一步(fadeIn/fadeOut,:1674-1710);appearance.animations === false 时直接落位。
  • 窗口只在"尺寸/位置设置变化"或"不可见"时重排(applyWindowLayout 前先比较 4 个值,:1598-1612);禁止在开合动画中改窗口几何(:1477-1479 的注释)。
  • suspended(被 custom 或课件覆盖时)必须停掉经典岛的测量与命中(IslandApp.vue:238-246 的 syncTargets 早退)。

7.2 行、背景、进度条、遮罩与淡出(ClassIsland 规格:custom 恒定 / fixed 默认)

7.2.1 行容器 DOM(目标态)

CustomIslandRuntime.vue 的常态页与展开页都必须渲染成下面这棵树(现状见 :396-504,需按此重排):

<div class="resident-window" data-anchor="left|center|right" data-bottom="true|false" data-family="classisland" :data-style="appearance.style">
  <section class="custom-card resident-card island-card">
    <svg class="shape" v-if="appearance.family === 'winisland'"><path :d="path" /></svg>
    <!-- classisland:不用 squircle 形状层,圆角由 CSS border-radius 承担(见 7.2.2) -->
    <span class="tint" aria-hidden="true"></span>
    <div class="resident-page resident-compact-page">
      <div class="resident-source resident-compact">
        <div class="layout-rows" style="gap: var(--ci-line-gap)">
          <section class="layout-row" data-row="main">
            <div class="row-content">
              <div class="row-components" style="gap: var(--ci-component-gap)">
                <ComponentNode … />
              </div>
            </div>
          </section>
        </div>
        <button class="resident-bell">…</button>       <!-- 消息入口,见 7.4.12 -->
      </div>
    </div>
    <div class="resident-page resident-detail-page">…</div>
    <div class="resident-page resident-lesson-page">…</div>
  </section>
</div>

强制要求:

  • 行容器类名保持 .layout-row / .row-content / .row-components(现有 CSS 已依赖,custom-island.css:145-164、custom-runtime.css:98-121),不要改名;新增的规格通过 CSS 变量注入。
  • .layout-rows 的 gap 从 scheme.spacing 改为 var(--ci-line-gap)(默认 5px,island-layout.ts:34),scheme.spacing 仅作为覆盖值(编辑器"行间距"仍是它)。
  • 组件列表的 gap 从 row.gap 改为 var(--ci-component-gap)(默认 12px)。语义映射:ClassIsland 的相邻组件间距 = 两个 Margin="6 0" 之和 = 12(repos/ClassIsland/ClassIsland/MainWindow.axaml:396)。因此 row.gap 的默认值应从 12 改为 12 并作为覆盖(island-layout.ts:343 默认 8 → 改为 12),范围 0–64 保留。

7.2.2 行的像素规格(ClassIsland 规格)

必须写死为 CSS 变量并在两个 CSS 文件里引用(custom-runtime.css 为主):

.resident-window {
  --ci-line-height: 40px;        /* ClassIsland IslandContainerHeight,MainWindowLine.axaml:16 */
  --ci-line-pad-x: 12px;         /* GridContentRoot Margin="12 0",FluentTheme/Styles.axaml:110 */
  --ci-line-gap: 5px;            /* MainWindowLineVerticalMargin 默认 5,Models/Settings.cs:219 */
  --ci-component-gap: 12px;      /* 组件 Margin="6 0" ×2,MainWindow.axaml:396 */
  --ci-radius-island: 8px;       /* RadiusX 默认 8,Models/Settings.cs:183 */
  --ci-island-opacity: 0.5;      /* Opacity 默认 0.5,Models/Settings.cs:64 */
  --ci-line-opacity: 1;          /* 行整体不透明度默认 1.0,MainWindowLineSettings.cs:42 */
  --ci-stroke-island-w: 1px;     /* line-background BorderThickness=1,FluentTheme/Styles.axaml:47 */
  --ci-shadow-island: 0 4px 8px 2px #48000000;   /* FluentTheme/Styles.axaml:49 */
  --ci-progress-height: 3px;     /* 本轮裁决,所有进度条共用 */
  --ci-fade-ratio: 0.05;         /* CLASSISLAND_ISLAND_METRICS.fadedOpacity,island-layout.ts:35 */
}

行本体:

.layout-row {
  min-height: var(--ci-line-height);
  padding: 0 var(--ci-line-pad-x);       /* ClassIsland 的 12px 内边距搬到行上 */
  box-sizing: border-box;
  display: flex;
  align-items: center;
  opacity: var(--ci-line-opacity);
}
.row-components {
  display: flex;
  align-items: center;
  gap: var(--ci-component-gap);
  min-height: var(--ci-line-height);
}

方案级覆盖:第 5、13 部分在 scheme 上新增 rowHeight / componentGap / showProgress 三个字段(默认 40 / 12 / true)。渲染时按"scheme 覆盖 > 外观族默认"写入 CSS 变量:

.resident-card {
  --ci-line-height: v-bind('scheme.rowHeight ? scheme.rowHeight + "px" : "40px"');
  --ci-component-gap: v-bind('scheme.componentGap + "px"');
}

showProgress=false 时所有行内进度条(课程进度、倒计时进度)隐藏;行距仍是 --ci-line-gap(默认 5,来自 scheme.spacing,island-layout.ts:34)。

背景(两种形态都要):

/* 整块岛背景(separated=false) */
.resident-card {
  background: var(--ci-bg-secondary);                    /* SolidBackgroundFillColorSecondaryBrush */
  border: var(--ci-stroke-island-w) solid var(--ci-stroke-island);
  border-radius: var(--ci-radius-island);
  box-shadow: var(--ci-shadow-island);
  opacity: var(--ci-island-opacity);                     /* 只作用背景,不作用文字 */
}
/* 分体(scheme.separated=true):每行一个独立胶囊 */
.resident-card.resident-separated { background: transparent; box-shadow: none; border: 0; padding: 0; }
.resident-separated .layout-row {
  background: var(--ci-bg-secondary);
  border: var(--ci-stroke-island-w) solid var(--ci-stroke-island);
  border-radius: var(--ci-radius-island);
  box-shadow: var(--ci-shadow-island);
  opacity: var(--ci-island-opacity);
  padding: 0 var(--ci-line-pad-x);
}

硬性要求:背景不透明度只允许作用在背景层;文字/图标必须 100% 不透明(ClassIsland 把 Opacity 绑在 Border.line-background 上,repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:44)。现有实现里卡片 opacity 由主进程 win.setOpacity() 统一控制(main/island.ts:1440-1442),不得把背景不透明度也塞进窗口 opacity——必须拆成"窗口 opacity(用户可调 0.4–1)+ 背景层 alpha(0.5)"两层。

7.2.3 组件内边距的归零

现在 ComponentNode 自身有 padding: 6px(ComponentNode.vue:295-301),行 .row-components 的 gap 又是 row.gap,两者叠加会让实际间距变成 gap + 12。ClassIsland 是"组件 0 内边距 + 行 12px 内边距 + 组件 6/6 外边距"。

改造:

.component-node { padding: 0; }                 /* 由行 padding 承担左右留白 */
.row-components { gap: var(--ci-component-gap); }  /* 12px */

同时在 ComponentNode 上移除 overflow: hidden(ComponentNode.vue:296)→ 改为 overflow: visible,否则倒计时进度环、天气预警告牌会被裁掉;裁切交给行(clip-path 或 overflow: hidden 挂在 .layout-row)。

7.2.4 进度条

三条进度条统一 3px、border-radius: 1.5px、贴底、transform-origin: left:

位置 现状 目标
课程行内进度 .lesson-progress { height: 2px; background: #ffffff24 }(ComponentNode.vue:348-355) height: var(--ci-progress-height);底色 rgba(255,255,255,0.08);填充 var(--ci-accent)
课程提醒卡进度 .ci-lesson-progress { height: 2px }(LessonReminderContent.vue:307-316) 同上,高度 3px
通知遮罩进度(新增) 无 同 7.2.6,贴在遮罩层底部

过渡:课程进度 transition: transform 1s linear(ComponentNode.vue:361)保留;提醒进度 50ms linear(LessonReminderContent.vue:315)保留;通知倒计时进度 100ms linear(prefers-reduced-motion 时 0)。

7.2.5 淡出与透明度

  • 整窗淡入淡出:主进程步进 0.16/0.2 × 16ms(main/island.ts:1674-1710)保留。
  • 指针避让淡化:residentFaded → win.setOpacity(displayOpacity × 0.05)(:1440-1442 + island-layout.ts:35)保留;必须保证展开态、编辑态、课程提醒期间不淡化(:1864-1873)。
  • 行级淡出(新增,P2):ClassIsland 的 IsLineFaded 是 0.05(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:73-75);ClassHelper 已有"整窗淡化",默认不叠加行级淡化,仅当 pointerFadeReversed 语义需要时用行级 opacity 过渡 100ms 实现。

7.2.6 斜切遮罩(通知遮罩必须复用课程提醒的实现)

ClassIsland 的遮罩是 5 段斜切,参数为:区段权重 [0, 0.1, 0.2, 0.4, 0.2, 0.1]、60° 斜面、区段 360ms、错峰 120ms、开 QuarticEaseOut / 关 QuadraticEaseIn、文字 delay 260ms + 250ms 淡入 + scale 1.1→1.0 / 750ms(repos/ClassIsland/ClassIsland/Controls/SlantedMaskControl.cs:177-200, 284-308;行模板 XamlThemes/FluentTheme/Styles.axaml:329-367)。

现状:这套算法已经存在于 LessonReminderContent.vue:59-102,并且参数完全对得上——edges = [0, 0.1, 0.3, 0.7, 0.9, 1](等价权重 0/0.1/0.2/0.4/0.2/0.1)、offset = tan(30°) × 40、delays = open ? [0,90,120,90,0] : [120,90,0,90,120]、ease = 1-(1-p)^4 / p²、总时长 480ms、兜底 600ms。

改造要求:

  1. 把 LessonReminderContent.vue 的遮罩部分抽成 packages/desktop-client/src/island/SlantedMask.vue(props:width、height、open: boolean、accent: string、motion: boolean、speed: number),LessonReminderContent 与新的通知遮罩共用。
  2. 通知遮罩的 DOM 必须与 ClassIsland 对齐:
<div class="ci-mask-layer" :class="{ open }">
  <svg class="ci-mask" :viewBox="`0 0 ${width} 40`" preserveAspectRatio="none">
    <polygon v-for="(p, i) in polygons" :key="i" :points="p" />
  </svg>
  <div class="ci-mask-title" :class="{ 'ci-title-in': open }">…通知标题…</div>
</div>
  1. 遮罩填充色 = var(--ci-accent-fill)(ClassIsland 用 AccentFillColorDefaultBrush);文字色 = var(--ci-text-on-accent)。
  2. prefers-reduced-motion 或 appearance.animations === false 时直接落位(regions 全部 0/1),不做逐帧插值(LessonReminderContent.vue:77-80 已有该分支,保留)。
  3. 不得给遮罩层加 filter: blur() 或 backdrop-filter(透明窗口下不可靠)。

7.2.7 行/组件隐藏规则

ClassHelper 现有的 advanced.hideRules 只有三值:in-class / no-message / no-class(island-layout.ts:101-106,判定在 ComponentNode.vue:108-120)。目标是 ClassIsland 的规则集(HideMode 基本/高级、HideOnClass、HideOnMaxWindow、HideOnFullscreen、规则集编辑器,repos/ClassIsland/ClassIsland/Models/Settings.cs:185, 418-505)。

本轮落地口径:

  • 保留现有三值并保证语义不变(in-class ↔ 上课隐藏、no-message ↔ 无消息隐藏、no-class ↔ 无课程隐藏)。
  • 新增(v2 schema,见第 5 部分):hideRules 允许字符串扩展 'max-window' | 'fullscreen',判定数据由主进程通过 IslandState 新增字段 foreground: { maximized: boolean; fullscreen: boolean } 下发(主进程已有前台窗口检测能力,repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:444-457 为语义参考)。
  • 行级隐藏(ClassIsland 的 MainWindowLineSettings.HideOnRule)本轮只做"行整体可隐藏",P1;规则集编辑器(RulesetControl)不在本轮范围,P2。
  • 编辑器必须能编辑 hideRules(现有三值)与 align/width/maxWidth;新增两项后要同步 IslandComponentSettings.vue 与 normalizeIslandLayouts 的白名单(island-layout.ts:321-328)。

7.2.8 消息气泡(family='classisland' 的 fixed 模式,40px 行内)

硬性约束:气泡不得增高行、不得增高窗口。 行高恒 40px(--ci-island-height: 40px 不得被气泡改写),气泡的遮罩、内容、进度条全部在这个 40px 容器内完成。

DOM(叠在行内容之上,position: absolute; inset: 0):

<div class="ci-row-shell">                       <!-- height: 40px; position: relative -->
  <div class="layout-row">…常态行组件…</div>       <!-- 气泡展开时 opacity → 0(不卸载,保留宽度测量) -->
  <div class="ci-row-bubble" v-if="mode==='expanded'">   <!-- inset: 0; border-radius: 8px; overflow: hidden -->
    <SlantedMask class="ci-bubble-mask" :open="true" :width="rowWidth" :height="40" />
    <div class="ci-bubble-row">                   <!-- display:flex; align-items:center; height:40px; padding: 0 12px -->
      <span class="ci-bubble-icon">…</span>       <!-- 16×16 线性图标 -->
      <span class="ci-bubble-badge">紧急</span>    <!-- 可选;高 18、圆角 9、字号 11 -->
      <div class="ci-bubble-text">                <!-- min-width:0; flex:1; display:flex; flex-direction:column; justify-content:center -->
        <span class="ci-bubble-title">…单行省略…</span>
        <span class="ci-bubble-body">…最多 2 行…</span>
      </div>
      <span class="ci-bubble-actions">…按钮…</span>
    </div>
    <div class="ci-bubble-progress" :style="{ transform: `scaleX(${leftProgress})` }" />
  </div>
</div>

CSS 规格:

.ci-row-bubble { position: absolute; inset: 0; border-radius: var(--ci-radius-island); overflow: hidden; }
.ci-bubble-text { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 0; }
.ci-bubble-title {
  font-size: var(--ci-fs-body);          /* 16px */
  font-weight: 600; line-height: 20px;   /* 40px 行内两行:20 + 18 */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ci-bubble-body {
  font-size: var(--ci-fs-secondary);     /* 14px */
  line-height: 18px; color: var(--ci-text-2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;                       /* 正文最多 2 行,超出省略 */
}
.ci-bubble-progress {
  position: absolute; left: 0; bottom: 0; width: 100%; height: var(--ci-progress-height);
  background: var(--ci-accent-fill); transform-origin: left;
  transition: transform 50ms linear;
}

行内的排版预算:标题 1 行(20px)+ 正文 2 行(18×2=36)已超过 40px,因此实际规则是——标题占用 1 行时正文最多 1 行;无标题时正文最多 2 行;两种都靠 -webkit-line-clamp + 固定行高把内容锁在 40px 内,不得用 overflow: visible 让文字溢出到行外。

动画(严格按 ClassIsland 原版参数):

阶段 属性 时长 / 延迟 缓动
遮罩斜掠(开) 5 段 scaleX 0→1 每段 360ms,错峰 0/90/90/120/120ms cubic-bezier(0.165,0.84,0.44,1)(QuarticEaseOut)
遮罩斜掠(关) 5 段 scaleX 1→0 每段 360ms,反向错峰 cubic-bezier(0.55,0.085,0.68,0.53)(QuadraticEaseIn)
气泡文字淡入 opacity 0→1 250ms,delay 260ms cubic-bezier(0.25,1,0.5,1)
气泡文字缩放 scale 1.1→1.0 750ms,delay 260ms cubic-bezier(0.25,1,0.5,1)
气泡文字淡出 opacity 1→0 200ms 线性
文字缩回 scale 1.0→1.1 200ms cubic-bezier(0.5,0,0.75,0)
常态行淡出 opacity 1→0 200ms SineEaseIn(同 ClassIsland 的 GridContentRoot 过渡)
进度条 scaleX 50ms 线性

以上参数与 ClassIsland 完全一致(repos/ClassIsland/ClassIsland/Controls/SlantedMaskControl.cs:177-200;XamlThemes/FluentTheme/Styles.axaml:329-367)。

Classic 主题(appearance.theme='classic'):遮罩改为 translateY(-50px → 0)、200ms QuadraticEaseIn;退出 0 → +50px、delay 100ms、200ms QuadraticEaseOut;文字 200ms delay / 100ms(XamlThemes/ClassicTheme/Styles.axaml:265-331)。

禁止:islandListLayout() 的高度再参与 fixed 气泡(它只用于 custom 详情页与 winisland 详情卡);把气泡做成"行下方新增一块 64px 高的区域"。

7.3 外观族:family='classisland'(默认)与 family='winisland'(兼容,仅 fixed)

适用范围(硬性):appearance.family 只影响 fixed 模式的外观;custom 始终按 ClassIsland 规格渲染(7.2、7.4),不提供 winisland 回退。

7.3.1 数据与开关(字段名以第 5 部分为准)

新增顶层字段 appearance.family: 'classisland' | 'winisland',与既有 appearance.style(black/glass/tinted 色调)不是一个概念:

  • family 决定 fixed 长什么样、怎么排(信息行 vs 胶囊、圆角 8 vs squircle);对 custom 无意义;
  • style 只在 fixed + family='winisland' 下有意义(纯黑/毛玻璃/主题色渐变),其余情况忽略。

落地规则:

  1. packages/shared/src/constants.ts 新增 ISLAND_FAMILIES = ['classisland','winisland'] as const + ISLAND_FAMILY_LABELS(classisland: 'ClassIsland(默认)'、winisland: 'WinIsland 经典');ISLAND_STYLES(:324)与 ISLAND_STYLE_LABELS(:326-330)保持原样不动。
  2. DEFAULT_ISLAND_APPEARANCE 增加 family: 'classisland'(:265-290)——这是"新安装默认"。
  3. normalizeAppearance() 接受 family 白名单(main/island.ts:2244-2264),非法值回退 'classisland'。
  4. 升级迁移(必须与第 5 部分一致):旧配置没有 family 字段 → 迁移时写入 family: 'winisland'(老用户观感不变);新安装 → 'classisland'。迁移函数放在 packages/shared/src/island-layout.ts 的 normalizeIslandLayouts() 里,与 version 一并处理。
  5. 设置页拆分(IslandSettingsView.vue:291-305 现绑定 style):
    • "外观族"下拉绑定 appearance.family,仅在 layout='fixed' 时可见;
    • "视觉风格/色调"下拉仅在 layout='fixed' && family='winisland' 时可见;
    • layout='custom' 时两者都隐藏(custom 恒 ClassIsland 规格),只保留圆角/行高/行距/字号等 Scheme 级设置;
    • 切换必须即时生效并可回退(走既有 appearance 通道,preload/island.ts:48-49)。
  6. 主题轴(appearance.theme: 'fluent' | 'classic')由第 6 部分定义;本部分只要求"行背景色/描边/阴影/遮罩动画参数随 theme 切换",落点在 custom-runtime.css 的 [data-theme='classic'] 覆盖组(custom 与 fixed-classisland 共用)。

7.3.2 family='classisland'(默认,必须实现)

fixed 模式下(重定义后)= 信息行 + 行内气泡:

维度 规格 落点
形态 1 行信息行(默认 日期 + 课程表),不是胶囊 IslandApp.vue 新增 classisland 模板分支(见下方改造点)
行高 40 --ci-line-height
行内边距 左右 12 --ci-line-pad-x
圆角 8(矩形圆角,不用 squircle) border-radius: var(--ci-radius-island)
背景 不透明度 0.5 + 1px 描边 + 阴影 0 4 8 2 #48000000 7.2.2
进度条 底部 3px(课程进度 / 提醒倒计时) 7.2.4
字号 14 / 16 / 18 / 20 --ci-fs-*
消息 在固定 40px 行内渲染 ClassIsland 气泡:斜切遮罩(5 段)+ 标题 + 正文(最多 2 行省略)+ 底部 3px 倒计时进度条;不增高行、不增高窗口 7.2.8 + 第 8 部分
空闲 保持 hidden / idleSliver 语义(不改成常驻) main/island.ts:1483-1496
形变 无高度形变:行高恒 40;只有"行 ↔ 气泡"的内容层交叉淡入/遮罩斜掠;radius 恒 8 IslandApp.vue:216-264 的 classisland 分支

custom 模式下 = 现有 schemes/rows/nodes 模型升级,始终 ClassIsland 规格(7.2、7.4 全部适用),与 family 无关;课程提醒接管规则见 7.3.3。

关键改造点(IslandApp.vue):现有 fixed 模板是"胶囊层 + 展开层 + 课程层"(:606-678),胶囊内容是"图标 + 标题 + 副标题 + chevron"(:616-659)。改造要求:

<!-- family=classisland:fixed 信息行(固定 40px 高,气泡在行内叠加) -->
<div class="ci-row-shell" style="height: 40px">
  <div class="layout-row"><div class="row-components">
    <ComponentNode v-for="node in fixedRowNodes" … />
  </div></div>
  <div v-if="mode==='expanded'" class="ci-row-bubble" role="region">
    <SlantedMask class="ci-row-mask" :open="true" :width="rowWidth" :height="40" />
    <div class="ci-bubble-content">…标题 / 正文(2 行省略)/ 操作…</div>
    <div class="ci-bubble-progress" :style="{ transform: `scaleX(${leftProgress})` }" />
  </div>
</div>
  • fixedRowNodes = 默认 [islandNode('date','fixed-date'), islandNode('schedule','fixed-schedule')];来源可以新建一个内置 fixed 方案(与 custom 的 scheme 结构复用),不要把节点写死在模板里。
  • targetSize() 增加 classisland 分支:高度恒 40,{ width: mode==='expanded' ? max(rowNaturalWidth + 24, 420) : rowNaturalWidth + 24, height: 40 };禁止出现 40 + bubbleHeight 之类的增高分支。
  • 展开/收起不做高度弹簧:mode 切换时只播"行内容 → 气泡"的遮罩斜掠与内容交叉淡入(7.2.8 的 360/120ms 与 260/250ms 参数);行高、窗口高全程不动。
  • family='winisland' 时走原模板,逻辑零改动。

7.3.3 fixed 与 custom 差异总表(含课程提醒接管)

先给出三种组合的行为差异总表(后续 7.3.4 细化 winisland、7.3.5 说明共享约束),不得自行发明第四种组合:

维度 fixed + family='classisland'(默认) fixed + family='winisland'(兼容) custom(常驻,恒 ClassIsland 规格,family 不适用)
常态 隐藏(或细缝);消息到来才显示 1 行信息行 隐藏;消息到来弹胶囊 常驻方案 rows
行高 恒 40 胶囊 44 / 展开卡按内容 scheme.rowHeight(默认 40)
消息展示 固定 40px 行内气泡(遮罩+标题+正文 2 行+3px 进度),行不增高 胶囊 → 详情卡(高度按 islandListLayout),窗口高度随形态变 铃铛 + 详情页(resident-detail-page),不改成行内气泡
课程提醒 显示完整课程提醒条:LessonReminderContent 渲染在 40px 行内(准备/上课/课间/下课前) 显示课程提醒条(现有 ci-fixed-lesson 覆盖) 不因 in-class/prepare/break 临时接管整岛;课程进度由 schedule 组件展示;ending 只在课程名右侧显示蓝色圆形秒数徽标(lesson-ending.css 的 30×30);不新增"临时接管一行"行为
课程进度 行底 3px 进度条(当前课程) 课程提醒条底部 2px(改 3px,7.2.4) schedule 组件内 3px 进度条
多消息 第 8 部分规则;气泡内最多 2 行正文,超出省略 列表模式(islandListLayout) 详情页 ClassicNotifications list-mode
空闲交互 鼠标移入不淡化(隐藏态);显示时按命中接收 指针避让淡化 0.05 指针避让淡化 0.05(updateResidentPointer)
窗口几何 宽 = min(工作区宽, 960),高恒 40+2×pad(pad 默认 20) 固定包围盒(最大形态 + 2×pad) 固定画布(内容上限 + 2×pad)

custom 课程提醒的落地改动(必须做,且只做这些):

  • CustomIslandRuntime.vue:31-33 的 lesson 计算改为 恒 null(不再让 prepare/on-class/break/in-class 触发 resident-lesson 页),并从模板移除/停用 resident-lesson-page(:505-524)——custom 不再整岛接管。
  • 保留 ending 徽标:ComponentNode.vue 的 .lesson-ending-badge(:261-266,样式 lesson-ending.css:1-16)继续在课程名右侧显示 -{remaining},尺寸 30×30、#0a84ff 底、白字 12px/600、等宽数字。
  • 不得新增 "custom 下临时接管一行显示课程提醒" 的行为(这是本轮明确排除的范围)。

7.3.4 family='winisland'(兼容,仅 fixed,保持现有外观)

  • 几何:胶囊 width × height(默认 268×44)、圆角 = min(appearance.radius, h/2)(IslandApp.vue:206-212)、展开卡半径 = min(48 × radius/20, w/2, h/2)。
  • 形状:squircle 超椭圆(squircle.ts:21-24,指数 4.2、每角 14 段)。
  • 底色:appearance.style 的 black / glass / tinted 三套 CSS 变量组(classic-island.css:60-86),用 data-style 属性表达(现有模板已写 :data-style="appearance.style",IslandApp.vue:587、CustomIslandRuntime.vue:348)。
  • 排版:--island-font 基础字号 + 系数(ISLAND_TYPE_SCALE,constants.ts:354-367),胶囊内边距 0 font*0.78、gap font*0.62(classic-island.css:207-216)。
  • 阴影:只在展开态 0 2px 3px rgba(0,0,0,.11),且声明常驻只改参数(classic-island.css:110-124, 148-153)。
  • 交互:胶囊点击展开、点击卡片空白收起、失焦宽限 500ms——全部保持现状。

7.3.5 三种组合的共享约束

无论 layout 与 family 如何组合都必须满足:

  • 命中/穿透/触摸/失焦/心跳逻辑完全不变(7.1.4、7.1.5):family 只改 fixed 的渲染,不改输入、不影响 custom。
  • 编辑器与设置页共用同一份 islandLayout 数据;family 不改变数据模型(除 7.4 的字段增补);custom 的 scheme/rows/nodes 在两套 family 下渲染完全一致。
  • 所有组合的 ComponentNode 是同一个组件,规格共用 7.4;差异只在行容器、背景层与气泡容器。
  • prefers-reduced-motion 与应用内"动画"开关同时生效;关闭动画时所有弹簧直接 snap()(CustomIslandRuntime.vue:148-151、IslandApp.vue:249-256)。
  • 可回退:feature flag(family)必须在 M2/M3 阶段即可一键切回旧 fixed 渲染;custom 无回退档(始终 ClassIsland 规格),验收时 fixed×2 family 与 custom 三条路径都要跑(第 11、12 部分)。

7.4 组件逐个规格(11 内置 + 3 消息入口 + 课件覆盖)

统一约定(所有组件适用):

  • 契约字段:id / kind / settings / appearance / advanced / children(island-layout.ts:77-108)。
  • appearance:resourceOverridingEnabled / secondaryFontSize / bodyFontSize / emphasizedFontSize / largeFontSize / customForegroundEnabled / foreground / customBackgroundEnabled / background / customBackgroundOpacityEnabled / backgroundOpacity / customCornerRadiusEnabled / cornerRadius / opacity(:61-76)。
  • advanced:hideRules / align / width / maxWidth(:101-106)。
  • 默认值来源:islandNode()(:173-216),规范化边界在 normalizeIslandLayouts(:257-403)。
  • 外观应用方式:ComponentNode.style 计算(ComponentNode.vue:179-210)——opacity、width、maxWidth、alignSelf(仅父级有宽度时)、textAlign、自定义前景/背景/圆角、字号覆盖、容器方向与 gap。
  • 字体:--secondary 默认 14、--emphasized 默认 18、--large 默认 20(由 CustomIslandRuntime.vue:119-122 按 appearance.fontSize/13 比例算出)→ 改为直接使用 --ci-fs-secondary/emphasized/large(14/18/20),不再按字号比例缩放;只有 appearance.fontSize 作为"整岛缩放"时再乘系数。
  • 图标:统一用 ComponentIcon.vue 的 24×24 线性图标(ComponentIcon.vue:4-35,stroke-width 1.5);ClassIsland 用 FluentSystemIcons,不引入新图标字体,按语义映射(见各组件)。

7.4.1 date 日期

  • 数据字段:仅 settings 常规字段(text 等未使用)。
  • 渲染:new Date(now).toLocaleDateString('zh-CN', { month:'long', day:'numeric', weekday:'long' })(ComponentNode.vue:134-135),如 11月12日星期三。
  • ClassIsland 基准:ddd MM/dd 格式(repos/ClassIsland/ClassIsland/Controls/Components/DateComponent.axaml:11),即 周三 11/12。
  • 改造:默认格式改为 周三 11/12(短星期 + 补零月日 + 斜杠),保留 zh-CN 区域;字号 var(--ci-fs-body)(16px)、white-space: nowrap、font-variant-numeric: tabular-nums。
  • 空数据兜底:now 异常时显示 --,不得抛错。
  • 动画:无。

7.4.2 clock 时钟

  • 数据字段:settings.hour12(默认 false)、settings.seconds(默认 true,island-layout.ts:182)。
  • 渲染:toLocaleTimeString('zh-CN', { hour:'2-digit', minute:'2-digit', ...(seconds?{second:'2-digit'}:{}), hour12 })(ComponentNode.vue:136-142)。
  • ClassIsland 基准:HH:mm / HH:mm:ss、冒号闪动(repos/ClassIsland/ClassIsland/Controls/Components/ClockComponent.axaml.cs:71-76)。
  • 改造:
    • 秒数默认改 false(与 ClassIsland 一致,Models/ComponentSettings/ClockComponentSettings.cs 的 ShowSeconds=false),旧配置不动;
    • 新增 settings.flashSeparator(默认 false,P2 才做闪动);闪动规则 sep = !flash || seconds || 秒%2===1;
    • 字号 var(--ci-fs-emphasized)(18px)、font-variant-numeric: tabular-nums、white-space: nowrap(现有 :305-308 已有)。
  • 空数据兜底:无。
  • 动画:秒变化不做过渡(避免数字滚动抖动)。

7.4.3 weather 天气简报

  • 数据字段:settings.locationName / latitude / longitude(island-layout.ts:87-90)。
  • 数据源:bridge.getIslandWeather({ latitude, longitude })(preload/island.ts:15),Open-Meteo,15 分钟刷新,断网保留缓存并标"缓存"(docs/app/island.md:45)。
  • 渲染:WeatherBrief.vue(现有 2.9KB,需按下面改写)。
<span class="weather-brief" :data-stale="stale">
  <icon class="weather-icon" />            <!-- 22×22,用 ComponentIcon 或天气码映射 -->
  <span class="weather-temp">
    <span class="value">23</span><span class="unit">°C</span>
  </span>
  <span v-if="windSpeed" class="weather-wind">…</span>
</span>
  • ClassIsland 基准(repos/ClassIsland/ClassIsland/Controls/Components/WeatherComponent.axaml:51-184):横排 gap 6;图标 22×22;温度值 + 单位 margin-left:6;湿度/风/气压/体感/AQI 为可选主信息(一次只显示一种);AQI 6 级配色;预警药丸高 22、圆角 11、文字 14px。
  • 改造(P1 做图标 + 温度;其余 P2):
    • 图标尺寸 22×22、margin-right: 6px;
    • 温度 font-size: var(--ci-fs-body)(16px);
    • 地点缺失(latitude/longitude === null)→ 显示"未设置地点"(opacity: 0.7),不得显示 NaN;
    • 数据缺失 → 只显示图标占位 + --;
    • stale === true → 追加小字"缓存"(var(--ci-fs-caption),opacity: 0.6)。
  • 动画:数据刷新时数字变化不做过渡(避免每 15 分钟闪一下)。

7.4.4 countdown 倒计时

  • 数据字段(island-layout.ts:85-88):target: 'auto'|'next'|'end'|'school'|'date'、targetDate、countdownTitle。
  • 渲染逻辑:ComponentNode.vue:150-176(自动/下一节/本节下课/放学/指定日期五种目标,文案 距上课 m:ss / 距下课 m:ss / 距放学 m:ss / 距X N天)。
  • ClassIsland 基准(repos/ClassIsland/ClassIsland/Controls/Components/CountDownComponent.axaml:64-114):距离 <名称> 还有 <数值>,紧凑模式去掉"距离/还有";进度环 22×22(或底部进度条);自定义格式 token %D/%H/%M/%S/%P/%p/%L/%d/%h/%m/%s/%x。
  • 改造(P1):
    • 文案结构改为 distance(默认"距离")+ countdownTitle + connector(默认"还有")+ 数值;新增 settings.compact(默认 false);
    • 新增 settings.showProgress(默认 false)+ settings.progressMode: 'ring'|'bar'(默认 ring):ring 用 22×22 SVG 弧(stroke-width 3.1、stroke-linecap: round、半径 9.5),bar 用 3px 底部进度条;
    • 数值格式化:%m/%s 补两位、%D/%H/%M/%S 向上取整(与 ClassIsland 一致),默认格式 %m:%s(mm:ss);
    • 颜色:var(--ci-text-1);settings.color 覆盖(P2)。
  • 空数据兜底:无课程数据 → 今日课程已结束(现有 :170);目标日期为空 → 请设置目标日期(:152)。
  • 动画:进度条 transition: transform 1s linear;ring 的 stroke-dashoffset 用 1s 线性过渡。

7.4.5 schedule 课程表

最复杂,必须逐条对照 ClassIsland。

  • 数据字段:settings.display: 'current'|'next'|'today'(默认 current)、settings.count(默认 5,范围 1–40)、settings.showPeriod(默认 true)、settings.showProgress(默认 true)。
  • 数据源:schedules + timePoints(ComponentNode.vue:30-61),时间点优先用服务端完整时间表,缺失时回退课表条目。
  • 显示规则(现有,保留):ComponentNode.vue:74-97——
    • current:当前时间点;无则下一节;
    • next:下一节课 1 条;
    • today:过滤 DIVIDER、当前时间点、未隐藏的 LESSON,按 count 取窗口并让当前项居中。
  • ClassIsland 基准(repos/ClassIsland/ClassIsland/Controls/Components/ScheduleComponent.axaml + LessonControlExpanded.axaml):
    • 每个时间点一个"课程 chip":左右各 16px × ScheduleSpacing(默认 1)、科目名 18px Bold、附加信息 14px、进度条 3px;
    • 隐藏规则:HideFinishedClass、ShowCurrentLessonOnlyOnClass、IsHideDefault、课间只在当前时展开;
    • 明天课表 4 种模式(P2);
    • 空状态占位"今天没有课程。"/"今日课程已全部结束。"。
  • 改造(P1):
<div class="node node-schedule" :data-display="display">
  <div v-for="p in schedulePoints" :key="p.id" class="lesson-line" :class="{ current, break: p.timeType==='BREAK' }">
    <strong class="lesson-name">{{ p.courseName || p.name || '—' }}</strong>
    <span v-if="isEnding" class="lesson-ending-badge">-{{ remaining }}</span>   <!-- 30×30 正圆,见 lesson-ending.css -->
    <small v-if="current && settings.showPeriod" class="lesson-time">{{ p.startTime }}–{{ p.endTime }}</small>
    <span v-if="current && p.timeType==='LESSON' && settings.showProgress" class="lesson-progress">
      <i :style="{ transform: `scaleX(${elapsed})` }" />
    </span>
  </div>
  <span v-if="!schedulePoints.length" class="lesson-empty">今天没有课程。</span>
</div>
  • 课程名 var(--ci-fs-emphasized)(18px)+ font-weight: 700;时间/附加信息 var(--ci-fs-secondary)(14px);
  • 行内 gap 6px(现状 ComponentNode.vue:313-321 已是 6px,保留);
  • 进度条 3px(7.2.4);
  • 已结束课程 opacity: 0.6(ClassIsland LessonsListBox.axaml:56-58),过渡 150ms cubic-bezier(0.645,0.045,0.355,1);
  • 分割线:1×18、opacity: 0.25(现状 :334-342),可提到 24px 与 ClassIsland 的 25px 对齐(P2);
  • 课后占位:现有"今日课程已结束"只用于 countdown;schedule 需新增 今天没有课程。/今日课程已全部结束。 两态(ClassIsland LessonControlSettings 默认文案)。
  • 空数据兜底:timePoints 与 schedules 都为空 → 显示占位、不占高度超过 40px;只有 schedules → 回退按条目渲染(现有逻辑)。
  • 动画:切换时间点时课程名不做滑动(ClassIsland 是整行裁切),只做进度条与徽标的数值更新。

7.4.6 text 文本

  • 数据字段:settings.text(默认 '班级信息')、settings.template(默认 false)。
  • 渲染:模板变量 {date} / {time} / {course}(ComponentNode.vue:143-149)。
  • ClassIsland 基准:TextComponent(文本内容 + 自定义字体颜色 + 字体大小 16–30)。
  • 改造:字号 var(--ci-fs-body);超长 text-overflow: ellipsis(现有 :125-129 已有);模板变量保留;新增 settings.fontSize(默认 16,范围 16–30)写入 inline font-size。
  • 空数据兜底:text 为空 → display: none(不占宽度与间距)。
  • 动画:无。

7.4.7 separator 分割线

  • 数据字段:无。
  • 现状:1px × 24px、opacity: 0.25(ComponentNode.vue:369-377,模板 :249)。
  • ClassIsland 基准:2×25、StrokeThickness=2、opacity 0.8(repos/ClassIsland/ClassIsland/Controls/Components/SeparatorComponent.axaml:10-14)。
  • 改造:宽 2px、高 25px、颜色 var(--ci-text-fill-secondary)(rgba(255,255,255,0.72))、opacity: 0.8。
  • 空数据兜底:无。
  • 动画:无。

7.4.8 slide 轮播容器

  • 数据字段:settings.interval(默认 5,范围 1–300)、settings.direction、settings.gap。
  • 现状:非编辑时按 floor(now / (interval*1000)) % children.length 只渲染一个子项(ComponentNode.vue:121-130),无过渡动画。
  • ClassIsland 基准:切换间隔默认 15s、三种模式(循环/随机/往复)、启用动画时 250ms 上滑淡入 + 上滑淡出、cubic-bezier(0.65,0,0.35,1)、位移 ±40px(repos/ClassIsland/ClassIsland/Controls/Components/SlideComponent.axaml:72-107)。
  • 改造(P1):
    • 新增 settings.interval 默认改 15(旧配置保留)、范围 1–300;
    • 新增 settings.slideMode: 'loop'|'random'|'repeat'(默认 loop);
    • 新增 settings.animate(默认 false):开启后切换用 CSS 类 slide-in/slide-out,250ms、translateY(-40px → 0)、opacity 0 → 1;关闭时直接换(现状);
    • 子项 max-height: var(--ci-line-height)。
  • 空数据兜底:0 子项 → 塌缩为 0 宽;1 子项 → 不轮播。
  • 动画:prefers-reduced-motion 时关闭过渡。

7.4.9 rolling 滚动容器

  • 数据字段:settings.speed(默认 30,范围 1–200)。
  • 现状:CSS 动画 rolling(translateX(30%) → -80%)+ --rolling-duration: max(1, 200/speed)s,无限线性(ComponentNode.vue:384-392, 446-453,:205)。
  • ClassIsland 基准(repos/ClassIsland/ClassIsland/Controls/Components/RollingComponent.axaml.cs:85-135):仅当内容宽 > 容器宽才滚动;dur = innerW / speed、可暂停、可回程;速度默认 40px/s。
  • 改造(P1):
    • 默认速度改 40;--rolling-duration: calc(var(--rolling-distance) / var(--rolling-speed) * 1s);
    • 仅当 scrollWidth > clientWidth 时加 rolling 类(用 JS 测量,ResizeObserver 监听容器与内容);
    • 保留"编辑时不滚动"(:390-392);
    • 暂停/停止规则 P2。
  • 空数据兜底:内容不溢出 → 静止、不抖动。
  • 动画:linear、无限循环;prefers-reduced-motion 时静止。

7.4.10 group 分组容器

  • 数据字段:children、settings.direction(默认 horizontal)、settings.gap(默认 8)。
  • 现状:display:flex、gap、align-items: center(ComponentNode.vue:206-208, 378-380)。
  • ClassIsland 基准:分组容器 = 横向 StackPanel + Margin="-6 0" + 子项 Margin="6 0"(repos/ClassIsland/ClassIsland/Controls/Components/GroupComponent.axaml:13-33),即子项间距 12。
  • 改造:gap 默认改 12;子项外边距 0;容器自身不带内边距。
  • 空数据兜底:0 子项 → 0 宽。
  • 动画:无(子项变化时行宽由行过渡)。

7.4.11 stack 堆叠容器

  • 数据字段:children、settings.direction、settings.gap。
  • 现状:flex-wrap: wrap(ComponentNode.vue:381-383)——这是"换行排列",不是 ClassIsland 的堆叠语义。
  • ClassIsland 基准:Panel,所有子项占据同一矩形、各自按对齐摆放(repos/ClassIsland/ClassIsland/Controls/Components/StackComponent.axaml:12-32)。
  • 改造(P1):.stack { display: grid; },所有子项 grid-area: 1 / 1,align-self/justify-self 由 advanced.align 决定;容器宽高取子项测量最大值;子项间距 0。
  • 空数据兜底:0 子项 → 0 宽。
  • 动画:无。

7.4.12 消息入口 notification / homework / call

这三个 kind 不是普通内容组件,而是"消息入口":

  • 现状:渲染时在 ComponentNode 里走分支(ComponentNode.vue:285-288),只有 node.id === alertNodeId 时才渲染 <slot name="alerts">,否则编辑态显示文案;数据来自 IslandState.active/queued。
  • 组件库只展示前 11 类(docs/app/island.md:40),消息类由铃铛与消息面板承担。
  • ClassIsland 对应:通知行/提醒气泡(行内遮罩 + 浮层 + 进度条,repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:70-202)。
  • 改造(P1):
    • 保留三个 kind 作为"入口标记":notification = 通知入口、homework = 作业入口、call = 叫人入口;
    • 铃铛显示逻辑(CustomIslandRuntime.vue:421-441)改为:有消息时显示图标 + 数量徽标(99+ 上限),无消息时显示 chevron;铃铛图标沿用 ComponentIcon 的 notification 路径;
    • 徽标尺寸保持 14px 高、圆角 7、#f45b58(custom-island.css:94-110),文字 9px/600;ClassIsland 风格下允许改为 var(--ci-accent-fill) 底 + 白字(一行样式覆盖);
    • 消息卡本体的内容与业务由第 8 部分规定;本部分只规定容器:
      • custom 详情页:高度 = islandListLayout().height(constants.ts:445+),宽度 = 岛上可用宽度,位置在 resident-detail-page;
      • fixed + classisland:固定 40px 行内气泡(7.2.8),islandListLayout() 不参与;
      • fixed + winisland:沿用现有详情卡高度 = islandListLayout().height。
  • 空数据兜底:无消息 → 三个入口都不渲染(hidden 计算已处理,ComponentNode.vue:108-120)。
  • 动画:徽标出现 resident-indicator-in(custom-runtime.css:147-149, 199-206)保留;custom 详情页展开/收起走高度弹簧;fixed + classisland 走 7.2.8 的遮罩/文字动画,不做高度动画。

7.4.13 CoursewareTransfer 课件覆盖

  • 触发:state.coursewareTransfer 非空时,LayoutIslandApp.vue:58, 71-75 直接渲染课件卡,并让经典岛 suspended、自定义岛不渲染(:57)。
  • 现状视觉:position: fixed、尺寸 COURSEWARE_ISLAND_SIZE、padding 17px 24px、border-radius 24px、底色 rgba(20,20,24,0.97)、阴影 0 2px 8px #0004、字号 13/1.4、进度条 5px(CoursewareTransfer.vue:67-131)。
  • ClassIsland 对应:课件传输不在 ClassIsland 中,属 ClassHelper 独有能力 → 保留现状视觉,只做两点对齐:
    1. 锚点/边距与岛一致(左/中/右 + ISLAND_SHADOW_PAD,CoursewareTransfer.vue:12-25),尺寸不随 style 变化;
    2. pointer-events: none 保留(:81),并由 setHitRect 上报矩形(:35-38),保证不吞桌面点击。
  • 空数据兜底:transfer.error 存在时显示错误行(:64),不隐藏整卡。

7.4.14 组件默认值总表(必须与代码一致)

islandNode() 当前默认(island-layout.ts:173-216)必须逐项落实;本轮改动用 粗体 标注:

字段 当前默认 本轮目标
settings.text 班级信息 保留
settings.template false 保留
settings.hour12 false 保留
settings.seconds true 保留(新节点默认仍 true,兼容)
settings.target auto 保留
settings.targetDate '' 保留
settings.countdownTitle 目标日 保留
settings.locationName '' 保留
settings.latitude/longitude null 保留
settings.display current 保留
settings.count 5 保留
settings.showPeriod true 保留
settings.showProgress true 保留
settings.direction horizontal 保留
settings.gap 8 12(组/堆叠容器子项间距)
settings.interval 5 15(轮播间隔,秒)
settings.speed 30 40(滚动 px/s)
appearance.* 字号 14/16/18/20 保留(改用 --ci-fs-*)
appearance.background #202024 保留
appearance.backgroundOpacity 0.5 保留
appearance.cornerRadius 16 8(与 ClassIsland 圆角一致)
appearance.opacity 1 保留
advanced.align stretch 保留
advanced.width/maxWidth 0/0 保留

7.5 编辑器渲染侧结构(组件库 / 拖拽 / 选中框 / 行设置)

编辑器是主窗口里的设置页(IslandEditorView.vue,860 行),不是岛的渲染进程;但它的 DOM/CSS 必须与岛保持同一套 token,避免"编辑器里好看、上岛变形"。

7.5.1 两态:标签排列 vs 实际预览

  • 非编辑态(editMode=false):component-lines 排列行与组件 chip(IslandEditorView.vue:659-695),每行一个 .component-line。
  • 编辑态(editMode=true):island-preview 按方案的真实布局渲染(:698-738),支持预览缩放 0.3–1.5、步进 0.1、重置(:739-750)。
  • 必须保留:"编辑预览不创建额外桌面窗口"(docs/app/island.md:47)。

7.5.2 尺寸与选中框(ClassIsland 规格)

元素 现状 目标
行容器 .component-line min-height: 54px、padding: 4px、border: 1px solid transparent、radius: var(--ch-radius-control)(island-editor.css:134-141) min-height: 48px(行高 40 + 上下 4)、padding: 4px 8px
行把手 .row-handle width: 24px(:149-154) 保留 24px
组件 selecetd background: var(--ch-accent-soft)、border-color: var(--ch-accent)(IslandEditorNode.vue:86-89) 改为 1.5px 虚线:outline: 1.5px dashed var(--ci-accent);outline-offset: -1.5px;不加背景色
容器组件 border: 1px dashed var(--ch-border-strong)、padding: 3px(:90-94) 1.5px dashed、padding: 3px、border-radius: 8px
拖入指示 .drop-before/after 3px 强调色竖条(:111-123);.drop-inside 2px 实线(:107-110) 保留,颜色改 var(--ci-accent)、圆角 1.5px
拖拽幽灵 .drag-ghost(IslandEditorView.vue:774-780) 保留,padding: 4px 8px、border-radius: 6px、背景 var(--ci-bg-tertiary)
详情抽屉 el-drawer 宽 min(360px, 94vw)(:785) 保留

7.5.3 行设置面板字段

现有行面板(IslandEditorView.vue:804-831):当前行、行名称(≤64 字)、组件间距(0–64)、允许显示提醒、设为主要行、上移/下移/删除。

目标态新增(与 ClassIsland 对齐,P1):

  • 行不透明度 opacity(默认 1.0,范围 0.1–1,步进 0.05)——写在行 style.opacity;
  • 行隐藏规则 hideOnRule(默认 false)+ 规则集(P2);
  • 行圆角覆盖(P2)、行背景色/不透明度覆盖(P2,对应 ClassIsland 行级"自定义背景颜色/不透明度/圆角")。

保存路径不变:bridge.saveLayout()(preload/island.ts:17)→ island:save-layout。

7.5.4 组件库

  • 11 个条目(date/clock/weather/countdown/schedule/text/separator/slide/rolling/group/stack),每项 ComponentIcon + 名称 + 描述(IslandEditorView.vue:751-770,数据来自 ISLAND_COMPONENT_LABELS + 本地描述表)。
  • 交互:pointerdown 开始拖拽、click 点击添加(:760-761)。
  • 目标态:图标尺寸 18×18(IslandEditorNode.vue:74-78)保持不变;卡片 min-height: 76px(island-editor.css:285)保留;不得把 notification/homework/call 加回组件库(docs/app/island.md:40 的既有约定)。

7.5.5 渲染侧必须新增/调整的 DOM 与 token

必须新增(岛渲染进程):

  • .ci-row-shell(classisland 风格 fixed 模式的行容器);
  • .ci-mask-layer / .ci-mask-title(7.2.6);
  • .lesson-empty(课程表占位);
  • .resident-bell 的 [data-family='classisland'] 覆盖;
  • .layout-row[data-hide='true'](隐藏规则命中)。

必须新增(CSS 变量,挂在 :root 与 .resident-card 上):

--ci-line-height: 40px; --ci-line-pad-x: 12px; --ci-line-gap: 5px; --ci-component-gap: 12px;
--ci-radius-island: 8px; --ci-island-opacity: 0.5; --ci-line-opacity: 1;
--ci-stroke-island: #ffffff26; --ci-stroke-island-w: 1px;
--ci-shadow-island: 0 4px 8px 2px #48000000;
--ci-progress-height: 3px;
--ci-fade-ratio: 0.05;
--ci-fs-secondary: 14px; --ci-fs-body: 16px; --ci-fs-emphasized: 18px; --ci-fs-large: 20px;
--ci-bg-secondary: #20202080; --ci-text-1: rgba(255,255,255,0.92); --ci-text-2: rgba(255,255,255,0.72);
--ci-accent: (用户强调色); --ci-accent-fill: color-mix(in srgb, var(--ci-accent) 100%, transparent);

命名对齐:packages/shared/src/island-layout.ts:20-36 的 CLASSISLAND_ISLAND_METRICS 已经是这些值的单一来源(rowHeight:40、horizontalPadding:12、radius:8、opacity:0.5、rowSpacing:5、字号 14/16/18/20、fadedOpacity:0.05)。渲染层必须从该常量生成 CSS 变量(通过 style 内联或 applyAppearance 写入 document.documentElement.style,位置同 CustomIslandRuntime.vue:102-124),不得在 CSS 里再抄一份数字。

7.6 验收标准

实现者必须逐条自查,并只在显式 --visual 的视觉验证里做桌面像素比对(默认后台验证不显示岛):

  1. 默认值:新安装 appearance.family = 'classisland';旧配置(无 family)升级后 family = 'winisland' 且 fixed 观感与升级前一致,不报错、不丢组件数据;设置页可一键切换并即时预览、可回退。 1.1 family 只影响 fixed:layout='custom' 时切 family 不改变任何渲染(custom 恒 ClassIsland 规格),设置页在 custom 下不展示"外观族/视觉风格"两项;custom 无 winisland 回退档。
  2. family='classisland' 的 fixed 模式是信息行不是胶囊:默认 1 行 = 日期 + 课程表、行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条;无消息时不显示(或按 idleSliver 留细缝);消息到来时在同一条 40px 行内渲染 ClassIsland 气泡(斜切遮罩 + 标题 + 正文 + 倒计时进度),收起后回到常态行。 2.1 气泡不增高行:气泡出现/消失前后,行高恒 40px(像素量测误差 ≤1px),窗口高恒 40+2×pad,win.getBounds() 不变;正文最多 2 行、超出省略号,不得出现文字溢出到行外或行高变成 48/56/64。 2.2 气泡动画参数与 ClassIsland 一致:遮罩每段 360ms、错峰 0/90/90/120/120ms(总 480ms)、开 QuarticEaseOut / 关 QuadraticEaseIn;文字 250ms delay 260ms + scale 1.1→1.0/750ms;退出 200ms、缩回 cubic-bezier(0.5,0,0.75,0);theme='classic' 走 translateY 变体。
  3. family='winisland' 的 fixed 模式仍是胶囊 + 详情卡:胶囊 268×44、圆角 h/2、squircle、纯黑/玻璃/渐变三色调、展开态阴影 0 2px 3px rgba(0,0,0,.11);与改造前逐像素一致。
  4. 岛在 6 个停靠位置、两个显示器、125%/150% DPI 下位置正确:居中停靠不受 marginX 影响;左右停靠距屏幕边缘 = marginX;底停靠时窗口贴底且岛不被裁。
  5. 窗口几何:开合过程中 win.getBounds() 不变化(用主进程日志或注入断言验证);只有设置变化/不可见时才 setBounds;classisland fixed 的窗口高恒为 40 + 2×ISLAND_SHADOW_PAD。
  6. 行:可见行高 40px(像素量测)、行内边距 12、行距 5、圆角 8、描边 1、阴影 0 4 8 2 #48000000;相邻组件间距 12;进度条 3px。
  7. 背景不透明度 0.5 只影响背景:文字在截图里为纯白 0.92/0.72 层级,不随背景变透明。
  8. 三条进度条高度都是 3px(课程、课程提醒、通知气泡),且不出现 2px 残留。
  9. custom 模式:rows 与 expandedRows 都按行渲染;scheme.separated=true 时每行独立背景胶囊,无整块背景;custom 渲染恒按 ClassIsland 规格,切换 family 不产生任何视觉变化。 9.1 custom 不被课程提醒接管:prepare / on-class / break / in-class 期间岛仍显示常态行与铃铛(课程进度由 schedule 组件呈现),不出现整岛替换;ending 只在课程名右侧显示蓝色圆形秒数徽标(30×30、-N);不存在"临时接管一行"的行为。
  10. 遮罩:通知/课程提醒共用 SlantedMask,5 段从两侧向中间打开(总 480ms)、关闭反向;开启减少动态效果或关闭动画时直接落位。
  11. 触摸屏:手指能点开岛(触摸模式生效),窗口贴合岛体,失焦宽限 1200ms;非触摸屏行为与改造前完全一致。
  12. 鼠标穿透:岛外点击一律落到桌面;命中矩形随形变更新(限流 50ms),形变结束后无"看不见却点不动"的死区。
  13. 11 个组件逐项:date 显示 周三 11/12、clock 显示 HH:mm、weather 显示 22px 图标 + 温度且无数据时不出现 NaN、countdown 文案与进度环/条可选、schedule 当前项高亮 + 3px 进度 + 已结束 0.6 淡化 + 空占位、text 空内容不占宽、separator 2×25/0.8、slide 15s 切换且可关动画、rolling 仅溢出时滚动且 40px/s、group 间距 12、stack 子项完全重叠。
  14. 消息入口:铃铛数量徽标正确(1–99,>99 显示 99+),无消息显示 chevron;notification/homework/call 不在组件库出现,但旧方案里的节点仍能渲染。
  15. 课件覆盖:传输中出现课件卡且岛隐藏;锚点与边距正确;pointer-events: none,不吞点击。
  16. 编辑器:选中组件为 1.5px 虚线强调框;容器组件为 1.5px 虚线;拖入指示在 before/after/inside 三种位置正确;预览缩放 0.3–1.5 不导致岛变形错位。
  17. prefers-reduced-motion 与设置里的"动画"开关都生效:关闭时所有弹簧 snap()、滚动静止、遮罩直接落位。
  18. 不得出现:setBackgroundMaterial、backdrop-filter、第二个 BrowserWindow、把窗口尺寸写进 CSS 的做法、在动画帧里 setBounds。