B2B网络软件

标题: floatUI深色控制台风格悬浮窗日志 [打印本页]

作者: YYPOST群发软件    时间: 3 小时前
标题: floatUI深色控制台风格悬浮窗日志
floatUI深色控制台风格悬浮窗日志
floatUI深色控制台风格悬浮窗日志 B2B网络软件

floatUI深色控制台风格悬浮窗日志 B2B网络软件

floatUI深色控制台风格悬浮窗日志 B2B网络软件

  1. /**
  2. * ╔══════════════════════════════════════════════════════════╗
  3. * ║   floatUI深色控制台风格悬浮窗日志
  4. * ║   知识点:floatUI 悬浮窗、彩色日志(Html)、              ║
  5. * ║          进度条、自动滚屏、event.onStopBtnEvent、        ║
  6. * ║          runTime.stopAllRuning()                         ║
  7. * ╚══════════════════════════════════════════════════════════╝
  8. *
  9. * 目标:深色控制台风格悬浮窗(标题栏 + 统计栏 + 进度条 + 彩色日志 + 状态栏),
  10. *       循环跑模拟任务实时打日志,点停止按钮时自动关闭并清理线程。
  11. * 难度:★★☆☆☆ 进阶第二课
  12. * 运行:直接在 AIWROK 里跑,不需要装任何 App
  13. *
  14. * 注意(Rhino ES5 规则):
  15. * 1. 不支持 ES6 模板字符串,一律用 + 拼接
  16. * 2. 函数必须先定义再调用
  17. * 3. 全局变量统一在文件顶部声明,避免 ReferenceError
  18. * 4. 日志用 printl,不要用 print.log()
  19. * 5. Import / importClass 必须放在全局最外层,不能进代码块
  20. */

  21. importClass(android.text.Html);

  22. // ==================== 顶部:全局变量声明 ====================
  23. var gWin = null;        // 悬浮窗对象(floatUI)
  24. var gTvTitle = null;    // 标题栏计数控件
  25. var gTvTask = null;     // 统计栏:任务数
  26. var gTvOk = null;       // 统计栏:成功数
  27. var gTvErr = null;      // 统计栏:失败数
  28. var gTvTime = null;     // 统计栏:耗时
  29. var gTvBar = null;      // 进度条控件
  30. var gTvStatus = null;   // 底部状态栏
  31. var gTvLog = null;      // 日志 TextView 控件
  32. var gScroll = null;     // 日志 ScrollView(用于自动滚屏)
  33. var gHtmlLines = [];    // 彩色日志历史(存 Html 字符串)
  34. var gLogCount = 0;      // 日志总条数
  35. var gMaxLines = 100;    // 最多保留 100 行
  36. var gOkCount = 0;       // 成功计数
  37. var gErrCount = 0;      // 失败计数
  38. var gStartTs = 0;       // 启动时间戳
  39. var gStopTick = false;  // 计时线程退出标志

  40. // ==================== 调色板(想换配色改这里)====================
  41. var C_BG_OUTER = "#33000000";   // 外框(半透明黑,形成描边感)
  42. var C_HEADER   = "#0EA5E9";     // 标题栏:天蓝
  43. var C_PANEL    = "#0F172A";     // 面板底:深蓝黑
  44. var C_BODY     = "#0B1220";     // 日志区底:更深
  45. var C_TEXT     = "#E2E8F0";     // 正文
  46. var C_DIM      = "#64748B";     // 时间戳(暗)
  47. var C_OK       = "#4ADE80";     // 成功(绿)
  48. var C_ERR      = "#F87171";     // 失败(红)
  49. var C_WARN     = "#FBBF24";     // 提醒(黄)
  50. var C_ACCENT   = "#38BDF8";     // 强调(亮蓝)

  51. // ==================== 工具函数(先定义,后使用)====================

  52. // 阻塞延时:Rhino 里用 Thread.sleep,统一封装成 doSleep
  53. function doSleep(ms) {
  54.     try { java.lang.Thread.sleep(ms); } catch (e) {}
  55. }

  56. // 取两位数时间戳,例如 09:05:03
  57. function getTimeStr() {
  58.     var d = new Date();
  59.     var h = d.getHours();     if (h < 10) h = "0" + h;
  60.     var m = d.getMinutes();   if (m < 10) m = "0" + m;
  61.     var s = d.getSeconds();   if (s < 10) s = "0" + s;
  62.     return h + ":" + m + ":" + s;
  63. }

  64. // 取 HH:MM:SS 耗时
  65. function getElapsedStr() {
  66.     var ms = new Date().getTime() - gStartTs;
  67.     var mm = Math.floor(ms / 60000);
  68.     var ss = Math.floor((ms % 60000) / 1000);
  69.     return (mm < 10 ? "0" : "") + mm + ":" + (ss < 10 ? "0" : "") + ss;
  70. }

  71. // 拼一行彩色日志:<font> 包时间戳 / 图标 / 正文
  72. function buildHtmlLine(ts, icon, iconColor, msg, msgColor) {
  73.     return "<font color='" + C_DIM + "'>" + ts + "</font>  " +
  74.            "<font color='" + iconColor + "'>" + icon + "</font>  " +
  75.            "<font color='" + msgColor + "'>" + msg + "</font>";
  76. }

  77. // ==================== 第 1 步:创建悬浮窗 ====================
  78. // floatUI 是 AIWROK 的悬浮窗控件,loadXML 用 XML 描述布局
  79. // 规则:尺寸用 "fill_parent" 而非 "*";padding 用单值;没有 <View> 组件
  80. function initFloatWindow() {
  81.     var sw = screen.getScreenWidth();
  82.     var sh = screen.getScreenHeight();
  83.     printl("[屏幕] " + sw + "x" + sh);

  84.     // 按屏幕百分比算尺寸,换任何手机都不会遮挡界面
  85.     var w = Math.floor(sw * 0.52);   // 宽 52%
  86.     var h = Math.floor(sh * 0.30);   // 高 30%
  87.     var x = 8;
  88.     var y = Math.floor(sh * 0.08);   // 距顶部 8%

  89.     // 布局结构:外框(描边感) → 标题栏 → 统计栏 → 进度条 → 日志区 → 状态栏
  90.     var xml =
  91.         '<LinearLayout orientation="vertical" w="' + w + '" h="' + h +
  92.         '" background="' + C_BG_OUTER + '" padding="3">' +

  93.         // ── 标题栏(标题不能用 fill_parent,否则右侧计数被挤出屏幕)──
  94.         '<LinearLayout orientation="horizontal" w="fill_parent" h="wrap_content" ' +
  95.         'background="' + C_HEADER + '" padding="8">' +
  96.         '  <TextView text="&#128640; 运行日志" textColor="#FFFFFF" textSize="13" ' +
  97.         'textStyle="bold"/>' +
  98.         '  <TextView id="hdrCount" text="0 条" textColor="#E0F2FE" textSize="11" ' +
  99.         'w="fill_parent" gravity="right"/>' +
  100.         '</LinearLayout>' +

  101.         // ── 统计栏 ──
  102.         '<LinearLayout orientation="horizontal" w="fill_parent" h="wrap_content" ' +
  103.         'background="' + C_PANEL + '" padding="6">' +
  104.         '  <TextView id="stTask" text="任务 0" textColor="#94A3B8" textSize="11"/>' +
  105.         '  <TextView id="stOk"   text="  ✓ 0" textColor="' + C_OK + '" textSize="11"/>' +
  106.         '  <TextView id="stErr"  text="  ✗ 0" textColor="' + C_ERR + '" textSize="11"/>' +
  107.         '  <TextView id="stTime" text="⏱ 00:00" textColor="' + C_WARN + '" textSize="11" ' +
  108.         'w="fill_parent" gravity="right"/>' +
  109.         '</LinearLayout>' +

  110.         // ── 进度条(用 ▰▱ 字符画,任何版本都支持)──
  111.         '<LinearLayout orientation="horizontal" w="fill_parent" h="wrap_content" ' +
  112.         'background="' + C_PANEL + '" padding="4">' +
  113.         '  <TextView id="progBar" text="▰▰▰▰▰▰▰▰▰▰▰▰ 0%" ' +
  114.         'textColor="' + C_ACCENT + '" textSize="11" w="fill_parent"/>' +
  115.         '</LinearLayout>' +

  116.         // ── 日志区 ──
  117.         '<ScrollView id="logScroll" w="fill_parent" h="fill_parent" background="' + C_BODY + '">' +
  118.         '  <TextView id="logBox" w="fill_parent" h="wrap_content" text="" ' +
  119.         'textColor="' + C_TEXT + '" textSize="11" padding="6"/>' +
  120.         '</ScrollView>' +

  121.         // ── 状态栏 ──
  122.         '<TextView id="statusBar" text="● 正在运行..." textColor="' + C_OK + '" ' +
  123.         'textSize="11" background="' + C_PANEL + '" padding="6" gravity="center"/>' +

  124.         '</LinearLayout>';

  125.     try {
  126.         gWin = new floatUI();
  127.         gWin.loadXML(xml);
  128.         gWin.setPosition(x, y);   // 必须在 loadXML 成功后再调用,否则会空指针
  129.         gTvTitle  = gWin.findViewById("hdrCount");
  130.         gTvTask   = gWin.findViewById("stTask");
  131.         gTvOk     = gWin.findViewById("stOk");
  132.         gTvErr    = gWin.findViewById("stErr");
  133.         gTvTime   = gWin.findViewById("stTime");
  134.         gTvBar    = gWin.findViewById("progBar");
  135.         gTvStatus = gWin.findViewById("statusBar");
  136.         gTvLog    = gWin.findViewById("logBox");
  137.         gScroll   = gWin.findViewById("logScroll");
  138.         printl("[悬浮窗] 创建成功 pos=(" + x + "," + y + ") size=" + w + "x" + h);
  139.         return true;
  140.     } catch (e) {
  141.         printl("[悬浮窗] 创建失败: " + e);
  142.         return false;
  143.     }
  144. }

  145. // ==================== UI 线程调度(本例最关键的修复)====================
  146. // 从脚本线程直接 setText 只会触发"重绘",不会触发"重新测量布局":
  147. // - wrap_content 的日志区高度永远卡在第 1 行 → 后面的日志全被裁掉(只显示一行)
  148. // - 文本变长的控件("任务 10")新内容被裁 → 数字显示不出来
  149. // 解决:用 view.post() 把更新调度到 UI 线程执行,布局才能正常重算。
  150. // (参考:floatUI功能/安卓可移动悬浮窗功能.js 的方案2)
  151. function uiPost(view, fn) {
  152.     if (view && view.post) {
  153.         try { view.post(fn); return; } catch (e) {}
  154.     }
  155.     try { fn(); } catch (e2) {}
  156. }

  157. // ==================== 第 2 步:写日志(彩色 + 自动滚屏)====================
  158. // 关键:用数组存历史 Html 行,setText 时 join 全部输出(防覆盖)。
  159. // Html.fromHtml 让每行的时间戳/图标/正文各显各的颜色。
  160. function appendLog(msg, color) {
  161.     gLogCount++;
  162.     var msgColor = color || C_TEXT;
  163.     if (msg.length > 200) msg = msg.substring(0, 200) + "...";

  164.     // 不同类型给不同图标和颜色
  165.     var icon = "•", iconColor = C_ACCENT;
  166.     if (color == C_OK)   { icon = "✓"; iconColor = C_OK; }
  167.     if (color == C_ERR)  { icon = "✗"; iconColor = C_ERR; }
  168.     if (color == C_WARN) { icon = "➤"; iconColor = C_WARN; }

  169.     gHtmlLines.push(buildHtmlLine(getTimeStr(), icon, iconColor, msg, msgColor));
  170.     if (gHtmlLines.length > gMaxLines) gHtmlLines.shift();  // 超出上限丢掉最旧的一行

  171.     // 在 UI 线程里更新日志 + 计数 + 滚屏,布局才会重新测量
  172.     uiPost(gTvLog, function () {
  173.         try {
  174.             var html = gHtmlLines.join("<br/>");
  175.             gTvLog.setText(Html.fromHtml(html));
  176.             gTvTitle.setText(gLogCount + " 条");
  177.             // 自动滚到最新一行(FOCUS_DOWN = 130)
  178.             if (gScroll) gScroll.fullScroll(130);
  179.         } catch (e) {}
  180.     });

  181.     // 同步打印到 AIWROK 控制台,方便调试
  182.     printl(msg);
  183. }

  184. // ==================== 第 3 步:统计栏 / 进度条刷新 ====================
  185. function refreshStats(taskName) {
  186.     var taskTotal = gOkCount + gErrCount;
  187.     var ok = gOkCount, err = gErrCount, elapsed = getElapsedStr();
  188.     uiPost(gTvTask, function () {
  189.         try {
  190.             gTvTask.setText("任务 " + taskTotal);
  191.             gTvOk.setText("  ✓ " + ok);
  192.             gTvErr.setText("  ✗ " + err);
  193.             gTvTime.setText("⏱ " + elapsed);
  194.         } catch (e) {}
  195.     });
  196. }

  197. // 进度条:12 格方块,按完成比例填充
  198. function refreshBar(done, total) {
  199.     var blocks = 12;
  200.     var filled = Math.round(done * blocks / total);
  201.     if (filled > blocks) filled = blocks;
  202.     var bar = "";
  203.     for (var i = 0; i < blocks; i++) bar += (i < filled) ? "▰" : "▱";
  204.     var pct = Math.round(done * 100 / total);
  205.     var text = bar + " " + pct + "%";
  206.     uiPost(gTvBar, function () {
  207.         try { gTvBar.setText(text); } catch (e) {}
  208.     });
  209. }

  210. function setStatus(text, color) {
  211.     var shown = "● " + text;
  212.     uiPost(gTvStatus, function () {
  213.         try { gTvStatus.setTextColor(android.graphics.Color.parseColor(color)); } catch (e) {}
  214.         try { gTvStatus.setText(shown); } catch (e2) {}
  215.     });
  216. }

  217. // ==================== 第 4 步:后台计时线程 ====================
  218. // new thread().runJsCode(fn, 名称) → 在独立线程里跑一段 JS 代码
  219. // 主线程结束不会自动杀它,所以必须靠 gStopTick 标志让它自己退出
  220. function startTickerThread() {
  221.     new thread().runJsCode(function () {
  222.         while (!gStopTick) {
  223.             doSleep(1000);
  224.             if (gStopTick) break;
  225.             refreshStats();
  226.         }
  227.     }, "计时线程");
  228.     printl("[线程] 计时线程已启动");
  229. }

  230. // ==================== 第 5 步:优雅停止 ====================
  231. // event.onStopBtnEvent → 点停止按钮【之前】触发,适合做清理
  232. // 注意:runTime.stopAllRuning() 必须放在清理动作的【最后】,
  233. // 否则它会中断还没执行完的清理代码。
  234. function cleanup() {
  235.     gStopTick = true;                 // 先让计时线程自己退出
  236.     setStatus("正在清理资源...", C_WARN);
  237.     appendLog("&#129529; 清理资源,关闭悬浮窗", C_WARN);
  238.     doSleep(400);
  239.     try { if (gWin) { gWin.close(); gWin = null; } } catch (e) {}   // floatUI 用 .close(),没有 .destroy()
  240.     printl("[清理] 悬浮窗已关闭,线程标志已置位");
  241.     runTime.stopAllRuning();          // 最后才停掉所有线程
  242.     stop();
  243. }

  244. event.onStopBtnEvent(function () {
  245.     cleanup();
  246. });

  247. // ==================== 第 6 步:主流程 ====================
  248. function runMain() {
  249.     printl("╔══════════════════════════════════════════╗");
  250.     printl("║  案例6:悬浮窗日志 + 优雅停止(美化版)  ║");
  251.     printl("╚══════════════════════════════════════════╝");

  252.     if (!initFloatWindow()) {
  253.         printl("悬浮窗创建失败,脚本退出");
  254.         return;
  255.     }

  256.     gStartTs = new Date().getTime();

  257.     // 模拟任务列表:每条 [任务名, 是否成功]
  258.     var tasks = [
  259.         ["检测设备权限", true],
  260.         ["连接远程服务器", true],
  261.         ["拉取配置文件", true],
  262.         ["OCR 图像识别", false],   // 故意失败,演示重试
  263.         ["重试 OCR", true],
  264.         ["解析识别结果", true],
  265.         ["写入本地数据库", true],
  266.         ["校验数据完整性", false],
  267.         ["自动修复", true],
  268.         ["生成运行报告", true]
  269.     ];

  270.     setStatus("正在运行...", C_OK);
  271.     appendLog("&#127916; 演示开始(约 15 秒)", C_WARN);
  272.     appendLog("═══════ 任务开始 ═══════", C_WARN);
  273.     startTickerThread();
  274.     refreshBar(0, tasks.length);

  275.     for (var i = 0; i < tasks.length; i++) {
  276.         var name = tasks[i][0];
  277.         var ok = tasks[i][1];

  278.         if (ok) {
  279.             gOkCount++;
  280.             appendLog("[ " + (i + 1) + "/" + tasks.length + " ] " + name, C_OK);
  281.         } else {
  282.             gErrCount++;
  283.             appendLog("[ " + (i + 1) + "/" + tasks.length + " ] " + name + " → 重试中", C_ERR);
  284.             refreshStats(name);
  285.             doSleep(800);
  286.             gErrCount--;            // 重试成功,失败数减回去
  287.             gOkCount++;
  288.             appendLog("  ↳ 重试成功", C_OK);
  289.         }
  290.         refreshStats(name);
  291.         refreshBar(i + 1, tasks.length);
  292.         doSleep(800);   // 真人节奏,别刷太快
  293.     }

  294.     // 收尾:让观众看清统计
  295.     appendLog("═══════ 任务全部结束 ═══════", C_WARN);
  296.     appendLog("共 " + gOkCount + " 成功 / " + gErrCount + " 失败,3 秒后自动关闭", C_OK);
  297.     setStatus("全部完成 ✓", C_OK);
  298.     refreshBar(tasks.length, tasks.length);
  299.     doSleep(3000);

  300.     // 正常跑完也要清理,否则悬浮窗会残留在屏幕上
  301.     cleanup();
  302. }

  303. runMain();
复制代码








欢迎光临 B2B网络软件 (http://bbs.niubt.cn/) Powered by Discuz! X3.2