/* ============================== src/page-records.js ============================== * 运行记录(规格书 D1/D2/D3,设计稿 30:871 / 30:958 / 30:1241)。 * * D1 是「记录」tab 的一级页,D3 是它的二级页(点任一条进)。 * D2 是 D1 的空态 —— **空态是设计稿专门画的一屏**,不是"没数据就不显示": * 一个空页面看起来像坏了,空态告诉用户"去哪能产生记录"。 * * 记录本身由 `Records` 模块负责读写(见 src/records.js), * 这一层只负责画:不猜字段、不编文案 —— 没有的东西就不渲染那一行。 */ var Pages = Pages || {}; /** 「脚本一 · 粉丝页提 ID」→「脚本一」。卡上只放得下短名,全名在详情页。 */ Pages.shortScriptName = function (name) { var text = String(name || ""); var cut = text.indexOf(" \u00B7 "); return cut > 0 ? text.substring(0, cut) : text; }; /** 记录条目 / 详情页统一用「任务 · 脚本」当标题。 */ Pages.recordTitle = function (record) { var task = record.taskName || Pages.taskName(record.taskKey); var script = Pages.shortScriptName(record.scriptName); return script === "" ? task : (task + " \u00B7 " + script); }; /** 记录用的任务色/图标(与任务卡一致,列表里一眼能对上号)。 */ function recordVisual(record) { var task = Pages.taskOf(record.taskKey); return { colorHex: task ? task.colorHex : Theme.hex.accent, iconChar: task ? task.icon : "\u25A3" }; } /* ------------------------------------------------------------ 日志行 */ /** * 日志行的配色(规格书:INFO 灰 / WARN 琥珀 / ERROR 玫瑰红)。 * STEP 归到 INFO:它是流程节点,不是异常。 */ function logColor(level) { if (level === "WARN") { return Theme.hex.warn; } if (level === "ERROR") { return Theme.hex.danger; } return Theme.hex.textFaint; } function pad2(n) { return n < 10 ? "0" + n : String(n); } /** 时间戳 → HH:MM:SS(日志行前缀,等宽显示才对齐)。 */ function logClock(ms) { var d = new Date(ms || 0); return pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds()); } /** * 一条日志行。 * * 用**等宽字体**并且手动补空格对齐 level:设计稿里 `10:24:01 INFO 启动脚本` * 的 level 是定宽的,用比例字体会让每一行的正文起点参差不齐。 */ function buildLogLine(entry) { var raw = String(entry.level || "INFO").toUpperCase(); var level = raw; while (level.length < 5) { level += " "; } var text = logClock(entry.t) + " " + level + " " + String(entry.text || ""); var view = U.monoText(text, Theme.m.logSize, logColor(raw)); U.params(view, -1, -2, 0); return view; } /* ------------------------------------------------------------ D1 记录列表 */ function buildRecordRow(record, snap) { var visual = recordVisual(record); var result = Records.RESULT[record.result] || Records.RESULT.failed; var resumable = Resume.isRecordResumable(record, snap); return W.recordRow({ title: Pages.recordTitle(record), iconChar: visual.iconChar, colorHex: visual.colorHex, resultLabel: result.label, resultTone: result.tone, timeLine: Records.timeLine(record), resultLine: Records.resultLine(record), /* 只有"这条记录就是那个没跑完的断点"时才给「继续」。 * 判据在 Resume.isRecordResumable 里(含 runId 精确匹配)—— * 否则一串历史「手动停止」记录上都会冒出「继续」,而只有最后一条能真续。 */ actionLabel: resumable ? "\u7EE7\u7EED" : "", actionTone: "accent", onAction: function () { App.resumeRun(record); }, onOpen: function () { App.openRecord(record.id); } }); } /** D1:记录列表(有记录时)。 */ Pages.records = function () { var inner = U.column(); var blocks = []; var list = Records.list(); /* 断点只读一次:50 条记录各读一次盘是没必要的浪费 */ var snap = Resume.snapshot(); /* 标题区由路由层统一加(见 App.renderContent)——页面再加一次就会渲染两遍 */ var section = U.column(); section.addView(W.sectionTitle("\u8FD0\u884C\u8BB0\u5F55")); section.addView(U.spacer(4)); /* 「共 N 条 · 点任一条查看执行日志」——条数是真实的,空列表走 D2 那一屏 */ section.addView(U.text("\u5171 " + list.length + " \u6761 \u00B7 \u70B9\u4EFB\u4E00\u6761\u67E5\u770B\u6267\u884C\u65E5\u5FD7", Theme.m.hintSize, Theme.hex.textDim, false)); blocks.push(section); blocks.push(U.spacer(Theme.m.gapCard)); for (var i = 0; i < list.length; i++) { blocks.push(buildRecordRow(list[i], snap)); if (i < list.length - 1) { blocks.push(U.spacer(Theme.m.gapCard)); } } blocks.push(U.spacer(8)); W.stack(inner, blocks, 0); var scroller = W.scroller(); scroller.addView(inner); return scroller; }; /** D2:记录空态。 */ Pages.recordsEmpty = function () { var inner = U.column(); /* 标题区同样由路由层提供;空态只剩「区标题 + 空态块」 */ inner.addView(W.sectionTitle("\u8FD0\u884C\u8BB0\u5F55")); inner.addView(W.emptyState("\u25F4", "\u8FD8\u6CA1\u6709\u8FD0\u884C\u8BB0\u5F55", "\u53BB\u300C\u4EFB\u52A1\u300D\u6807\u7B7E\u9009\u4E00\u4E2A\u811A\u672C\u8DD1\u8D77\u6765")); return inner; }; /* ------------------------------------------------------------ D3 运行详情 */ /** * 结果摘要卡。 * 三行都是**有条件渲染**的:结果文案/结果文件只有引擎回传了才有, * 没有就不画那一行 —— 空着的一行会让用户以为"数据丢了"。 */ function buildSummaryCard(record) { var card = W.card(false); var rows = []; var result = Records.RESULT[record.result] || Records.RESULT.failed; var titleRow = U.row(); var title = U.text(Pages.recordTitle(record), Theme.m.taskNameSize, Theme.hex.textPrimary, true); title.setSingleLine(true); title.setEllipsize(android.text.TextUtils.TruncateAt.END); U.params(title, 0, -2, 1); titleRow.addView(title); titleRow.addView(U.hgap(8)); titleRow.addView(W.badge(result.label, result.tone).root); rows.push(titleRow); rows.push(U.text(Records.timeLine(record), Theme.m.chipSize, Theme.hex.textFaint, false)); if (record.resultText) { rows.push(U.text(record.resultText, Theme.m.chipSize, Theme.hex.textDim, false)); } if (record.resultFile) { pushFileRows(rows, "\u7ED3\u679C\u6587\u4EF6", record.resultFile); } if (record.mapFile) { pushFileRows(rows, "uid \u6620\u5C04\uFF08username\u2192\u6570\u5B57 uid\uFF09", record.mapFile); } W.stack(card, rows, 10); return card; } /** * 一份产物文件:说明行 + 路径行(点路径打开)+ 打开/分享两个按钮。 * * 抽成函数是因为现在有两份产物(结果文件 + uid 映射文件), * 各自都要能打开和分享 —— 光在界面上写一句「已生成 uid 映射」而取不到文件, * 等于让用户自己去翻 /sdcard。 */ function pushFileRows(rows, caption, path) { rows.push(U.text(caption, Theme.m.chipSize, Theme.hex.textFaint, false)); var fileRow = U.row(); var pathText = U.text(path, Theme.m.chipSize, Theme.hex.accentText, false); pathText.setSingleLine(true); pathText.setEllipsize(android.text.TextUtils.TruncateAt.MIDDLE); U.params(pathText, 0, -2, 1); fileRow.addView(pathText); fileRow.addView(U.hgap(8)); fileRow.addView(W.smallButton("\u6253\u5F00", "accent", function () { App.openResultFile(path); }).root); fileRow.addView(U.hgap(6)); fileRow.addView(W.smallButton("\u5206\u4EAB", "default", function () { App.shareResultFile(path); }).root); rows.push(fileRow); } /** 日志卡:标题行(执行日志 + 复制)+ 日志行。 */ function buildLogCard(record) { var card = W.card(false); var logs = record.logs || []; var head = U.row(); head.addView(U.text("\u6267\u884C\u65E5\u5FD7", Theme.m.labelSize, Theme.hex.textSecondary, true)); head.addView(U.flexible()); head.addView(W.smallButton("\u590D\u5236", "default", function () { App.copyLogs(record); }).root); card.addView(head); card.addView(U.spacer(10)); if (logs.length === 0) { /* 如实说明,而不是画一张空卡片。日志来自引擎的 --log 通道(见 Engine.readLog), * 没接引擎的任务本来就不会有日志。 */ card.addView(U.text("\u672C\u6B21\u8FD0\u884C\u6CA1\u6709\u56DE\u4F20\u65E5\u5FD7", Theme.m.chipSize, Theme.hex.textDim, false)); return card; } for (var i = 0; i < logs.length; i++) { if (i > 0) { card.addView(U.spacer(6)); } card.addView(buildLogLine(logs[i])); } return card; } /** * D3:运行详情。 * 记录被清理掉(超过 Records.MAX 或被手工清空)时,**不能白屏** —— * 给一句"这条记录已不在本地"和一条回到列表的路。 */ Pages.recordDetail = function (record) { var inner = U.column(); inner.addView(W.backHeader("\u8FD0\u884C\u8BE6\u60C5", function () { App.closeRecord(); })); if (!record) { inner.addView(U.spacer(Theme.m.gapPage)); inner.addView(W.emptyState("\u25F4", "\u8FD9\u6761\u8BB0\u5F55\u5DF2\u4E0D\u5728\u672C\u5730", "\u8BB0\u5F55\u4EC5\u4FDD\u7559\u6700\u8FD1 " + Records.MAX + " \u6761")); var scroller0 = W.scroller(); scroller0.addView(inner); return scroller0; } inner.addView(U.spacer(Theme.m.gapCard)); /* 这条记录还能接着跑 → 在看日志的地方也放一个入口(列表行那个是"一眼可见", * 这个是"看完日志顺手继续")。判据与列表行同一份。 */ var snap = Resume.snapshot(); if (Resume.isRecordResumable(record, snap)) { inner.addView(W.noticeBar({ title: "\u8FD9\u6B21\u8FD0\u884C\u6CA1\u8DD1\u5B8C\uFF08\u5DF2\u6536 " + snap.extracted + " \u4E2A\uFF09", detail: Resume.detail(snap), tone: "accent", actions: [{ label: "\u7EE7\u7EED", tone: "accent", onClick: function () { App.resumeRun(record); } }] }).root); inner.addView(U.spacer(Theme.m.gapCard)); } inner.addView(buildSummaryCard(record)); inner.addView(U.spacer(Theme.m.gapCard)); inner.addView(buildLogCard(record)); inner.addView(U.spacer(8)); var scroller = W.scroller(); scroller.addView(inner); return scroller; };