学习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"
}
}
浙公网安备 33010602011771号