2026-08-26 13:36:13 +08:00
|
|
|
|
<!doctype html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
|
<meta name="theme-color" content="#0d2438">
|
|
|
|
|
|
<title>技术路线评估 | Flutter vs uni-app x</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500;600;700;800&display=swap');
|
|
|
|
|
|
|
|
|
|
|
|
:root {
|
2026-08-26 15:57:06 +08:00
|
|
|
|
--ink: #1b1b1b;
|
|
|
|
|
|
--muted: #5f6368;
|
|
|
|
|
|
--paper: #f7f7f8;
|
|
|
|
|
|
--paper-deep: #e6e6e9;
|
|
|
|
|
|
--line: #d4d4d8;
|
|
|
|
|
|
--orange: #e20074;
|
|
|
|
|
|
--teal: #1f1f1f;
|
|
|
|
|
|
--lime: #e20074;
|
|
|
|
|
|
--blue: #4b4b50;
|
|
|
|
|
|
--shadow: 0 22px 55px rgba(27, 27, 27, .16);
|
2026-08-26 13:36:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
|
|
html { background: var(--ink); }
|
|
|
|
|
|
body {
|
|
|
|
|
|
min-width: 320px;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: var(--ink);
|
|
|
|
|
|
font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
|
|
|
|
|
background:
|
|
|
|
|
|
linear-gradient(90deg, rgba(16, 45, 67, .035) 1px, transparent 1px),
|
|
|
|
|
|
linear-gradient(rgba(16, 45, 67, .035) 1px, transparent 1px),
|
|
|
|
|
|
var(--paper);
|
|
|
|
|
|
background-size: 24px 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
button { font: inherit; }
|
|
|
|
|
|
.topbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
max-width: 1220px;
|
|
|
|
|
|
min-height: 82px;
|
|
|
|
|
|
padding: 16px 28px;
|
|
|
|
|
|
margin: auto;
|
|
|
|
|
|
border-bottom: 1px solid var(--line);
|
|
|
|
|
|
}
|
|
|
|
|
|
.brand { display: flex; align-items: center; gap: 13px; font-weight: 800; letter-spacing: 0; }
|
|
|
|
|
|
.brand-mark { display: grid; width: 30px; height: 30px; place-items: center; color: white; background: var(--orange); font-family: "DM Mono", monospace; font-size: 13px; }
|
|
|
|
|
|
.meta { color: var(--muted); font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: .04em; }
|
|
|
|
|
|
.shell { width: min(1164px, calc(100% - 48px)); margin: 28px auto 0; }
|
|
|
|
|
|
.chapter-strip { display: flex; gap: 6px; margin-bottom: 18px; overflow-x: auto; scrollbar-width: thin; }
|
|
|
|
|
|
.chapter-tab {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
transition: color .2s, background .2s, border-color .2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chapter-tab:hover, .chapter-tab[aria-selected="true"] { color: var(--ink); border-color: var(--ink); background: #fffdf8; }
|
|
|
|
|
|
.chapter-tab[aria-selected="true"] { font-weight: 700; }
|
|
|
|
|
|
.deck { position: relative; min-height: 602px; }
|
|
|
|
|
|
.page {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
min-height: 602px;
|
|
|
|
|
|
padding: 42px clamp(25px, 5vw, 64px) 36px;
|
|
|
|
|
|
border-top: 7px solid var(--teal);
|
|
|
|
|
|
background: #fffdf8;
|
|
|
|
|
|
box-shadow: var(--shadow);
|
|
|
|
|
|
animation: enter .35s ease both;
|
|
|
|
|
|
}
|
|
|
|
|
|
.page.active { display: block; }
|
|
|
|
|
|
.page.orange { border-color: var(--orange); }
|
|
|
|
|
|
.page.blue { border-color: var(--blue); }
|
|
|
|
|
|
.page.lime { border-color: var(--lime); }
|
|
|
|
|
|
@keyframes enter { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
|
|
|
|
.eyebrow { margin: 0 0 11px; color: var(--teal); font-family: "DM Mono", monospace; font-size: 12px; font-weight: 500; letter-spacing: .1em; text-transform: uppercase; }
|
2026-08-26 15:57:06 +08:00
|
|
|
|
.orange .eyebrow { color: #a00052; }
|
|
|
|
|
|
.blue .eyebrow { color: #4b4b50; }
|
2026-08-26 13:36:13 +08:00
|
|
|
|
h1, h2, h3, p { margin-top: 0; }
|
|
|
|
|
|
h1 { max-width: 920px; margin-bottom: 18px; font-size: clamp(34px, 5vw, 62px); line-height: 1.12; letter-spacing: 0; }
|
|
|
|
|
|
h2 { max-width: 760px; margin-bottom: 26px; font-size: clamp(28px, 4vw, 46px); line-height: 1.18; letter-spacing: 0; }
|
|
|
|
|
|
h3 { margin-bottom: 10px; font-size: 17px; }
|
|
|
|
|
|
.lede { max-width: 760px; color: #385266; font-size: 17px; line-height: 1.8; }
|
|
|
|
|
|
.rule { height: 1px; margin: 27px 0; background: var(--line); }
|
|
|
|
|
|
.key-decision { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(240px, .65fr); gap: 24px; align-items: stretch; }
|
|
|
|
|
|
.decision { display: flex; flex-direction: column; justify-content: space-between; padding: 26px; color: #fffdf8; background: var(--ink); }
|
|
|
|
|
|
.decision-label { color: var(--lime); font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: .08em; }
|
|
|
|
|
|
.decision strong { display: block; margin: 26px 0 12px; font-size: clamp(32px, 4.4vw, 55px); line-height: 1; }
|
|
|
|
|
|
.decision p { max-width: 500px; margin-bottom: 0; color: #d6e2e8; line-height: 1.7; }
|
|
|
|
|
|
.facts { display: grid; gap: 1px; background: var(--line); }
|
2026-08-26 15:57:06 +08:00
|
|
|
|
.fact { padding: 17px 20px; background: #ffffff; }
|
2026-08-26 13:36:13 +08:00
|
|
|
|
.fact span { display: block; color: var(--muted); font-size: 12px; }
|
|
|
|
|
|
.fact b { display: block; margin-top: 5px; font-family: "DM Mono", monospace; font-size: 20px; }
|
|
|
|
|
|
.architecture { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid var(--line); border-left: 1px solid var(--line); }
|
|
|
|
|
|
.route { min-height: 203px; padding: 19px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
|
|
|
|
|
.route-no { color: var(--orange); font-family: "DM Mono", monospace; font-size: 13px; }
|
|
|
|
|
|
.route h3 { margin: 16px 0 8px; }
|
|
|
|
|
|
.route p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.65; }
|
|
|
|
|
|
.route.highlight { color: #fffdf8; background: var(--teal); }
|
|
|
|
|
|
.route.highlight .route-no { color: var(--lime); }
|
|
|
|
|
|
.route.highlight p { color: #d4f0e7; }
|
|
|
|
|
|
.compare { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
|
|
|
|
.compare th, .compare td { padding: 13px 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; line-height: 1.55; }
|
|
|
|
|
|
.compare th { color: var(--muted); font-size: 12px; font-weight: 500; }
|
|
|
|
|
|
.compare th:last-child, .compare td:last-child { color: var(--teal); font-weight: 700; }
|
|
|
|
|
|
.callout { margin-top: 24px; padding: 18px 21px; border-left: 4px solid var(--orange); color: #3d5263; background: #fff0e8; line-height: 1.7; }
|
|
|
|
|
|
.bridge { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
|
|
|
|
|
|
.bridge-way { padding: 21px; border: 1px solid var(--line); }
|
2026-08-26 15:57:06 +08:00
|
|
|
|
.bridge-way.good { border-color: var(--teal); background: #f3f3f4; }
|
|
|
|
|
|
.bridge-way.risk { border-color: #e20074; background: #fff0f7; }
|
2026-08-26 13:36:13 +08:00
|
|
|
|
.bridge-way h3 { display: flex; align-items: center; gap: 8px; }
|
|
|
|
|
|
.tag { display: inline-block; padding: 2px 6px; color: white; background: var(--teal); font-family: "DM Mono", monospace; font-size: 11px; font-weight: 400; }
|
|
|
|
|
|
.risk .tag { background: var(--orange); }
|
|
|
|
|
|
.bridge-way p { margin-bottom: 0; color: #425b6c; font-size: 14px; line-height: 1.7; }
|
|
|
|
|
|
.capabilities { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; }
|
2026-08-26 15:57:06 +08:00
|
|
|
|
.capability { min-height: 84px; padding: 14px; border-top: 3px solid var(--blue); background: #f1f1f3; font-size: 14px; font-weight: 700; }
|
|
|
|
|
|
.capability:nth-child(2n) { border-color: var(--orange); background: #fff0f7; }
|
|
|
|
|
|
.capability:nth-child(3n) { border-color: var(--teal); background: #eeeeef; }
|
2026-08-26 13:36:13 +08:00
|
|
|
|
.numbers { display: grid; grid-template-columns: 1.1fr .9fr .9fr; gap: 1px; background: var(--line); }
|
|
|
|
|
|
.number { min-height: 175px; padding: 24px; background: #fffdf8; }
|
|
|
|
|
|
.number b { display: block; color: var(--teal); font-family: "DM Mono", monospace; font-size: clamp(34px, 5vw, 61px); line-height: 1; }
|
|
|
|
|
|
.number p { margin: 14px 0 0; color: var(--muted); font-size: 14px; line-height: 1.6; }
|
|
|
|
|
|
.split-note { display: grid; grid-template-columns: .9fr 1.1fr; gap: 26px; margin-top: 24px; }
|
|
|
|
|
|
.split-note p { margin: 0; color: #3c5566; line-height: 1.75; }
|
|
|
|
|
|
.case-studies { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-top: 24px; }
|
|
|
|
|
|
.case-study { display: grid; grid-template-columns: 152px 1fr; min-height: 168px; border: 1px solid var(--line); background: #fffdf8; overflow: hidden; }
|
|
|
|
|
|
.case-study img { width: 100%; height: 100%; min-height: 168px; object-fit: cover; }
|
|
|
|
|
|
.case-copy { padding: 18px; }
|
|
|
|
|
|
.case-copy small { color: var(--teal); font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: .06em; }
|
|
|
|
|
|
.case-copy h3 { margin: 9px 0 7px; }
|
|
|
|
|
|
.case-copy p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.65; }
|
2026-08-26 15:57:06 +08:00
|
|
|
|
.page-note { max-width: 820px; margin: 0 0 22px; color: var(--muted); font-size: 15px; line-height: 1.75; }
|
|
|
|
|
|
.signal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
|
|
|
|
|
.signal { min-height: 150px; padding: 20px; border-top: 4px solid var(--ink); background: #f1f1f3; }
|
|
|
|
|
|
.signal.emphasis { color: white; border-color: var(--orange); background: var(--ink); }
|
|
|
|
|
|
.signal small { display: block; margin-bottom: 16px; color: var(--muted); font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: .06em; }
|
|
|
|
|
|
.signal.emphasis small { color: #ffb5d9; }
|
|
|
|
|
|
.signal b { display: block; margin-bottom: 8px; font-size: 18px; }
|
|
|
|
|
|
.signal p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.6; }
|
|
|
|
|
|
.signal.emphasis p { color: #f3d8e6; }
|
|
|
|
|
|
.timeline { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--line); }
|
|
|
|
|
|
.timeline div { min-height: 145px; padding: 18px; background: #fff; }
|
|
|
|
|
|
.timeline b { display: block; margin-bottom: 12px; color: var(--orange); font-family: "DM Mono", monospace; font-size: 14px; }
|
|
|
|
|
|
.timeline p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.55; }
|
|
|
|
|
|
.decision-list { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
|
|
|
|
.decision-item { padding: 18px; border-left: 4px solid var(--orange); background: #fff0f7; line-height: 1.65; }
|
|
|
|
|
|
.decision-item b { display: block; margin-bottom: 5px; }
|
2026-08-26 13:36:13 +08:00
|
|
|
|
.pipeline { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 28px; }
|
|
|
|
|
|
.pipeline-step { position: relative; min-height: 155px; padding: 20px; border: 1px solid var(--line); }
|
|
|
|
|
|
.pipeline-step b { display: block; margin-bottom: 12px; font-family: "DM Mono", monospace; font-size: 12px; }
|
|
|
|
|
|
.pipeline-step p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.6; }
|
|
|
|
|
|
.pipeline-step.ok { border-top: 5px solid var(--teal); }
|
|
|
|
|
|
.pipeline-step.block { border-top: 5px solid var(--orange); background: #fff7f2; }
|
|
|
|
|
|
.cost-bars { display: grid; gap: 13px; margin: 27px 0; }
|
|
|
|
|
|
.cost-row { display: grid; grid-template-columns: 150px 1fr 160px; gap: 14px; align-items: center; font-size: 14px; }
|
|
|
|
|
|
.bar { height: 14px; background: #e1e7e4; }
|
|
|
|
|
|
.bar span { display: block; height: 100%; background: var(--teal); }
|
|
|
|
|
|
.cost-row:nth-child(2) .bar span { background: var(--blue); }
|
|
|
|
|
|
.cost-row:nth-child(3) .bar span, .cost-row:nth-child(4) .bar span { background: var(--orange); }
|
|
|
|
|
|
.cost-impact { color: var(--muted); font-size: 13px; }
|
|
|
|
|
|
.finale { display: grid; grid-template-columns: 1.1fr .9fr; gap: 32px; align-items: end; }
|
|
|
|
|
|
.reasons { padding: 0; margin: 0; list-style: none; counter-reset: reason; }
|
|
|
|
|
|
.reasons li { position: relative; min-height: 55px; padding: 12px 0 12px 49px; border-top: 1px solid var(--line); line-height: 1.6; }
|
|
|
|
|
|
.reasons li::before { position: absolute; left: 0; top: 12px; color: var(--orange); content: "0" counter(reason); counter-increment: reason; font-family: "DM Mono", monospace; font-weight: 500; }
|
|
|
|
|
|
.stamp { padding: 27px; color: #fffdf8; background: var(--ink); }
|
|
|
|
|
|
.stamp small { color: var(--lime); font-family: "DM Mono", monospace; letter-spacing: .08em; }
|
|
|
|
|
|
.stamp b { display: block; margin: 17px 0 9px; font-size: 31px; }
|
|
|
|
|
|
.stamp p { margin: 0; color: #d6e2e8; font-size: 14px; line-height: 1.7; }
|
|
|
|
|
|
.controls { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 20px; padding: 23px 0 30px; }
|
|
|
|
|
|
.pager-button { display: inline-grid; width: 43px; height: 43px; place-items: center; border: 1px solid var(--ink); color: var(--ink); background: transparent; cursor: pointer; font-size: 24px; transition: background .2s, color .2s; }
|
|
|
|
|
|
.pager-button:hover:not(:disabled) { color: #fffdf8; background: var(--ink); }
|
|
|
|
|
|
.pager-button:disabled { opacity: .25; cursor: not-allowed; }
|
|
|
|
|
|
#next { justify-self: end; }
|
|
|
|
|
|
.page-count { color: var(--muted); font-family: "DM Mono", monospace; font-size: 13px; }
|
|
|
|
|
|
.page-dots { display: flex; justify-content: center; gap: 7px; }
|
|
|
|
|
|
.dot { width: 8px; height: 8px; padding: 0; border: 0; background: #aebeba; cursor: pointer; }
|
|
|
|
|
|
.dot.active { width: 26px; background: var(--orange); }
|
|
|
|
|
|
.source { padding: 0 28px 28px; color: #c1d0d6; font-size: 12px; text-align: center; }
|
|
|
|
|
|
.source a { color: #d9ed8f; }
|
|
|
|
|
|
@media (max-width: 760px) {
|
|
|
|
|
|
.topbar { min-height: 64px; padding: 13px 18px; }
|
|
|
|
|
|
.meta { display: none; }
|
|
|
|
|
|
.shell { width: min(100% - 28px, 680px); margin-top: 14px; }
|
|
|
|
|
|
.page { min-height: 0; padding: 30px 20px; }
|
2026-08-26 15:57:06 +08:00
|
|
|
|
.key-decision, .bridge, .split-note, .finale, .case-studies, .signal-grid, .decision-list { grid-template-columns: 1fr; }
|
2026-08-26 13:36:13 +08:00
|
|
|
|
.case-study { grid-template-columns: 128px 1fr; }
|
|
|
|
|
|
.architecture { grid-template-columns: repeat(2, 1fr); }
|
|
|
|
|
|
.capabilities { grid-template-columns: repeat(2, 1fr); }
|
2026-08-26 15:57:06 +08:00
|
|
|
|
.numbers, .timeline { grid-template-columns: 1fr; }
|
2026-08-26 13:36:13 +08:00
|
|
|
|
.pipeline { grid-template-columns: repeat(2, 1fr); }
|
|
|
|
|
|
.cost-row { grid-template-columns: 92px 1fr; }
|
|
|
|
|
|
.cost-impact { grid-column: 2; }
|
|
|
|
|
|
.compare { min-width: 610px; }
|
|
|
|
|
|
.table-scroll { overflow-x: auto; }
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 420px) { .architecture, .pipeline { grid-template-columns: 1fr; } }
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<header class="topbar">
|
|
|
|
|
|
<div class="brand"><span class="brand-mark">CR</span><span>Continental Retail APP</span></div>
|
|
|
|
|
|
<span class="meta">TECHNOLOGY ROUTE REVIEW / 2026.08</span>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<main class="shell">
|
|
|
|
|
|
<div class="chapter-strip" aria-label="章节导航" role="tablist">
|
2026-08-26 15:57:06 +08:00
|
|
|
|
<button class="chapter-tab" role="tab" aria-selected="true" aria-controls="page-1" data-page="0">0.1 摘要</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-2" data-page="1">0.2 结论</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-3" data-page="2">0.3 总览</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-4" data-page="3">1.1 栈盘点</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-5" data-page="4">1.2 四路线</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-6" data-page="5">1.3 天花板</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-7" data-page="6">2.1 uni-app</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-8" data-page="7">2.2 uni-app x</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-9" data-page="8">2.3 Flutter</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-10" data-page="9">3.1 F6 集成</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-11" data-page="10">3.2 原生能力</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-12" data-page="11">3.3 工程边界</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-13" data-page="12">4.1 开放性</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-14" data-page="13">4.2 落地案例</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-15" data-page="14">4.3 AI 生态</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-16" data-page="15">5.1 CI/CD</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-17" data-page="16">5.2 打包边界</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-18" data-page="17">5.3 扩展能力</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-19" data-page="18">6.1 成本</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-20" data-page="19">6.2 公平比较</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-21" data-page="20">6.3 建议</button>
|
2026-08-26 13:36:13 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="deck" aria-live="polite">
|
2026-08-26 15:57:06 +08:00
|
|
|
|
<article class="page active orange" id="page-1" role="tabpanel"><p class="eyebrow">0.1 / Executive Summary</p><h1>面向五年周期的<br>独立 App 技术路线选择。</h1><p class="lede">Continental Retail APP 将整合并关停六套小程序。首版只面向 Android 与 iOS;评估重点不在“画页面快不快”,而在系统能力、第三方网页集成与长期工程主动权。</p><div class="key-decision"><section class="decision"><span class="decision-label">ASSESSMENT DECISION</span><strong>推荐 Flutter</strong><p>首版投入与 uni-app x 基本持平。真正拉开差距的是后续系统级需求、自动化交付和运行时问题的处置空间。</p></section><aside class="facts" aria-label="评估信息"><div class="fact"><span>评估对象</span><b>Flutter / uni-app / uni-app x</b></div><div class="fact"><span>首版平台</span><b>Android + iOS</b></div><div class="fact"><span>外部数据核验</span><b>2026.08.25</b></div></aside></div></article>
|
|
|
|
|
|
<article class="page" id="page-2" role="tabpanel" hidden><p class="eyebrow">0.2 / Decision Logic</p><h2>四条可核验的推荐理由。</h2><div class="signal-grid"><section class="signal emphasis"><small>01 / ARCHITECTURE</small><b>适配独立 App</b><p>项目正在关停小程序,不获得 uni-app 系最核心的多端复用收益。</p></section><section class="signal"><small>02 / NATIVE DENSITY</small><b>原生能力密集</b><p>扫码、OCR、上传、定位、离线、权限和 SDK 集成不会因更换框架而消失。</p></section><section class="signal"><small>03 / OWNERSHIP</small><b>能力上限可控</b><p>Flutter 的边界是自身工程能力;uni-app x 的关键边界仍受 Runtime、插件和打包体系影响。</p></section></div><div class="callout"><b>关键判断:</b>这不是“哪种技术更流行”的偏好选择,而是当前项目是否愿意为不使用的多端复用能力,承担工具链和运行时上的约束。</div></article>
|
|
|
|
|
|
<article class="page blue" id="page-3" role="tabpanel" hidden><p class="eyebrow">0.3 / Comparison At A Glance</p><h2>优势与代价,都应放到同一张桌子上。</h2><div class="table-scroll"><table class="compare"><thead><tr><th>维度</th><th>Flutter</th><th>uni-app x</th></tr></thead><tbody><tr><td>架构路线</td><td>自绘引擎</td><td>编译到系统控件</td></tr><tr><td>F6 网页</td><td>App 侧建立通道,F6 零改动</td><td>F6 需引入 DCloud JS 文件</td></tr><tr><td>原生能力</td><td>Pigeon 强类型生成,支持 CLI</td><td>UTS 插件,工具链绑定 HBuilderX</td></tr><tr><td>工程归属</td><td>完整自有 Android / iOS 工程</td><td>围绕闭源 Runtime 的壳工程</td></tr><tr><td>客观优势</td><td>生态、开放性与界面一致性</td><td>鸿蒙、一套代码复用、包体与内存</td></tr></tbody></table></div><p class="page-note">包体、内存与鸿蒙是 uni-app x 的真实优势,但均不在本项目首版的敏感维度。</p></article>
|
|
|
|
|
|
<article class="page" id="page-4" role="tabpanel" hidden><p class="eyebrow">1.1 / Market Landscape</p><h2>十一种候选技术栈,先完整摆上台面。</h2><div class="table-scroll"><table class="compare"><thead><tr><th>路线</th><th>代表方案</th><th>适用前提</th></tr></thead><tbody><tr><td>原生双端</td><td>Kotlin / Swift</td><td>能力上限优先,接受双端团队</td></tr><tr><td>网页套壳</td><td>uni-app、Taro、Cordova、Capacitor</td><td>小程序 / H5 为主,App 为补充</td></tr><tr><td>编译到系统控件</td><td>uni-app x、React Native、Kuikly、MAUI</td><td>翻译层与插件覆盖需求范围</td></tr><tr><td>自绘引擎</td><td>Flutter</td><td>需要跨端一致且长期演进的独立 App</td></tr><tr><td>逻辑层共享</td><td>Kotlin Multiplatform</td><td>愿意保留两端 UI 开发</td></tr></tbody></table></div><div class="callout"><b>本轮不展开的方案:</b>React Native 的核心前提是已有 React 人力;Taro、Cordova、Capacitor 的重心仍是网页路线;KMP 只共享逻辑;Kuikly 则对外生态仍处早期。</div></article>
|
|
|
|
|
|
<article class="page orange" id="page-5" role="tabpanel" hidden><p class="eyebrow">1.2 / Four Architectural Routes</p><h2>品牌不同,能力上限来自同一套底层规律。</h2><div class="architecture"><section class="route"><span class="route-no">01 / NATIVE</span><h3>原生双端</h3><p>系统给什么就能用什么;代价是两份 UI 和两组工程协作。</p></section><section class="route"><span class="route-no">02 / WEBVIEW</span><h3>网页套壳</h3><p>一套 Web 代码覆盖多端;系统能力必须借插件从网页环境打洞。</p></section><section class="route"><span class="route-no">03 / COMPILE</span><h3>编译到控件</h3><p>由语言和词典翻译到 Kotlin / Swift;词典外的能力需要自行补齐。</p></section><section class="route highlight"><span class="route-no">04 / RENDER</span><h3>自绘引擎</h3><p>Flutter 自己绘制像素,系统能力通过标准原生插件直接连接。</p></section></div><p class="page-note">Kotlin Multiplatform 是例外:它默认只共享逻辑层;配上 Compose Multiplatform 才进入 UI 跨端路线。</p></article>
|
|
|
|
|
|
<article class="page blue" id="page-6" role="tabpanel" hidden><p class="eyebrow">1.3 / Capability Ceiling</p><h2>五年后真正要问的:天花板在谁手里?</h2><div class="signal-grid"><section class="signal"><small>WEBVIEW</small><b>插件生态</b><p>没有人提供或持续维护的能力,就会成为业务路线的阻塞点。</p></section><section class="signal"><small>COMPILE TO NATIVE</small><b>词典 + 插件</b><p>既受平台映射能力影响,也受插件和 IDE 工具链制约。</p></section><section class="signal emphasis"><small>FLUTTER</small><b>自身工程能力</b><p>原生工程、渲染框架和桥接协议都在项目可掌控范围内。</p></section></div><div class="callout"><b>本项目的结论:</b>需求只会增加,能力上限归属不是技术偏好,而是五年周期的风险管理问题。</div></article>
|
|
|
|
|
|
<article class="page" id="page-7" role="tabpanel" hidden><p class="eyebrow">2.1 / uni-app</p><h2>uni-app 的价值,在小程序主导的多端复用。</h2><div class="split-note"><p><b>它是什么。</b><br>使用 Vue / JavaScript 开发,App 端运行在系统 WebView。它的目标是让小程序团队顺便发布 App 与 H5。</p><p><b>它适合什么。</b><br>小程序是主要入口、App 只是辅助触点的商城或业务工具。此时同一套页面复用有明确回报。</p></div><div class="decision-list"><div class="decision-item"><b>项目不匹配点</b>当前项目正在整合并关闭六套小程序,首版也不发布 H5。</div><div class="decision-item"><b>直接后果</b>界面本质仍是网页,重原生能力需要反复通过插件打通。</div></div></article>
|
|
|
|
|
|
<article class="page orange" id="page-8" role="tabpanel" hidden><p class="eyebrow">2.2 / uni-app x</p><h2>uni-app x 不是传统 uni-app 的简单升级。</h2><p class="page-note">它使用 UTS / UVue,编译到 Android Kotlin、iOS Swift、鸿蒙 ArkTS,并使用系统原生控件渲染。设计方向值得肯定,但其能力边界仍由映射层、插件和工具链共同决定。</p><div class="signal-grid"><section class="signal"><small>STRENGTH</small><b>UTS 桥接思路清晰</b><p>直接编译目标端语言,相比字符串式方法调用更具类型表达力。</p></section><section class="signal"><small>CONSTRAINT</small><b>旧插件不能直接迁移</b><p>传统 uni-app 原生插件不兼容,旧市场也已停止接受新增上架。</p></section><section class="signal emphasis"><small>PROJECT FIT</small><b>部分适合</b><p>可完成 App,但无法消除本项目中原生集成与自动化交付的复杂度。</p></section></div></article>
|
|
|
|
|
|
<article class="page blue" id="page-9" role="tabpanel" hidden><p class="eyebrow">2.3 / Flutter</p><h2>Flutter 的出发点,就是构建独立 App。</h2><div class="split-note"><p><b>一致性。</b><br>Dart 提前编译;界面由自带引擎逐像素绘制,不依赖 Android 与 iOS 控件的默认差异。</p><p><b>连接原生。</b><br>每项系统能力可封装为独立插件。Pigeon 从一份定义生成 Dart、Kotlin、Swift 三端强类型接口,接口失配在编译期暴露。</p></div><div class="callout"><b>架构判断:</b>本项目要做的是一个独立 App,而不是一个小程序附属品。仅从架构选型,Flutter 已满足最直接的适配条件。</div></article>
|
|
|
|
|
|
<article class="page orange" id="page-10" role="tabpanel" hidden><p class="eyebrow">3.1 / F6 Web Integration</p><h2>嵌入 F6 网页,是项目最高风险项。</h2><p class="page-note">报价开单、施工查车、结算收银和提醒模块都依赖嵌入的 F6 页面。网页需要调用扫码、拍照、上传、拨号、登录态和门店上下文等 13 项能力、15 个方法,App 也必须反向通知网页。</p><div class="bridge"><section class="bridge-way good"><h3><span class="tag">Flutter</span> 单一双向通道</h3><p>在页面加载完成后由 App 自动注入胶水代码。请求与回调共用协议,例如 <code>{ id, method, params }</code>,F6 无需修改。</p></section><section class="bridge-way risk"><h3><span class="tag">uni-app x</span> 两条单向通道</h3><p>网页须引入 <code>uni.webview.js</code>;网页到 App 用 postMessage,App 到网页用 evalJS,需自行补齐协议与异常处理。</p></section></div></article>
|
|
|
|
|
|
<article class="page" id="page-11" role="tabpanel" hidden><p class="eyebrow">3.2 / Native Capability Density</p><h2>真正的工作重心,不是页面,而是系统集成。</h2><div class="capabilities"><div class="capability">扫码 / VIN OCR</div><div class="capability">相机 / 相册多选</div><div class="capability">文件上传进度</div><div class="capability">弱网离线与重传</div><div class="capability">图片压缩</div><div class="capability">定位与权限</div><div class="capability">Bugly 崩溃桥</div><div class="capability">神策 SDK</div><div class="capability">返回键拦截</div><div class="capability">WebView 文件选择</div></div><div class="callout"><b>成本含义:</b>上述能力无论选择哪条路线都需要实现、维护、双端验证。框架对 UI 的效率影响,只覆盖整体工作中较小的一部分。</div></article>
|
|
|
|
|
|
<article class="page blue" id="page-12" role="tabpanel" hidden><p class="eyebrow">3.3 / Module Boundaries</p><h2>十九个模块并行开发,边界必须能被强制执行。</h2><div class="timeline"><div><b>19 模块</b><p>近三百条需求,长期多人并行开发。</p></div><div><b>21 包</b><p>Flutter 以 Melos 组织基础设施、业务模块与原生能力层。</p></div><div><b>编译期</b><p>包依赖机制可阻止业务模块直接引用彼此内部实现。</p></div><div><b>第二年</b><p>边界约束的价值,会在重构、替换和新增能力时放大。</p></div></div><p class="page-note">uni-app x 也可以组织工程化目录,但模块边界更多依赖团队自觉和评审纪律,缺少同等强度的物理阻断。</p></article>
|
|
|
|
|
|
<article class="page" id="page-13" role="tabpanel" hidden><p class="eyebrow">4.1 / Openness & Continuity</p><h2>关键不是“是否开源”,而是开到哪一层。</h2><div class="numbers"><section class="number"><b>91,171</b><p>Flutter 主仓库公开提交数,包含渲染引擎到框架层。</p></section><section class="number"><b>31,002</b><p>Flutter Fork 数,体现公开协作与问题排查广度。</p></section><section class="number"><b>~8x</b><p>相对 uni-app 主仓库提交数与 Fork 数的量级差异。</p></section></div><div class="split-note"><p><b>Flutter。</b><br>引擎和框架均以 BSD 开源,原生工程由项目自身掌握。</p><p><b>uni-app x。</b><br>组件与 API 开源;Runtime、SDK 与 HBuilderX 为 DCloud 自有闭源能力。</p></div></article>
|
|
|
|
|
|
<article class="page orange" id="page-14" role="tabpanel" hidden><p class="eyebrow">4.2 / Production Evidence</p><h2>大型 App 的真实形态,是混合底座加跨端业务主干。</h2><div class="case-studies"><article class="case-study"><img src="assets/bmw1.png" alt="My BMW App 的车辆状态和车控界面"><div class="case-copy"><small>BMW / 甲档案例</small><h3>96 个变体,自动化交付</h3><p>My BMW App 覆盖 47 国。Mobile 2.0 Platform 每次构建可自动生成、测试并部署 96 个变体。</p></div></article><article class="case-study"><img src="assets/vw4.png" alt="ID. UNYX App 的车辆状态和远程车控界面"><div class="case-copy"><small>ID. UNYX / 乙档案例</small><h3>主干页面承载车控场景</h3><p>大众安徽法定 SDK 披露将 Flutter 列为 App 全部 L1、L2、L3 页面开发语言。</p></div></article></div><div class="callout"><b>同一形态的公开案例:</b>字节 90+ 款应用、闲鱼主链路、美团商家端、京东、携程、BMW 与 ID. UNYX 都证明:原生能力仍在原生侧,业务页面可以稳定由 Flutter 承担。</div></article>
|
|
|
|
|
|
<article class="page blue" id="page-15" role="tabpanel" hidden><p class="eyebrow">4.3 / AI Toolchain</p><h2>AI 辅助效率,来自开放生态与公开代码沉淀。</h2><div class="table-scroll"><table class="compare"><thead><tr><th>维度</th><th>Flutter / Dart</th><th>uni-app x</th></tr></thead><tbody><tr><td>官方 Skills</td><td>独立开源仓库,可 Fork / PR</td><td>主要内置于 uni-agent</td></tr><tr><td>MCP</td><td>Dart & Flutter MCP Server</td><td>uni-app-x-mcp</td></tr><tr><td>工具适配</td><td>可接入多种 AI 编程工具</td><td>以自家生态和少数工具为主</td></tr><tr><td>公开语料</td><td>近十年全球 Dart / Flutter 项目</td><td>UTS / UVue 较新,公开沉淀有限</td></tr></tbody></table></div><div class="callout"><b>实际影响:</b>当开发者需要生成、调试、重构或验证原生桥接与业务组件时,公开生态的深度会直接反映在生成质量和返工率上。</div></article>
|
|
|
|
|
|
<article class="page" id="page-16" role="tabpanel" hidden><p class="eyebrow">5.1 / Delivery Pipeline</p><h2>目标是每个版本都能从一次提交中重建。</h2><p class="lede">既定工程链路:GitLab CI/CD,lint / 格式化 / 测试卡点,dev / uat / prod 三环境,远程 Mac 出 iOS 包,Android OTA 与 iOS TestFlight 分发。</p><div class="pipeline"><section class="pipeline-step ok"><b>01 / COMMIT</b><p>提交触发静态检查、格式化和测试。</p></section><section class="pipeline-step ok"><b>02 / BUILD</b><p>Android / iOS 在自有节点完成可追溯构建。</p></section><section class="pipeline-step block"><b>03 / UTS PLUGIN</b><p>UTS 插件创建和使用绑定 HBuilderX,官方不支持 CLI。</p></section><section class="pipeline-step block"><b>04 / PACKAGE</b><p>云打包、离线打包和加密插件之间存在结构性取舍。</p></section></div></article>
|
|
|
|
|
|
<article class="page orange" id="page-17" role="tabpanel" hidden><p class="eyebrow">5.2 / Packaging Boundaries</p><h2>“离线打包”给的是壳,不是完全可控的原生工程。</h2><div class="table-scroll"><table class="compare"><thead><tr><th>范围</th><th>可以修改</th><th>不能修改</th></tr></thead><tbody><tr><td>工程外层</td><td>包名、图标、权限、ABI、原生模块</td><td>运行时页面容器和生命周期</td></tr><tr><td>Android</td><td>Manifest 与 Gradle 配置</td><td>闭源 AAR 内部实现与 UTS 通信机制</td></tr><tr><td>应用入口</td><td>可在约束下定制</td><td>Application 必须继承 DCloudApplication</td></tr><tr><td>语言选择</td><td>Flutter 标准 Kotlin / Swift</td><td>传统离线 SDK 官方声明不支持 Kotlin</td></tr></tbody></table></div><div class="callout"><b>核心区别:</b>Flutter 以模块挂入自己的标准 Android / iOS 工程;uni-app x 离线 / 嵌入路径则是把自定义内容放入围绕闭源运行时构成的宿主壳中。</div></article>
|
|
|
|
|
|
<article class="page blue" id="page-18" role="tabpanel" hidden><p class="eyebrow">5.3 / Extensions</p><h2>桌面小组件、Siri 和分享扩展,最终都会回到原生。</h2><div class="signal-grid"><section class="signal"><small>WIDGET</small><b>小组件</b><p>iOS 需要 Xcode、SwiftUI 与 WidgetKit 的 .appex;Android 同样需要 Kotlin、Manifest 和 XML。</p></section><section class="signal"><small>SIRI / INTENTS</small><b>系统语音能力</b><p>官方建议离线打包后在原生工程实现 AppShortcutsProvider,再通过 UTS 衔接业务。</p></section><section class="signal emphasis"><small>CONFLICT</small><b>加密插件与离线打包</b><p>加密 UTS 插件仅支持云端传统打包,和部分扩展所需的离线路径互斥。</p></section></div><p class="page-note">做得到不等于低成本:两端仍需写 Swift / Kotlin,并额外承受一层厂商打包与基座规则。</p></article>
|
|
|
|
|
|
<article class="page" id="page-19" role="tabpanel" hidden><p class="eyebrow">6.1 / First Release Cost</p><h2>首版成本基本持平,差异在 ±10% 内。</h2><div class="cost-bars"><div class="cost-row"><b>业务集成 45–55%</b><div class="bar"><span style="width: 88%"></span></div><span class="cost-impact">框架基本无差异</span></div><div class="cost-row"><b>UI 实现 20–25%</b><div class="bar"><span style="width: 40%"></span></div><span class="cost-impact">uni-app x 略省</span></div><div class="cost-row"><b>原生桥接 10–15%</b><div class="bar"><span style="width: 27%"></span></div><span class="cost-impact">uni-app x 更贵</span></div><div class="cost-row"><b>工程与可观测 10–15%</b><div class="bar"><span style="width: 27%"></span></div><span class="cost-impact">uni-app x 更贵</span></div></div><div class="callout"><b>成本不是决策理由。</b> UI 熟悉度带来的节省被原生桥接、插件工具链与 CI / 发布成本抵消,首版不应以工时差异作为主导依据。</div></article>
|
|
|
|
|
|
<article class="page orange" id="page-20" role="tabpanel" hidden><p class="eyebrow">6.2 / Fair Comparison</p><h2>uni-app x 的三项真实优势,应当被正面计入。</h2><div class="decision-list"><div class="decision-item"><b>鸿蒙支持更完整</b>UTS 可直接编译 ArkTS;Flutter 主线不支持鸿蒙,社区分支正在跟进。</div><div class="decision-item"><b>小程序与 H5 复用</b>若未来需要 App、小程序、H5 同时发布,价值会立即显现。</div><div class="decision-item"><b>UTS 的语言方向正确</b>直接编译 Kotlin / Swift 的桥接方式比传统字符串式调用更干净。</div><div class="decision-item"><b>包体、内存与热更新</b>uni-app x 在这些方面有实际优势,但本项目没有包体 KPI,发版也采用正规分发链路。</div></div><p class="page-note">这些优势不应被回避;它们只是没有改变本项目在 F6 集成、原生能力密度和 CI 可控性上的关键判断。</p></article>
|
|
|
|
|
|
<article class="page blue" id="page-21" role="tabpanel" hidden><p class="eyebrow">6.3 / Final Recommendation</p><h2>推荐 Flutter,把能力上限留在自己的工程里。</h2><div class="finale"><ol class="reasons"><li><b>架构适配:</b>项目是独立 App,无法获得多端小程序复用收益。</li><li><b>业务风险:</b>F6 集成和十余项原生能力需要直接、可控的通道。</li><li><b>长期治理:</b>开放生态与完整原生工程为问题处置保留主动权。</li><li><b>交付能力:</b>Flutter 匹配既有 GitLab CI/CD 与标准原生扩展方式。</li></ol><aside class="stamp" aria-label="最终技术路线建议"><small>FINAL DECISION</small><b>推荐 Flutter</b><p>若最终仍选择 uni-app x,应在开工前落实 F6 改造承诺、神策 SDK、Bugly UTS 封装、CLI/CI 路径以及云端与离线打包的取舍。</p></aside></div></article>
|
2026-08-26 13:36:13 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<footer class="controls">
|
|
|
|
|
|
<button class="pager-button" id="previous" type="button" aria-label="上一章" title="上一章">←</button>
|
|
|
|
|
|
<div><div class="page-dots" aria-label="分页"></div><div class="page-count">01 / 06</div></div>
|
|
|
|
|
|
<button class="pager-button" id="next" type="button" aria-label="下一章" title="下一章">→</button>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
const pages = Array.from(document.querySelectorAll('.page'));
|
|
|
|
|
|
const tabs = Array.from(document.querySelectorAll('.chapter-tab'));
|
|
|
|
|
|
const dotsHost = document.querySelector('.page-dots');
|
|
|
|
|
|
const count = document.querySelector('.page-count');
|
|
|
|
|
|
const previous = document.querySelector('#previous');
|
|
|
|
|
|
const next = document.querySelector('#next');
|
|
|
|
|
|
let currentPage = 0;
|
|
|
|
|
|
let lastPagerAction = 0;
|
|
|
|
|
|
|
|
|
|
|
|
pages.forEach((page, index) => {
|
|
|
|
|
|
const dot = document.createElement('button');
|
|
|
|
|
|
dot.className = 'dot';
|
|
|
|
|
|
dot.type = 'button';
|
|
|
|
|
|
dot.setAttribute('aria-label', `跳转至第 ${index + 1} 章`);
|
|
|
|
|
|
dot.addEventListener('click', () => showPage(index));
|
|
|
|
|
|
dotsHost.append(dot);
|
|
|
|
|
|
});
|
|
|
|
|
|
const dots = Array.from(document.querySelectorAll('.dot'));
|
|
|
|
|
|
|
|
|
|
|
|
function showPage(index) {
|
|
|
|
|
|
currentPage = Math.max(0, Math.min(index, pages.length - 1));
|
|
|
|
|
|
pages.forEach((page, itemIndex) => {
|
|
|
|
|
|
const active = itemIndex === currentPage;
|
|
|
|
|
|
page.classList.toggle('active', active);
|
|
|
|
|
|
page.hidden = !active;
|
|
|
|
|
|
tabs[itemIndex].setAttribute('aria-selected', String(active));
|
|
|
|
|
|
dots[itemIndex].classList.toggle('active', active);
|
|
|
|
|
|
});
|
|
|
|
|
|
count.textContent = `${String(currentPage + 1).padStart(2, '0')} / ${String(pages.length).padStart(2, '0')}`;
|
|
|
|
|
|
previous.disabled = currentPage === 0;
|
|
|
|
|
|
next.disabled = currentPage === pages.length - 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tabs.forEach((tab, index) => tab.addEventListener('click', () => showPage(index)));
|
|
|
|
|
|
function movePage(direction) {
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
if (now - lastPagerAction < 250) return;
|
|
|
|
|
|
lastPagerAction = now;
|
|
|
|
|
|
showPage(currentPage + direction);
|
|
|
|
|
|
}
|
|
|
|
|
|
previous.addEventListener('click', () => movePage(-1));
|
|
|
|
|
|
next.addEventListener('click', () => movePage(1));
|
|
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
|
|
|
|
if (event.key === 'ArrowLeft') showPage(currentPage - 1);
|
|
|
|
|
|
if (event.key === 'ArrowRight') showPage(currentPage + 1);
|
|
|
|
|
|
});
|
|
|
|
|
|
showPage(0);
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|