You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

380 lines
17 KiB

<!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>EAIMAR · 下载</title>
<meta name="description" content="EAIMAR —— 实时记录会议纪要,多语言翻译">
<meta name="theme-color" content="#13171c" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#eef1f6" media="(prefers-color-scheme: light)">
<style>
/* ───────────────────────────────────────────────────────────────
配色取自 App 图标本身(apps/client/assets/images/app_icon.png):
深色场 #13171c → #3a3f49,图形色 #d1d6e3。
于是页面沿用图标的视觉语言——「深底浅形」,主按钮在深色下是浅的、
在浅色下是深的,始终与背景相反,和图标上那个浅色蝶形一个道理。
─────────────────────────────────────────────────────────────── */
:root{
--mark:#d1d6e3; /* 图标上那个浅蓝灰,全站强调色 */
--bg-a:#eef1f6; --bg-b:#dde3ee;
--fg:#1b1f27; --muted:#66707f; --line:rgba(23,27,34,.10);
--card:rgba(255,255,255,.72);
--btn-bg:linear-gradient(180deg,#2c333f,#171b22);
--btn-fg:#f4f6fa;
--btn-shadow:0 8px 22px -8px rgba(23,27,34,.55);
--net:rgba(23,27,34,.30); /* 背景星座线条 */
--glow:rgba(120,138,190,.38); /* 图标后的光晕 */
--dot-halo:rgba(209,214,227,.22);
}
@media (prefers-color-scheme:dark){
:root{
--bg-a:#1c212a; --bg-b:#0c0f13;
--fg:#e9ecf3; --muted:#8e97a6; --line:rgba(255,255,255,.09);
--card:rgba(255,255,255,.045);
--btn-bg:linear-gradient(180deg,#e7ebf4,#c5cde0);
--btn-fg:#151a21;
--btn-shadow:0 10px 26px -10px rgba(0,0,0,.85);
--net:rgba(209,214,227,.30);
--glow:rgba(140,158,210,.30);
--dot-halo:rgba(209,214,227,.16);
}
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;min-height:100%}
body{
color:var(--fg);
font:15px/1.65 -apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif;
-webkit-text-size-adjust:100%;
-webkit-font-smoothing:antialiased;
/* 斜向渐变的方向与图标一致:左上亮、右下暗 */
background:linear-gradient(155deg,var(--bg-a) 0%,var(--bg-b) 72%) fixed;
}
/* 背景星座:呼应图标里那层网点连线。纯内联 SVG,不额外发请求 */
.net{
position:fixed;inset:0;z-index:0;pointer-events:none;
color:var(--net);opacity:.42;
/* 中心淡出:内容区在中间,线条横穿卡片会很乱。只让它在四周留一点氛围 */
-webkit-mask-image:radial-gradient(ellipse 62% 46% at 50% 42%,transparent 10%,#000 78%);
mask-image:radial-gradient(ellipse 62% 46% at 50% 42%,transparent 10%,#000 78%);
}
.net svg{width:100%;height:100%;display:block}
.stage{position:relative;z-index:1;max-width:460px;margin:0 auto;padding:52px 20px 44px;text-align:center}
/* ── 图标 ── */
.hero{position:relative;width:100px;height:100px;margin:0 auto 20px}
.hero::before{ /* 背后的柔光,让图标从背景里浮起来 */
content:"";position:absolute;inset:-26%;border-radius:50%;
background:radial-gradient(circle,var(--glow) 0%,transparent 68%);
filter:blur(2px);
}
.icon{
position:relative;width:100px;height:100px;display:block;border-radius:23px;
box-shadow:0 14px 34px -12px rgba(0,0,0,.55),0 0 0 1px var(--line);
}
h1{font-size:28px;margin:0 0 6px;letter-spacing:1.5px;font-weight:700}
.sub{color:var(--muted);margin:0 0 6px;font-size:14px}
.ver{color:var(--muted);font-size:12.5px;margin:0 0 30px;min-height:19px;letter-spacing:.3px}
/* ── 按钮 ── */
.btn{
position:relative;display:block;width:100%;padding:16px 18px;margin:0 0 11px;
border:0;border-radius:14px;background:var(--btn-bg);color:var(--btn-fg);
font:600 16px/1.2 inherit;text-decoration:none;cursor:pointer;
box-shadow:var(--btn-shadow);
transition:transform .14s ease,box-shadow .14s ease,opacity .14s ease;
}
.btn:active{transform:translateY(1px) scale(.988);box-shadow:0 4px 12px -6px rgba(0,0,0,.5)}
@media (hover:hover){.btn:hover{transform:translateY(-1px)}}
.btn small{display:block;font-weight:400;font-size:12px;opacity:.7;margin-top:4px;letter-spacing:.2px}
.btn.ghost{
background:var(--card);color:var(--fg);
border:1px solid var(--line);font-weight:500;
box-shadow:none;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
}
.btn.ghost:active{transform:translateY(1px) scale(.988)}
/* ── 卡片 ── */
.log,.feat li{
background:var(--card);border:1px solid var(--line);border-radius:14px;
-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
}
.log{padding:14px 16px;margin:20px 0 0;text-align:left;font-size:13px;color:var(--muted);white-space:pre-wrap;word-break:break-word}
.log b{display:block;color:var(--fg);font-size:13px;margin-bottom:6px}
.feat{margin:26px 0 0;padding:0;list-style:none;text-align:left}
.feat li{position:relative;padding:14px 16px 14px 34px;margin-bottom:10px;font-size:14px}
.feat li::before{ /* 小圆点用强调色,把图标的颜色带下来 */
content:"";position:absolute;left:16px;top:20px;
width:6px;height:6px;border-radius:50%;background:var(--mark);
box-shadow:0 0 0 3px var(--dot-halo);
}
.feat b{display:block;margin-bottom:2px;font-weight:600}
.feat span{color:var(--muted);font-size:13px}
.foot{color:var(--muted);font-size:12px;margin-top:30px;line-height:1.9}
.foot a{color:var(--fg);text-decoration:none;border-bottom:1px solid var(--line)}
/* 入场动画。reduced-motion 下直接显示,不做位移 */
@keyframes rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.stage>*{animation:rise .5s cubic-bezier(.2,.7,.3,1) both}
.stage>*:nth-child(2){animation-delay:.04s}
.stage>*:nth-child(3){animation-delay:.07s}
.stage>*:nth-child(4){animation-delay:.10s}
.stage>*:nth-child(5){animation-delay:.13s}
.stage>*:nth-child(6){animation-delay:.16s}
.stage>*:nth-child(7){animation-delay:.19s}
@media (prefers-reduced-motion:reduce){
.stage>*{animation:none}
.btn{transition:none}
}
/* 微信/QQ 内置浏览器的引导遮罩:不做这层,安卓上点下载就是静默无反应 */
#mask{position:fixed;inset:0;z-index:99;background:rgba(6,8,11,.88);color:#fff;padding:22px;display:none;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}
#mask.on{display:block}
#mask .arrow{text-align:right;font-size:46px;line-height:1;padding:0 8px}
#mask .tip{max-width:330px;margin:14px 0 0 auto;text-align:right;font-size:17px;line-height:1.75}
#mask .tip em{font-style:normal;color:var(--mark);font-weight:600}
</style>
</head>
<body>
<div class="net" aria-hidden="true">
<svg viewBox="0 0 400 800" preserveAspectRatio="xMidYMid slice" fill="none">
<g stroke="currentColor" stroke-width=".8" opacity=".45">
<path d="M40 90 L150 40 L300 120 L370 60"/>
<path d="M150 40 L120 200 L300 120"/>
<path d="M120 200 L30 300 L110 430 L250 380 L300 120"/>
<path d="M250 380 L370 300 L300 120"/>
<path d="M110 430 L60 600 L200 690 L330 600 L370 300"/>
<path d="M200 690 L250 380"/>
<path d="M330 600 L360 760"/>
<path d="M60 600 L20 740"/>
</g>
<g fill="currentColor">
<circle cx="40" cy="90" r="3"/><circle cx="150" cy="40" r="4"/><circle cx="300" cy="120" r="3.5"/>
<circle cx="370" cy="60" r="2.5"/><circle cx="120" cy="200" r="3"/><circle cx="30" cy="300" r="3.5"/>
<circle cx="110" cy="430" r="3"/><circle cx="250" cy="380" r="4"/><circle cx="370" cy="300" r="3"/>
<circle cx="60" cy="600" r="3.5"/><circle cx="200" cy="690" r="3"/><circle cx="330" cy="600" r="3.5"/>
<circle cx="360" cy="760" r="2.5"/><circle cx="20" cy="740" r="2.5"/>
</g>
</svg>
</div>
<div id="mask" aria-hidden="true">
<div class="arrow">⤴</div>
<div class="tip">
请点击右上角 <em>···</em><br>
选择「<em>在浏览器中打开</em>」<br>
再进行下载
</div>
</div>
<div class="stage">
<div class="hero"><img class="icon" src="__ICON_DATA_URI__" alt="EAIMAR"></div>
<h1>EAIMAR</h1>
<p class="sub">实时记录会议纪要 · 多语言翻译</p>
<p class="ver" id="ver"></p>
<div id="actions"></div>
<div class="log" id="log" hidden></div>
<ul class="feat">
<li><b>会议纪要</b><span>录音自动转写,AI 生成结构化纪要</span></li>
<li><b>同声传译</b><span>通话 / 面对面实时翻译,支持数十种语言</span></li>
<li><b>智能硬件</b><span>配套耳机、支架、车载设备开箱即用</span></li>
</ul>
<div class="foot">
<span id="tip"></span><a href="https://web.ymaikj.com">官网</a>
</div>
</div>
<script>
(function () {
'use strict';
// ══════════════════════════════════════════════════════════════════
// 配置区
//
// ⚠️ 版本号与各商店链接**不在这里维护** —— 它们实时取自管理后台:
// · 版本号 ← 「服务配置 → 版本控制」的线上版本(app_release.version)
// · 各渠道商店链接 ← 「SaaS 管理 → 渠道分发」各渠道的下载地址,
// 渠道留空时后端自动回退到「版本控制」里的默认下载地址
// 后台改完,这个页面**刷新即生效,不需要重新上传**。
//
// 下面的 fallback 只在「接口打不通」时兜底(断网、网关挂了、跨域被拦)。
// 它的值会比后台旧,所以别把它当成配置入口去维护。
// ══════════════════════════════════════════════════════════════════
var CFG = {
// 取版本与商店链接的后端。必须是**正式**网关,别指到 ym-dev。
api: 'https://ym.ymaikj.com',
fallback: {
version: '1.0.2',
// TODO(store-links): 后台「渠道分发」把各渠道地址配齐之后,这几行就只是兜底了。
// · appStore → https://apps.apple.com/cn/app/id<AppID>
// AppID 在 App Store Connect 的「App 信息 → Apple ID」,一串纯数字
// · 安卓各商店填各自开发者后台的应用详情页链接
// · apk 填安装包地址。安装包与本页同桶(ymai-dl),所以走 dl.ymaikj.com;
// 文件名按仓库约定 eaimar-release-<版本号>-<时间>.apk,别留 app-release.apk
appStore: 'https://emai.ymaikj.com/1223',
apk: 'https://dl.ymaikj.com/android/eaimar-release-1.0.2-latest.apk',
stores: {
huawei: 'https://emai.ymaikj.com/1223',
honor: 'https://emai.ymaikj.com/1223',
xiaomi: 'https://emai.ymaikj.com/1223',
oppo: 'https://emai.ymaikj.com/1223',
vivo: 'https://emai.ymaikj.com/1223'
}
},
// 后台渠道枚举 → 本页用的 key。与客户端 UserChannel(channel_app_model.dart) 同一套编号,
// ⚠️ 加渠道要两边一起加,这里漏了只表现为「后台配了地址但页面还用兜底值」。
channels: {
1: 'huawei', 2: 'xiaomi', 3: 'oppo', 4: 'vivo', 5: 'honor',
100: 'apk', // 官方渠道:约定用它的下载地址放 APK 直链
101: 'appStore' // 全球-苹果
// 102 = GLOBAL_GOOGLE(Google Play),国内落地页用不到,刻意不映射
}
};
// ══════════════════════════════════════════════════════════════════
var ua = navigator.userAgent || '';
var isIOS = /iPad|iPhone|iPod/.test(ua) || (/Macintosh/.test(ua) && 'ontouchend' in document);
var isAndroid = /Android/i.test(ua);
// 微信与 QQ 的内置浏览器都会拦截 apk 下载,而且**静默失败**:点了什么都不发生。
// 必须先引导到系统浏览器,否则用户只会觉得"这页坏了"。
var blocked = /MicroMessenger/i.test(ua) || /\sQQ\/|QQBrowser/i.test(ua);
var $ = function (id) { return document.getElementById(id); };
// 图标只在 <img> 里内联一份,favicon / apple-touch-icon 复用同一个 data URI。
// 直接在 <head> 里再写两个 href 会把几十 KB 的 base64 存三份,页面体积直接翻三倍。
(function () {
var src = document.querySelector('.icon').getAttribute('src');
['icon', 'apple-touch-icon'].forEach(function (rel) {
var l = document.createElement('link');
l.rel = rel; l.href = src;
document.head.appendChild(l);
});
})();
function openMask() {
var m = $('mask');
m.className = 'on';
m.setAttribute('aria-hidden', 'false');
m.onclick = function () { m.className = ''; m.setAttribute('aria-hidden', 'true'); };
}
function btn(href, label, note, ghost) {
var a = document.createElement('a');
a.className = 'btn' + (ghost ? ' ghost' : '');
a.href = href;
a.innerHTML = label + (note ? '<small>' + note + '</small>' : '');
if (blocked) a.onclick = function (e) { e.preventDefault(); openMask(); };
return a;
}
function androidStoreKey() {
// 判的是**手机品牌**不是下载来源,与 App 内 getAndroidStoreInfoByBrand 同口径
// (国内五家共用一个包)。认不出就走 APK 直链,这个兜底方向是安全的。
if (/honor/i.test(ua)) return 'honor';
if (/huawei/i.test(ua)) return 'huawei';
if (/xiaomi|redmi|poco/i.test(ua)) return 'xiaomi';
if (/oppo|realme|oneplus/i.test(ua)) return 'oppo';
if (/vivo|iqoo/i.test(ua)) return 'vivo';
return '';
}
// 渲染整页。后台数据回来后会再调一次,所以这里必须是**幂等重绘**,不能只做增量追加。
function render(cfg) {
$('ver').textContent = cfg.version ? ('版本 ' + cfg.version) : '';
var actions = $('actions');
actions.innerHTML = '';
if (isIOS) {
actions.appendChild(btn(cfg.appStore, 'App Store 下载'));
$('tip').innerHTML = '需要 iOS 13 及以上 · ';
} else if (isAndroid) {
var key = androidStoreKey();
var store = key && cfg.stores[key];
// 商店地址与 APK 是同一个值时(后台该渠道没单独配、回退到了默认下载地址),
// 只给一个按钮——两个一模一样的按钮只会让人犹豫点哪个。
if (store && store !== cfg.apk) {
actions.appendChild(btn(store, '应用商店下载', '推荐,可自动更新'));
actions.appendChild(btn(cfg.apk, '直接下载安装包', 'APK · ' + cfg.version, true));
} else {
actions.appendChild(btn(cfg.apk, '下载安装包', 'APK · ' + cfg.version));
}
$('tip').innerHTML = '需要 Android 8.0 及以上 · ';
} else {
actions.appendChild(btn(cfg.appStore, 'iPhone / iPad', 'App Store', true));
actions.appendChild(btn(cfg.apk, 'Android', 'APK · ' + cfg.version, true));
$('tip').innerHTML = '请用手机打开本页面下载 · ';
}
var log = $('log');
if (cfg.changelog) {
log.innerHTML = '<b>本次更新</b>' + String(cfg.changelog)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
log.hidden = false;
} else {
log.hidden = true;
}
}
// 先用兜底值画一版:接口再快也有几百毫秒,先白屏再出内容的观感很差。
var cur = {
version: CFG.fallback.version,
appStore: CFG.fallback.appStore,
apk: CFG.fallback.apk,
stores: CFG.fallback.stores,
changelog: ''
};
render(cur);
if (blocked) openMask();
// ── 从管理后台取线上版本与各渠道下载地址 ──────────────────────────
// 走 user_getchannelapps:它在网关白名单里(未登录可调),下发的每一条是
// {channel, version, address, changelog, ...},version 来自 app_release(全渠道同一个)。
var xhr = new XMLHttpRequest();
xhr.open('POST', CFG.api + '/api/home/user_getchannelapps', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.timeout = 8000;
xhr.onload = function () {
var res;
try { res = JSON.parse(xhr.responseText); } catch (e) { return; } // 拿不到就保持兜底值
if (!res || res.code !== 0 || !res.data || !res.data.apps) return;
var apps = res.data.apps, next = {
version: cur.version, appStore: cur.appStore, apk: cur.apk,
stores: {}, changelog: ''
};
for (var k in cur.stores) if (cur.stores.hasOwnProperty(k)) next.stores[k] = cur.stores[k];
for (var i = 0; i < apps.length; i++) {
var a = apps[i] || {};
if (a.version) next.version = a.version;
if (a.changelog && !next.changelog) next.changelog = a.changelog;
var key = CFG.channels[a.channel];
// 地址为空就保留兜底值:后台某个渠道没配时,不该把按钮变成一个点了没反应的空链接
if (!key || !a.address) continue;
if (key === 'appStore') next.appStore = a.address;
else if (key === 'apk') next.apk = a.address;
else next.stores[key] = a.address;
}
cur = next;
render(cur);
};
// onerror/ontimeout 不做任何事:页面已经用兜底值渲染过了,此时报错只会吓到用户
xhr.send(JSON.stringify({ version: '' }));
})();
</script>
</body>
</html>