Files
conti-docs/tech-selection/Flutter-vs-UniApp-deck.html
T

1218 lines
76 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>App 技术栈介绍 — T-Systems</title>
<style>
/* ============================================================
Telekom Scale design tokens — 取自
unpkg.com/@telekom/design-tokens/dist/telekom/css/telekom-design-tokens.all.css
============================================================ */
:root{
--tk-magenta:#e20074; --tk-magenta-hover:#c00063; --tk-magenta-press:#9e0051;
--tk-text:#000000; --tk-text-add:hsla(0,0%,0%,.65); --tk-text-dis:hsla(0,0%,0%,.4);
--tk-inverted:#ffffff;
--tk-canvas:#ffffff; --tk-canvas-subtle:#fbfbfb;
--tk-surface-subtle:#efeff0; --tk-surface-hl:#242426;
--tk-link:#2238df;
--tk-info:#2238df; --tk-info-subtle:#d3d7f9;
--tk-success:#008653; --tk-success-subtle:#ccf0e1;
--tk-danger:#d91308; --tk-warning:#b63d00;
--tk-neutral:#8e8e93; --tk-neutral-subtle:#e5e5e7;
--tk-r-sm:.25rem; --tk-r:.5rem; --tk-r-lg:.75rem; --tk-r-pill:62.4375rem;
--tk-sh-raised:0 8px 32px 0 hsla(0,0%,0%,.1), 0 4px 8px 0 hsla(0,0%,0%,.1);
--tk-sh-float:0 12px 48px 0 hsla(0,0%,0%,.1), 0 6px 12px 0 hsla(0,0%,0%,.1);
--tk-font:'TeleNeoWeb','PingFang SC','Microsoft YaHei','Noto Sans SC','Hiragino Sans GB',sans-serif;
--tk-mono:'SF Mono','Cascadia Mono','Consolas','Menlo',monospace;
}
/* TeleNeoWeb 是授权品牌字体,公开包里不含文件。
放进 assets/fonts/ 即生效,缺失则静默回退。见 assets/fonts/README.md */
@font-face{font-family:'TeleNeoWeb';src:url('./assets/fonts/TeleNeoWeb-Regular.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'TeleNeoWeb';src:url('./assets/fonts/TeleNeoWeb-Medium.woff2') format('woff2');font-weight:500;font-style:normal;font-display:swap}
@font-face{font-family:'TeleNeoWeb';src:url('./assets/fonts/TeleNeoWeb-Bold.woff2') format('woff2');font-weight:700;font-style:normal;font-display:swap}
@font-face{font-family:'TeleNeoWeb';src:url('./assets/fonts/TeleNeoWeb-ExtraBold.woff2') format('woff2');font-weight:800;font-style:normal;font-display:swap}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden;background:#1a1a1c}
body{font-family:var(--tk-font);color:var(--tk-text);-webkit-font-smoothing:antialiased}
/* ---------- 舞台:固定 1280×720,等比缩放 ---------- */
#viewport{position:fixed;inset:0;display:flex;align-items:center;justify-content:center}
#stage{width:1280px;height:720px;background:var(--tk-canvas);position:relative;overflow:hidden;
transform-origin:center center;box-shadow:0 0 0 1px rgba(0,0,0,.35),0 24px 96px rgba(0,0,0,.5)}
#deck{position:absolute;inset:0}
.holder{position:absolute;inset:0}
.slide{position:absolute;inset:0;padding:56px 72px 64px;display:flex;flex-direction:column;
background:var(--tk-canvas);opacity:0;transform:translateX(48px);pointer-events:none;
transition:opacity .3s ease,transform .32s cubic-bezier(.4,0,.2,1)}
.slide.active{opacity:1;transform:none;pointer-events:auto}
.slide.before{transform:translateX(-48px)}
@media (prefers-reduced-motion:reduce){.slide{transition:opacity .01s}}
/* ---------- 页眉页脚 ---------- */
#chrome{position:absolute;inset:0;pointer-events:none;z-index:20;opacity:0;transition:opacity .25s}
#chrome.on{opacity:1}
#brand{position:absolute;top:26px;left:72px;display:flex;align-items:center;gap:10px;
font-size:15px;font-weight:700;letter-spacing:.01em}
#brand::before{content:"";width:5px;height:19px;background:var(--tk-magenta);border-radius:1px}
#meta{position:absolute;top:28px;right:72px;font-size:13px;font-weight:500;color:var(--tk-text-dis);
display:flex;align-items:center;gap:14px}
#meta .num{font-variant-numeric:tabular-nums;color:var(--tk-text-add);font-weight:700}
/* 进度条独立于页眉:分隔页/封面隐藏页眉,但进度条始终可见 */
#bar{position:absolute;left:0;bottom:0;height:3px;background:var(--tk-magenta);width:0;z-index:21;
transition:width .32s cubic-bezier(.4,0,.2,1),opacity .25s}
#bar.off{opacity:0}
/* ---------- 排版原语 ---------- */
.eyebrow{font-size:13px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
color:var(--tk-magenta);margin-bottom:10px}
h1{font-size:54px;font-weight:800;line-height:1.14;letter-spacing:-.01em}
h2{font-size:38px;font-weight:800;line-height:1.16;letter-spacing:-.005em}
h3{font-size:23px;font-weight:800;line-height:1.28}
h4{font-size:17px;font-weight:800;line-height:1.35}
.lead{font-size:19px;font-weight:400;line-height:1.5;color:var(--tk-text-add)}
.sub{font-size:16px;line-height:1.55;color:var(--tk-text-add);margin-top:12px;max-width:1010px}
.body{font-size:15px;line-height:1.6}
.hd{margin-bottom:26px;flex-shrink:0}
.bd{flex:1;min-height:0;display:flex;flex-direction:column;justify-content:center}
.bd.top{justify-content:flex-start}
em{font-style:normal;color:var(--tk-magenta);font-weight:700}
strong{font-weight:700}
code{font-family:var(--tk-mono);font-size:.88em;background:var(--tk-surface-subtle);
padding:1px 6px;border-radius:var(--tk-r-sm)}
.note{font-size:12.5px;line-height:1.5;color:var(--tk-text-dis);margin-top:16px;flex-shrink:0}
.note b{color:var(--tk-text-add);font-weight:700}
/* ---------- 封面 / 分隔 / 断言 ---------- */
.cover{justify-content:center;padding-left:96px}
.cover::before{content:"";position:absolute;left:0;top:0;bottom:0;width:10px;background:var(--tk-magenta)}
.cover .kicker{font-size:14px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;
color:var(--tk-magenta);margin-bottom:22px}
.cover h1{max-width:900px}
.cover .foot{position:absolute;left:96px;bottom:64px;display:flex;align-items:flex-end;
justify-content:space-between;width:calc(100% - 168px)}
.cover .co{font-size:26px;font-weight:800;letter-spacing:.005em}
.cover .co span{color:var(--tk-magenta)}
.divider{justify-content:center;background:var(--tk-canvas-subtle)}
.divider::before{content:"";position:absolute;left:0;top:0;bottom:0;width:10px;background:var(--tk-magenta)}
.divider .no{font-size:132px;font-weight:800;line-height:.9;color:var(--tk-magenta);
letter-spacing:-.03em;margin-bottom:16px}
.divider h2{font-size:46px;max-width:820px}
.divider .pts{margin-top:28px;display:flex;gap:10px;flex-wrap:wrap;max-width:840px}
.divider .pts span{font-size:13.5px;font-weight:500;color:var(--tk-text-add);
border:1px solid var(--tk-neutral-subtle);background:#fff;padding:6px 14px;border-radius:var(--tk-r-pill)}
.statement{justify-content:center;align-items:flex-start}
.statement .big{font-size:42px;font-weight:800;line-height:1.3;max-width:1060px;letter-spacing:-.005em}
.statement .big em{color:var(--tk-magenta)}
/* ---------- 卡片 ---------- */
.grid{display:grid;gap:16px}
.g2{grid-template-columns:1fr 1fr}
.g3{grid-template-columns:repeat(3,1fr)}
.g4{grid-template-columns:repeat(4,1fr)}
.card{background:var(--tk-canvas-subtle);border:1px solid var(--tk-neutral-subtle);
border-radius:var(--tk-r-lg);padding:20px 22px;display:flex;flex-direction:column}
.card.hi{background:#fff;border-color:var(--tk-magenta);box-shadow:var(--tk-sh-raised)}
.card.ok{background:var(--tk-success-subtle);border-color:#9fdcc2}
.card .n{font-size:12px;font-weight:800;color:var(--tk-magenta);letter-spacing:.08em;margin-bottom:8px}
.card h4{margin-bottom:8px}
.card p{font-size:13.5px;line-height:1.55;color:var(--tk-text-add)}
.card p+p{margin-top:7px}
/* ---------- 表格 ---------- */
table{width:100%;border-collapse:collapse;font-size:14px}
th,td{text-align:left;padding:9px 14px;border-bottom:1px solid var(--tk-neutral-subtle);vertical-align:top;line-height:1.45}
thead th{font-size:12px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;
color:var(--tk-text-dis);border-bottom:2px solid var(--tk-text);padding-bottom:8px}
tbody tr:last-child td{border-bottom:none}
td.k{font-weight:700;width:1%;white-space:nowrap}
.col-f{color:var(--tk-text);font-weight:700}
.col-f.mark{color:var(--tk-magenta)}
.col-u{color:var(--tk-text-add)}
table.compact th,table.compact td{padding:7px 12px;font-size:13px}
table.tiny th,table.tiny td{padding:5.5px 11px;font-size:12.5px}
/* 表格密集的几页收一档行高 */
.slide.dense table.tiny th,.slide.dense table.tiny td{padding:3.5px 10px;font-size:12px;line-height:1.4}
.slide.dense table.compact th,.slide.dense table.compact td{padding:5px 11px;font-size:12.5px;line-height:1.4}
.slide.dense .hd{margin-bottom:14px}
.slide.dense .card{padding:11px 14px}
.slide.dense .card p{font-size:12.5px;line-height:1.5}
.slide.dense .quote{font-size:13.5px;line-height:1.55}
.slide.dense .brow{margin-bottom:6px}
/* ---------- 条形图 ---------- */
.chart{display:flex;flex-direction:column;gap:20px}
.cgroup .ctitle{font-size:13px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;
color:var(--tk-text-dis);margin-bottom:11px;display:flex;align-items:baseline;gap:12px}
.cgroup .ctitle .x{color:var(--tk-magenta);font-size:14px;letter-spacing:0;text-transform:none}
.brow{display:flex;align-items:center;gap:14px;margin-bottom:8px}
.brow .bl{width:96px;flex-shrink:0;font-size:13.5px;font-weight:700;text-align:right}
.brow .bt{flex:1;height:26px;background:var(--tk-surface-subtle);border-radius:var(--tk-r-sm);overflow:hidden}
.brow .bf{height:100%;width:0;border-radius:var(--tk-r-sm);
transition:width .75s cubic-bezier(.25,.8,.35,1) .12s}
.slide.active .brow .bf{width:var(--w)}
.bf.f{background:var(--tk-magenta)}
.bf.u{background:var(--tk-neutral)}
.bf.g{background:var(--tk-success)}
.brow .bv{width:104px;flex-shrink:0;font-size:15px;font-weight:800;font-variant-numeric:tabular-nums}
.brow .bv.dim{color:var(--tk-text-add);font-weight:700}
/* ---------- 大数字 ---------- */
.stats{display:flex;gap:20px}
.stat{flex:1;background:var(--tk-canvas-subtle);border:1px solid var(--tk-neutral-subtle);
border-radius:var(--tk-r-lg);padding:18px 20px}
.stat .v{font-size:42px;font-weight:800;line-height:1;color:var(--tk-magenta);
font-variant-numeric:tabular-nums;letter-spacing:-.02em}
.stat .v small{font-size:19px;font-weight:800;margin-left:3px}
.stat .l{font-size:12.5px;line-height:1.45;color:var(--tk-text-add);margin-top:9px}
/* 案例页的大数字与图片并排,收窄一档 */
.case .stat{padding:11px 16px}
.case .stat .v{font-size:34px}
.case .stat .v small{font-size:16px}
.case .stat .l{font-size:12px;margin-top:7px}
/* ---------- chip ---------- */
.chips{display:flex;flex-wrap:wrap;gap:8px}
.chip{font-size:13.5px;font-weight:500;padding:7px 14px;border-radius:var(--tk-r-pill);
border:1px solid var(--tk-neutral-subtle);background:#fff;white-space:nowrap}
.chip.f{border-color:var(--tk-magenta);color:var(--tk-magenta);font-weight:700;background:#fff}
.chip.n{background:var(--tk-surface-subtle);border-color:transparent}
.chip.d{color:var(--tk-text-dis)}
/* ---------- 引用块 ---------- */
.quote{border-left:4px solid var(--tk-magenta);padding:4px 0 4px 18px;font-size:15.5px;
line-height:1.6;color:var(--tk-text)}
.quote.warn{border-left-color:var(--tk-warning)}
.quote.ok{border-left-color:var(--tk-success)}
.quote .src{display:block;font-size:12.5px;color:var(--tk-text-dis);margin-top:8px}
/* ---------- 架构栈图 ----------
每一列 = 一条技术路线 / 一个框架:代码层 → 中间层 → 绘制层。
.sb 用等分栅格排层,各列的 .sh / .sf 高度锁死,
这样「界面由谁绘制」在四列里落在同一条水平线上,可以横着扫。
.on 用洋红点出这一层,全篇图示的主语都是它。 */
.stacks{display:flex;gap:14px;align-items:stretch;min-height:400px}
.stk{flex:1;display:flex;flex-direction:column;border:1px solid var(--tk-neutral-subtle);
border-radius:var(--tk-r-lg);overflow:hidden;background:#fff;min-width:0}
.stk.on{border-color:var(--tk-magenta);box-shadow:var(--tk-sh-raised)}
.stk .sh{padding:10px 12px;background:var(--tk-surface-subtle);text-align:center;min-height:58px;
display:flex;flex-direction:column;justify-content:center}
.stk.on .sh{background:var(--tk-magenta);color:#fff}
.stk .sh .no{font-size:11px;font-weight:800;letter-spacing:.12em;opacity:.55;display:block}
.stk .sh .nm{font-size:14.5px;font-weight:800;margin-top:2px;display:block;line-height:1.25}
.stk .sb{flex:1;padding:14px 12px;display:grid;grid-template-rows:1fr 16px 1fr 16px 1fr}
.stk .sb.four{grid-template-rows:1fr 16px 1fr 16px 1fr 16px 1fr}
.lyr{border-radius:var(--tk-r-sm);padding:6px 9px;text-align:center;font-size:12.5px;line-height:1.35;font-weight:700;
display:flex;flex-direction:column;align-items:center;justify-content:center}
.lyr .t{font-size:10px;font-weight:800;letter-spacing:.06em;color:var(--tk-text-dis);
text-transform:uppercase;display:block;margin-bottom:3px}
.lyr.code{background:var(--tk-surface-subtle)}
.lyr.mid{background:#fff;border:1px dashed var(--tk-neutral-subtle);color:var(--tk-text-add);font-weight:500}
.lyr.draw{background:var(--tk-canvas-subtle);border:1px solid var(--tk-neutral-subtle)}
.stk.on .lyr.draw{background:#fce6f1;border-color:var(--tk-magenta);color:var(--tk-magenta-press)}
.lyr.draw .t{color:inherit;opacity:.65}
.dn{display:flex;align-items:center;justify-content:center;color:var(--tk-text-dis);font-size:12px;line-height:1}
.stk .sf{padding:9px 12px;border-top:1px solid var(--tk-neutral-subtle);min-height:60px;
font-size:11.5px;line-height:1.45;color:var(--tk-text-add);background:var(--tk-canvas-subtle);
display:flex;align-items:center}
.stk.on .sf{color:var(--tk-text);background:#fff}
/* ---------- 开源分层 ---------- */
.layers{display:flex;gap:24px}
.lcol{flex:1}
.lcol .lh{font-size:15px;font-weight:800;margin-bottom:10px;padding-bottom:7px;border-bottom:2px solid var(--tk-text)}
.layer{padding:11px 14px;border-radius:var(--tk-r);margin-bottom:7px;font-size:13.5px;font-weight:700;
display:flex;justify-content:space-between;align-items:center;gap:10px}
.layer .tag{font-size:11px;font-weight:800;letter-spacing:.04em;padding:3px 9px;border-radius:var(--tk-r-pill);white-space:nowrap}
.layer.open{background:var(--tk-success-subtle);color:#04543a}
.layer.open .tag{background:var(--tk-success);color:#fff}
.layer.closed{color:#5f5f63;
background:repeating-linear-gradient(45deg,#efeff0,#efeff0 7px,#e0e0e3 7px,#e0e0e3 14px)}
.layer.closed .tag{background:var(--tk-surface-hl);color:#fff}
/* ---------- 互斥示意 ---------- */
.dead{display:flex;align-items:center;gap:22px}
.dead .side{flex:1;background:#fff;border:2px solid var(--tk-neutral-subtle);border-radius:var(--tk-r-lg);padding:18px}
.dead .side h4{margin-bottom:9px}
.dead .side .req{font-size:13px;line-height:1.55;color:var(--tk-text-add)}
.dead .side .verdict{margin-top:12px;font-size:13px;font-weight:800;padding:8px 12px;border-radius:var(--tk-r);
background:var(--tk-surface-subtle)}
.dead .side.a .verdict{color:var(--tk-magenta-press);background:#fce6f1}
.dead .side.b .verdict{color:#7a2900;background:#fdeade}
.dead .x{width:88px;flex-shrink:0;text-align:center}
.dead .x .sym{font-size:44px;font-weight:800;color:var(--tk-danger);line-height:1}
.dead .x .lb{font-size:12.5px;font-weight:800;color:var(--tk-danger);margin-top:6px}
/* ---------- 成本堆叠条 ---------- */
.cost{display:flex;height:70px;border-radius:var(--tk-r);overflow:hidden;border:1px solid var(--tk-neutral-subtle)}
.cost .seg{display:flex;flex-direction:column;align-items:center;justify-content:center;
border-right:1px solid #fff;padding:0 8px;text-align:center}
.cost .seg:last-child{border-right:none}
.cost .seg .p{font-size:19px;font-weight:800;line-height:1}
.cost .seg .t{font-size:11.5px;line-height:1.3;margin-top:6px;font-weight:500}
.cost .s1{background:var(--tk-surface-subtle);color:var(--tk-text-add)}
.cost .s2{background:var(--tk-success-subtle);color:#04543a}
.cost .s3{background:#fce6f1;color:var(--tk-magenta-press)}
.cost .s4{background:#f9d5e7;color:var(--tk-magenta-press)}
.costlegend{display:flex;gap:26px;margin-top:16px;font-size:13px}
.costlegend div{display:flex;align-items:center;gap:8px;color:var(--tk-text-add)}
.costlegend .ar{font-size:15px;font-weight:800}
.costlegend .up{color:var(--tk-magenta)}
.costlegend .dn{color:var(--tk-success)}
/* ---------- 案例图片 ---------- */
.case{display:flex;gap:30px;align-items:stretch;flex:1;min-height:0}
.case .txt{flex:1;display:flex;flex-direction:column;justify-content:center;min-width:0}
.case .shots{display:flex;gap:12px;align-items:center;flex-shrink:0}
.shot{width:150px;border-radius:12px;overflow:hidden;border:1px solid var(--tk-neutral-subtle);
box-shadow:var(--tk-sh-raised);background:#fff}
.shot img{width:100%;display:block}
.shot .cap{font-size:10.5px;line-height:1.35;padding:7px 8px;color:var(--tk-text-add);
background:#fff;text-align:center;font-weight:500}
/* ---------- 证据分级 A / B / C ---------- */
.grade{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;
border-radius:var(--tk-r-sm);font-size:11.5px;font-weight:800;flex-shrink:0}
.grade.a{background:var(--tk-magenta);color:#fff}
.grade.b{background:var(--tk-info);color:#fff}
.grade.c{background:var(--tk-neutral);color:#fff}
.legend{display:flex;gap:18px;font-size:12.5px;color:var(--tk-text-add);align-items:center;flex-wrap:wrap}
.legend span{display:flex;align-items:center;gap:7px}
/* ---------- 缩略图总览 ---------- */
#deck.grid-mode{--ts:.18;display:grid;grid-template-columns:repeat(5,1fr);gap:13px;padding:22px;
overflow-y:auto;background:var(--tk-surface-subtle);align-content:start}
#deck.grid-mode .holder{position:relative;inset:auto;aspect-ratio:16/9;overflow:hidden;cursor:pointer;
border-radius:6px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.14);
outline:2px solid transparent;transition:outline-color .12s}
#deck.grid-mode .holder:hover{outline-color:var(--tk-neutral)}
#deck.grid-mode .holder.cur{outline-color:var(--tk-magenta)}
#deck.grid-mode .holder::after{content:attr(data-n);position:absolute;right:4px;bottom:3px;z-index:3;
font-size:9px;font-weight:800;color:var(--tk-text-dis);background:rgba(255,255,255,.9);
padding:1px 4px;border-radius:3px}
#deck.grid-mode .slide{width:1280px;height:720px;inset:auto;top:0;left:0;opacity:1;transform:scale(var(--ts));
transform-origin:0 0;pointer-events:none;transition:none}
#deck.grid-mode::-webkit-scrollbar{width:9px}
#deck.grid-mode::-webkit-scrollbar-thumb{background:var(--tk-neutral);border-radius:5px}
/* ---------- 提示 ---------- */
#hint{position:absolute;left:50%;bottom:22px;transform:translateX(-50%);z-index:30;
background:rgba(36,36,38,.9);color:#fff;font-size:12px;font-weight:500;padding:8px 18px;
border-radius:var(--tk-r-pill);white-space:nowrap;opacity:0;transition:opacity .4s;pointer-events:none}
#hint.on{opacity:1}
#hint b{color:#f61488;font-weight:800}
</style>
</head>
<body>
<div id="viewport">
<div id="stage">
<div id="deck">
<!-- ══════════════ 1 · 封面 ══════════════ -->
<div class="holder"><section class="slide cover" data-ch="">
<div class="kicker">技术方案说明</div>
<h1>App 技术栈介绍</h1>
<p class="lead" style="margin-top:26px;max-width:760px">Flutter 与 uni-app / uni-app x 的技术路线、生态与落地情况</p>
<div class="foot">
<div class="co">T<span>-</span>Systems</div>
<div style="font-size:13px;color:var(--tk-text-dis);text-align:right;line-height:1.6">2026 年 8 月</div>
</div>
</section></div>
<!-- ══════════════ 2 · 目录 ══════════════ -->
<div class="holder"><section class="slide" data-ch="目录">
<div class="hd"><div class="eyebrow">目录</div><h2>本次介绍分五个部分</h2></div>
<div class="bd">
<table class="compact">
<tbody>
<tr><td class="k" style="color:var(--tk-magenta);font-size:19px;width:70px">01</td>
<td style="width:290px"><strong style="font-size:16px">跨端开发的技术路线</strong></td>
<td class="col-u">市面上的方案归类,四条路线的工作方式,界面由谁绘制</td></tr>
<tr><td class="k" style="color:var(--tk-magenta);font-size:19px">02</td>
<td><strong style="font-size:16px">三个框架的定位</strong></td>
<td class="col-u">Flutter、uni-app、uni-app x 各走哪条路线,各自为谁设计</td></tr>
<tr><td class="k" style="color:var(--tk-magenta);font-size:19px">03</td>
<td><strong style="font-size:16px">生态与落地规模</strong></td>
<td class="col-u">开源程度、社区体量、在用的团队,以及两个整车厂案例</td></tr>
<tr><td class="k" style="color:var(--tk-magenta);font-size:19px">04</td>
<td><strong style="font-size:16px">产品能力与工程链路</strong></td>
<td class="col-u">小组件与系统能力、原生工程的开放程度、构建发版、AI 工具链</td></tr>
<tr><td class="k" style="color:var(--tk-magenta);font-size:19px">05</td>
<td><strong style="font-size:16px">结论</strong></td>
<td class="col-u">成本构成、uni-app x 更优的项、推荐方案</td></tr>
</tbody>
</table>
</div>
</section></div>
<!-- ══════════════ 3 · 结论摘要 · 推荐 ══════════════ -->
<div class="holder"><section class="slide" data-ch="结论摘要">
<div class="hd">
<div class="eyebrow">结论摘要</div>
<h2>技术栈推荐 <em>Flutter</em></h2>
</div>
<div class="bd">
<div class="grid g2" style="gap:16px">
<div class="card hi">
<div class="n">01</div><h4>界面在两端完全一致</h4>
<p>Flutter 用自己的渲染引擎绘制界面,不依赖系统控件,也不依赖各机型的浏览器内核。同一份设计稿在 iOS 和 Android 上呈现一致,不需要为两端分别调样式。</p>
</div>
<div class="card hi">
<div class="n">02</div><h4>系统能力没有额外限制</h4>
<p>原生侧就是标准的 Xcode 与 Android Studio 工程。桌面小组件、Siri、系统级推送这类能力按常规原生方式实现,不需要再学一套框架方的插件规则。</p>
</div>
<div class="card">
<div class="n">03</div><h4>大型 App 上有充分先例</h4>
<p>BMW、大众汽车(安徽)、字节跳动、阿里巴巴、腾讯、美团、京东、贝壳、携程都在核心业务中使用。引擎与框架代码全部开源。</p>
</div>
<div class="card">
<div class="n">04</div><h4>技术栈稳定</h4>
<p>Flutter 自 2015 年起是同一条技术主线,没有发生过技术栈更替。uni-app 系历经 5+ App → nvue → uni-app x 两次更替,前两代已于 2024 年停止官方维护。</p>
</div>
</div>
</div>
<div class="note">uni-app 与 uni-app x 是成熟且被广泛使用的框架,本次不做优劣评判,只对照两者与本项目形态的匹配程度。uni-app x 更优的三项在第 5 章列出。</div>
</section></div>
<!-- ══════════════ 4 · 结论摘要 · 三方对照 ══════════════ -->
<div class="holder"><section class="slide" data-ch="结论摘要">
<div class="hd"><div class="eyebrow">结论摘要</div><h2>三个框架的核心对照</h2></div>
<div class="bd">
<table class="compact">
<thead><tr><th style="width:16%">维度</th><th style="width:29%">Flutter</th><th style="width:26%">uni-app</th><th>uni-app x</th></tr></thead>
<tbody>
<tr><td class="k">界面由谁绘制</td><td class="col-f mark">框架自带的渲染引擎</td><td class="col-u">机型自带的浏览器内核</td><td class="col-u">系统原生控件</td></tr>
<tr><td class="k">两端一致性</td><td class="col-f">逐像素一致</td><td class="col-u">受各机型内核版本影响</td><td class="col-u">用系统控件,两端有差异</td></tr>
<tr><td class="k">主要面向</td><td class="col-f">独立 App</td><td class="col-u">小程序为主、App 为辅</td><td class="col-u">已有小程序团队转做 App</td></tr>
<tr><td class="k">嵌入 F6 网页</td><td class="col-f mark">通道由 App 侧建立,F6 无需改动</td><td class="col-u">同左</td><td class="col-u">需 F6 引入 DCloud 的 JS 文件</td></tr>
<tr><td class="k">原生能力开发</td><td class="col-f">强类型代码生成,支持命令行</td><td class="col-u">原生插件已停止新增上架</td><td class="col-u">只能经 HBuilderX,不支持命令行</td></tr>
<tr><td class="k">开源程度</td><td class="col-f mark">引擎 + 框架全开源(BSD</td><td class="col-u">框架层开源(Apache-2.0</td><td class="col-u">框架层开源,Runtime / SDK 闭源</td></tr>
<tr><td class="k">大型 App 案例</td><td class="col-f">字节 90+ 款、闲鱼、美团、京东、贝壳、携程、BMW、大众安徽 ID. UNYX</td><td class="col-u"></td><td class="col-u">未检索到同等量级公开案例</td></tr>
<tr><td class="k">鸿蒙</td><td class="col-u">社区分支,3.41 已发布</td><td class="col-u">支持</td><td style="color:var(--tk-success);font-weight:700">一等公民 ✓</td></tr>
<tr><td class="k">安装包与内存</td><td class="col-u">约多 10 MB / 35 MB</td><td class="col-u"></td><td style="color:var(--tk-success);font-weight:700">更小 ✓</td></tr>
<tr><td class="k">首版投入</td><td class="col-f">基准</td><td class="col-u"></td><td class="col-u">差异在 ±10% 以内</td></tr>
</tbody>
</table>
</div>
<div class="note">最后三行是 uni-app x 占优的项。</div>
</section></div>
<!-- ══════════════ 5 · 分隔 01 ══════════════ -->
<div class="holder"><section class="slide divider" data-ch="">
<div class="no">01</div>
<h2>跨端开发的技术路线</h2>
<div class="pts"><span>市面上的方案</span><span>四条路线的工作方式</span><span>界面由谁绘制</span></div>
</section></div>
<!-- ══════════════ 6 · 1.1 市面上的方案 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 1 章 · 技术路线">
<div class="hd">
<div class="eyebrow">1.1 方案总览</div>
<h2>市面上的方案,按底层原理只有四类</h2>
<p class="sub">框架名字很多,但决定 App 能力和体验的是底层怎么把界面画到屏幕上。按这一点归类,选择面并不大。</p>
</div>
<div class="bd">
<table class="compact">
<thead><tr><th style="width:15%">路线</th><th style="width:51%">代表方案</th><th>什么情况下选它</th></tr></thead>
<tbody>
<tr>
<td class="k"><span style="color:var(--tk-text-dis);font-weight:800"></span> 原生双端</td>
<td><div class="chips"><span class="chip n">Swift / SwiftUI</span><span class="chip n">Kotlin / Compose</span><span class="chip n">Kotlin Multiplatform</span></div></td>
<td class="col-u">能力上限优先,接受两端各写一套 UI</td>
</tr>
<tr>
<td class="k"><span style="color:var(--tk-text-dis);font-weight:800"></span> 网页套壳</td>
<td><div class="chips"><span class="chip">Cordova</span><span class="chip">Capacitor</span><span class="chip">uni-app</span><span class="chip">Taro</span><span class="chip d">5+ App / nvue</span></div></td>
<td class="col-u">小程序 / H5 是主要入口,App 是补充</td>
</tr>
<tr>
<td class="k"><span style="color:var(--tk-text-dis);font-weight:800"></span> 编译到系统控件</td>
<td><div class="chips"><span class="chip">React Native</span><span class="chip">uni-app x</span><span class="chip">Kuikly(腾讯)</span><span class="chip">.NET MAUI</span></div></td>
<td class="col-u">框架的翻译表能覆盖住需求范围</td>
</tr>
<tr>
<td class="k"><span style="color:var(--tk-magenta);font-weight:800"></span> 自绘引擎</td>
<td><div class="chips"><span class="chip f">Flutter</span></div></td>
<td class="col-f">要做两端一致、长期迭代的独立 App</td>
</tr>
</tbody>
</table>
</div>
<div class="note"><b>本次不展开的几个:</b>React Native 的前提是团队已有 React 技术栈;Taro、Cordova、Capacitor 的重心仍是网页路线;Kotlin Multiplatform 只共享业务逻辑,界面仍由两端各自实现,严格说不属于跨端 UI 方案;Kuikly 是腾讯 2025 年开源的自研框架,目前主要在腾讯内部使用,对外生态尚早。灰色的 5+ App / nvue 是 DCloud 的上一代方案,已停止维护。</div>
</section></div>
<!-- ══════════════ 7 · 1.2 四条路线图示 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 1 章 · 技术路线">
<div class="hd">
<div class="eyebrow">1.2 工作方式</div>
<h2>四条路线,从代码到屏幕</h2>
</div>
<div class="bd">
<div class="stacks">
<div class="stk">
<div class="sh"><span class="no">路线 ①</span><span class="nm">原生双端</span></div>
<div class="sb">
<div class="lyr code"><span class="t">代码</span>Swift + Kotlin<br>两套</div>
<div class="dn"></div>
<div class="lyr mid">无中间层<br><span style="font-size:11px">直接调用系统 API</span></div>
<div class="dn"></div>
<div class="lyr draw"><span class="t">界面由谁绘制</span>系统控件</div>
</div>
<div class="sf">系统给什么就能用什么,能力上限最高;两端各写一遍。</div>
</div>
<div class="stk">
<div class="sh"><span class="no">路线 ②</span><span class="nm">网页套壳</span></div>
<div class="sb">
<div class="lyr code"><span class="t">代码</span>HTML / CSS / JS<br>一套</div>
<div class="dn"></div>
<div class="lyr mid">WebView<br><span style="font-size:11px">(机型自带的浏览器)</span></div>
<div class="dn"></div>
<div class="lyr draw"><span class="t">界面由谁绘制</span>浏览器内核</div>
</div>
<div class="sf">开发最快;界面随机型内核版本变化,系统能力要靠插件从网页环境打通。</div>
</div>
<div class="stk">
<div class="sh"><span class="no">路线 ③</span><span class="nm">编译到系统控件</span></div>
<div class="sb">
<div class="lyr code"><span class="t">代码</span>JS / UTS<br>一套</div>
<div class="dn"></div>
<div class="lyr mid">框架桥接层<br><span style="font-size:11px">(翻译成原生控件)</span></div>
<div class="dn"></div>
<div class="lyr draw"><span class="t">界面由谁绘制</span>系统控件</div>
</div>
<div class="sf">代码经框架的翻译表转成 Kotlin / Swift;表里没有的能力要自己补齐。</div>
</div>
<div class="stk on">
<div class="sh"><span class="no">路线 ④</span><span class="nm">自绘引擎</span></div>
<div class="sb">
<div class="lyr code"><span class="t">代码</span>Dart<br>一套</div>
<div class="dn"></div>
<div class="lyr mid">Flutter 渲染引擎<br><span style="font-size:11px">(自带一块画布)</span></div>
<div class="dn"></div>
<div class="lyr draw"><span class="t">界面由谁绘制</span>引擎自己绘制</div>
</div>
<div class="sf">界面自己画,不受机型影响;系统能力经通道调用两端的原生代码。</div>
</div>
</div>
</div>
<div class="note">路线 ② 与 ③ 的界面最终都交给别人来画:一个交给浏览器内核,一个交给系统控件。路线 ④ 把这一层拿回到框架自己手里。</div>
</section></div>
<!-- ══════════════ 8 · 1.3 界面由谁绘制的影响 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 1 章 · 技术路线">
<div class="hd">
<div class="eyebrow">1.3 影响</div>
<h2>"界面由谁绘制"影响三件事</h2>
</div>
<div class="bd">
<table class="compact">
<thead><tr><th style="width:19%"></th><th style="width:20%">① 原生双端</th><th style="width:20%">② 网页套壳</th><th style="width:20%">③ 编译到系统控件</th><th>④ 自绘引擎</th></tr></thead>
<tbody>
<tr>
<td class="k">两端界面一致性</td>
<td class="col-u">两套代码各自维护</td>
<td class="col-u">受机型内核版本影响</td>
<td class="col-u">控件本身两端有差异</td>
<td class="col-f mark">逐像素一致</td>
</tr>
<tr>
<td class="k">做一个设计稿上的特殊效果</td>
<td class="col-u">自己实现,无限制</td>
<td class="col-u">用 CSS 实现,复杂动效性能受限</td>
<td class="col-u">受系统控件能力限制</td>
<td class="col-f mark">自己实现,无限制</td>
</tr>
<tr>
<td class="k">用一个新的系统能力</td>
<td class="col-u">自己写</td>
<td class="col-u">写原生插件</td>
<td class="col-u">按框架方的插件规则写</td>
<td class="col-f mark">写插件,原生工程完全开放</td>
</tr>
</tbody>
</table>
<div class="grid g2" style="margin-top:20px;gap:18px">
<div class="card">
<h4>本项目的形态</h4>
<p>要嵌入 F6 的网页业务、要用到扫码、定位、推送、相机、蓝牙等一批系统能力,同时要在两端保持一致的品牌体验。</p>
</div>
<div class="card hi">
<h4>对应到上表</h4>
<p>一致性和系统能力这两列同时重要。路线 ④ 在这两列上都不受第三方约束,这是选择 Flutter 的技术出发点。</p>
</div>
</div>
</div>
</section></div>
<!-- ══════════════ 9 · 分隔 02 ══════════════ -->
<div class="holder"><section class="slide divider" data-ch="">
<div class="no">02</div>
<h2>三个框架的定位</h2>
<div class="pts"><span>各走哪条路线</span><span>为谁设计</span><span>适用场景</span></div>
</section></div>
<!-- ══════════════ 10 · 2.1 三者的技术路线 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 2 章 · 框架定位">
<div class="hd">
<div class="eyebrow">2.1 技术路线</div>
<h2>三个框架各走哪条路线</h2>
</div>
<div class="bd">
<div class="stacks">
<div class="stk on">
<div class="sh"><span class="no">路线 ④ · 自绘引擎</span><span class="nm">Flutter</span></div>
<div class="sb four">
<div class="lyr code"><span class="t">开发语言</span>Dart</div>
<div class="dn"></div>
<div class="lyr mid">编译为机器码<br><span style="font-size:11px">AOT,无 JS 解释器</span></div>
<div class="dn"></div>
<div class="lyr mid">Flutter 渲染引擎<br><span style="font-size:11px">Impeller / Skia</span></div>
<div class="dn"></div>
<div class="lyr draw"><span class="t">界面</span>引擎自己绘制</div>
</div>
<div class="sf">Google 主导,2015 年开源。引擎与框架代码全部公开(BSD-3-Clause)。</div>
</div>
<div class="stk">
<div class="sh"><span class="no">路线 ② · 网页套壳</span><span class="nm">uni-app</span></div>
<div class="sb four">
<div class="lyr code"><span class="t">开发语言</span>Vue / JavaScript</div>
<div class="dn"></div>
<div class="lyr mid">编译为小程序 / H5 / App<br><span style="font-size:11px">一套代码多端发布</span></div>
<div class="dn"></div>
<div class="lyr mid">App 端:WebView<br><span style="font-size:11px">或已停维护的 nvue</span></div>
<div class="dn"></div>
<div class="lyr draw"><span class="t">界面</span>浏览器内核</div>
</div>
<div class="sf">DCloud 出品,2018 年开源。核心场景是同时发布小程序、H5 与 App。</div>
</div>
<div class="stk">
<div class="sh"><span class="no">路线 ③ · 编译到系统控件</span><span class="nm">uni-app x</span></div>
<div class="sb four">
<div class="lyr code"><span class="t">开发语言</span>UTS / UVue</div>
<div class="dn"></div>
<div class="lyr mid">编译为 Kotlin / Swift<br><span style="font-size:11px">保留 Vue 写法</span></div>
<div class="dn"></div>
<div class="lyr mid">DCloud Runtime<br><span style="font-size:11px">闭源</span></div>
<div class="dn"></div>
<div class="lyr draw"><span class="t">界面</span>系统控件</div>
</div>
<div class="sf">DCloud 的新一代方案,让已有的 Vue / 小程序团队用熟悉的写法产出原生 App。</div>
</div>
</div>
</div>
<div class="note">uni-app x 与 uni-app 是两套独立的技术栈,语言、运行时和渲染方式都不同,不是版本升级关系。</div>
</section></div>
<!-- ══════════════ 11 · 2.2 为谁设计 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 2 章 · 框架定位">
<div class="hd">
<div class="eyebrow">2.2 设计目标</div>
<h2>三个框架各自为谁设计</h2>
</div>
<div class="bd">
<table class="compact">
<thead><tr><th style="width:17%"></th><th style="width:28%">Flutter</th><th style="width:27%">uni-app</th><th>uni-app x</th></tr></thead>
<tbody>
<tr><td class="k">最初要解决的问题</td><td class="col-f">一套代码做出体验接近原生的独立 App</td><td class="col-u">一套 Vue 代码同时发布小程序、H5 与 App</td><td class="col-u">让 Vue / 小程序团队产出真正的原生 App</td></tr>
<tr><td class="k">典型使用者</td><td class="col-f">已有原生团队,要统一两端体验</td><td class="col-u">以小程序为主要渠道的业务</td><td class="col-u">熟悉 Vue、希望不换语言做 App 的团队</td></tr>
<tr><td class="k">最擅长的场景</td><td class="col-f">界面复杂、系统能力多、长期迭代的主力 App</td><td class="col-u">多渠道同时上线、以内容和交易页面为主</td><td class="col-u">交互不复杂、以页面和接口为主的 App</td></tr>
<tr><td class="k">发布渠道</td><td class="col-f">iOS、Android(鸿蒙有社区分支)</td><td class="col-u">各家小程序、H5、iOS、Android、鸿蒙</td><td class="col-u">iOS、Android、鸿蒙、部分小程序</td></tr>
</tbody>
</table>
<div class="grid g2" style="margin-top:20px;gap:18px">
<div class="card">
<h4>本项目的渠道范围</h4>
<p>首版只发 iOS 与 Android 两个应用商店,不发小程序,也不需要 H5 版本。</p>
</div>
<div class="card">
<h4>已确定的能力清单</h4>
<p>扫码、定位、推送、相机、蓝牙、文件、车牌与 VIN 识别等系统能力,以及对 F6 网页业务的嵌入与双向通信。</p>
</div>
</div>
</div>
</section></div>
<!-- ══════════════ 12 · 分隔 03 ══════════════ -->
<div class="holder"><section class="slide divider" data-ch="">
<div class="no">03</div>
<h2>生态与落地规模</h2>
<div class="pts"><span>开源程度</span><span>社区体量</span><span>谁在用</span><span>两个整车厂案例</span></div>
</section></div>
<!-- ══════════════ 13 · 3.1 开源程度与社区体量 ══════════════ -->
<div class="holder"><section class="slide dense" data-ch="第 3 章 · 生态与落地规模">
<div class="hd">
<div class="eyebrow">3.1 开源程度与社区体量</div>
<h2>代码开到哪一层,社区有多大</h2>
</div>
<div class="bd">
<div class="layers" style="margin-bottom:22px">
<div class="lcol">
<div class="lh">Flutter</div>
<div class="layer open">业务框架层 <span class="tag">开源</span></div>
<div class="layer open">渲染引擎 <span class="tag">开源</span></div>
<div class="layer open">命令行与构建工具 <span class="tag">开源</span></div>
</div>
<div class="lcol">
<div class="lh">uni-app x</div>
<div class="layer open">业务框架层 <span class="tag">开源</span></div>
<div class="layer closed">Runtime 与原生 SDK <span class="tag">闭源</span></div>
<div class="layer closed">HBuilderX 与打包服务 <span class="tag">闭源</span></div>
</div>
</div>
<div class="chart" style="gap:12px">
<div class="cgroup">
<div class="ctitle">主仓库提交数<span class="x">约 8.1 倍</span></div>
<div class="brow"><span class="bl">Flutter</span><div class="bt"><div class="bf f" style="--w:100%"></div></div><span class="bv">91,171</span></div>
<div class="brow"><span class="bl">uni-app</span><div class="bt"><div class="bf u" style="--w:12.3%"></div></div><span class="bv dim">11,216</span></div>
</div>
<div class="cgroup">
<div class="ctitle">Star 数<span class="x">约 4.3 倍</span></div>
<div class="brow"><span class="bl">Flutter</span><div class="bt"><div class="bf f" style="--w:100%"></div></div><span class="bv">178,643</span></div>
<div class="brow"><span class="bl">uni-app</span><div class="bt"><div class="bf u" style="--w:23.3%"></div></div><span class="bv dim">41,600</span></div>
</div>
</div>
</div>
<div class="note">GitHub 数据核验于 2026-08-25,取自 <b>flutter/flutter</b><b>dcloudio/uni-app</b>Fork 数为 31,002 与 3,709。star 数反映的是全球开源关注度,uni-app 在国内企业与政企项目中的落地量不体现在这项数据里。</div>
</section></div>
<!-- ══════════════ 14 · 3.2 谁在用 ══════════════ -->
<div class="holder"><section class="slide dense" data-ch="第 3 章 · 生态与落地规模">
<div class="hd">
<div class="eyebrow">3.2 落地情况</div>
<h2>哪些团队在主力 App 上用 Flutter</h2>
<div class="legend" style="margin-top:12px">
<span><i class="grade a">A</i> Flutter 官方站与 Showcase 收录</span>
<span><i class="grade b">B</i> 使用方自己公开披露</span>
<span><i class="grade c">C</i> 第三方对应用安装包的扫描统计</span>
</div>
</div>
<div class="bd">
<table class="tiny">
<thead><tr><th style="width:14%">使用方</th><th style="width:44%">App / 业务</th><th style="width:8%">证据</th><th>补充</th></tr></thead>
<tbody>
<tr><td class="k">字节跳动</td><td class="col-u">90 多款应用:抖音火山版、飞书 Lark、Coze、掘金等</td><td><i class="grade a">A</i></td><td class="col-u">官方开发者故事,2026-06 更新</td></tr>
<tr><td class="k">阿里巴巴</td><td class="col-u">闲鱼,已推进到线上主链路</td><td><i class="grade a">A</i></td><td class="col-u">并开源了 FlutterBoost 等基建</td></tr>
<tr><td class="k">腾讯</td><td class="col-u">多个团队将应用完整迁移到 Flutter</td><td><i class="grade a">A</i></td><td class="col-u">官方页:90% 代码多端复用</td></tr>
<tr><td class="k">贝壳找房</td><td class="col-u">主业务多平台</td><td><i class="grade a">A</i></td><td class="col-u">Showcase 收录,2021-03</td></tr>
<tr style="background:#fdf2f8"><td class="k" style="color:var(--tk-magenta)">BMW</td><td class="col-f">My BMW App,覆盖 47 个国家</td><td><i class="grade a">A</i></td><td class="col-u">见下一页</td></tr>
<tr style="background:#fdf2f8"><td class="k" style="color:var(--tk-magenta)">大众汽车(安徽)</td><td class="col-f">ID. UNYX App:购车定制、车联车控、社区、商城</td><td><i class="grade b">B</i></td><td class="col-u">见下下页</td></tr>
<tr><td class="k">美团</td><td class="col-u">外卖商家端 App,覆盖 90% 以上业务</td><td><i class="grade b">B</i></td><td class="col-u">美团技术团队博客,2021-03</td></tr>
<tr><td class="k">京东</td><td class="col-u">JDFlutter:容器、工具链与组件平台</td><td><i class="grade b">B</i></td><td class="col-u">京东 EMOP 官方文档</td></tr>
<tr><td class="k">携程</td><td class="col-u">火车票、酒店等十多个核心业务的列表页与主流程</td><td><i class="grade b">B</i></td><td class="col-u">原生 + React Native + Flutter 混合</td></tr>
</tbody>
</table>
<div class="grid g2" style="margin-top:14px;gap:16px">
<div class="card">
<h4>落地形态一致</h4>
<p>这些团队都不是把整个 App 用跨端框架重写,而是<strong>原生工程为主体、Flutter 承载业务页面、系统能力仍走原生</strong>。本项目采用同样的形态。</p>
</div>
<div class="card hi">
<h4>第三方统计 <i class="grade c" style="width:auto;padding:0 6px">C</i></h4>
<p>Appfigures 对新上架应用的 SDK 扫描:<strong>Flutter 检出率 11.07%React Native 6.75%</strong>2024 年 110 月;2022 年同项为 10.15% 与 4.73%)。该统计口径不含 uni-app 系,不作对比。</p>
</div>
</div>
</div>
</section></div>
<!-- ══════════════ 15 · 3.3 案例 A · BMW ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 3 章 · 生态与落地规模">
<div class="hd">
<div class="eyebrow">3.3 案例一 &nbsp;·&nbsp; <i class="grade a" style="width:auto;padding:0 6px">A</i> Flutter 官方 Showcase 收录</div>
<h2>BMWMy BMW App</h2>
</div>
<div class="case">
<div class="txt">
<p class="body" style="color:var(--tk-text-add)">BMW 原先 iOS 与 Android 各有一套 BMW Connected / MINI Connected,两端功能与设计逐渐分叉。2018 年启动跨端方案评估,选型时明确排除了基于浏览器的方案,最终选定 Flutter。</p>
<div class="stats" style="margin:10px 0 8px">
<div class="stat"><div class="v"><span data-to="47">47</span></div><div class="l">个国家、五大洲<br>双端合一,同时服务 BMW 与 MINI</div></div>
<div class="stat"><div class="v"><span data-to="96">96</span></div><div class="l">每个构建自动生成、测试<br>并部署的<strong>变体数</strong></div></div>
<div class="stat"><div class="v"><small>&gt;</small><span data-to="10000">10,000</span></div><div class="l">截至 2021-10<br><strong>每个变体</strong>累计构建的版本数</div></div>
</div>
<table class="tiny" style="margin-bottom:10px">
<tbody>
<tr><td class="k" style="width:22%">2018 年</td><td>确认双端分叉问题,启动跨端方案评估</td></tr>
<tr><td class="k">2019 年 10 月</td><td>慕尼黑工程会议敲定方案</td></tr>
<tr style="background:#fdf2f8"><td class="k" style="color:var(--tk-magenta)">2020 年 7 月</td><td><strong>My BMW App 上线,从定案到发布不到一年</strong></td></tr>
<tr><td class="k">团队结构</td><td>按域拆分:车辆连接与通信抽象层 / 共享应用平台 / 面向客户的功能</td></tr>
</tbody>
</table>
<div class="quote">
远程车控、实时车况回传、车辆查找、充电桩与地图,全部落在系统 API 和车联网长连接上。<strong>这是一个原生能力密集、且要在多品牌多市场维持同一套体验的 App。</strong>
</div>
</div>
<div class="shots">
<div class="shot"><img src="assets/bmw3.png" alt="My BMW App"><div class="cap">品牌主页</div></div>
<div class="shot"><img src="assets/bmw4.png" alt="My BMW 操控"><div class="cap">远程车控 · 电量与续航回传</div></div>
<div class="shot"><img src="assets/bmw5.png" alt="My BMW 充电"><div class="cap">公共充电 · 实时功率与费用</div></div>
</div>
</div>
<div class="note">配图为 My BMW App 官方商店截图,<b>用于说明该 App 的功能面,技术栈依据是 Flutter 官方 Showcase 页</b>。BMW 集团 Offboard Platform 副总裁 Dr. Nicolai Kraemer 表述:新应用平台建立在三根支柱之上——<b>易用性、安全性与可靠性</b></div>
</section></div>
<!-- ══════════════ 16 · 3.4 案例 B · ID. UNYX ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 3 章 · 生态与落地规模">
<div class="hd">
<div class="eyebrow">3.4 案例二 &nbsp;·&nbsp; <i class="grade b" style="width:auto;padding:0 6px">B</i> 使用方官网合规披露</div>
<h2>大众汽车(安徽):ID. UNYX App</h2>
</div>
<div class="case">
<div class="txt">
<p class="body" style="color:var(--tk-text-add)">ID. UNYX 是大众汽车(安徽)面向国内市场的品牌 App,覆盖购车定制、车联车控、社区与商城。</p>
<div class="quote" style="margin:16px 0">
大众安徽官网《第三方个人信息共享清单》列明 <strong>Flutter SDKGoogle LLC</strong>,使用情形写作
<strong>「App 所有的 L1、L2、L3 页面开发语言」</strong>
<span class="src">即:一级、二级、三级页面均由 Flutter 实现。</span>
</div>
<div class="grid g2" style="gap:14px">
<div class="card">
<h4>为什么用这条作证据</h4>
<p>合规披露是法定义务,内容需与实际使用一致,可信度高于技术文章的转述。</p>
</div>
<div class="card">
<h4>同站另一份说明</h4>
<p>另列有 React Native SDK,说明该 App 是混合形态,不是纯 Flutter。此处一并注明。</p>
</div>
</div>
</div>
<div class="shots">
<div class="shot"><img src="assets/vw1.png" alt="ID. UNYX 资讯"><div class="cap">资讯与购车转化</div></div>
<div class="shot"><img src="assets/vw3.png" alt="ID. UNYX 车型"><div class="cap">车型与在线配置</div></div>
<div class="shot"><img src="assets/vw4.png" alt="ID. UNYX 车控"><div class="cap">车联车控</div></div>
</div>
</div>
<div class="note">配图取自 ID. UNYX 应用商店官方截图,<b>用于说明该 App 的功能面</b>。与 BMW 一样,这类 App 的主干功能建立在定位、推送与车联网长连接之上。</div>
</section></div>
<!-- ══════════════ 17 · 分隔 04 ══════════════ -->
<div class="holder"><section class="slide divider" data-ch="">
<div class="no">04</div>
<h2>产品能力与工程链路</h2>
<div class="pts"><span>小组件与系统能力</span><span>原生工程的开放程度</span><span>构建与发版</span><span>AI 工具链</span></div>
</section></div>
<!-- ══════════════ 18 · 4.1 App 能做到哪些形态 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 4 章 · 产品能力与工程链路">
<div class="hd">
<div class="eyebrow">4.1 扩展能力</div>
<h2>App 的形态不止"页面 + 接口"</h2>
<p class="sub">桌面与锁屏小组件、系统语音助手、系统级多媒体与进度通知、各类 App Extension——这些是后续容易提出来的需求。</p>
</div>
<div class="bd">
<table class="compact">
<thead><tr><th style="width:22%">能力</th><th style="width:39%">Flutter</th><th>uni-app x</th></tr></thead>
<tbody>
<tr><td class="k">iOS 桌面小组件</td><td class="col-f">在自己的 Xcode 工程里按标准方式做</td><td class="col-u">无内置 API。需在 Xcode 里做成 <code>.appex</code>,再放进 UTS 插件目录并额外配置</td></tr>
<tr><td class="k">Siri / App Intents</td><td class="col-f">标准原生实现</td><td class="col-u">官方说明:仅靠 UTS 插件无法实现,建议走离线打包的原生工程</td></tr>
<tr><td class="k">安卓桌面小组件</td><td class="col-f">标准原生实现</td><td class="col-u">三个 Kotlin 文件 + Manifest 注册 + 三个 XML 布局 + UTS 桥接层</td></tr>
<tr><td class="k">改一次要重来多少</td><td class="col-f mark">一次增量编译</td><td class="col-u">官方教程:每次修改这些文件后必须重新制作自定义基座</td></tr>
<tr><td class="k">取资源 ID 的方式</td><td class="col-f">直接引用,编译期检查</td><td class="col-u">不能直接引用 <code>R</code> 类,需在运行时按字符串查找</td></tr>
</tbody>
</table>
<div class="quote" style="margin-top:20px">
两条路线都做得到,区别在做的方式:<strong>Flutter 侧就是打开 Xcode 和 Android Studio 写原生代码</strong>;uni-app x 侧同样要写原生代码,并且还要额外走一遍 DCloud 的插件与打包规则。
</div>
</div>
<div class="note">uni-app x 侧内容取自 DCloud 官方文档,安卓小组件清单取自社区 2026-05 的一篇 UTS 插件实战教程,<b>后者为社区记录,非官方文档</b></div>
</section></div>
<!-- ══════════════ 19 · 4.2 打包方式 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 4 章 · 产品能力与工程链路">
<div class="hd">
<div class="eyebrow">4.2 打包方式</div>
<h2>两项官方限制,同时满足不了</h2>
<p class="sub">uni-app x 有云端打包与离线打包两种方式,部分能力只支持其中一种。以下两条都取自 DCloud 官方文档。</p>
</div>
<div class="bd">
<div class="dead">
<div class="side a">
<div class="eyebrow" style="font-size:11.5px;margin-bottom:6px">情形 A</div>
<h4>要用插件市场上的加密付费插件</h4>
<div class="req">官方文档:「uts 加密插件只支持云端传统打包,不支持离线打包、也不支持安心打包」</div>
<div class="verdict">⇒ 只能走云端打包</div>
</div>
<div class="x"><div class="sym"></div><div class="lb">互斥</div></div>
<div class="side b">
<div class="eyebrow" style="font-size:11.5px;margin-bottom:6px;color:var(--tk-warning)">情形 B</div>
<h4>要做 Siri 快捷指令这类扩展</h4>
<div class="req">官方建议的实现路径:在离线打包的原生工程里实现 <code>AppShortcutsProvider</code>,再用 UTS 插件衔接业务逻辑</div>
<div class="verdict">⇒ 要求离线打包</div>
</div>
</div>
<div class="grid g2" style="margin-top:20px;gap:18px">
<div>
<h4 style="margin:0 0 8px">而离线打包给到的,是一个围绕闭源二进制的壳工程</h4>
<table class="compact">
<thead><tr><th style="width:50%">在这个工程里能改的</th><th>改不了的</th></tr></thead>
<tbody>
<tr><td class="col-u">包名、版本号、图标、启动图</td><td class="col-u">页面怎么渲染</td></tr>
<tr><td class="col-u">Gradle 依赖、ABI、SDK 版本</td><td class="col-u">页面容器与生命周期</td></tr>
<tr><td class="col-u">权限项等清单配置</td><td class="col-u">UTS 与原生之间的通信机制</td></tr>
<tr><td class="col-u">在壳外面加自己的原生模块</td><td class="col-u">运行时的任何行为</td></tr>
</tbody>
</table>
</div>
<div class="card hi" style="align-self:start">
<h4>Flutter 侧没有这个分支</h4>
<p>只有一种构建方式,<code>ios/</code><code>android/</code> 目录就是两个完整的原生工程,源码级可控,能力之间不存在互斥。</p>
<p style="margin-top:10px">加不加插件、上不上 Siri,都不影响出包方式。</p>
</div>
</div>
</div>
<div class="note">壳工程的核心是 DCloud 预先编译好的一组 AAR,没有源码;自定义的应用类必须继承 <code>DCloudApplication</code>,官方另注明离线 SDK 不支持 Kotlin。另有两条商务约束同样来自官方文档:付费插件的授权绑定唯一的 appid 和包名,更换其一需重新购买;试用版只能用于自定义基座,不能用于正式发布。</div>
</section></div>
<!-- ══════════════ 20 · 4.3 构建与发版 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 4 章 · 产品能力与工程链路">
<div class="hd">
<div class="eyebrow">4.3 构建与发版</div>
<h2>发版链路能不能全自动</h2>
</div>
<div class="bd">
<table class="compact">
<thead><tr><th style="width:24%">环节</th><th style="width:38%">Flutter</th><th>uni-app x</th></tr></thead>
<tbody>
<tr><td class="k">出包命令</td><td class="col-f"><code>flutter build</code>,命令行完整</td><td class="col-u">原生插件与 UTS 插件的编译需经 HBuilderX</td></tr>
<tr><td class="k">接入 CI 流水线</td><td class="col-f mark">标准做法,构建机上无人值守</td><td class="col-u">图形化 IDE 环节无法在流水线中自动执行</td></tr>
<tr><td class="k">多渠道 / 多环境包</td><td class="col-f">同一条命令批量产出</td><td class="col-u">云端打包按次计费,自定义基座需重制</td></tr>
<tr><td class="k">版本与产物追溯</td><td class="col-f">产物在自己的构建机上,可完整留档</td><td class="col-u">云端打包由 DCloud 服务完成</td></tr>
</tbody>
</table>
<div class="quote" style="margin-top:22px">
第 3 章 BMW 的数据可以作参照:<strong>每个构建自动生成、测试并部署 96 个变体,每个变体累计构建超过 10,000 个版本。</strong>这个量级的自动化建立在命令行完整、构建可无人值守的基础上。
</div>
</div>
<div class="note">这一栏关心的不是"能不能出包",两者都能出包;而是出包这一步能否放进自动化流水线,由此决定多环境、多渠道版本的发布节奏。</div>
</section></div>
<!-- ══════════════ 21 · 4.4 AI 工具链 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 4 章 · 产品能力与工程链路">
<div class="hd">
<div class="eyebrow">4.4 AI 工具链</div>
<h2>AI 编程工具的支持情况</h2>
<p class="sub">两边都有官方 AI 工具,区别在能装到哪些工具上,以及公开语料的体量。</p>
</div>
<div class="bd">
<table class="compact">
<thead><tr><th style="width:22%"></th><th style="width:40%">Flutter / Dart</th><th>uni-app x</th></tr></thead>
<tbody>
<tr><td class="k">官方 skills</td><td class="col-f"><code>flutter/agent-plugins</code><code>dart-lang/skills</code>,独立开源仓库</td><td class="col-u">内置在官方 <code>uni-agent</code> 助手中</td></tr>
<tr><td class="k">官方 MCP 服务</td><td class="col-f">Dart &amp; Flutter MCP server</td><td class="col-u"><code>uni-app-x-mcp</code></td></tr>
<tr><td class="k">能用在哪些工具上</td><td class="col-f mark">Claude Code、Codex、Cursor 等,装到哪个都行</td><td class="col-u">主要围绕自家生态与少数几个工具</td></tr>
<tr><td class="k">能否自己扩展</td><td class="col-f">仓库开源,可 fork、可加自己项目的 skill</td><td class="col-u">受限于官方提供</td></tr>
<tr><td class="k">公开语料体量</td><td class="col-f">Dart / Flutter 近十年的全球公开代码</td><td class="col-u">UTS / UVue 为新语言,语料以中文官方文档为主</td></tr>
</tbody>
</table>
<div class="quote" style="margin-top:22px">
AI 生成代码的准确率与这门语言在公开代码里的沉淀量相关。<strong>uni-app x 插件市场的付费插件是加密分发的,这部分代码不会进入任何公开语料。</strong>
</div>
</div>
<div class="note">Flutter 的 skills 是任务粒度的,官方已列出十项以上:生成单元测试、生成集成测试、统计测试覆盖率、配置多语言、构建响应式布局、配置声明式路由等,仓库持续新增。</div>
</section></div>
<!-- ══════════════ 22 · 分隔 05 ══════════════ -->
<div class="holder"><section class="slide divider" data-ch="">
<div class="no">05</div>
<h2>结论</h2>
<div class="pts"><span>成本构成</span><span>uni-app x 更优的三项</span><span>推荐方案</span></div>
</section></div>
<!-- ══════════════ 23 · 5.1 成本构成 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 5 章 · 结论">
<div class="hd">
<div class="eyebrow">5.1 成本</div>
<h2>首版投入的构成</h2>
<p class="sub">占比区间是基于本项目形态的估算,不同团队会有出入。</p>
</div>
<div class="bd">
<div class="cost">
<div class="seg s1" style="width:50%"><div class="p">4555%</div><div class="t">业务逻辑与外围系统<br>集成、联调</div></div>
<div class="seg s2" style="width:22.5%"><div class="p">2025%</div><div class="t">UI 实现</div></div>
<div class="seg s3" style="width:13.75%"><div class="p">1015%</div><div class="t">原生能力<br>与桥接</div></div>
<div class="seg s4" style="width:13.75%"><div class="p">1015%</div><div class="t">工程、CI、发布<br>可观测</div></div>
</div>
<div class="costlegend">
<div><span class="ar"></span> 业务集成:<strong>无差异</strong>(跟框架无关)</div>
<div><span class="ar dn"></span> UI 实现:<strong>uni-app x 略省</strong>Vue 熟悉度高)</div>
<div><span class="ar up"></span> 原生桥接:<strong style="color:var(--tk-magenta)">更贵</strong></div>
<div><span class="ar up"></span> 工程 CI<strong style="color:var(--tk-magenta)">更贵</strong></div>
</div>
<div class="grid g2" style="margin-top:30px;gap:18px">
<div class="card">
<h4>uni-app x 省在哪里</h4>
<p>省在 UI 实现,也就是占比第二小的那一块。团队熟悉 Vue,页面写得更快。</p>
</div>
<div class="card hi">
<h4>额外成本在哪里</h4>
<p>原生桥接与工程 CI 两块更贵。一加一减,<strong>首版总投入的差异在 ±10% 以内</strong></p>
</div>
</div>
</div>
<div class="note">占比不含后续运维与迭代。业务逻辑与外围系统的集成、联调是最大的一块,这部分与选哪个框架无关。</div>
</section></div>
<!-- ══════════════ 24 · 5.2 uni-app x 更优的项 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 5 章 · 结论">
<div class="hd">
<div class="eyebrow">5.2 对方更优的项</div>
<h2>uni-app x 强于 Flutter 的三项</h2>
</div>
<div class="bd">
<div class="grid g3" style="gap:18px">
<div class="card ok">
<div class="n" style="color:#04543a">01</div><h4>鸿蒙是一等公民</h4>
<p>uni-app x 官方直接支持鸿蒙。Flutter 主线暂未纳入鸿蒙,目前走 OpenHarmony-SIG 的社区分支:3.41 已发布,3.44 预计 2026 年 9 月。</p>
<p><strong>本项目首版范围是 iOS 与 Android,鸿蒙不在内。</strong></p>
</div>
<div class="card ok">
<div class="n" style="color:#04543a">02</div><h4>安装包与内存更小</h4>
<p>DCloud 官方实测:安装包 <strong>8.5 MB</strong>Flutter 约 18 MB),运行内存 <strong>103 MB</strong>Flutter 约 138 MB)。</p>
<p>Flutter 需要带自己的渲染引擎,这部分体积无法省去。</p>
</div>
<div class="card ok">
<div class="n" style="color:#04543a">03</div><h4>热更新</h4>
<p>uni-app 系有成熟的整包与分包热更新方案。Flutter 的 Dart 代码是提前编译的机器码,同类能力受平台规则限制。</p>
</div>
</div>
<div class="quote ok" style="margin-top:26px">
这三项是真实的技术差异。<strong>是否构成决定性因素,取决于产品对鸿蒙、安装包体积和热更新的实际要求。</strong>
</div>
</div>
<div class="note">uni-app x 的离线打包与嵌入式集成会额外增加体积:iOS 约 8.7 MBAndroid arm64-v8a 约 8.1 MB。</div>
</section></div>
<!-- ══════════════ 25 · 5.3 推荐方案 ══════════════ -->
<div class="holder"><section class="slide" data-ch="第 5 章 · 结论">
<div class="hd"><div class="eyebrow">5.3 推荐</div><h2>推荐方案</h2></div>
<div class="bd">
<div class="statement" style="padding:0;margin-bottom:28px">
<div class="big" style="font-size:34px">技术栈采用 <em>Flutter</em>,原生工程为主体,<br>业务页面由 Flutter 承载,系统能力走原生。</div>
</div>
<table class="compact">
<tbody>
<tr><td class="k" style="width:22%;color:var(--tk-magenta)">界面一致性</td><td>自绘引擎,不依赖系统控件与浏览器内核,两端与各机型呈现一致</td></tr>
<tr><td class="k" style="color:var(--tk-magenta)">系统能力</td><td>原生工程完全开放,小组件、Siri、系统级能力按标准原生方式实现,无第二套规则</td></tr>
<tr><td class="k" style="color:var(--tk-magenta)">工程链路</td><td>命令行完整,可接入 CI 无人值守构建与自动分发</td></tr>
<tr><td class="k" style="color:var(--tk-magenta)">先例充分</td><td>BMW、大众安徽 ID. UNYX 等同类形态的 App 已在同一条路线上长期运行</td></tr>
</tbody>
</table>
</div>
<div class="note">版本基线 Flutter 3.44.9,首版发布 iOS 与 Android 双端。</div>
</section></div>
<!-- ══════════════ 26 · 数据来源 ══════════════ -->
<div class="holder"><section class="slide dense" data-ch="附录">
<div class="hd"><div class="eyebrow">附录</div><h2>数据与事实来源</h2></div>
<div class="bd">
<table class="tiny">
<thead><tr><th style="width:26%">内容</th><th>来源</th></tr></thead>
<tbody>
<tr><td class="k">框架技术路线与能力</td><td class="col-u">Flutter 官方文档;DCloud uni-app / uni-app x 官方文档</td></tr>
<tr><td class="k">开源程度与社区数据</td><td class="col-u">GitHub <code>flutter/flutter</code><code>dcloudio/uni-app</code>,核验于 2026-08-25</td></tr>
<tr><td class="k">在用团队清单</td><td class="col-u">Flutter 官方站开发者故事与 Showcase 页;美团技术团队博客;京东 EMOP 官方文档;携程技术公众号</td></tr>
<tr><td class="k">BMW 案例</td><td class="col-u">Flutter 官方 Showcase 的 BMW 页,含时间线与工程数据;配图为 App 官方商店截图</td></tr>
<tr><td class="k">ID. UNYX 案例</td><td class="col-u">大众汽车(安徽)官网《第三方个人信息共享清单》与 SDK 权限说明;配图为 App 官方商店截图</td></tr>
<tr><td class="k">第三方框架分布统计</td><td class="col-u">Appfigures 对应用安装包的 SDK 扫描,2024 年 110 月与 2025 年 12 月两次口径</td></tr>
<tr><td class="k">安装包与内存实测</td><td class="col-u">DCloud 官方发布的性能对比材料</td></tr>
<tr><td class="k">打包与扩展能力限制</td><td class="col-u">DCloud 官方文档;安卓小组件交付清单取自社区 2026-05 的 UTS 插件实战教程</td></tr>
</tbody>
</table>
</div>
<div class="note">证据分级:<b>A</b> 框架方官方收录 · <b>B</b> 使用方自己公开披露 · <b>C</b> 第三方对应用安装包的扫描统计。凡检索不到近年官方更新的条目,均已就地注明。</div>
</section></div>
<!-- ══════════════ 27 · 收尾 ══════════════ -->
<div class="holder"><section class="slide cover" data-ch="">
<div class="kicker">谢谢</div>
<h1 style="font-size:44px">问题与讨论</h1>
<div class="foot">
<div class="co">T<span>-</span>Systems</div>
<div style="font-size:13px;color:var(--tk-text-dis);text-align:right;line-height:1.6">
App 技术栈介绍 · 2026-08<br>Flutter / uni-app / uni-app x</div>
</div>
</section></div>
</div><!-- /#deck -->
<div id="chrome">
<div id="brand">T-Systems</div>
<div id="meta"><span id="ch"></span><span class="num"><span id="cur">1</span> / <span id="tot">27</span></span></div>
</div>
<div id="bar"></div>
<div id="hint"><b></b> 翻页 · <b>O</b> 缩略图总览 · <b>F</b> 全屏</div>
</div><!-- /#stage -->
</div><!-- /#viewport -->
<script>
(function(){
var stage = document.getElementById('stage'),
deck = document.getElementById('deck'),
chrome = document.getElementById('chrome'),
holders = Array.prototype.slice.call(deck.querySelectorAll('.holder')),
slides = holders.map(function(h){ return h.querySelector('.slide'); }),
elCur = document.getElementById('cur'),
elTot = document.getElementById('tot'),
elCh = document.getElementById('ch'),
elBar = document.getElementById('bar'),
elHint = document.getElementById('hint'),
N = slides.length,
idx = 0,
grid = false,
counted = [];
elTot.textContent = N;
holders.forEach(function(h,i){ h.dataset.n = i + 1; });
/* ---------- 等比缩放 ---------- */
function fit(){
var s = Math.min(window.innerWidth / 1280, window.innerHeight / 720);
stage.style.transform = 'scale(' + s + ')';
// 缩略图缩放比:按 holder 实际宽度算,避免硬编码
if (grid && holders[0]) {
var w = holders[0].getBoundingClientRect().width / s;
deck.style.setProperty('--ts', w / 1280);
}
}
window.addEventListener('resize', fit);
/* ---------- 大数字递增 ----------
HTML 里写的就是最终值,这里从 0 动画回到它。
所以 JS 不跑 / RAF 不触发 / 用户关了动效,屏幕上留下的都是正确数字,不会是 0。 */
var noAnim = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function countUp(slide){
if (noAnim) return;
slide.querySelectorAll('[data-to]').forEach(function(el){
if (el.dataset.done) return;
el.dataset.done = '1';
var to = parseInt(el.dataset.to, 10), t0 = null, dur = 900, done = false;
var end = to.toLocaleString('en-US');
function step(ts){
if (done) return;
if (t0 === null) t0 = ts;
var p = Math.min((ts - t0) / dur, 1);
p = 1 - Math.pow(1 - p, 3); // easeOutCubic
el.textContent = Math.round(to * p).toLocaleString('en-US');
if (p < 1) requestAnimationFrame(step);
}
el.textContent = '0';
requestAnimationFrame(step);
// rAF 在后台标签页会被节流、甚至完全挂起,动画会卡在 9,786 这种半截数上不动。
// setTimeout 不依赖帧产出,到点写终值并置 done 收掉动画循环,
// 否则迟到的 rAF 会把数字又改回中间值。这些数字是论据,不能显示错的。
setTimeout(function(){ done = true; el.textContent = end; }, dur + 80);
});
}
/* ---------- 翻页 ---------- */
function show(n){
idx = Math.max(0, Math.min(N - 1, n));
slides.forEach(function(s, i){
s.classList.toggle('active', i === idx);
s.classList.toggle('before', i < idx);
});
var ch = slides[idx].dataset.ch || '';
elCh.textContent = ch;
elCur.textContent = idx + 1;
elBar.style.width = ((idx + 1) / N * 100) + '%';
elBar.classList.toggle('off', grid);
chrome.classList.toggle('on', !grid && ch !== '');
if (!counted[idx]) { counted[idx] = 1; countUp(slides[idx]); }
// file:// 下个别浏览器会拒绝 replaceState,静默忽略即可
try { if (history.replaceState) history.replaceState(null, '', '#' + (idx + 1)); } catch (err) {}
}
function next(){ if (idx < N - 1) show(idx + 1); }
function prev(){ if (idx > 0) show(idx - 1); }
/* ---------- 缩略图总览 ---------- */
function setGrid(on){
grid = on;
deck.classList.toggle('grid-mode', on);
chrome.classList.toggle('on', !on && (slides[idx].dataset.ch || '') !== '');
elBar.classList.toggle('off', on);
holders.forEach(function(h, i){ h.classList.toggle('cur', on && i === idx); });
if (on) {
fit();
var c = holders[idx];
if (c) c.scrollIntoView({ block: 'nearest' });
} else {
show(idx);
}
}
deck.addEventListener('click', function(e){
if (!grid) return;
var h = e.target.closest('.holder');
if (!h) return;
e.stopPropagation();
idx = holders.indexOf(h);
setGrid(false);
}, true);
/* ---------- 键盘 ---------- */
document.addEventListener('keydown', function(e){
if (e.metaKey || e.ctrlKey || e.altKey) return;
var k = e.key;
if (grid) {
if (k === 'Escape' || k === 'o' || k === 'O') { setGrid(false); e.preventDefault(); }
else if (k === 'ArrowRight'){ idx = Math.min(N-1, idx+1); setGrid(true); e.preventDefault(); }
else if (k === 'ArrowLeft') { idx = Math.max(0, idx-1); setGrid(true); e.preventDefault(); }
else if (k === 'ArrowDown') { idx = Math.min(N-1, idx+5); setGrid(true); e.preventDefault(); }
else if (k === 'ArrowUp') { idx = Math.max(0, idx-5); setGrid(true); e.preventDefault(); }
else if (k === 'Enter') { setGrid(false); e.preventDefault(); }
return;
}
if (k === 'ArrowRight' || k === 'ArrowDown' || k === ' ' || k === 'PageDown'){ next(); e.preventDefault(); }
else if (k === 'ArrowLeft' || k === 'ArrowUp' || k === 'PageUp'){ prev(); e.preventDefault(); }
else if (k === 'Home'){ show(0); e.preventDefault(); }
else if (k === 'End'){ show(N - 1); e.preventDefault(); }
else if (k === 'o' || k === 'O'){ setGrid(true); e.preventDefault(); }
// Escape 不在这里开总览:全屏时 Escape 归浏览器管,抢过来会两件事一起发生
else if (k === 'f' || k === 'F'){
if (document.fullscreenElement) document.exitFullscreen();
else document.documentElement.requestFullscreen();
e.preventDefault();
}
});
/* ---------- 点击左右半屏 ---------- */
stage.addEventListener('click', function(e){
if (grid) return;
// 只挡链接和图片;正文表格照样可点翻页,靠下面的选区判断避免打断划词
if (e.target.closest('a, img')) return;
if (window.getSelection && String(window.getSelection())) return;
var r = stage.getBoundingClientRect();
if (e.clientX - r.left > r.width / 2) next(); else prev();
});
/* ---------- 触摸滑动 ---------- */
var tx = 0, ty = 0, tt = 0;
stage.addEventListener('touchstart', function(e){
var t = e.changedTouches[0]; tx = t.clientX; ty = t.clientY; tt = e.timeStamp;
}, { passive: true });
stage.addEventListener('touchend', function(e){
if (grid) return;
var t = e.changedTouches[0], dx = t.clientX - tx, dy = t.clientY - ty;
// 横向位移足够大、纵向位移小、时间短 —— 才算翻页
if (Math.abs(dx) > 55 && Math.abs(dx) > Math.abs(dy) * 1.8 && e.timeStamp - tt < 700) {
if (dx < 0) next(); else prev();
}
}, { passive: true });
/* ---------- 启动 ---------- */
var start = parseInt((location.hash || '').replace('#',''), 10);
fit();
show(isNaN(start) ? 0 : start - 1);
elHint.classList.add('on');
setTimeout(function(){ elHint.classList.remove('on'); }, 4200);
})();
</script>
</body>
</html>