USEGEAR

导航

学习unigui【47】扫码安装apk

使用unigui生成的app,在Android/鸿蒙和iPhone上安装。
在Android上,需要自己写个壳子。,然后调用地址或域名。
在iPhone上,最简单的企业内网使用app就是使用其浏览器生成快捷键。

1、静态网页,在浏览器打开,让移动设备扫码

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>企业内网 App 下载与安装</title>
  <style>
    :root { --blue:#1677ff; --gray:#f5f5f5; --border:#e6e6e6; --text:#111; --muted:#666; --warnbg:#fff8e6; --warnbd:#ffe2a8; }
    body{
      font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, "PingFang SC","Microsoft YaHei", sans-serif;
      max-width: 820px; margin: 24px auto; padding: 0 16px; color: var(--text);
    }
    h1{ font-size: 24px; margin: 6px 0 10px; }
    .muted{ color: var(--muted); line-height: 1.6; }
    .row{ display:flex; gap:12px; flex-wrap:wrap; margin: 14px 0 8px; }
    .btn{
      display:inline-block; padding: 12px 16px; border-radius: 12px; text-decoration:none;
      font-weight: 700; border: 1px solid var(--border);
    }
    .primary{ background: var(--blue); border-color: var(--blue); color:#fff; }
    .ghost{ background: var(--gray); color: var(--text); }
    .card{
      border: 1px solid var(--border); border-radius: 14px; padding: 16px; margin: 12px 0; background: #fff;
    }
    .hide{ display:none; }
    .tag{
      display:inline-block; font-size: 12px; padding: 2px 8px; border-radius: 999px;
      background: #eef3ff; color:#2a5bd7; margin-left: 8px;
    }
    code{
      background:#fafafa; border:1px solid var(--border); padding: 2px 6px; border-radius: 8px;
    }
    ul,ol{ margin: 8px 0 0 20px; }
    li{ margin: 6px 0; }
    .warn{
      background: var(--warnbg); border: 1px solid var(--warnbd); border-radius: 12px; padding: 12px; margin-top: 12px;
    }
    .grid{
      display:grid; grid-template-columns: 1fr; gap: 12px;
    }
    @media (min-width: 780px){
      .grid{ grid-template-columns: 1fr 1fr; }
    }
    .qrwrap{
      display:flex; gap:16px; align-items:center; flex-wrap:wrap;
    }
    .qrbox{
      width: 100px; height: 100px; border:1px dashed var(--border); border-radius: 12px;
      display:flex; align-items:center; justify-content:center; background:#fff;
    }
    .small{ font-size: 13px; }
    .mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
    .hr{ height:1px; background: var(--border); margin: 14px 0; }
  </style>
</head>

<body>
  <h1>App 下载与安装</h1>
  <div class="muted">
    请确保手机连接公司 Wi-Fi / 内网。本页面会自动识别设备并显示入口;如识别不准,可手动选择。
  </div>

  <div class="row">
    <a class="btn ghost" href="javascript:void(0)" onclick="showPanel('android')">Android / 鸿蒙</a>
    <a class="btn ghost" href="javascript:void(0)" onclick="showPanel('ios')">iPhone / iPad</a>
    <a class="btn ghost" href="javascript:void(0)" onclick="showPanel('web')">直接打开网页版</a>
  </div>

  <!-- Android / Harmony -->
  <div id="panel-android" class="card hide">
    <h2>Android / 鸿蒙安装 <span class="tag">APK</span></h2>
    <div class="muted">
      点击下载后,在通知栏/下载列表中打开安装。系统出于安全原因不会静默安装。
    </div>

    <div class="row">
      <a class="btn primary" id="btn-apk" href="/files/app/app-release.apk">下载 APK</a>
      <a class="btn ghost" id="btn-web-1" href="/">打开网页版</a>
    </div>

    <div class="warn">
      <div class="muted">
        <b>安装被拦/无法安装?</b><br>
        1) 若提示“禁止安装未知应用”:设置 → 安全/隐私 → 安装未知应用 → 允许当前浏览器/企业微信/微信/Chrome。<br>
        2) 若提示“不支持此文件/无法安装 APK”:可能是 <b>HarmonyOS NEXT/5</b>(不原生支持 APK),请改用网页版或联系 IT 获取 HarmonyOS 的 HAP 包/企业分发。
      </div>
    </div>
  </div>

  <!-- iOS -->
  <div id="panel-ios" class="card hide">
    <h2>iPhone / iPad <span class="tag">iOS</span></h2>
    <div class="muted" id="iosHelpText">
      iOS 推荐用 PWA(添加到主屏幕)方式使用;也可直接打开网页版。
    </div>

    <div class="row">
      <a class="btn primary" id="btn-pwa" href="/files/app/pwa.html">PWA 方式使用(Safari)</a>
      <a class="btn ghost" id="btn-web-2" href="/">打开网页版</a>
    </div>

    <div class="warn">
      <div class="muted">
        <b>添加到主屏幕(PWA 方式)</b><br>
        请用 Safari 打开 → 点“分享” → 选择“添加到主屏幕”。(微信/企业微信内置浏览器可能没有该选项)
      </div>
    </div>
  </div>

  <!-- Web only -->
  <div id="panel-web" class="card hide">
    <h2>直接打开网页版</h2>
    <div class="muted">适用于:iPhone、鸿蒙 NEXT、或不方便安装的设备。</div>
    <div class="row">
      <a class="btn primary" id="btn-web-3" href="/">打开网页版</a>
    </div>
  </div>

  <!-- PC / other -->
  <div id="panel-pc" class="card hide">
    <h2>PC / 未识别设备</h2>
    <div class="grid">
      <div class="card" style="margin:0;">
        <h3 style="margin:0 0 8px; font-size:16px;">手机扫码打开本页面</h3>
        <div class="qrwrap">
          <div class="qrbox" id="qrBox">
            <div class="muted small">二维码加载中…</div>
          </div>
          <div class="muted small">
            <div>扫码地址(当前页):</div>
            <div class="mono" id="pcUrlText" style="word-break:break-all;"></div>
            <div class="hr"></div>
            <div>Android 直接下载(可选):</div>
            <div class="mono" id="apkUrlText" style="word-break:break-all;"></div>
            <div style="margin-top:10px;">
              <button class="btn ghost" style="cursor:pointer;" onclick="copyText('pcUrlText')">复制扫码地址</button>
              <button class="btn ghost" style="cursor:pointer;" onclick="copyText('apkUrlText')">复制 APK 地址</button>
            </div>
          </div>
        </div>
      </div>

      <div class="card" style="margin:0;">
        <h3 style="margin:0 0 8px; font-size:16px;">说明</h3>
        <div class="muted small">
          <ul>
            <li>二维码请使用“手机浏览器/企业微信/微信”扫码打开。</li>
            <li>二维码地址里必须是手机可达的内网 IP/域名。</li>
          </ul>
        </div>
      </div>
    </div>
  </div>

  <script src="/files/js/qrcode.min.js"></script>

  <script>
    // ====== 默认配置(可被 /files/app/install.config.json 覆盖)======
    const DEFAULT_CFG = {
      // 关键:App 指定入口(scheme://host:port),不带路径更清爽
      // 例如:"http://xxx.xxx.xxx.xxx:82"
      appBaseUrl: "",

      // App 入口路径(例如 "/m" 或 "/m/")
      webAppPath: "/",

      // 下面两个是静态资源(通常跟 install.html 同站点即可)
      apkPath: "/files/app/app-release.apk",
      pwaEntryPath: "/files/app/pwa.html",

      iosHelpText: "请用 Safari 打开 → 分享 → 添加到主屏幕"
    };

    function hideAllPanels(){
      document.getElementById('panel-android').classList.add('hide');
      document.getElementById('panel-ios').classList.add('hide');
      document.getElementById('panel-web').classList.add('hide');
      document.getElementById('panel-pc').classList.add('hide');
    }
    function showPanel(which){
      hideAllPanels();
      if(which === 'android') document.getElementById('panel-android').classList.remove('hide');
      else if(which === 'ios') document.getElementById('panel-ios').classList.remove('hide');
      else if(which === 'web') document.getElementById('panel-web').classList.remove('hide');
      else document.getElementById('panel-pc').classList.remove('hide');
    }

    function copyText(id){
      const el = document.getElementById(id);
      if(!el) return;
      const text = el.textContent || '';
      navigator.clipboard?.writeText(text).then(function(){
        alert('已复制到剪贴板');
      }).catch(function(){
        const ta = document.createElement('textarea');
        ta.value = text;
        document.body.appendChild(ta);
        ta.select();
        try { document.execCommand('copy'); alert('已复制到剪贴板'); } catch(e){ alert('复制失败,请手动复制'); }
        document.body.removeChild(ta);
      });
    }

    function normalizePath(p){
      if(!p) return "/";
      if(/^https?:\/\//i.test(p)) return p;   // 允许写完整URL
      if(p[0] !== "/") p = "/" + p;
      return p;
    }

    function normalizeBasePrefix(baseUrl){
      // 把 "http://x:82/" 这种尾巴斜杠去掉;同时允许你误写带路径
      if(!baseUrl) return "";
      try{
        const u = new URL(baseUrl);
        let prefix = u.origin;
        let p = u.pathname || "/";
        if(p !== "/" && p.endsWith("/")) p = p.slice(0, -1);
        if(p && p !== "/") prefix += p;
        return prefix.replace(/\/+$/, "");
      }catch(e){
        return String(baseUrl).trim().replace(/\/+$/, "");
      }
    }

    function joinBaseAndPath(basePrefix, path){
      // basePrefix 不带末尾 /
      // path 必须以 / 开头
      const p = normalizePath(path);
      if(p === "/") return basePrefix + "/";
      // 防重复:base 已经以 /m 结尾,同时 path=/m
      if(basePrefix.endsWith(p)) return basePrefix;
      return basePrefix + p;
    }

    function isFullUrl(x){
      return /^https?:\/\//i.test(x || "");
    }

    function detectDevice(){
      const ua = navigator.userAgent || '';
      const isIOS = /iPhone|iPad|iPod/i.test(ua);
      const isAndroidLike = /Android/i.test(ua) || /HarmonyOS/i.test(ua) || /HUAWEI|HONOR/i.test(ua);
      const isMobile = /Mobi|Mobile/i.test(ua) || isIOS || isAndroidLike;
      return { ua, isIOS, isAndroidLike, isMobile };
    }

    async function loadConfig(){
      const cfg = { ...DEFAULT_CFG };

      // URL参数临时覆盖(可选)
      // ?base=http://ip:82&web=/m&apk=/files/app/app-release.apk&pwa=/files/app/pwa.html
      const sp = new URLSearchParams(window.location.search);
      if(sp.get('base')) cfg.appBaseUrl = sp.get('base');
      if(sp.get('web'))  cfg.webAppPath = sp.get('web');
      if(sp.get('apk'))  cfg.apkPath = sp.get('apk');
      if(sp.get('pwa'))  cfg.pwaEntryPath = sp.get('pwa');

      // 同目录配置文件:/files/app/install.config.json
      try{
        const resp = await fetch('./install.config.json?_v=' + Date.now(), { cache: 'no-store' });
        if(resp.ok){
          const j = await resp.json();
          if(j.appBaseUrl)  cfg.appBaseUrl = j.appBaseUrl;
          if(j.webAppPath)  cfg.webAppPath = j.webAppPath;
          if(j.apkPath)     cfg.apkPath = j.apkPath;
          if(j.pwaEntryPath)cfg.pwaEntryPath = j.pwaEntryPath;
          if(j.iosHelpText) cfg.iosHelpText = j.iosHelpText;
        }
      }catch(e){}

      cfg.appBaseUrl  = (cfg.appBaseUrl || "").trim();
      cfg.webAppPath  = normalizePath(cfg.webAppPath || "/");
      cfg.apkPath     = normalizePath(cfg.apkPath || "/files/app/app-release.apk");
      cfg.pwaEntryPath= normalizePath(cfg.pwaEntryPath || "/files/app/pwa.html");
      cfg.iosHelpText = (cfg.iosHelpText || "").trim() || DEFAULT_CFG.iosHelpText;

      return cfg;
    }

    function applyConfig(cfg){
      // App 入口基准:优先用 config.appBaseUrl,否则退回当前访问 origin
      const appBase = cfg.appBaseUrl ? normalizeBasePrefix(cfg.appBaseUrl) : window.location.origin;
      const appHome = isFullUrl(cfg.webAppPath) ? cfg.webAppPath : joinBaseAndPath(appBase, cfg.webAppPath);

      // “打开网页版”统一指向 appHome(解决你现在的 81/82 问题)
      const w1 = document.getElementById('btn-web-1');
      const w2 = document.getElementById('btn-web-2');
      const w3 = document.getElementById('btn-web-3');
      if(w1) w1.href = appHome;
      if(w2) w2.href = appHome;
      if(w3) w3.href = appHome;

      // APK:默认仍用相对路径(跟 install.html 同站点)
      const apkBtn = document.getElementById('btn-apk');
      if(apkBtn) apkBtn.href = cfg.apkPath;

      // PWA 入口:默认还是当前站点的静态页;如果你想强制走 appBaseUrl,可把 pwaEntryPath 写成完整URL
      const pwaBtn = document.getElementById('btn-pwa');
      if(pwaBtn){
        pwaBtn.href = isFullUrl(cfg.pwaEntryPath) ? cfg.pwaEntryPath : (window.location.origin + cfg.pwaEntryPath);
      }

      const iosHelp = document.getElementById('iosHelpText');
      if(iosHelp) iosHelp.textContent = cfg.iosHelpText;
    }

    function renderPCQRCode(cfg){
      const url = window.location.href;

      // APK 显示用:如果写的是相对路径,就按当前站点拼绝对
      const apkAbs = isFullUrl(cfg.apkPath) ? cfg.apkPath : (window.location.origin + cfg.apkPath);

      const pcUrlText = document.getElementById('pcUrlText');
      const apkUrlText = document.getElementById('apkUrlText');
      if(pcUrlText) pcUrlText.textContent = url;
      if(apkUrlText) apkUrlText.textContent = apkAbs;

      const qrBox = document.getElementById('qrBox');
      if(qrBox && window.QRCode){
        qrBox.innerHTML = '';
        new QRCode(qrBox, { text: url, width:100, height:100 });
      }else if(qrBox){
        qrBox.innerHTML = '<div class="muted small">二维码库未加载:请把 qrcode.min.js 放到 /files/js/ 目录</div>';
      }
    }

    (async function init(){
      const cfg = await loadConfig();
      applyConfig(cfg);

      const d = detectDevice();
      if(!d.isMobile){
        showPanel('pc');
        renderPCQRCode(cfg);
        return;
      }

      if(d.isIOS){
        showPanel('ios');
      }else if(d.isAndroidLike){
        showPanel('android');
      }else{
        showPanel('web');
      }
    })();
  </script>
</body>
</html>

 


2、上面代码会自动判断你的设备给出不同的提示。

androi会狭窄apk。iPhone会给出浏览器的地址。

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
  <title>标题</title>

  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="default">
  <meta name="apple-mobile-web-app-title" content=标题">

  <link rel="apple-touch-icon" sizes="180x180" href="/files/app/icons/apple-touch-icon.png">
  <link rel="icon" type="image/png" sizes="32x32" href="/files/app/icons/favicon-32.png">
  <link rel="manifest" href="/files/app/manifest.webmanifest">

  <style>
    body{font-family:system-ui,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;margin:18px;}
    .card{border:1px solid #e6e6e6;border-radius:14px;padding:16px;}
    .btn{display:inline-block;padding:12px 16px;border-radius:12px;background:#1677ff;color:#fff;text-decoration:none;font-weight:700;}
    .muted{color:#666;line-height:1.6;}
    code{background:#fafafa;border:1px solid #e6e6e6;padding:2px 6px;border-radius:8px;}
  </style>
</head>
<body>
  <div class="card">
    <h2 style="margin:0 0 8px;">iPhone / iPad(PWA)</h2>
    <div class="muted" id="help">
      1) 请用 <b>Safari</b> 打开本页<br>
      2) 点“分享” → <b>添加到主屏幕</b><br>
      3) 以后从桌面图标进入(全屏更像 App)<br><br>
      也可以直接点下面按钮进入系统。
    </div>

    <div style="margin-top:12px;">
      <a class="btn" id="btnGo" href="#">进入系统</a>
    </div>

    <div class="muted" style="margin-top:10px;">
      应用入口:<code id="origin"></code>
    </div>

    <!-- 关键:调试显示区(避免“代码在写 cfgDebug 但页面没有元素”导致你误判) -->
    <div class="muted" style="margin-top:10px;">
<!--      配置调试:<code id="cfgDebug"></code> -->
    </div>
  </div>

<script>
  // ===== 默认配置:若配置文件读取失败,会用这些兜底(此时可能退回到 window.location.origin)=====
  const DEFAULT_CFG = {
    appBaseUrl: "",
    webAppPath: "/",
    iosHelpText: "请用 Safari 打开 → 分享 → 添加到主屏幕"
  };

  function isFullUrl(x){
    return /^https?:\/\//i.test(x || "");
  }

  function normalizePath(p){
    // 只负责把“路径”规范成以 / 开头;如果是完整URL,原样返回
    if(!p) return "/";
    if(isFullUrl(p)) return p;
    p = String(p).trim();
    if(p === "") return "/";
    if(p[0] !== "/") p = "/" + p;
    // 去掉末尾多余空格,但不强制去尾部 /
    return p;
  }

  function normalizeBasePrefix(baseUrl){
    // 允许用户写成 "http://x:82" 或 "http://x:82/" 或误写带路径
    if(!baseUrl) return "";
    baseUrl = String(baseUrl).trim();
    if(baseUrl === "") return "";
    try{
      const u = new URL(baseUrl);
      let prefix = u.origin;
      let p = u.pathname || "/";
      // "/m/" -> "/m"
      if(p !== "/" && p.endsWith("/")) p = p.slice(0, -1);
      if(p && p !== "/") prefix += p;
      return prefix.replace(/\/+$/, "");
    }catch(e){
      return baseUrl.replace(/\/+$/, "");
    }
  }

  function joinBaseAndPath(basePrefix, path){
    // basePrefix 不带末尾 /
    // path 以 / 开头;若 path="/" 则返回 basePrefix+"/"
    const p = normalizePath(path);
    if(p === "/") return basePrefix + "/";
    // 防重复:basePrefix 已经以 "/m" 结尾,同时 path="/m"
    if(basePrefix.endsWith(p)) return basePrefix;
    return basePrefix + p;
  }

  async function fetchCfgJson(url){
    // 强制“每次都新鲜”,最大概率撕掉缓存(尤其微信内置)
    const full = url + (url.includes("?") ? "&" : "?") + "_v=" + Date.now();

    const resp = await fetch(full, {
      cache: "no-store",
      // 同源带 cookie;如果你 /files/app 有鉴权/会话,必须带上
      credentials: "same-origin"
    });

    const text = await resp.text();

    if(!resp.ok){
      return { ok:false, url:full, status:resp.status, text };
    }

    try{
      const j = JSON.parse(text);
      return { ok:true, url:full, status:resp.status, json:j };
    }catch(e){
      return { ok:false, url:full, status:resp.status, text, parseError:String(e) };
    }
  }

  function normalizeCfg(cfg){
    cfg.appBaseUrl = (cfg.appBaseUrl || "").trim();
    cfg.webAppPath = normalizePath(cfg.webAppPath || "/");
    cfg.iosHelpText = (cfg.iosHelpText || "").trim() || DEFAULT_CFG.iosHelpText;
    return cfg;
  }

  async function loadConfig(){
    const cfg = { ...DEFAULT_CFG };

    // 多路径兜底:相对路径在某些内置浏览器里可能“错目录”
    const CANDIDATES = [
      "./install.config.json",
      "/files/app/install.config.json"
    ];

    const debug = [];

    // 允许 URL 临时覆盖(紧急排障用)
    // pwa.html?base=http://xxx.xxx.xxx.xxx:82&web=/m
    try{
      const sp = new URLSearchParams(window.location.search);
      if(sp.get("base")) cfg.appBaseUrl = sp.get("base");
      if(sp.get("web"))  cfg.webAppPath = sp.get("web");
    }catch(e){}

    for(const u of CANDIDATES){
      try{
        const r = await fetchCfgJson(u);
        if(r.ok){
          const j = r.json || {};
          if(j.appBaseUrl)  cfg.appBaseUrl  = j.appBaseUrl;
          if(j.webAppPath)  cfg.webAppPath  = j.webAppPath;
          if(j.iosHelpText) cfg.iosHelpText = j.iosHelpText;

          normalizeCfg(cfg);

          debug.push(`OK ${r.status} ${r.url}`);
          debug.push(`appBaseUrl=${cfg.appBaseUrl || "(empty)"}`);
          debug.push(`webAppPath=${cfg.webAppPath || "(empty)"}`);
          return { cfg, debug: debug.join(" | ") };
        }else{
          debug.push(`FAIL ${r.status} ${r.url}`);
          if(r.parseError) debug.push(`JSON_PARSE_ERR=${r.parseError}`);
          if(r.text) debug.push(`BODY=${r.text.slice(0,120).replace(/\s+/g,' ')}`);
        }
      }catch(e){
        debug.push(`EX ${u} ${String(e)}`);
      }
    }

    normalizeCfg(cfg);
    debug.push(`USING_DEFAULT appBaseUrl=${cfg.appBaseUrl || "(empty)"} webAppPath=${cfg.webAppPath || "(empty)"}`);
    return { cfg, debug: debug.join(" | ") };
  }

  (async function init(){
    const r = await loadConfig();
    const cfg = r.cfg;

    const debugEl = document.getElementById("cfgDebug");
    if(debugEl) debugEl.textContent = r.debug || "(no debug)";

    // 关键:优先用 appBaseUrl(例如 82),读不到才退回当前 origin(经常是 81)
    const appBase = cfg.appBaseUrl ? normalizeBasePrefix(cfg.appBaseUrl) : window.location.origin;

    // webAppPath 若写成完整 URL,直接用;否则 base + path 组合
    const target = isFullUrl(cfg.webAppPath) ? cfg.webAppPath : joinBaseAndPath(appBase, cfg.webAppPath);

    document.getElementById("origin").textContent = target;
    document.getElementById("btnGo").href = target;

    const help = document.getElementById("help");
    if(help && cfg.iosHelpText){
      help.innerHTML =
        '1) ' + cfg.iosHelpText + '<br>' +
        '2) 以后从桌面图标进入(全屏更像 App)<br><br>' +
        '也可以直接点下面按钮进入系统。';
    }

    // 从主屏幕启动(standalone)时自动跳转
    const isStandalone =
      window.matchMedia('(display-mode: standalone)').matches ||
      window.navigator.standalone === true;

    if(isStandalone){
      window.location.replace(target);
    }
  })();
</script>
</body>
</html>

 


3、需要配置的的文件

{
  "appBaseUrl": "http://127.0.0.1:82", 你的移动入口   
  "webAppPath": "/m",
  "apkPath": "/files/app/app-release.apk",
  "pwaEntryPath": "/files/app/pwa.html",
  "iosHelpText": "请用 Safari 打开 → 分享 → 添加到主屏幕",
  "icons": {
    "appleTouchIcon180": "/files/app/icons/apple-touch-icon.png"
  }
}

 

posted on 2026-01-20 15:31  USEGEAR  阅读(1)  评论(0)    收藏  举报