Files
X-Financial/mobile/mobile-architecture-design.html

1243 lines
36 KiB
HTML
Raw Normal View History

<!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>