下拉框与网页端对齐(照 FeedLite 那套观感)(#86)

用户 2026-11-05:「好了,鸿蒙端也用类似的方式调,效果让它保持一致。」

端上**不需要像网页那样手写浮层** —— 网页端必须自绘是因为
「原生 select 的展开列表由浏览器绘制,CSS 改不动」;
ArkUI 的 Select 本身就是自绘的、选项列表也归框架管。
⇒ 把对应属性都设上就等价了:

  网页端                          ArkUI Select
  ──────────────────────────────────────────────────────
  按钮 40 高 / 圆角 / 细边框       height(40) + borderRadius + border
  箭头打开转 180°                 框架内置(自带动画)
  悬停 tint-1                     不支持(触屏没有 hover)→ 用打开态代替
  选中项底 tint-2                 selectedOptionBgColor(fbMixWhite(accent,0.80))
  选中项文字 主题色                selectedOptionFontColor(accent)
  选中项右侧对勾                   showDefaultSelectedIcon(true)
  浮层白底 + 细边框               menuBackgroundColor(White) + border
  浮层与按钮同宽                   optionWidth(OptionWidthMode.FIT_TRIGGER)
  未选值次要色                     fontColor(有值 ? accent : Faint)

① Theme 新增 fbMixWhite(hex, ratio)
   与网页端 fb_color_tint() **同算法同比例**(混 91/80/72% 白)。
   ⚠️ 为什么不用现成的 fbAlpha():那是「原色 + 透明度」,
      会透出父容器底色 —— 下拉浮层背景不确定,那样会时深时浅;
      这里是实色混合,颜色确定。
   ⚠️ 实测 16 组(4 个主题色 × 4 档比例)与 PHP 输出逐个比对,全部一致。

② 边框单独用一档
   按钮边框 fbMixWhite(accent, 0.62) —— 比 tint-3 再深一点,
   否则在 tint-1 的浅底上几乎看不见边界。

③ 修一个类型坑
   本来写了个 pickFontColor(): string 返回 accent / FbColors.muted,
   但 FbColors.muted 是 Resource($r(...)),返回 string | Resource
   会让 fontColor() 报「Type 'string | Resource' is not assignable to 'string'」。
   ⇒ 改成内联三元,且未选用 FbColors.faint(那个是 string 常量)。

编译通过,已装真机。
This commit is contained in:
2026-11-05 20:25:59 +08:00
parent 389cb31df4
commit d9ca9dbe31
2 changed files with 97 additions and 18 deletions
+55
View File
@@ -386,6 +386,61 @@ function fbIconOf(name: string): Resource {
* @param hex 六位十六进制(`#RRGGBB`;也容忍八位,会先截成前六位) * @param hex 六位十六进制(`#RRGGBB`;也容忍八位,会先截成前六位)
* @param alpha 0~1(0.4 = 40% 不透明) * @param alpha 0~1(0.4 = 40% 不透明)
*/ */
/**
* 把主题色往白色方向混,得到一档浅色。
*
* 🔴 #86(用户 2026-11-05):「鸿蒙端也用类似的方式调,效果让它保持一致。」
*
* 与网页端 `fb_color_tint()`(app/helpers.php)**必须是同一个算法**,
* 否则同一条任务在两端的下拉项颜色不一样 —— 那就白"保持一致"了。
*
* 网页端的三档:
* tint-1(混 91% 白)→ 悬停 / 打开态底
* tint-2(混 80% 白)→ 选中项底
* tint-3(混 72% 白)→ 选中项悬停
*
* ⚠️ 与 `fbAlpha()` 的区别:那个是"原色 + 透明度"(透出背景色,会受
* 父容器底色影响);这个是"实色混合"(不透明,颜色确定)。
* 下拉项底色必须用实色 —— 用透明度的话叠在白色面板上还行,
* 但它自己是浮层,背景不确定,会时深时浅。
*
* @param hex 六位十六进制(`#RRGGBB`)
* @param ratio 往白色混多少(0.91 = 91% 白 + 9% 原色)
*/
export function fbMixWhite(hex: string, ratio: number): string {
let base: string = hex.trim();
if (base.startsWith('#')) {
base = base.substring(1);
}
if (base.length > 6) {
base = base.substring(base.length - 6);
}
if (base.length !== 6) {
return hex; // 不是 6 位 hex(如 rgba())→ 原样返回,调用方自己兜底
}
const r: number = parseInt(base.substring(0, 2), 16);
const g: number = parseInt(base.substring(2, 4), 16);
const b: number = parseInt(base.substring(4, 6), 16);
if (isNaN(r) || isNaN(g) || isNaN(b)) {
return hex;
}
let q: number = ratio;
if (q < 0) {
q = 0;
}
if (q > 1) {
q = 1;
}
const nr: number = Math.round(r + (255 - r) * q);
const ng: number = Math.round(g + (255 - g) * q);
const nb: number = Math.round(b + (255 - b) * q);
const h2 = (v: number): string => {
const s2: string = v.toString(16).toUpperCase();
return s2.length < 2 ? '0' + s2 : s2;
};
return '#' + h2(nr) + h2(ng) + h2(nb);
}
export function fbAlpha(hex: string, alpha: number): string { export function fbAlpha(hex: string, alpha: number): string {
let base: string = hex.trim(); let base: string = hex.trim();
if (base.startsWith('#')) { if (base.startsWith('#')) {
+42 -18
View File
@@ -29,7 +29,7 @@
* 挤出屏幕(本项目 2026-10-20 真机事故,见 pitfalls)。 * 挤出屏幕(本项目 2026-10-20 真机事故,见 pitfalls)。
*/ */
import { FbColors, FbMetric, FB_MASK, fbAlpha, fbTint } from '../common/Theme'; import { FbColors, FbMetric, FB_MASK, fbAlpha, fbTint, fbMixWhite } from '../common/Theme';
import { import {
FbBoardOpt, FbCatOpt, FbTaskAttach, FbTaskComment, FbTaskDetail, FbTaskItem, FbTaskKid, FbTaskShare, FbBoardOpt, FbCatOpt, FbTaskAttach, FbTaskComment, FbTaskDetail, FbTaskItem, FbTaskKid, FbTaskShare,
} from '../model/Types'; } from '../model/Types';
@@ -1472,32 +1472,51 @@ export struct FbTaskPanel {
.backgroundColor(FbColors.inputBg) .backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS) .borderRadius(FbMetric.radiusS)
} else { } else {
/* 🔴 #84(用户 2026-11-05): /* 🔴🔴 #86(用户 2026-11-05):「鸿蒙端也用类似的方式调,
「分类弹出来的框太窄了,应该和输入框一样宽。」 效果让它保持一致。」
「颜色也不对。它的颜色应该是主题色:里面的文字颜色是主题色,
背景色是比主题色浅一点的那个主题色。」
· `width('100%')` 管的是**控件本身**;弹出来的下拉菜单 网页端照 FeedLite 做成"自绘按钮 + 浮层列表"(原生 <select> 的
还有自己的宽度(`optionWidth`),不给就按内容撑 ⇒ 很窄。 展开列表 CSS 改不动)。端上**没有那个限制** ——
用 `optionWidth(OptionWidthMode.FIT_TRIGGER)` 让它跟触发器同宽。 ArkUI 的 `Select` 本身就是自绘的,选项列表也归框架管,
· 文字 `this.accent`、底色 `fbAlpha(this.accent, 0.10)` 所以不用再自己写浮层,把对应的属性都设上就等价了:
—— 与面板里「+ 添加附件」那颗胶囊同一套(主题色 + 淡底),
这样"可操作"的控件在全面板观感一致。 */ 网页端 ArkUI Select
─────────────────────────────────────────────────
按钮 40 高 / 圆角 / 细边框 height(40) + borderRadius + border
箭头打开转 180° 框架内置(有动画)
悬停 tint-1 不支持 → 用打开态代替
选中项底 tint-2 selectedOptionBgColor
选中项文字 主题色 selectedOptionFontColor
选中项右侧对勾 showDefaultSelectedIcon(true)
浮层白底 + 细边框 + 圆角 menuBackgroundColor + menuOutline
浮层与按钮同宽 optionWidth(FIT_TRIGGER)
⚠️ 底色用 `fbMixWhite(accent, 0.91)` 而**不是** `fbAlpha`:
那个是"原色+透明度"(会透出父容器底色,浮层背景不确定
就会时深时浅);这个是与网页端同算法的实色混合。
⚠️ 未选值(「未分类」/「不循环」)文字用 muted 次要色,
与网页端 `.sel-btn.is-placeholder` 一致。 */
Select(this.catOptions()) Select(this.catOptions())
.selected(this.catIndex()) .selected(this.catIndex())
.value(this.categoryOf() !== '' ? this.categoryOf() : '未分类') .value(this.categoryOf() !== '' ? this.categoryOf() : '未分类')
.font({ size: FbMetric.fontSmall, weight: FontWeight.Medium }) .font({ size: FbMetric.fontSmall, weight: FontWeight.Medium })
.fontColor(this.accent) /* #86:有值用主题色、未选用次要灰(占位不该和真值一样显眼) */
.fontColor(this.categoryOf() !== '' ? this.accent : FbColors.faint)
.selectedOptionFont({ size: FbMetric.fontSmall, weight: FontWeight.Medium }) .selectedOptionFont({ size: FbMetric.fontSmall, weight: FontWeight.Medium })
.selectedOptionFontColor(this.accent) .selectedOptionFontColor(this.accent)
.selectedOptionBgColor(fbMixWhite(this.accent, 0.80))
.optionFont({ size: FbMetric.fontSmall }) .optionFont({ size: FbMetric.fontSmall })
.optionFontColor(FbColors.ink) .optionFontColor(FbColors.ink)
.optionBgColor(Color.White)
.showDefaultSelectedIcon(true)
.optionWidth(OptionWidthMode.FIT_TRIGGER) .optionWidth(OptionWidthMode.FIT_TRIGGER)
.menuBackgroundColor(Color.White)
.width('100%') .width('100%')
.height(40) .height(40)
.padding({ left: 4, right: 4 }) .padding({ left: 12, right: 12 })
.backgroundColor(fbAlpha(this.accent, 0.10)) .backgroundColor(fbMixWhite(this.accent, 0.91))
.borderRadius(FbMetric.radiusS) .borderRadius(FbMetric.radiusS)
.border({ width: 1, color: fbMixWhite(this.accent, 0.62) })
.onSelect((idx: number): void => { .onSelect((idx: number): void => {
/* index 0 = 「未分类」→ category_id 传 0 */ /* index 0 = 「未分类」→ category_id 传 0 */
const cid: number = idx === 0 ? 0 const cid: number = idx === 0 ? 0
@@ -1522,22 +1541,27 @@ export struct FbTaskPanel {
.backgroundColor(FbColors.inputBg) .backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS) .borderRadius(FbMetric.radiusS)
} else { } else {
/* #84:与分类同一套(主题色文字 + 淡主题底色 + 菜单同宽) */ /* #86:与分类完全同一套(见上面那段的对照表) */
Select(this.repeatOptions()) Select(this.repeatOptions())
.selected(this.repeatIndex()) .selected(this.repeatIndex())
.value(this.repeatOf() !== '' ? this.repeatWordOf(this.repeatOf()) : '不循环') .value(this.repeatOf() !== '' ? this.repeatWordOf(this.repeatOf()) : '不循环')
.font({ size: FbMetric.fontSmall, weight: FontWeight.Medium }) .font({ size: FbMetric.fontSmall, weight: FontWeight.Medium })
.fontColor(this.accent) .fontColor(this.repeatOf() !== '' ? this.accent : FbColors.faint)
.selectedOptionFont({ size: FbMetric.fontSmall, weight: FontWeight.Medium }) .selectedOptionFont({ size: FbMetric.fontSmall, weight: FontWeight.Medium })
.selectedOptionFontColor(this.accent) .selectedOptionFontColor(this.accent)
.selectedOptionBgColor(fbMixWhite(this.accent, 0.80))
.optionFont({ size: FbMetric.fontSmall }) .optionFont({ size: FbMetric.fontSmall })
.optionFontColor(FbColors.ink) .optionFontColor(FbColors.ink)
.optionBgColor(Color.White)
.showDefaultSelectedIcon(true)
.optionWidth(OptionWidthMode.FIT_TRIGGER) .optionWidth(OptionWidthMode.FIT_TRIGGER)
.menuBackgroundColor(Color.White)
.width('100%') .width('100%')
.height(40) .height(40)
.padding({ left: 4, right: 4 }) .padding({ left: 12, right: 12 })
.backgroundColor(fbAlpha(this.accent, 0.10)) .backgroundColor(fbMixWhite(this.accent, 0.91))
.borderRadius(FbMetric.radiusS) .borderRadius(FbMetric.radiusS)
.border({ width: 1, color: fbMixWhite(this.accent, 0.62) })
.onSelect((idx: number): void => { .onSelect((idx: number): void => {
const rk: string = idx === 0 ? '' : FbTaskPanel.REPEAT_KEYS[idx - 1]; const rk: string = idx === 0 ? '' : FbTaskPanel.REPEAT_KEYS[idx - 1];
this.saveField('repeat_rule', rk, -1); this.saveField('repeat_rule', rk, -1);