fix(webui): 修「列表与正文之间那条大空隙」—— 包装层撑满而列表面板固定 320

用户发截图指出:「外框列表和右边正文那么大一个空隙」。

## 根因(在代码里很难看出来)

我为了让「通信」有内部页签与悬浮加号,给中栏加了一层包装 div,它写着 `flex-1`;
而里面的列表面板是 `lg:w-[320px]` **固定宽度** ⇒ 包装层把剩余宽度全占了,
**空隙在包装层里、不在任何面板里** —— 于是既看不出是哪一层的错,也不像"布局 bug",
看起来就像设计上多留了一条白。

## 改法

`​.app-shell .comm-pane { flex: 0 0 auto; width: 320px }` —— 宽屏下中栏贴住列表面板的宽度。
窄屏不动:那边外壳是**列**方向,`flex-1` 管的是高度,宽度由 `w-full` 决定。

## 实测(自有浏览器)

    宽屏 1600: 包装层 320 / 列表 320 / 详情 1180  →  包装层内空隙 0px,到详情 10px(设计间距)
    窄屏 390:  包装层 370 / 列表 370              →  包装层内空隙 0px

顺带说一句:这条与前面「日历没有圆角」「列表项变成一张大框」是**同一个包装层**引出的
三个症状(撑满宽度 / 内层直角戳出圆角 / 每项没有独立卡)。包装层是我为合并导航加的,
当时的验证只看了结构(页签在不在、点得到点不到),**没有量过宽度**,
所以它一连串地出问题。现在宽度已有判据(上面的数字就是判据)。
This commit is contained in:
2026-09-14 14:29:27 +08:00
parent a6b5e52f45
commit 2e42aacc07
3 changed files with 356 additions and 4 deletions

View File

