1243 lines
36 KiB
HTML
1243 lines
36 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>X-Financial Mobile 架构与设计方案</title>
|
||
<style>
|
||
:root {
|
||
--green-900: #047857;
|
||
--green-700: #059669;
|
||
--green-600: #10b981;
|
||
--green-100: #dff8ec;
|
||
--green-50: #effcf6;
|
||
--ink-900: #071124;
|
||
--ink-700: #24324a;
|
||
--ink-600: #58677f;
|
||
--ink-500: #728098;
|
||
--line: #dbe5ef;
|
||
--line-strong: #c6d4e2;
|
||
--surface: #ffffff;
|
||
--surface-soft: #f7fafc;
|
||
--warning: #f59e0b;
|
||
--danger: #ef4444;
|
||
--blue: #2563eb;
|
||
--shadow: 0 10px 26px rgba(15, 23, 42, 0.08);
|
||
--radius: 8px;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html {
|
||
scroll-behavior: smooth;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
background:
|
||
linear-gradient(180deg, rgba(239, 252, 246, 0.76), rgba(247, 250, 252, 0.92) 360px),
|
||
var(--surface-soft);
|
||
color: var(--ink-900);
|
||
font-family: "IBM Plex Sans", "Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", sans-serif;
|
||
line-height: 1.62;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
a {
|
||
color: inherit;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.shell {
|
||
display: grid;
|
||
grid-template-columns: 272px minmax(0, 1fr);
|
||
min-height: 100dvh;
|
||
}
|
||
|
||
.sidebar {
|
||
position: sticky;
|
||
top: 0;
|
||
align-self: start;
|
||
height: 100dvh;
|
||
padding: 28px 22px;
|
||
border-right: 1px solid var(--line);
|
||
background: rgba(255, 255, 255, 0.82);
|
||
backdrop-filter: blur(18px);
|
||
}
|
||
|
||
.brand {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.logo-mark {
|
||
display: grid;
|
||
width: 42px;
|
||
height: 42px;
|
||
place-items: center;
|
||
border-radius: 8px;
|
||
background: linear-gradient(145deg, var(--green-700), var(--green-600));
|
||
color: #fff;
|
||
font-weight: 800;
|
||
box-shadow: 0 10px 18px rgba(16, 185, 129, 0.26);
|
||
}
|
||
|
||
.brand-title {
|
||
font-size: 15px;
|
||
font-weight: 800;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.brand-subtitle {
|
||
margin-top: 4px;
|
||
color: var(--ink-500);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.nav-label {
|
||
margin: 22px 0 8px;
|
||
color: var(--ink-500);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.nav {
|
||
display: grid;
|
||
gap: 6px;
|
||
}
|
||
|
||
.nav a {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 38px;
|
||
padding: 8px 10px;
|
||
border-radius: 8px;
|
||
color: var(--ink-700);
|
||
font-size: 13px;
|
||
font-weight: 650;
|
||
}
|
||
|
||
.nav a:hover {
|
||
background: var(--green-50);
|
||
color: var(--green-900);
|
||
}
|
||
|
||
.nav-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
margin-right: 10px;
|
||
border-radius: 999px;
|
||
background: var(--line-strong);
|
||
}
|
||
|
||
.nav a:hover .nav-dot {
|
||
background: var(--green-600);
|
||
}
|
||
|
||
main {
|
||
min-width: 0;
|
||
padding: 34px 42px 56px;
|
||
}
|
||
|
||
.hero {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.15fr) minmax(300px, 0.85fr);
|
||
gap: 24px;
|
||
align-items: stretch;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.hero-panel {
|
||
padding: 30px;
|
||
border: 1px solid rgba(16, 185, 129, 0.28);
|
||
border-radius: var(--radius);
|
||
background:
|
||
linear-gradient(135deg, rgba(255,255,255,0.94), rgba(239,252,246,0.88)),
|
||
var(--surface);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.kicker {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-height: 28px;
|
||
padding: 4px 10px;
|
||
border: 1px solid rgba(16, 185, 129, 0.22);
|
||
border-radius: 999px;
|
||
background: rgba(16, 185, 129, 0.1);
|
||
color: var(--green-900);
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
h1 {
|
||
max-width: 760px;
|
||
margin: 16px 0 14px;
|
||
font-size: 34px;
|
||
line-height: 1.18;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.hero-copy {
|
||
max-width: 780px;
|
||
margin: 0;
|
||
color: var(--ink-600);
|
||
font-size: 15px;
|
||
}
|
||
|
||
.hero-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-top: 22px;
|
||
}
|
||
|
||
.pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
min-height: 32px;
|
||
padding: 6px 11px;
|
||
border-radius: 999px;
|
||
background: #fff;
|
||
border: 1px solid var(--line);
|
||
color: var(--ink-700);
|
||
font-size: 12px;
|
||
font-weight: 750;
|
||
}
|
||
|
||
.pill.primary {
|
||
background: var(--green-700);
|
||
border-color: var(--green-700);
|
||
color: #fff;
|
||
}
|
||
|
||
.hero-metrics {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.metric {
|
||
padding: 18px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: rgba(255, 255, 255, 0.88);
|
||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.05);
|
||
}
|
||
|
||
.metric strong {
|
||
display: block;
|
||
font-size: 24px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.metric span {
|
||
display: block;
|
||
margin-top: 8px;
|
||
color: var(--ink-500);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
section {
|
||
margin-top: 24px;
|
||
padding: 26px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: rgba(255, 255, 255, 0.92);
|
||
box-shadow: var(--shadow);
|
||
scroll-margin-top: 24px;
|
||
}
|
||
|
||
.section-head {
|
||
display: flex;
|
||
gap: 14px;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.section-title {
|
||
margin: 0;
|
||
font-size: 22px;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.section-desc {
|
||
max-width: 780px;
|
||
margin: 8px 0 0;
|
||
color: var(--ink-600);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.tag {
|
||
flex: none;
|
||
padding: 5px 10px;
|
||
border-radius: 999px;
|
||
background: var(--green-50);
|
||
color: var(--green-900);
|
||
border: 1px solid rgba(16, 185, 129, 0.18);
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.grid-2,
|
||
.grid-3,
|
||
.grid-4 {
|
||
display: grid;
|
||
gap: 14px;
|
||
}
|
||
|
||
.grid-2 {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.grid-3 {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
.grid-4 {
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
}
|
||
|
||
.card {
|
||
padding: 18px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: #fff;
|
||
}
|
||
|
||
.card.emphasis {
|
||
border-color: rgba(16, 185, 129, 0.3);
|
||
background: linear-gradient(135deg, rgba(239, 252, 246, 0.9), #fff);
|
||
}
|
||
|
||
.card h3 {
|
||
margin: 0 0 8px;
|
||
font-size: 16px;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.card p {
|
||
margin: 0;
|
||
color: var(--ink-600);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.card ul {
|
||
margin: 10px 0 0;
|
||
padding-left: 18px;
|
||
color: var(--ink-600);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.card li + li {
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.flow {
|
||
display: grid;
|
||
gap: 10px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.flow-row {
|
||
display: grid;
|
||
grid-template-columns: 190px minmax(0, 1fr);
|
||
gap: 12px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.flow-label {
|
||
padding: 14px 16px;
|
||
border-radius: var(--radius);
|
||
background: var(--ink-900);
|
||
color: #fff;
|
||
font-weight: 800;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.flow-content {
|
||
padding: 14px 16px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: var(--surface-soft);
|
||
color: var(--ink-700);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.architecture {
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
|
||
.layer {
|
||
display: grid;
|
||
grid-template-columns: 172px minmax(0, 1fr);
|
||
gap: 12px;
|
||
align-items: center;
|
||
}
|
||
|
||
.layer-name {
|
||
padding: 15px 16px;
|
||
border-radius: var(--radius);
|
||
background: var(--green-700);
|
||
color: #fff;
|
||
font-weight: 850;
|
||
}
|
||
|
||
.layer-body {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.layer-item {
|
||
min-height: 66px;
|
||
padding: 12px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: #fff;
|
||
font-size: 12px;
|
||
color: var(--ink-700);
|
||
}
|
||
|
||
.layer-item strong {
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
color: var(--ink-900);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.route-map {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.route {
|
||
min-height: 148px;
|
||
padding: 14px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: #fff;
|
||
}
|
||
|
||
.route-icon {
|
||
display: grid;
|
||
width: 34px;
|
||
height: 34px;
|
||
margin-bottom: 10px;
|
||
place-items: center;
|
||
border-radius: 8px;
|
||
background: var(--green-50);
|
||
color: var(--green-900);
|
||
font-weight: 900;
|
||
}
|
||
|
||
.route strong {
|
||
display: block;
|
||
font-size: 14px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.route span {
|
||
color: var(--ink-600);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.shot-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.shot {
|
||
overflow: hidden;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: #fff;
|
||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.05);
|
||
}
|
||
|
||
.shot img {
|
||
display: block;
|
||
width: 100%;
|
||
aspect-ratio: 9 / 16;
|
||
object-fit: cover;
|
||
object-position: top;
|
||
background: var(--surface-soft);
|
||
}
|
||
|
||
.shot figcaption {
|
||
padding: 10px;
|
||
color: var(--ink-700);
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.tokens {
|
||
display: grid;
|
||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.token {
|
||
min-height: 86px;
|
||
padding: 12px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: #fff;
|
||
}
|
||
|
||
.swatch {
|
||
width: 100%;
|
||
height: 28px;
|
||
margin-bottom: 9px;
|
||
border-radius: 6px;
|
||
border: 1px solid rgba(15, 23, 42, 0.1);
|
||
}
|
||
|
||
.token strong,
|
||
.token span {
|
||
display: block;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.token span {
|
||
margin-top: 3px;
|
||
color: var(--ink-500);
|
||
font-family: "Cascadia Code", Consolas, monospace;
|
||
}
|
||
|
||
.api-list {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.api {
|
||
display: grid;
|
||
grid-template-columns: 92px minmax(0, 1fr) 170px;
|
||
gap: 12px;
|
||
align-items: center;
|
||
padding: 12px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: #fff;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.method {
|
||
display: inline-flex;
|
||
justify-content: center;
|
||
min-width: 72px;
|
||
padding: 4px 8px;
|
||
border-radius: 999px;
|
||
background: var(--ink-900);
|
||
color: #fff;
|
||
font-weight: 850;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.path {
|
||
color: var(--ink-900);
|
||
font-family: "Cascadia Code", Consolas, monospace;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.api-note {
|
||
color: var(--ink-500);
|
||
font-size: 12px;
|
||
text-align: right;
|
||
}
|
||
|
||
.timeline {
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
|
||
.phase {
|
||
display: grid;
|
||
grid-template-columns: 126px minmax(0, 1fr);
|
||
gap: 12px;
|
||
padding: 14px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: #fff;
|
||
}
|
||
|
||
.phase-key {
|
||
color: var(--green-900);
|
||
font-weight: 900;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.phase-body strong {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.phase-body span {
|
||
color: var(--ink-600);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.checklist {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 10px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.check {
|
||
padding: 12px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: var(--surface-soft);
|
||
color: var(--ink-700);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.check::before {
|
||
content: "✓";
|
||
display: inline-grid;
|
||
width: 18px;
|
||
height: 18px;
|
||
margin-right: 8px;
|
||
place-items: center;
|
||
border-radius: 999px;
|
||
background: var(--green-700);
|
||
color: #fff;
|
||
font-size: 11px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
code {
|
||
padding: 2px 5px;
|
||
border-radius: 5px;
|
||
background: #eef4f8;
|
||
color: #0f5132;
|
||
font-family: "Cascadia Code", Consolas, monospace;
|
||
font-size: 0.92em;
|
||
}
|
||
|
||
pre {
|
||
overflow: auto;
|
||
margin: 0;
|
||
padding: 16px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: #0b1220;
|
||
color: #d9fbe8;
|
||
font-family: "Cascadia Code", Consolas, monospace;
|
||
font-size: 12px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.note {
|
||
padding: 14px 16px;
|
||
border: 1px solid rgba(245, 158, 11, 0.28);
|
||
border-radius: var(--radius);
|
||
background: #fff8eb;
|
||
color: #7c4a03;
|
||
font-size: 13px;
|
||
}
|
||
|
||
footer {
|
||
margin-top: 28px;
|
||
color: var(--ink-500);
|
||
font-size: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
@media (max-width: 1180px) {
|
||
.shell {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.sidebar {
|
||
position: static;
|
||
height: auto;
|
||
border-right: none;
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
.nav {
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
}
|
||
|
||
main {
|
||
padding: 26px 22px 44px;
|
||
}
|
||
|
||
.hero,
|
||
.grid-2,
|
||
.grid-3,
|
||
.grid-4,
|
||
.checklist {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.layer,
|
||
.flow-row,
|
||
.phase {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.layer-body {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.route-map {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.shot-grid,
|
||
.tokens {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
.api {
|
||
grid-template-columns: 86px minmax(0, 1fr);
|
||
}
|
||
|
||
.api-note {
|
||
grid-column: 1 / -1;
|
||
text-align: left;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.nav,
|
||
.route-map,
|
||
.layer-body,
|
||
.shot-grid,
|
||
.tokens,
|
||
.hero-metrics {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 28px;
|
||
}
|
||
|
||
section,
|
||
.hero-panel {
|
||
padding: 20px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="shell">
|
||
<aside class="sidebar">
|
||
<div class="brand">
|
||
<div class="logo-mark">XF</div>
|
||
<div>
|
||
<div class="brand-title">X-Financial Mobile</div>
|
||
<div class="brand-subtitle">架构设计 / 页面设计 / 能力蓝图</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="nav-label">文档目录</div>
|
||
<nav class="nav" aria-label="架构设计目录">
|
||
<a href="#overview"><span class="nav-dot"></span>方案总览</a>
|
||
<a href="#architecture"><span class="nav-dot"></span>端侧架构</a>
|
||
<a href="#capabilities"><span class="nav-dot"></span>相机与语音</a>
|
||
<a href="#pages"><span class="nav-dot"></span>页面设计</a>
|
||
<a href="#design-system"><span class="nav-dot"></span>视觉规范</a>
|
||
<a href="#api"><span class="nav-dot"></span>接口同步</a>
|
||
<a href="#delivery"><span class="nav-dot"></span>交付计划</a>
|
||
<a href="#quality"><span class="nav-dot"></span>验收标准</a>
|
||
</nav>
|
||
</aside>
|
||
|
||
<main>
|
||
<header class="hero" id="overview">
|
||
<div class="hero-panel">
|
||
<span class="kicker">移动端优先 · Android 首发 · iOS 后续平滑扩展</span>
|
||
<h1>基于现有接口构建原生移动报销应用,保留 AI 助手、相机拍票和语音输入能力。</h1>
|
||
<p class="hero-copy">
|
||
移动端作为 X-Financial 的新客户端,不重新发明业务系统。后端仍是唯一业务真相,
|
||
App 侧专注手机场景:快速拍票、语音询问、查看进度、补材料、处理审批。
|
||
页面风格对齐 <code>mobile/UI</code> 设计稿,使用浅绿色企业金融风格、轻量卡片、
|
||
底部导航和固定操作条。
|
||
</p>
|
||
<div class="hero-actions">
|
||
<span class="pill primary">推荐栈:React Native + Expo + TypeScript</span>
|
||
<span class="pill">接口契约:OpenAPI 生成类型</span>
|
||
<span class="pill">数据缓存:TanStack Query</span>
|
||
<span class="pill">导航:React Navigation</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hero-panel">
|
||
<div class="hero-metrics">
|
||
<div class="metric">
|
||
<strong>5</strong>
|
||
<span>底部主导航</span>
|
||
</div>
|
||
<div class="metric">
|
||
<strong>2</strong>
|
||
<span>平台能力:相机 / 语音</span>
|
||
</div>
|
||
<div class="metric">
|
||
<strong>1</strong>
|
||
<span>后端业务真相源</span>
|
||
</div>
|
||
<div class="metric">
|
||
<strong>0</strong>
|
||
<span>预览阶段默认创建草稿</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<section id="architecture">
|
||
<div class="section-head">
|
||
<div>
|
||
<h2 class="section-title">端侧架构</h2>
|
||
<p class="section-desc">
|
||
App 按“页面功能、共享业务、平台能力”拆分。相机、语音、上传等能力不直接散落在页面里,
|
||
页面只消费服务接口,后续替换原生模块或增加 iOS 适配时不会影响业务页面。
|
||
</p>
|
||
</div>
|
||
<span class="tag">分层清晰</span>
|
||
</div>
|
||
|
||
<div class="architecture">
|
||
<div class="layer">
|
||
<div class="layer-name">App Shell</div>
|
||
<div class="layer-body">
|
||
<div class="layer-item"><strong>启动</strong>登录态恢复、版本检查、权限提示</div>
|
||
<div class="layer-item"><strong>导航</strong>底部 Tab、页面 Stack、深链</div>
|
||
<div class="layer-item"><strong>主题</strong>浅色企业绿、状态色、字号密度</div>
|
||
<div class="layer-item"><strong>错误边界</strong>接口失败、弱网、权限拒绝</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="layer">
|
||
<div class="layer-name">Features</div>
|
||
<div class="layer-body">
|
||
<div class="layer-item"><strong>home</strong>首页、待办、最近进度、快捷入口</div>
|
||
<div class="layer-item"><strong>claims</strong>我的报销、新建、详情、补材料</div>
|
||
<div class="layer-item"><strong>approvals</strong>审批列表、同意、驳回、转交</div>
|
||
<div class="layer-item"><strong>assistant</strong>AI 对话、语音输入、票据建议</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="layer">
|
||
<div class="layer-name">Shared</div>
|
||
<div class="layer-body">
|
||
<div class="layer-item"><strong>api</strong>OpenAPI client、请求拦截、错误映射</div>
|
||
<div class="layer-item"><strong>domain</strong>状态机、权限、审批节点、金额格式化</div>
|
||
<div class="layer-item"><strong>design</strong>token、组件、图标、空状态</div>
|
||
<div class="layer-item"><strong>auth</strong>token、用户上下文、审计请求头</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="layer">
|
||
<div class="layer-name">Platform</div>
|
||
<div class="layer-body">
|
||
<div class="layer-item"><strong>camera</strong>拍照、相册、图片压缩、票据预览</div>
|
||
<div class="layer-item"><strong>voice</strong>录音、转写、麦克风权限、回填输入框</div>
|
||
<div class="layer-item"><strong>upload</strong>multipart、重试、进度、临时附件</div>
|
||
<div class="layer-item"><strong>permission</strong>Android/iOS 权限声明与降级</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top:14px">
|
||
<pre>mobile
|
||
├── app
|
||
│ ├── navigation
|
||
│ └── bootstrap
|
||
├── features
|
||
│ ├── home
|
||
│ ├── claims
|
||
│ ├── approvals
|
||
│ ├── assistant
|
||
│ └── profile
|
||
├── shared
|
||
│ ├── api
|
||
│ ├── auth
|
||
│ ├── domain
|
||
│ ├── design
|
||
│ └── components
|
||
└── platform
|
||
├── camera
|
||
├── voice
|
||
├── upload
|
||
└── permissions</pre>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="capabilities">
|
||
<div class="section-head">
|
||
<div>
|
||
<h2 class="section-title">相机与语音能力</h2>
|
||
<p class="section-desc">
|
||
相机和语音是移动端的一等能力。相机服务票据生产流,语音服务助手输入流。
|
||
两者都先产生“用户可确认的中间结果”,不直接触发不可逆业务动作。
|
||
</p>
|
||
</div>
|
||
<span class="tag">平台能力</span>
|
||
</div>
|
||
|
||
<div class="grid-2">
|
||
<div class="card emphasis">
|
||
<h3>相机拍票流程</h3>
|
||
<p>拍照或相册选择后,先进入临时附件和 OCR 识别流程,用户确认后再绑定真实报销单。</p>
|
||
<div class="flow">
|
||
<div class="flow-row">
|
||
<div class="flow-label">采集</div>
|
||
<div class="flow-content">拍照 / 相册 / 文件选择,校验 jpg、png、pdf 与大小限制。</div>
|
||
</div>
|
||
<div class="flow-row">
|
||
<div class="flow-label">预处理</div>
|
||
<div class="flow-content">压缩图片、保留清晰度、生成预览图、记录本地上传任务。</div>
|
||
</div>
|
||
<div class="flow-row">
|
||
<div class="flow-label">识别</div>
|
||
<div class="flow-content">上传到临时附件区,触发 OCR 与票据分类,返回结构化识别结果。</div>
|
||
</div>
|
||
<div class="flow-row">
|
||
<div class="flow-label">确认</div>
|
||
<div class="flow-content">用户选择保存草稿、生成报销单、继续补充,才进入持久化。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card emphasis">
|
||
<h3>语音输入流程</h3>
|
||
<p>语音只作为输入方式,转写结果回填输入框,由用户确认后再发送给 AI 助手。</p>
|
||
<div class="flow">
|
||
<div class="flow-row">
|
||
<div class="flow-label">录音</div>
|
||
<div class="flow-content">点击或长按麦克风,展示录音时长、取消和完成状态。</div>
|
||
</div>
|
||
<div class="flow-row">
|
||
<div class="flow-label">转写</div>
|
||
<div class="flow-content">上传音频到后端统一转写,返回文本、时长、置信度。</div>
|
||
</div>
|
||
<div class="flow-row">
|
||
<div class="flow-label">确认</div>
|
||
<div class="flow-content">转写文本回填输入框,用户可编辑后发送,避免误提交。</div>
|
||
</div>
|
||
<div class="flow-row">
|
||
<div class="flow-label">助手</div>
|
||
<div class="flow-content">最终仍调用 <code>/api/v1/orchestrator/run</code>,保持助手逻辑一致。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="note" style="margin-top:14px">
|
||
关键边界:普通询问、票据预览和 AI 识别建议不自动创建草稿。只有用户明确选择保存草稿、生成报销单、
|
||
继续提交或关联已有草稿,才进入持久化链路。
|
||
</div>
|
||
</section>
|
||
|
||
<section id="pages">
|
||
<div class="section-head">
|
||
<div>
|
||
<h2 class="section-title">页面设计与信息架构</h2>
|
||
<p class="section-desc">
|
||
页面结构沿用移动稿的底部导航:首页、报销、审批、AI 助手、我的。
|
||
首页聚合待办与最近进度,报销和审批分别承载个人申请与处理他人申请。
|
||
</p>
|
||
</div>
|
||
<span class="tag">5 个主入口</span>
|
||
</div>
|
||
|
||
<div class="route-map">
|
||
<div class="route">
|
||
<div class="route-icon">1</div>
|
||
<strong>首页</strong>
|
||
<span>问候、AI 报销助手入口、待办、最近报销进度、通知提醒。</span>
|
||
</div>
|
||
<div class="route">
|
||
<div class="route-icon">2</div>
|
||
<strong>报销</strong>
|
||
<span>我的报销列表、筛选、搜索、新建报销、草稿继续填写。</span>
|
||
</div>
|
||
<div class="route">
|
||
<div class="route-icon">3</div>
|
||
<strong>审批</strong>
|
||
<span>待我审批、审批详情、同意、驳回、转交、审批意见。</span>
|
||
</div>
|
||
<div class="route">
|
||
<div class="route-icon">4</div>
|
||
<strong>AI 助手</strong>
|
||
<span>文本/语音问答、上传票据、生成报销建议、查看制度。</span>
|
||
</div>
|
||
<div class="route">
|
||
<div class="route-icon">5</div>
|
||
<strong>我的</strong>
|
||
<span>个人信息、角色、部门、设置、退出登录、权限说明。</span>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="margin:24px 0 12px">现有移动端设计稿参照</h3>
|
||
<div class="shot-grid">
|
||
<figure class="shot">
|
||
<img src="UI/移动端-1.png" alt="移动端首页设计稿" />
|
||
<figcaption>首页</figcaption>
|
||
</figure>
|
||
<figure class="shot">
|
||
<img src="UI/移动端-2.png" alt="我的报销列表设计稿" />
|
||
<figcaption>我的报销</figcaption>
|
||
</figure>
|
||
<figure class="shot">
|
||
<img src="UI/移动端-3.png" alt="新建报销设计稿" />
|
||
<figcaption>新建报销</figcaption>
|
||
</figure>
|
||
<figure class="shot">
|
||
<img src="UI/移动端-4.png" alt="报销详情设计稿" />
|
||
<figcaption>报销详情</figcaption>
|
||
</figure>
|
||
<figure class="shot">
|
||
<img src="UI/移动端-5.png" alt="审批详情设计稿" />
|
||
<figcaption>审批详情</figcaption>
|
||
</figure>
|
||
<figure class="shot">
|
||
<img src="UI/移动端-6.png" alt="AI 助手设计稿" />
|
||
<figcaption>AI 助手</figcaption>
|
||
</figure>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="design-system">
|
||
<div class="section-head">
|
||
<div>
|
||
<h2 class="section-title">视觉与组件规范</h2>
|
||
<p class="section-desc">
|
||
从 <code>mobile/UI</code> 抽象设计 token,保证 mobile 和 web 能共享品牌语言。
|
||
组件要偏企业应用密度,避免营销页式大装饰。
|
||
</p>
|
||
</div>
|
||
<span class="tag">Token 驱动</span>
|
||
</div>
|
||
|
||
<div class="tokens">
|
||
<div class="token">
|
||
<div class="swatch" style="background:#059669"></div>
|
||
<strong>Brand Green</strong>
|
||
<span>#059669</span>
|
||
</div>
|
||
<div class="token">
|
||
<div class="swatch" style="background:#effcf6"></div>
|
||
<strong>Soft Green</strong>
|
||
<span>#effcf6</span>
|
||
</div>
|
||
<div class="token">
|
||
<div class="swatch" style="background:#071124"></div>
|
||
<strong>Text Primary</strong>
|
||
<span>#071124</span>
|
||
</div>
|
||
<div class="token">
|
||
<div class="swatch" style="background:#728098"></div>
|
||
<strong>Text Muted</strong>
|
||
<span>#728098</span>
|
||
</div>
|
||
<div class="token">
|
||
<div class="swatch" style="background:#f59e0b"></div>
|
||
<strong>Warning</strong>
|
||
<span>#f59e0b</span>
|
||
</div>
|
||
<div class="token">
|
||
<div class="swatch" style="background:#ef4444"></div>
|
||
<strong>Danger</strong>
|
||
<span>#ef4444</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid-3" style="margin-top:14px">
|
||
<div class="card">
|
||
<h3>基础组件</h3>
|
||
<ul>
|
||
<li>Button:主按钮、次按钮、危险按钮、底部固定 CTA。</li>
|
||
<li>Card:报销卡、待办卡、AI 结果卡、附件卡。</li>
|
||
<li>StatusPill:草稿、审批中、待补充、已付款、已驳回。</li>
|
||
</ul>
|
||
</div>
|
||
<div class="card">
|
||
<h3>业务组件</h3>
|
||
<ul>
|
||
<li>ReceiptPreview:票据缩略图、识别状态、删除。</li>
|
||
<li>ClaimSummary:金额、单号、申请人、审批节点。</li>
|
||
<li>RiskBrief:AI 风控提示、关注、需补充、正常。</li>
|
||
</ul>
|
||
</div>
|
||
<div class="card">
|
||
<h3>交互规范</h3>
|
||
<ul>
|
||
<li>所有触控区域 Android 不小于 48dp。</li>
|
||
<li>底部操作条必须预留安全区。</li>
|
||
<li>长表单使用分组和折叠,避免一屏过载。</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="api">
|
||
<div class="section-head">
|
||
<div>
|
||
<h2 class="section-title">接口与同步策略</h2>
|
||
<p class="section-desc">
|
||
后端仍是唯一业务真相。移动端只做客户端适配,所有报销、审批、助手动作都走真实接口。
|
||
建议新增很薄的 mobile facade,用于首页聚合、临时附件和语音转写。
|
||
</p>
|
||
</div>
|
||
<span class="tag">契约优先</span>
|
||
</div>
|
||
|
||
<div class="grid-2">
|
||
<div class="card">
|
||
<h3>复用现有接口</h3>
|
||
<div class="api-list">
|
||
<div class="api">
|
||
<span class="method">POST</span>
|
||
<span class="path">/api/v1/auth/login</span>
|
||
<span class="api-note">登录</span>
|
||
</div>
|
||
<div class="api">
|
||
<span class="method">GET</span>
|
||
<span class="path">/api/v1/reimbursements/claims</span>
|
||
<span class="api-note">我的报销</span>
|
||
</div>
|
||
<div class="api">
|
||
<span class="method">GET</span>
|
||
<span class="path">/api/v1/reimbursements/claims/approvals</span>
|
||
<span class="api-note">审批中心</span>
|
||
</div>
|
||
<div class="api">
|
||
<span class="method">POST</span>
|
||
<span class="path">/api/v1/reimbursements/claims/{claim_id}/submit</span>
|
||
<span class="api-note">提交</span>
|
||
</div>
|
||
<div class="api">
|
||
<span class="method">POST</span>
|
||
<span class="path">/api/v1/orchestrator/run</span>
|
||
<span class="api-note">AI 助手</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>建议新增移动端适配接口</h3>
|
||
<div class="api-list">
|
||
<div class="api">
|
||
<span class="method">GET</span>
|
||
<span class="path">/api/v1/mobile/bootstrap</span>
|
||
<span class="api-note">配置与用户上下文</span>
|
||
</div>
|
||
<div class="api">
|
||
<span class="method">GET</span>
|
||
<span class="path">/api/v1/mobile/home</span>
|
||
<span class="api-note">首页聚合</span>
|
||
</div>
|
||
<div class="api">
|
||
<span class="method">POST</span>
|
||
<span class="path">/api/v1/mobile/attachments/intake</span>
|
||
<span class="api-note">临时附件</span>
|
||
</div>
|
||
<div class="api">
|
||
<span class="method">POST</span>
|
||
<span class="path">/api/v1/mobile/voice/transcribe</span>
|
||
<span class="api-note">语音转写</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid-3" style="margin-top:14px">
|
||
<div class="card emphasis">
|
||
<h3>接口类型同步</h3>
|
||
<p>从 <code>document/development/backend_api/openapi.json</code> 生成 TypeScript 类型与 API client。</p>
|
||
</div>
|
||
<div class="card emphasis">
|
||
<h3>状态同步</h3>
|
||
<p><code>status</code>、<code>approval_stage</code>、可编辑性、可上传性统一在 <code>shared/domain</code> 映射。</p>
|
||
</div>
|
||
<div class="card emphasis">
|
||
<h3>设计同步</h3>
|
||
<p>设计 token 同时输出给 Web CSS variables 和 Mobile theme,减少双端视觉漂移。</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="delivery">
|
||
<div class="section-head">
|
||
<div>
|
||
<h2 class="section-title">交付计划</h2>
|
||
<p class="section-desc">
|
||
先做 Android MVP,确保核心报销闭环可用;再补齐语音、通知、离线草稿等体验增强;
|
||
最后进入 iOS 权限、打包和商店侧适配。
|
||
</p>
|
||
</div>
|
||
<span class="tag">分阶段</span>
|
||
</div>
|
||
|
||
<div class="timeline">
|
||
<div class="phase">
|
||
<div class="phase-key">P0 / Android MVP</div>
|
||
<div class="phase-body">
|
||
<strong>打通核心链路</strong>
|
||
<span>登录、首页、我的报销、新建报销、拍照上传、AI 助手文本输入、审批详情、同意/驳回。</span>
|
||
</div>
|
||
</div>
|
||
<div class="phase">
|
||
<div class="phase-key">P1 / 智能能力</div>
|
||
<div class="phase-body">
|
||
<strong>强化移动生产力</strong>
|
||
<span>语音输入、临时附件识别、助手生成报销单、风险前置、上传重试、会话恢复。</span>
|
||
</div>
|
||
</div>
|
||
<div class="phase">
|
||
<div class="phase-key">P2 / iOS 适配</div>
|
||
<div class="phase-body">
|
||
<strong>平台一致性</strong>
|
||
<span>iOS 相机/相册/麦克风权限、安全区、返回手势、TestFlight、App Store 配置。</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="quality">
|
||
<div class="section-head">
|
||
<div>
|
||
<h2 class="section-title">验收标准</h2>
|
||
<p class="section-desc">
|
||
验收要围绕真实业务结果,不只看 UI 是否像设计稿。报销状态、审批节点、助手结果和附件绑定必须与后端一致。
|
||
</p>
|
||
</div>
|
||
<span class="tag">真实验证</span>
|
||
</div>
|
||
|
||
<div class="checklist">
|
||
<div class="check">Android 真机可拍照、选相册、上传票据并看到识别结果。</div>
|
||
<div class="check">语音输入可录音、转写、回填、编辑后发送给 AI 助手。</div>
|
||
<div class="check">AI 预览、普通问答不自动保存草稿或创建报销单。</div>
|
||
<div class="check">保存草稿、生成报销单、提交审批必须有明确用户动作。</div>
|
||
<div class="check">报销列表、详情、审批列表与后端真实状态一致。</div>
|
||
<div class="check">退回单据能回到可编辑/可补充状态,并展示正确操作。</div>
|
||
<div class="check">弱网、上传失败、权限拒绝都有明确恢复路径。</div>
|
||
<div class="check">触控区域、安全区、字体缩放和深链返回符合移动端体验。</div>
|
||
</div>
|
||
</section>
|
||
|
||
<footer>
|
||
X-Financial Mobile 架构设计文档 · 放置位置:mobile/mobile-architecture-design.html
|
||
</footer>
|
||
</main>
|
||
</div>
|
||
</body>
|
||
</html>
|