/* ============================== src/theme.js ============================== * 设计令牌 + 绘制工具。 * * 所有颜色/尺寸/字号都取自 Ardot 设计稿(file 734917825641912), * 页面代码里**不允许出现裸色值或裸尺寸**——要改视觉先改这里。 * * 为什么用「代码建 View」而不是 XML 布局: * ① 设计稿用了渐变、径向光晕、多层描边、按状态换色,XML 属性表达不了, * 最后仍要落到代码里 setBackground(GradientDrawable),反而两套并存; * ② XML 字面量(E4X)不是标准 JS,PC 侧无法用 node 做语法检查与离线自检, * 而 AutoX 设备端是没有 linter 的(见规范 SKILL.md「设备端没有门禁」)。 * 纯代码建 View 让整个产物能在 PC 上被语法检查和桩测。 */ var Theme = {}; /* ---------------------------------------------------------------- 颜色 */ Theme.hex = { bg: "#030014", tabBar: "#0A0A17", dialogBg: "#17142B", accent: "#2EA6FF", accentText: "#7CC7FF", accentDeep: "#9381FF", /* 任务卡图标衬底的紫。设计稿里 UID私信 的图标框是 #9482FF@16%, * 与主色渐变里的紫(#9381FF)**不是同一个值** —— 不要"统一"成一个。 */ violet: "#9482FF", textPrimary: "#FFFFFF", textSecondary: "#CFCCE6", /* 弹窗里的设置名用的是更亮一档的正文色(设计稿 §6:正文 #CFCCE6 / #E6E4F5) */ textBody: "#E6E4F5", textMuted: "#9A97B8", textFaint: "#8E8BA8", /* 弱提示(区说明 / 「上次运行」/ 底部提示)。比 textFaint 再暗一档, * 三级文字层次来自设计稿 §6:次要 #9A97B8·#8E8BA8 / 弱提示 #6F6C8A。 */ textDim: "#6F6C8A", /* 未选中页签的颜色。**不要删**:它一旦缺失,argb(undefined) 会算出 0 * (全透明),非活动页签会整片消失 —— 而且 uiautomator 里它们的 * bounds 是正常的,只有肉眼看得出来,极难定位。 * 取值 2026-10-13 更新为 #8E8BA8:旧的 #7A7794 对比度只有 4.4:1, * 未达 WCAG AA(规格书 §9 第 5 条已统一改为新值)。 */ tabIdle: "#8E8BA8", /* 警示色:设计稿里「未开启」状态标签与「确认解绑」按钮用的是两种不同的暖色。 * warn 偏琥珀(权限告缺、卡密到期),danger 偏红(不可逆操作)。 */ warn: "#F5C24B", danger: "#FF5470", ok: "#13DDC4", divider: "#FFFFFF" }; /* 卡片与标签的填充/描边一律是「白色 + 透明度」,不是独立色值。 * 这样叠在深色背景与叠在渐变光晕上的观感一致。 */ Theme.alpha = { cardFill: 0.04, cardFillActive: 0.06, cardStroke: 0.08, cardStrokeActive: 0.35, chipFill: 0.06, chipStroke: 0.10, accentChipFill: 0.20, /* 全局运行条:青蓝 12% 填充(设计稿 B2 的 30:304,无描边) */ runBarFill: 0.12, runBarStroke: 0.28, /* 警示图标底 / 任务图标框:设计稿里是 14% 的同色衬底 */ warnFill: 0.14, iconBoxFill: 0.14, mask: 0.72, glowA: 0.30, glowB: 0.20 }; /* ---------------------------------------------------------------- 尺寸 */ Theme.m = { screenW: 375, contentPadding: 20, cardPadding: 14, cardRadius: 16, chipRadius: 8, pillRadius: 99, dialogRadius: 24, gapPage: 16, gapCard: 12, gapTight: 8, gapInline: 10, titleSize: 22, sectionSize: 16, labelSize: 13, chipSize: 11, hintSize: 11, tabSize: 11, rowHeight: 26, chipPadV: 6, chipPadH: 6, tabBarHeight: 49, tabIconSize: 22, runBtnHeight: 48, runBtnWidth: 335, runBarRadius: 12, runBarPadV: 12, runBarPadH: 10, warnIconSize: 40, warnIconRadius: 12, loginLogoSize: 72, loginLogoRadius: 20, loginBtnHeight: 48, dialogWidth: 315, statusBarHeight: 62, safeAreaHeight: 33, touchTarget: 44, /* ===== 2026-10-13 规格书新增(§6 尺寸与圆角 / 触控标准)===== */ /* 二级页返回头:高 44,返回热区 44×44,标题 21 SemiBold */ backHeaderHeight: 44, backHotSize: 44, backArrowSize: 20, detailTitleSize: 21, /* 任务卡(B1/B2):图标框 40 圆角 12 / 图标 22 / 卡名 15 / * 运行按钮 68×44(触控标准明确写了 68×44) */ taskIconBox: 40, taskIconSize: 22, taskIconRadius: 12, taskNameSize: 15, taskRunBtnW: 68, taskRunBtnH: 44, /* 记录条目(D1/D3):图标框 36 圆角 10 / 图标 20 / 卡片圆角 14 */ recordIconBox: 36, recordIconSize: 20, recordIconRadius: 10, recordCardRadius: 14, /* 状态徽标(空闲/运行中/未配置)与结果标签:全圆角小胶囊 */ badgePadH: 8, badgePadV: 3, /* 底栏整体高度 = 页签行 49 + 安全区 33(设计稿 §6) */ tabBarTotal: 83, /* 区间输入框:**单框宽 56、数字居中**(§6)。比早期实现的 64 窄, * 是为了让「标签 + 56 ~ 56」一行放得下而不挤。 */ rangeFieldW: 56, /* 内容区底部渐隐:y=577 h=80(§6),右侧滚动指示条 x=367 w=3 */ fadeHeight: 80, indicatorW: 3, /* 弹窗按钮:行动按钮 44 / 主按钮 46(A4 的去授权是 46) */ dialogBtnHeight: 44, dialogPrimaryHeight: 46, /* 日志行(D3):等宽 11 / 行距紧凑 */ logSize: 11 }; /* ---------------------------------------------------------------- 工具 */ /** dp → px。设计稿是 375 逻辑宽,必须换算后再交给原生控件。 */ function dp(value) { var density = 1; try { density = context.getResources().getDisplayMetrics().density; } catch (e) { /* 离线桩测环境没有 Android context,退化为 1:1,不影响逻辑正确性 */ density = 1; } return Math.round(value * density); } /** * "#2EA6FF" → Java 可用的 ARGB int(负数形式,与 colors.parseColor 同构)。 * * 色值写错时返回**醒目的洋红**而不是 0。0 是全透明: * 界面上的表现是「某块东西不见了」,不报错、不留痕 —— * tabIdle 掉过一次,整排非活动页签就是这么消失的, * 而 uiautomator 里它们的 bounds 一切正常,只有肉眼能发现。 * 洋红至少让错误在一眼之内就暴露出来。 */ function argb(hex) { var h = String(hex).replace("#", ""); if (h.length === 6) { h = "FF" + h; } var value = parseInt(h, 16); if (isNaN(value)) { console.log("[theme] 非法色值(已用洋红代替): " + hex); return -65281; /* 0xFFFF00FF */ } return value | 0; } /** 给 hex 色叠加不透明度,得到 ARGB int。#FFFFFF + 0.06 → 约 #0FFFFFFF */ function alphaOf(hex, alpha) { var h = String(hex).replace("#", ""); if (h.length === 8) { h = h.substring(2); } var a = Math.round(Math.max(0, Math.min(1, alpha)) * 255); return parseInt(("0" + a.toString(16)).slice(-2) + h, 16) | 0; } /** 纯色圆角背景。 */ function solidBg(colorInt, radiusDp) { var drawable = new android.graphics.drawable.GradientDrawable(); drawable.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE); drawable.setColor(colorInt); if (radiusDp) { drawable.setCornerRadius(dp(radiusDp)); } return drawable; } /** 圆角 + 描边(卡片默认形态)。 */ function strokeBg(fillAlpha, strokeAlpha, strokeHex, radiusDp) { var drawable = new android.graphics.drawable.GradientDrawable(); drawable.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE); drawable.setColor(alphaOf("#FFFFFF", fillAlpha)); drawable.setCornerRadius(dp(radiusDp)); drawable.setStroke(dp(1), alphaOf(strokeHex || "#FFFFFF", strokeAlpha)); return drawable; } /** 线性渐变(主按钮、选中态)。默认左→右,与设计稿一致。 */ function gradientBg(fromHex, toHex, radiusDp) { var drawable = new android.graphics.drawable.GradientDrawable(); drawable.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE); drawable.setOrientation(android.graphics.drawable.GradientDrawable.Orientation.LEFT_RIGHT); drawable.setColors([argb(fromHex), argb(toHex)]); if (radiusDp) { drawable.setCornerRadius(dp(radiusDp)); } return drawable; } /** 径向光晕:中心有色、边缘透明,用来复刻设计稿背景的两团光。 */ function glowBg(hex, alpha) { var drawable = new android.graphics.drawable.GradientDrawable(); var colors = [alphaOf(hex, alpha), alphaOf(hex, 0)]; drawable.setShape(android.graphics.drawable.GradientDrawable.OVAL); drawable.setGradientType(android.graphics.drawable.GradientDrawable.RADIAL_GRADIENT); drawable.setGradientRadius(dp(220)); drawable.setColors(colors); return drawable; } /** 页面根背景:底色 + 两团光晕,所有承载体统一从这里取,避免各自拼。 */ function screenBackground() { return solidBg(argb(Theme.hex.bg)); } /** * 竖向渐隐(透明 → 色),用在内容区底部(设计稿 §6「底部渐隐 y=577 h=80」)。 * * 为什么要它:内容区是可滚动的,卡片被底栏"硬切"一刀会让人以为列表到头了。 * 加一层渐隐,滚动的连续性一眼可见。 * 颜色取**页面底色**:叠在空内容上时它等于背景,不会露出多余的色块。 */ function fadeBg(hex) { var drawable = new android.graphics.drawable.GradientDrawable(); drawable.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE); drawable.setOrientation(android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM); drawable.setColors([alphaOf(hex, 0), argb(hex)]); return drawable; } /** 同色衬底(任务图标框 / 警示图标底):hex + 透明度,圆角方块。 */ function tintBg(hex, alpha, radiusDp) { var drawable = new android.graphics.drawable.GradientDrawable(); drawable.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE); drawable.setColor(alphaOf(hex, alpha)); if (radiusDp) { drawable.setCornerRadius(dp(radiusDp)); } return drawable; } /* ------------------------------------------------------------- 基础控件 */ /** 统一的 TextView 工厂,防止每个页面各写一遍字号字色。 */ function text(content, sizeSp, hex, bold) { var view = new android.widget.TextView(context); view.setText(String(content === null || content === undefined ? "" : content)); view.setTextSize(sizeSp); view.setTextColor(argb(hex)); if (bold) { view.setTypeface(android.graphics.Typeface.DEFAULT_BOLD); } return view; } /** * 等宽数字(设计稿里 200 / 30、2026.10.10 用的是 JetBrains Mono)。 * 与 text() 一样带 undefined 兜底——String(undefined) 会变成界面上的 "undefined"。 */ function monoText(content, sizeSp, hex) { var view = new android.widget.TextView(context); view.setText(content === null || content === undefined ? "" : String(content)); view.setTextSize(sizeSp); view.setTextColor(argb(hex)); view.setTypeface(android.graphics.Typeface.MONOSPACE); return view; } function box(widthDp, heightDp) { var view = new android.view.View(context); view.setLayoutParams(new android.widget.LinearLayout.LayoutParams( widthDp >= 0 ? dp(widthDp) : widthDp, heightDp >= 0 ? dp(heightDp) : heightDp )); return view; } /** * 垂直间距。⚠️ 只能用在**竖向** LinearLayout 里。 * * 它的宽度是 MATCH_PARENT,放进横向容器会占满整行剩余空间, * 把同行后面那些 weight=1 的 View 挤成 0 宽(真机上表现为「文字全部消失」)。 * 横向间距请用 hgap()。 */ function spacer(heightDp) { return box(-1, heightDp); } /** * 横向间距。固定宽度、高度 1px。 * 与 spacer 的区别只在宽度策略,但用错了就是「文字不见了」这种难查的现象, * 所以刻意分成两个名字,让用错的时候在代码上一眼能看出来。 */ function hgap(widthDp) { var view = new android.view.View(context); view.setLayoutParams(new android.widget.LinearLayout.LayoutParams(dp(widthDp), 1)); return view; } /** 撑开剩余空间,用于「左标题 / 右控件」两栏布局。 */ function flexible() { var view = new android.view.View(context); var lp = new android.widget.LinearLayout.LayoutParams(0, 1); lp.weight = 1; view.setLayoutParams(lp); return view; } /** * 横向行。 * * fill 默认为 true:横向行几乎都是「左标签(weight=1) + 右控件」的结构, * **必须显式填满宽度**,否则父容器按 WRAP_CONTENT 测量时, * weight=1 的左侧标签会分到 0 宽 —— 真机上表现为「这一行的文字不见了」。 * 需要包裹内容的场合(例如作为另一行右侧的控件组)显式传 false。 */ function row(fill) { var layout = new android.widget.LinearLayout(context); layout.setOrientation(android.widget.LinearLayout.HORIZONTAL); layout.setGravity(android.view.Gravity.CENTER_VERTICAL); if (fill === undefined || fill) { layout.setLayoutParams(new android.widget.LinearLayout.LayoutParams(-1, -2)); } return layout; } /** 竖向容器,同样默认填满宽度(卡片、页面主体都属于这类)。 */ function column(fill) { var layout = new android.widget.LinearLayout(context); layout.setOrientation(android.widget.LinearLayout.VERTICAL); if (fill === undefined || fill) { layout.setLayoutParams(new android.widget.LinearLayout.LayoutParams(-1, -2)); } return layout; } /** 给 LinearLayout 统一挂 LayoutParams,省掉每处的重复样板。 */ function params(view, widthDp, heightDp, weight) { var lp = new android.widget.LinearLayout.LayoutParams( widthDp >= 0 ? dp(widthDp) : widthDp, heightDp >= 0 ? dp(heightDp) : heightDp ); if (weight) { lp.weight = weight; } view.setLayoutParams(lp); return view; } function marginParams(view, widthDp, heightDp, l, t, r, b) { var lp = new android.widget.LinearLayout.LayoutParams( widthDp >= 0 ? dp(widthDp) : widthDp, heightDp >= 0 ? dp(heightDp) : heightDp ); lp.setMargins(dp(l), dp(t), dp(r), dp(b)); view.setLayoutParams(lp); return view; } Theme.util = { dp: dp, argb: argb, alphaOf: alphaOf, solidBg: solidBg, strokeBg: strokeBg, gradientBg: gradientBg, glowBg: glowBg, fadeBg: fadeBg, tintBg: tintBg, screenBackground: screenBackground, text: text, monoText: monoText, box: box, spacer: spacer, hgap: hgap, flexible: flexible, row: row, column: column, params: params, marginParams: marginParams }; /* 便捷短名:页面代码里 90% 的调用都是这几个,长名前缀会淹掉结构。 */ var U = Theme.util;