@ -0,0 +1,207 @@
/*
* 壁纸的**画法**:预设档(渐变)与图片档 —— 纯逻辑,无 UI 依赖。
*
* 为什么单独一层:服务端存的只是 `bg_kind` + `bg_preset_id` + 参数,
* 「这个 id 该画成什么样」是客户端的事。这层决定"画哪些层、什么色、什么位置",
* 页面只负责把层交给系统原语(`radialGradient` / `linearGradient` / `Canvas`)。
*
* ⚠️ 这一层是**能力对等**的关键:pi 指出(2026-09-14)WebUI 的背景有 6 个预设渐变,
* 若鸿蒙只认 `image` 与 `none`,那"换账号后外观跟随"对预设档就是**不成立的** ——
* 用户设了预设,在鸿蒙看到的是没有背景。这是信息对等缺口,比"能不能上传壁纸"更基础。
*
* 参考实现:`client/electron/src/index.css` 的 `.bg-preset-*` 与
* `stores/backgroundStore.ts` 的 `PRESETS`(id / 标签 / 归一化规则)。
* 色值取自那边的调色板变量(浅色档),**逐一对照**,不是凭印象写的。
*
* ⚠️ 类型可擦除(无 enum / namespace / 构造器参数属性),判据用 node strip-types 直接跑它。
*/
/**
* 一层渐变(预设由 1~3 层叠出来,与 WebUI 的多重 background-image 一一对应)。
*
* `colors` 与 `stops` 分开存(而不是 `[色, 位置][]`):ArkTS 对嵌套元组数组支持有限,
* 而分开存更好判。页面按 `stops[i]` 给 `colors[i]` 组对。
*/
export class PresetLayer {
/** 'radial' | 'linear' | 'grid' */
kind: string = 'radial';
/** radial:中心点百分比(0~100);linear/grid 不用 */
cx: number = 50;
cy: number = 50;
/** radial:半径(交给系统,用百分比或 vp);linear:角度(度) */
radius: string = '55%';
angle: number = 180;
colors: string[] = [];
/** 色标位置(0~100),与 `colors` 一一对应 */
stops: number[] = [];
/** grid 用:线色 / 透明度 / 间隔 */
lineColor: string = '';
lineAlpha: number = 0;
step: number = 0;
}
/**
* 预设色板(浅色档,取自 WebUI 的调色板变量)。
*
* 这些色**不走系统语义色**:它们是这套预设的设计内容(与品牌蓝同类),
* 系统里没有"极光渐变"这种东西。深色档下预设的观感**未验**(见文档)。
*/
const GRAY_100: string = '#F3F4F6'; // --c-gray-100
const GRAY_200: string = '#EAECF1'; // --c-gray-200
const BLUE_100: string = '#DBEAFE'; // --c-blue-100
const BLUE_200: string = '#BFDCFE'; // --c-blue-200
const GREEN_100: string = '#DCFCE7'; // --c-green-100
const AMBER_100: string = '#FEF3C7'; // --c-amber-100
const ORANGE_100: string = '#FFEDD5'; // --c-orange-100
/**
* `transparent` 用**关键字**而不是 8 位半透明色值:
* 一是系统认识它,二是本仓有一条判据禁止 8 位色值(那类色值曾经是手写玻璃的证据)。
* 页面把它映射成 `Color.Transparent`。
*/
export const TRANSPARENT: string = 'transparent';
/** 预设清单:id 与顺序与 WebUI 的 `PRESETS` 逐字一致(判据会去比对) */
export const PRESET_IDS: string[] = ['aurora', 'dusk', 'mint', 'sand', 'ink', 'mesh'];
export function presetLabel(id: string): string {
if (id === 'aurora') {
return '极光';
}
if (id === 'dusk') {
return '暮色';
}
if (id === 'mint') {
return '薄荷';
}
if (id === 'sand') {
return '沙丘';
}
if (id === 'ink') {
return '墨色';
}
if (id === 'mesh') {
return '网格';
}
return '';
}
const DEFAULT_PRESET: string = 'aurora';
/** 认不出的 preset id → 回到默认(与 WebUI 的 `normalize` 同一规则,避免"空白背景") */
export function normalizePreset(id: string): string {
return PRESET_IDS.indexOf(id) >= 0 ? id : DEFAULT_PRESET;
}
function radial(cx: number, cy: number, radius: string, color: string, stop: number): PresetLayer {
const l: PresetLayer = new PresetLayer();
l.kind = 'radial';
l.cx = cx;
l.cy = cy;
l.radius = radius;
l.colors = [color, TRANSPARENT];
l.stops = [0, stop];
return l;
}
function linear(angle: number, from: string, to: string): PresetLayer {
const l: PresetLayer = new PresetLayer();
l.kind = 'linear';
l.angle = angle;
l.colors = [from, to];
l.stops = [0, 100];
return l;
}
/**
* 某个预设由哪些层叠出来(与 `.bg-preset-*` 的多重背景一一对应)。
*
* 层序 = 绘制顺序:先画的在下层。WebUI 的 CSS 里第一段是最上层(CSS 多重背景
* 是后面的在下),为保持观感一致,这里**反过来给**:CSS 里最后一段先画。
*/
export function layersFor(id: string): PresetLayer[] {
const key: string = normalizePreset(id);
if (key === 'dusk') {
return [
radial(85, 25, '55%', BLUE_200, 55),
radial(12, 80, '55%', ORANGE_100, 55)
];
}
if (key === 'mint') {
return [
radial(78, 70, '55%', BLUE_100, 55),
radial(25, 30, '55%', GREEN_100, 55)
];
}
if (key === 'sand') {
return [
radial(80, 75, '55%', ORANGE_100, 55),
radial(20, 25, '60%', AMBER_100, 60)
];
}
if (key === 'ink') {
return [
linear(160, GRAY_100, GRAY_200),
radial(30, 20, '60%', GRAY_200, 60)
];
}
if (key === 'mesh') {
const g: PresetLayer = new PresetLayer();
g.kind = 'grid';
/*
* 网格:CSS 用的是 `repeating-linear-gradient`,**系统没有对应的渐变原语**,
* 所以这一档用系统 `Canvas` 画线(仍是系统绘制能力,不是手写模糊那一类)。
* 线色与间隔照抄 CSS:gray-200 / 0.55 / 28px。
*/
g.lineColor = GRAY_200;
g.lineAlpha = 0.55;
g.step = 28;
const base: PresetLayer = linear(180, GRAY_100, GRAY_100);
return [base, g];
}
// aurora(默认)
return [
radial(68, 82, '55%', BLUE_100, 55),
radial(82, 12, '50%', GREEN_100, 50),
radial(18, 22, '55%', BLUE_200, 55)
];
}
/** 背景计划:页面照着画,判据照着判 */
export class BackgroundPlan {
/** 'none' | 'preset' | 'image' */
kind: string = 'none';
presetId: string = '';
layers: PresetLayer[] = [];
/** 压暗(0~1)—— image 档压在图上;preset 档不用 */
scrim: number = 0;
}
/**
* 快照 + "有没有图" → 画什么。
*
* 与 `mergeAppearance` 的分工:那边决定**用谁的值**(服务端 vs 本地),
* 这里决定**画成什么**。`image` 档但图没取回来 → `none`:
* 宁可什么都不画,也不要画一块空白(用户会以为壁纸坏了)。
*/
export function resolveBackground(bgKind: string, presetId: string, scrim: number, hasImage: boolean): BackgroundPlan {
const plan: BackgroundPlan = new BackgroundPlan();
if (bgKind === 'preset') {
plan.kind = 'preset';
plan.presetId = normalizePreset(presetId);
plan.layers = layersFor(plan.presetId);
return plan;
}
if (bgKind === 'image' && hasImage) {
plan.kind = 'image';
plan.scrim = scrim;
return plan;
}
plan.kind = 'none';
return plan;
}
/** 预设缩略图/选择器要显示的清单(id + 标签) */
export function presetChoices(): string[] {
return PRESET_IDS;
}