Files
X-Financial/mobile/mobile-architecture-design.html
2026-05-22 12:41:45 +08:00

1243 lines
36 KiB
HTML
Raw Permalink 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>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>RiskBriefAI 风控提示、关注、需补充、正常。</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>