+
+
消费者保护
+
+
+
+ {/* Stats Grid */}
+
+ {stats.map(stat => (
+
+
+ {stat.label}
+
+
+ {stat.value}
+
+
+ {stat.change}
+
+
+ ))}
+
+
+ {/* Complaint Categories + CSAT Trend */}
+
+ {/* Complaint Category Breakdown */}
+
+
投诉分类
+ {complaintCategories.map(cat => (
+
+
+ {cat.name}
+
+ {cat.count} 件 ({cat.percent}%)
+
+
+
+
+ ))}
+
+
+ {/* CSAT Trend + Protection Fund */}
+
+ {/* Consumer Satisfaction Trend */}
+
+
消费者满意度 (CSAT)
+
+ 4.5
+ /5.0
+ +0.1
+
+
+ {csatTrend.map(item => (
+
+
+
= 4.4 ? 'var(--color-success)' : item.score >= 4.2 ? 'var(--color-info)' : 'var(--color-warning)',
+ borderRadius: '4px 4px 0 0',
+ opacity: 0.7,
+ }} />
+
+
{item.month}
+
{item.score}
+
+ ))}
+
+
+
+ {/* Protection Fund Utilization */}
+
+
保障基金使用率
+
+ Recharts 仪表盘图 (基金池 $520K / 已用 $78K / 使用率 15%)
+
+
+
+
+
+ {/* Recent Complaints Table */}
+
+
+ 近期投诉
+
+
+
+
+
+ {['编号', '严重度', '分类', '描述', '状态', '负责人', '日期'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {recentComplaints.map(row => {
+ const sev = severityConfig[row.severity];
+ const st = statusConfig[row.status];
+ return (
+
+ |
+ {row.id}
+ |
+
+ {row.severity}
+ |
+ {row.category} |
+ {row.title} |
+
+ {row.status}
+ |
+ {row.assignee} |
+ {row.created} |
+
+ );
+ })}
+
+
+
+
+ {/* Refund Policy Compliance */}
+
+
+ 退款合规 - 不合规发行方 Top 5
+
+
+
+
+ {['排名', '发行方', '违规次数', '退款通过率', '平均处理延迟', '风险等级', '操作'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {nonCompliantIssuers.map(row => {
+ const risk = severityConfig[row.riskLevel];
+ return (
+
+ | {row.rank} |
+ {row.issuer} |
+ {row.violations} |
+
+
+ |
+ {row.avgDelay} |
+
+ {row.riskLevel}
+ |
+
+
+
+ |
+
+ );
+ })}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/analytics/CouponAnalyticsPage.tsx b/frontend/admin-web/src/pages/analytics/CouponAnalyticsPage.tsx
new file mode 100644
index 0000000..a45a016
--- /dev/null
+++ b/frontend/admin-web/src/pages/analytics/CouponAnalyticsPage.tsx
@@ -0,0 +1,271 @@
+import React from 'react';
+
+/**
+ * 券分析仪表盘
+ *
+ * 券发行/核销/过期统计、品类分布、热销排行、Breakage趋势、二级市场分析
+ */
+
+const stats = [
+ { label: '券总量', value: '45,230', change: '+6.5%', trend: 'up' as const, color: 'var(--color-primary)' },
+ { label: '活跃券', value: '32,100', change: '+3.2%', trend: 'up' as const, color: 'var(--color-success)' },
+ { label: '已核销', value: '8,450', change: '+12.1%', trend: 'up' as const, color: 'var(--color-info)' },
+ { label: '即将过期', value: '2,340', change: '+8.7%', trend: 'up' as const, color: 'var(--color-warning)' },
+];
+
+const categoryDistribution = [
+ { name: '餐饮', count: 14_474, percent: 32, color: 'var(--color-primary)' },
+ { name: '零售', count: 11_308, percent: 25, color: 'var(--color-success)' },
+ { name: '娱乐', count: 9_046, percent: 20, color: 'var(--color-info)' },
+ { name: '旅游', count: 5_428, percent: 12, color: 'var(--color-warning)' },
+ { name: '数码', count: 4_974, percent: 11, color: 'var(--color-error)' },
+];
+
+const topCoupons = [
+ { rank: 1, brand: '星巴克', name: '大杯拿铁兑换券', sales: 4_230, revenue: '$105,750', rating: 4.8 },
+ { rank: 2, brand: 'Amazon', name: '$100电子礼品卡', sales: 3_890, revenue: '$389,000', rating: 4.9 },
+ { rank: 3, brand: 'Nike', name: '旗舰店8折券', sales: 2_750, revenue: '$220,000', rating: 4.6 },
+ { rank: 4, brand: '海底捞', name: '双人套餐券', sales: 2_340, revenue: '$187,200', rating: 4.7 },
+ { rank: 5, brand: 'Target', name: '$30消费券', sales: 2_100, revenue: '$63,000', rating: 4.5 },
+ { rank: 6, brand: 'Apple', name: 'App Store $25', sales: 1_980, revenue: '$49,500', rating: 4.8 },
+ { rank: 7, brand: '万达影城', name: '双人电影票', sales: 1_750, revenue: '$52,500', rating: 4.4 },
+ { rank: 8, brand: 'Uber', name: '$20出行券', sales: 1_620, revenue: '$32,400', rating: 4.3 },
+ { rank: 9, brand: '携程', name: '酒店满减券', sales: 1_480, revenue: '$148,000', rating: 4.6 },
+ { rank: 10, brand: 'Steam', name: '$50充值卡', sales: 1_310, revenue: '$65,500', rating: 4.7 },
+];
+
+const breakageTrend = [
+ { month: '9月', rate: '18.2%' },
+ { month: '10月', rate: '17.5%' },
+ { month: '11月', rate: '16.8%' },
+ { month: '12月', rate: '19.3%' },
+ { month: '1月', rate: '17.1%' },
+ { month: '2月', rate: '16.5%' },
+];
+
+const secondaryMarket = [
+ { metric: '挂牌率', value: '23.5%', change: '+1.2%', trend: 'up' as const },
+ { metric: '平均加价率', value: '8.3%', change: '-0.5%', trend: 'down' as const },
+ { metric: '日均交易量', value: '1,230', change: '+15.2%', trend: 'up' as const },
+ { metric: '日均交易额', value: '$98,400', change: '+11.8%', trend: 'up' as const },
+ { metric: '平均成交时间', value: '4.2h', change: '-8.3%', trend: 'down' as const },
+ { metric: '撤单率', value: '12.1%', change: '+0.8%', trend: 'up' as const },
+];
+
+export const CouponAnalyticsPage: React.FC = () => {
+ return (
+
+
+ 券分析
+
+
+ {/* Stats Grid */}
+
+ {stats.map(stat => (
+
+
+ {stat.label}
+
+
+ {stat.value}
+
+
+ {stat.change}
+
+
+ ))}
+
+
+ {/* Category Distribution + Price Histogram */}
+
+ {/* Category Distribution */}
+
+
品类分布
+ {categoryDistribution.map(cat => (
+
+
+ {cat.name}
+
+ {cat.count.toLocaleString()} ({cat.percent}%)
+
+
+
+
+ ))}
+
+
+ {/* Price Distribution Histogram */}
+
+
面值分布
+
+ Recharts 柱状图 (面值区间: $0-25 / $25-50 / $50-100 / $100-200 / $200+)
+
+
+
+
+ {/* Top 10 Best-Selling Coupons */}
+
+
+ 热销券 Top 10
+
+
+
+
+ {['排名', '品牌', '券名称', '销量', '收入', '评分'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {topCoupons.map(row => (
+
+ | {row.rank} |
+ {row.brand} |
+ {row.name} |
+ {row.sales.toLocaleString()} |
+ {row.revenue} |
+
+ = 4.7 ? 'var(--color-success-light)' : 'var(--color-warning-light)',
+ color: row.rating >= 4.7 ? 'var(--color-success)' : 'var(--color-warning)',
+ font: 'var(--text-caption)',
+ }}>
+ {row.rating}
+
+ |
+
+ ))}
+
+
+
+
+ {/* Breakage Rate Trend + Secondary Market */}
+
+ {/* Breakage Rate Trend */}
+
+
Breakage趋势 (未核销率)
+
+ Recharts 折线图 (月度 Breakage Rate)
+
+
+ {breakageTrend.map(item => (
+
+ {item.month}: {item.rate}
+
+ ))}
+
+
+
+ {/* Secondary Market Analytics */}
+
+
二级市场分析
+
+ {secondaryMarket.map(item => (
+
+
+ {item.metric}
+
+
+ {item.value}
+
+
+ {item.change}
+
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/analytics/MarketMakerPage.tsx b/frontend/admin-web/src/pages/analytics/MarketMakerPage.tsx
new file mode 100644
index 0000000..287896f
--- /dev/null
+++ b/frontend/admin-web/src/pages/analytics/MarketMakerPage.tsx
@@ -0,0 +1,317 @@
+import React from 'react';
+
+/**
+ * 做市商管理仪表盘
+ *
+ * 做市商列表、流动性池、订单簿深度、市场健康指标、风险预警
+ */
+
+const stats = [
+ { label: '活跃做市商', value: '12', change: '+2', trend: 'up' as const, color: 'var(--color-primary)' },
+ { label: '总流动性', value: '$5.2M', change: '+8.3%', trend: 'up' as const, color: 'var(--color-success)' },
+ { label: '日均交易量', value: '$320K', change: '+12.5%', trend: 'up' as const, color: 'var(--color-info)' },
+ { label: '平均价差', value: '1.8%', change: '-0.3%', trend: 'down' as const, color: 'var(--color-warning)' },
+];
+
+const marketMakers = [
+ { name: 'AlphaLiquidity', status: 'active' as const, tvl: '$1,250,000', spread: '1.2%', volume: '$85,000', pnl: '+$12,340' },
+ { name: 'BetaMarkets', status: 'active' as const, tvl: '$980,000', spread: '1.5%', volume: '$72,000', pnl: '+$8,920' },
+ { name: 'GammaTrading', status: 'active' as const, tvl: '$850,000', spread: '1.8%', volume: '$65,400', pnl: '+$6,780' },
+ { name: 'DeltaCapital', status: 'paused' as const, tvl: '$620,000', spread: '2.1%', volume: '$0', pnl: '-$1,230' },
+ { name: 'EpsilonFund', status: 'active' as const, tvl: '$540,000', spread: '1.6%', volume: '$43,200', pnl: '+$5,410' },
+ { name: 'ZetaPartners', status: 'active' as const, tvl: '$430,000', spread: '2.0%', volume: '$31,800', pnl: '+$3,670' },
+ { name: 'EtaVentures', status: 'suspended' as const, tvl: '$0', spread: '-', volume: '$0', pnl: '-$4,560' },
+ { name: 'ThetaQuant', status: 'active' as const, tvl: '$280,000', spread: '1.9%', volume: '$22,600', pnl: '+$2,890' },
+];
+
+const statusConfig: Record
= {
+ active: { bg: 'var(--color-success-light)', color: 'var(--color-success)', label: '活跃' },
+ paused: { bg: 'var(--color-warning-light)', color: 'var(--color-warning)', label: '暂停' },
+ suspended: { bg: 'var(--color-error-light)', color: 'var(--color-error)', label: '停用' },
+};
+
+const liquidityPools = [
+ { category: '餐饮', tvl: '$1,560,000', percent: 30, makers: 8, color: 'var(--color-primary)' },
+ { category: '零售', tvl: '$1,300,000', percent: 25, makers: 7, color: 'var(--color-success)' },
+ { category: '娱乐', tvl: '$1,040,000', percent: 20, makers: 6, color: 'var(--color-info)' },
+ { category: '旅游', tvl: '$780,000', percent: 15, makers: 5, color: 'var(--color-warning)' },
+ { category: '数码', tvl: '$520,000', percent: 10, makers: 4, color: 'var(--color-error)' },
+];
+
+const healthIndicators = [
+ { name: 'Bid-Ask 价差', value: '1.8%', target: '< 3.0%', status: 'good' as const },
+ { name: '滑点 (Slippage)', value: '0.42%', target: '< 1.0%', status: 'good' as const },
+ { name: '成交率 (Fill Rate)', value: '94.7%', target: '> 90%', status: 'good' as const },
+ { name: '流动性深度', value: '$5.2M', target: '> $3M', status: 'good' as const },
+ { name: '价格偏差', value: '2.1%', target: '< 2.0%', status: 'warning' as const },
+ { name: '做市商覆盖率', value: '87%', target: '> 85%', status: 'good' as const },
+];
+
+const riskAlerts = [
+ { time: '14:25', maker: 'DeltaCapital', type: '流动性撤出', desc: '30分钟内撤出65%流动性,已自动暂停', severity: 'high' as const },
+ { time: '13:40', maker: 'EtaVentures', type: '异常交易', desc: '检测到自成交行为,账户已停用待审', severity: 'high' as const },
+ { time: '12:15', maker: 'ZetaPartners', type: '价差偏高', desc: '餐饮品类价差达3.2%,超出阈值', severity: 'medium' as const },
+ { time: '11:00', maker: 'ThetaQuant', type: 'API延迟', desc: '报价延迟升至800ms,可能影响做市质量', severity: 'low' as const },
+];
+
+const severityConfig: Record = {
+ high: { bg: 'var(--color-error-light)', color: 'var(--color-error)', label: '高' },
+ medium: { bg: 'var(--color-warning-light)', color: 'var(--color-warning)', label: '中' },
+ low: { bg: 'var(--color-info-light)', color: 'var(--color-info)', label: '低' },
+};
+
+export const MarketMakerPage: React.FC = () => {
+ return (
+
+
+
做市商管理
+
+
+
+ {/* Stats Grid */}
+
+ {stats.map(stat => (
+
+
+ {stat.label}
+
+
+ {stat.value}
+
+
+ {stat.change}
+
+
+ ))}
+
+
+ {/* Market Maker Table */}
+
+
+ 做市商列表
+
+
+
+
+ {['做市商', '状态', 'TVL', '价差', '日交易量', 'P&L', '操作'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {marketMakers.map(mm => {
+ const s = statusConfig[mm.status];
+ return (
+
+ | {mm.name} |
+
+ {s.label}
+ |
+ {mm.tvl} |
+ {mm.spread} |
+ {mm.volume} |
+ {mm.pnl} |
+
+
+ {mm.status === 'active' && (
+
+ )}
+ {mm.status === 'paused' && (
+
+ )}
+ |
+
+ );
+ })}
+
+
+
+
+ {/* Liquidity Pools + Order Book Depth */}
+
+ {/* Liquidity Pool Distribution */}
+
+
流动性池分布
+ {liquidityPools.map(pool => (
+
+
+
+ {pool.category}
+
+ {pool.makers} 做市商
+
+
+
+ {pool.tvl} ({pool.percent}%)
+
+
+
+
+ ))}
+
+
+ {/* Order Book Depth */}
+
+
订单簿深度
+
+ Recharts 面积图 (Bid/Ask 深度分布)
+
+
+
+
+ {/* Market Health + Risk Alerts */}
+
+ {/* Market Health Indicators */}
+
+
市场健康指标
+ {healthIndicators.map(ind => (
+
+
+ {ind.name}
+ {ind.value}
+ {ind.target}
+
+ ))}
+
+
+ {/* Risk Alerts */}
+
+
+
风险预警
+
+ {riskAlerts.filter(a => a.severity === 'high').length} 高风险
+
+
+ {riskAlerts.map((alert, i) => {
+ const sev = severityConfig[alert.severity];
+ return (
+
+
+
+ {sev.label}
+ {alert.maker}
+ {alert.type}
+
+
{alert.time}
+
+
+ {alert.desc}
+
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/analytics/UserAnalyticsPage.tsx b/frontend/admin-web/src/pages/analytics/UserAnalyticsPage.tsx
new file mode 100644
index 0000000..c2badad
--- /dev/null
+++ b/frontend/admin-web/src/pages/analytics/UserAnalyticsPage.tsx
@@ -0,0 +1,290 @@
+import React from 'react';
+
+/**
+ * 用户分析仪表盘
+ *
+ * 用户增长趋势、KYC分布、地理分布、留存矩阵、活跃分群
+ */
+
+const stats = [
+ { label: '总用户数', value: '128,456', change: '+3.2%', trend: 'up' as const, color: 'var(--color-primary)' },
+ { label: 'DAU', value: '12,340', change: '+5.8%', trend: 'up' as const, color: 'var(--color-success)' },
+ { label: 'MAU', value: '45,678', change: '+2.1%', trend: 'up' as const, color: 'var(--color-info)' },
+ { label: '新增用户/周', value: '1,234', change: '-1.4%', trend: 'down' as const, color: 'var(--color-warning)' },
+];
+
+const kycDistribution = [
+ { level: 'L0 - 未验证', count: 32_114, percent: 25, color: 'var(--color-gray-400)' },
+ { level: 'L1 - 基础验证', count: 51_382, percent: 40, color: 'var(--color-info)' },
+ { level: 'L2 - 身份验证', count: 33_399, percent: 26, color: 'var(--color-primary)' },
+ { level: 'L3 - 高级验证', count: 11_561, percent: 9, color: 'var(--color-success)' },
+];
+
+const geoDistribution = [
+ { rank: 1, region: '北美', users: '38,536', percent: '30.0%' },
+ { rank: 2, region: '东亚', users: '29,545', percent: '23.0%' },
+ { rank: 3, region: '东南亚', users: '19,268', percent: '15.0%' },
+ { rank: 4, region: '欧洲', users: '14,130', percent: '11.0%' },
+ { rank: 5, region: '南美', users: '9,003', percent: '7.0%' },
+ { rank: 6, region: '中东', users: '5,138', percent: '4.0%' },
+ { rank: 7, region: '南亚', users: '3,854', percent: '3.0%' },
+ { rank: 8, region: '非洲', users: '3,854', percent: '3.0%' },
+ { rank: 9, region: '大洋洲', users: '2,569', percent: '2.0%' },
+ { rank: 10, region: '其他', users: '2,559', percent: '2.0%' },
+];
+
+const cohortRetention = [
+ { cohort: '第1周 (01/06)', week0: '100%', week1: '68%', week2: '52%', week3: '41%', week4: '35%' },
+ { cohort: '第2周 (01/13)', week0: '100%', week1: '71%', week2: '55%', week3: '44%', week4: '38%' },
+ { cohort: '第3周 (01/20)', week0: '100%', week1: '65%', week2: '49%', week3: '40%', week4: '-' },
+ { cohort: '第4周 (01/27)', week0: '100%', week1: '70%', week2: '53%', week3: '-', week4: '-' },
+ { cohort: '第5周 (02/03)', week0: '100%', week1: '67%', week2: '-', week3: '-', week4: '-' },
+];
+
+const userSegments = [
+ { name: '高频交易', count: '8,456', percent: 6.6, color: 'var(--color-primary)' },
+ { name: '偶尔购买', count: '34,230', percent: 26.6, color: 'var(--color-success)' },
+ { name: '仅浏览', count: '52,890', percent: 41.2, color: 'var(--color-warning)' },
+ { name: '流失用户', count: '32,880', percent: 25.6, color: 'var(--color-error)' },
+];
+
+export const UserAnalyticsPage: React.FC = () => {
+ return (
+
+
+ 用户分析
+
+
+ {/* Stats Grid */}
+
+ {stats.map(stat => (
+
+
+ {stat.label}
+
+
+ {stat.value}
+
+
+ {stat.change}
+
+
+ ))}
+
+
+ {/* User Growth Chart */}
+
+
+
用户增长趋势
+
+ {['7D', '30D', '90D', '1Y'].map(p => (
+
+ ))}
+
+
+
+ Recharts 面积图 (用户增长 / DAU / MAU)
+
+
+
+ {/* KYC Distribution + Geographic Distribution */}
+
+ {/* KYC Distribution */}
+
+
KYC等级分布
+ {kycDistribution.map(item => (
+
+
+ {item.level}
+
+ {item.count.toLocaleString()} ({item.percent}%)
+
+
+
+
+ ))}
+
+
+ {/* Geographic Distribution */}
+
+
+ 地理分布 (Top 10)
+
+
+
+
+ {['排名', '地区', '用户数', '占比'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {geoDistribution.map(row => (
+
+ | {row.rank} |
+ {row.region} |
+ {row.users} |
+ {row.percent} |
+
+ ))}
+
+
+
+
+
+ {/* Cohort Retention Matrix + User Segments */}
+
+ {/* Cohort Retention Matrix */}
+
+
+ 用户留存矩阵
+
+
+
+
+ {['注册周', 'Week 0', 'Week 1', 'Week 2', 'Week 3', 'Week 4'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {cohortRetention.map(row => (
+
+ | {row.cohort} |
+ {[row.week0, row.week1, row.week2, row.week3, row.week4].map((val, i) => {
+ const numVal = parseInt(val);
+ const bgOpacity = !isNaN(numVal) ? Math.max(0.05, numVal / 100 * 0.4) : 0;
+ return (
+
+ {val}
+ |
+ );
+ })}
+
+ ))}
+
+
+
+
+ {/* Active User Segments */}
+
+
活跃用户分群
+ {userSegments.map(seg => (
+
+
+
+
{seg.name}
+
{seg.count} 人
+
+
+
+ ))}
+
+ Recharts 环形图 (用户分群占比)
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/chain/ChainMonitorPage.tsx b/frontend/admin-web/src/pages/chain/ChainMonitorPage.tsx
new file mode 100644
index 0000000..542b2e3
--- /dev/null
+++ b/frontend/admin-web/src/pages/chain/ChainMonitorPage.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+
+/**
+ * D4. 链上监控 - 合约状态与链上数据
+ *
+ * 合约状态、铸造/销毁记录、Gas费监控、链上异常检测
+ * 注:此页面仅对平台管理员可见,发行方/消费者不可见
+ */
+
+const contractStats = [
+ { label: 'CouponFactory', status: 'Active', txCount: '45,231', lastBlock: '#18,234,567' },
+ { label: 'Marketplace', status: 'Active', txCount: '12,890', lastBlock: '#18,234,560' },
+ { label: 'RedemptionGateway', status: 'Active', txCount: '38,456', lastBlock: '#18,234,565' },
+ { label: 'StablecoinBridge', status: 'Active', txCount: '8,901', lastBlock: '#18,234,555' },
+];
+
+const recentEvents = [
+ { event: 'Mint', detail: 'Starbucks $25 Gift Card x500', hash: '0xabc...def', time: '2分钟前', type: 'mint' },
+ { event: 'Transfer', detail: 'P2P Transfer #1234', hash: '0x123...456', time: '5分钟前', type: 'transfer' },
+ { event: 'Redeem', detail: 'Batch Redeem x8', hash: '0x789...abc', time: '8分钟前', type: 'redeem' },
+ { event: 'Burn', detail: 'Expired coupons x12', hash: '0xdef...789', time: '15分钟前', type: 'burn' },
+];
+
+const eventColors: Record = {
+ mint: 'var(--color-success)',
+ transfer: 'var(--color-info)',
+ redeem: 'var(--color-primary)',
+ burn: 'var(--color-error)',
+};
+
+export const ChainMonitorPage: React.FC = () => {
+ return (
+
+
链上监控
+
+ {/* Contract Status */}
+
+ {contractStats.map(c => (
+
+
+ {c.label}
+ {c.status}
+
+
TX: {c.txCount} · Block: {c.lastBlock}
+
+ ))}
+
+
+
+ {/* Chain Events */}
+
+
最近链上事件
+ {recentEvents.map((e, i) => (
+
+
+
+
{e.event}
+
{e.detail}
+
+
+
+ ))}
+
+
+ {/* Gas Monitor */}
+
+
Gas费监控
+
+ {[
+ { label: '当前Gas', value: '12 gwei', color: 'var(--color-success)' },
+ { label: '今日均值', value: '18 gwei', color: 'var(--color-info)' },
+ { label: '今日Gas支出', value: '$1,234', color: 'var(--color-warning)' },
+ ].map(g => (
+
+
{g.value}
+
{g.label}
+
+ ))}
+
+
Gas费24小时趋势图
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/compliance/CompliancePage.tsx b/frontend/admin-web/src/pages/compliance/CompliancePage.tsx
new file mode 100644
index 0000000..1565c00
--- /dev/null
+++ b/frontend/admin-web/src/pages/compliance/CompliancePage.tsx
@@ -0,0 +1,224 @@
+import React, { useState } from 'react';
+
+/**
+ * D6. 合规报表
+ *
+ * SAR管理、CTR管理、审计日志、监管报表
+ */
+
+export const CompliancePage: React.FC = () => {
+ const [activeTab, setActiveTab] = useState<'sar' | 'ctr' | 'audit' | 'reports'>('sar');
+
+ return (
+
+
+
合规报表
+
+
+
+ {/* Tabs */}
+
+ {[
+ { key: 'sar', label: 'SAR管理', badge: 3 },
+ { key: 'ctr', label: 'CTR管理', badge: 0 },
+ { key: 'audit', label: '审计日志', badge: 0 },
+ { key: 'reports', label: '监管报表', badge: 0 },
+ ].map(t => (
+
+ ))}
+
+
+ {/* SAR Tab Content */}
+ {activeTab === 'sar' && (
+
+
+
+
+ {['SAR编号', '相关交易', '涉及用户', '金额', '风险类型', '状态', '创建时间', '操作'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {[
+ { id: 'SAR-2026-001', txn: 'TXN-8901', user: 'U-045', amount: '$4,560', type: '高频交易', status: '待提交' },
+ { id: 'SAR-2026-002', txn: 'TXN-8900', user: 'U-078', amount: '$8,900', type: '大额异常', status: '已提交' },
+ { id: 'SAR-2026-003', txn: 'TXN-8850', user: 'U-012', amount: '$12,000', type: '结构化交易', status: '已提交' },
+ ].map(sar => (
+
+ | {sar.id} |
+ {sar.txn} |
+ {sar.user} |
+ {sar.amount} |
+
+ {sar.type}
+ |
+
+ {sar.status}
+ |
+ 2026-02-{10 - parseInt(sar.id.slice(-1))} |
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+ {/* CTR Tab */}
+ {activeTab === 'ctr' && (
+
+
📋
+
大额交易报告
+
超过$10,000的交易自动生成CTR,当前无待处理项
+
+ )}
+
+ {/* Audit Log Tab */}
+ {activeTab === 'audit' && (
+
+
+
+
+
+ {Array.from({ length: 6 }, (_, i) => (
+
+ 14:{30 + i}:00
+
+ {['登录', '审核', '配置', '冻结', '导出', '查询'][i]}
+
+
+ 管理员 admin{i + 1} {['登录系统', '审核发行方ISS-003通过', '修改手续费率为2.5%', '冻结用户U-045', '导出月度报表', '查询OFAC筛查记录'][i]}
+
+
+ 192.168.1.{100 + i}
+
+
+ ))}
+
+ )}
+
+ {/* Reports Tab */}
+ {activeTab === 'reports' && (
+
+ {[
+ { title: '日报', desc: '每日交易汇总、异常事件', date: '2026-02-10', auto: true },
+ { title: '月报', desc: '月度运营指标、合规状态', date: '2026-01-31', auto: true },
+ { title: '季度SAR汇总', desc: '季度可疑活动报告汇总', date: '2025-12-31', auto: false },
+ { title: '年度合规报告', desc: '年度合规审计报告', date: '2025-12-31', auto: false },
+ ].map(report => (
+
+
+ {report.title}
+ {report.auto && (
+ 自动生成
+ )}
+
+
{report.desc}
+
+ 截至 {report.date}
+
+
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/compliance/IpoReadinessPage.tsx b/frontend/admin-web/src/pages/compliance/IpoReadinessPage.tsx
new file mode 100644
index 0000000..e10d9b7
--- /dev/null
+++ b/frontend/admin-web/src/pages/compliance/IpoReadinessPage.tsx
@@ -0,0 +1,316 @@
+import React from 'react';
+
+/**
+ * D8.4 IPO准备度检查清单 - 独立页面
+ *
+ * 法律/财务/合规/治理/保险 五大类别
+ * Gantt时间线、依赖管理、阻塞项跟踪
+ */
+
+interface CheckItem {
+ id: string;
+ item: string;
+ category: string;
+ status: 'done' | 'progress' | 'blocked' | 'pending';
+ owner: string;
+ deadline: string;
+ dependency?: string;
+ note?: string;
+}
+
+const categories = [
+ { key: 'legal', label: '法律合规', icon: '§', color: 'var(--color-primary)' },
+ { key: 'financial', label: '财务审计', icon: '$', color: 'var(--color-success)' },
+ { key: 'sox', label: 'SOX合规', icon: '✓', color: 'var(--color-info)' },
+ { key: 'governance', label: '公司治理', icon: '◆', color: 'var(--color-warning)' },
+ { key: 'insurance', label: '保险保障', icon: '☂', color: '#FF6B6B' },
+];
+
+const overallProgress = {
+ total: 28,
+ done: 16,
+ inProgress: 7,
+ blocked: 2,
+ pending: 3,
+ percent: 72,
+};
+
+const milestones = [
+ { name: 'S-1初稿提交', date: '2026-Q2', status: 'progress' as const },
+ { name: 'SEC审核期', date: '2026-Q3', status: 'pending' as const },
+ { name: '路演 (Roadshow)', date: '2026-Q3', status: 'pending' as const },
+ { name: '定价 & 上市', date: '2026-Q4', status: 'pending' as const },
+];
+
+const checklistItems: CheckItem[] = [
+ // Legal
+ { id: 'L1', item: 'FinCEN MSB牌照', category: 'legal', status: 'done', owner: 'Legal', deadline: '2026-01-15' },
+ { id: 'L2', item: 'NY BitLicense申请', category: 'legal', status: 'progress', owner: 'Legal', deadline: '2026-06-30', note: '材料已提交,等待审核' },
+ { id: 'L3', item: '各州MTL注册 (48州)', category: 'legal', status: 'progress', owner: 'Legal', deadline: '2026-05-31', note: '已完成38/48州' },
+ { id: 'L4', item: 'SEC法律顾问意见书', category: 'legal', status: 'progress', owner: 'External Counsel', deadline: '2026-04-30' },
+ { id: 'L5', item: '知识产权审计 (IP Audit)', category: 'legal', status: 'done', owner: 'Legal', deadline: '2026-02-01' },
+ { id: 'L6', item: '商标注册 (USPTO)', category: 'legal', status: 'done', owner: 'Legal', deadline: '2026-01-20' },
+ // Financial
+ { id: 'F1', item: '独立审计报告 (Deloitte)', category: 'financial', status: 'progress', owner: 'Finance', deadline: '2026-05-15', dependency: 'F2' },
+ { id: 'F2', item: 'GAAP财务报表 (3年)', category: 'financial', status: 'done', owner: 'Finance', deadline: '2026-03-01' },
+ { id: 'F3', item: '税务合规证明', category: 'financial', status: 'done', owner: 'Finance', deadline: '2026-02-28' },
+ { id: 'F4', item: '收入确认政策 (ASC 606)', category: 'financial', status: 'done', owner: 'Finance', deadline: '2026-02-15' },
+ { id: 'F5', item: '估值模型 & 定价区间', category: 'financial', status: 'pending', owner: 'Finance + IB', deadline: '2026-07-31' },
+ // SOX
+ { id: 'S1', item: 'ICFR内部控制框架', category: 'sox', status: 'done', owner: 'Compliance', deadline: '2026-01-31' },
+ { id: 'S2', item: 'IT通用控制 (ITGC)', category: 'sox', status: 'done', owner: 'Engineering', deadline: '2026-02-15' },
+ { id: 'S3', item: '访问控制审计', category: 'sox', status: 'done', owner: 'Security', deadline: '2026-02-10' },
+ { id: 'S4', item: '变更管理流程', category: 'sox', status: 'done', owner: 'Engineering', deadline: '2026-02-01' },
+ { id: 'S5', item: 'SOX 404管理层评估', category: 'sox', status: 'progress', owner: 'Compliance', deadline: '2026-05-31', dependency: 'S1' },
+ // Governance
+ { id: 'G1', item: '独立董事会组建 (3+)', category: 'governance', status: 'done', owner: 'Board', deadline: '2026-03-01', note: '4名独立董事已任命' },
+ { id: 'G2', item: '审计委员会', category: 'governance', status: 'done', owner: 'Board', deadline: '2026-03-15' },
+ { id: 'G3', item: '薪酬委员会', category: 'governance', status: 'done', owner: 'Board', deadline: '2026-03-15' },
+ { id: 'G4', item: '公司章程 & 治理政策', category: 'governance', status: 'done', owner: 'Legal', deadline: '2026-02-28' },
+ { id: 'G5', item: 'D&O保险', category: 'governance', status: 'blocked', owner: 'Legal', deadline: '2026-04-30', note: '等待承保方最终报价' },
+ { id: 'G6', item: 'Insider Trading Policy', category: 'governance', status: 'done', owner: 'Legal', deadline: '2026-03-01' },
+ // Insurance
+ { id: 'I1', item: '消费者保护基金 ≥$2M', category: 'insurance', status: 'done', owner: 'Finance', deadline: '2026-02-01' },
+ { id: 'I2', item: 'AML/KYC体系', category: 'insurance', status: 'done', owner: 'Compliance', deadline: '2026-01-15' },
+ { id: 'I3', item: '赔付机制运行报告', category: 'insurance', status: 'progress', owner: 'Operations', deadline: '2026-05-01' },
+ { id: 'I4', item: 'Cyber保险', category: 'insurance', status: 'blocked', owner: 'Legal', deadline: '2026-04-15', note: '正在比价3家承保方' },
+ { id: 'I5', item: '做市商协议签署', category: 'insurance', status: 'pending', owner: 'Finance + IB', deadline: '2026-07-15' },
+ { id: 'I6', item: '承销商尽职调查', category: 'insurance', status: 'pending', owner: 'External', deadline: '2026-08-01' },
+];
+
+const statusConfig: Record = {
+ done: { label: '已完成', bg: 'var(--color-success-light)', fg: 'var(--color-success)' },
+ progress: { label: '进行中', bg: 'var(--color-warning-light)', fg: 'var(--color-warning)' },
+ blocked: { label: '阻塞', bg: 'var(--color-error-light)', fg: 'var(--color-error)' },
+ pending: { label: '待开始', bg: 'var(--color-gray-100)', fg: 'var(--color-text-tertiary)' },
+};
+
+export const IpoReadinessPage: React.FC = () => {
+ return (
+
+
IPO准备度检查清单
+
+ 跟踪所有IPO里程碑、合规项、依赖关系和阻塞项
+
+
+ {/* Summary Stats */}
+
+ {[
+ { label: '总计检查项', value: overallProgress.total, color: 'var(--color-text-primary)' },
+ { label: '已完成', value: overallProgress.done, color: 'var(--color-success)' },
+ { label: '进行中', value: overallProgress.inProgress, color: 'var(--color-warning)' },
+ { label: '阻塞项', value: overallProgress.blocked, color: 'var(--color-error)' },
+ { label: '待开始', value: overallProgress.pending, color: 'var(--color-text-tertiary)' },
+ ].map(s => (
+
+
{s.label}
+
{s.value}
+
+ ))}
+
+
+ {/* Overall Progress Bar */}
+
+
+ 总体IPO准备进度
+ {overallProgress.percent}%
+
+
+
+ {[
+ { label: '已完成', color: 'var(--color-success)' },
+ { label: '进行中', color: 'var(--color-warning)' },
+ { label: '阻塞', color: 'var(--color-error)' },
+ { label: '待开始', color: 'var(--color-gray-200)' },
+ ].map(l => (
+
+ ))}
+
+
+
+
+ {/* Left: Checklist by Category */}
+
+ {categories.map(cat => {
+ const items = checklistItems.filter(i => i.category === cat.key);
+ const catDone = items.filter(i => i.status === 'done').length;
+ return (
+
+
+
+
+ {cat.icon}
+
+
{cat.label}
+
+
+ {catDone}/{items.length} 完成
+
+
+
+
+
+
+ {['编号', '检查项', '负责方', '截止日', '状态'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {items.map(item => {
+ const st = statusConfig[item.status];
+ return (
+
+ | {item.id} |
+
+ {item.item}
+ {item.note && {item.note} }
+ {item.dependency && (
+
+ 依赖: {item.dependency}
+
+ )}
+ |
+ {item.owner} |
+ {item.deadline} |
+
+ {st.label}
+ |
+
+ );
+ })}
+
+
+
+ );
+ })}
+
+
+ {/* Right: Timeline & Blockers */}
+
+ {/* IPO Timeline */}
+
+
IPO时间线
+ {milestones.map((m, i) => (
+
+
+
+ {i < milestones.length - 1 && (
+
+ )}
+
+
+
+ ))}
+
+
+ {/* Blockers */}
+
+
阻塞项
+ {checklistItems.filter(i => i.status === 'blocked').map(item => (
+
+
{item.id}: {item.item}
+
+ 负责: {item.owner} · 截止: {item.deadline}
+
+ {item.note && (
+
{item.note}
+ )}
+
+ ))}
+
+
+ {/* Category Progress */}
+
+
分类进度
+ {categories.map(cat => {
+ const items = checklistItems.filter(i => i.category === cat.key);
+ const catDone = items.filter(i => i.status === 'done').length;
+ const pct = Math.round(catDone / items.length * 100);
+ return (
+
+
+ {cat.label}
+ {pct}%
+
+
+
+ );
+ })}
+
+
+ {/* Key Contacts */}
+
+
关键联系方
+ {[
+ { role: '承销商 (Lead)', name: 'Goldman Sachs', status: '已签约' },
+ { role: '审计师', name: 'Deloitte', status: '审计中' },
+ { role: '法律顾问', name: 'Skadden, Arps', status: '已签约' },
+ { role: 'SEC联络', name: 'SEC Division of Corp Finance', status: '对接中' },
+ ].map(c => (
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/compliance/LicenseManagementPage.tsx b/frontend/admin-web/src/pages/compliance/LicenseManagementPage.tsx
new file mode 100644
index 0000000..4d0b984
--- /dev/null
+++ b/frontend/admin-web/src/pages/compliance/LicenseManagementPage.tsx
@@ -0,0 +1,236 @@
+import React from 'react';
+
+/**
+ * License & Regulatory Permits Management - 牌照与监管许可管理
+ *
+ * 管理平台在各司法管辖区的金融牌照、监管许可证状态,
+ * 包括MSB、MTL、各州Money Transmitter License等,追踪续期与申请进度。
+ */
+
+const licenseStats = [
+ { label: '活跃牌照数', value: '12', color: 'var(--color-success)' },
+ { label: '待申请', value: '4', color: 'var(--color-info)' },
+ { label: '即将到期', value: '2', color: 'var(--color-warning)' },
+ { label: '已吊销', value: '0', color: 'var(--color-error)' },
+];
+
+const licenses = [
+ { id: 'LIC-001', name: 'FinCEN MSB Registration', jurisdiction: 'Federal (US)', regBody: 'FinCEN', status: '有效', issueDate: '2024-06-01', expiryDate: '2026-06-01' },
+ { id: 'LIC-002', name: 'New York BitLicense', jurisdiction: 'New York', regBody: 'NYDFS', status: '有效', issueDate: '2024-09-15', expiryDate: '2026-09-15' },
+ { id: 'LIC-003', name: 'California MTL', jurisdiction: 'California', regBody: 'DFPI', status: '有效', issueDate: '2025-01-10', expiryDate: '2027-01-10' },
+ { id: 'LIC-004', name: 'Texas Money Transmitter', jurisdiction: 'Texas', regBody: 'TDSML', status: '即将到期', issueDate: '2024-03-20', expiryDate: '2026-03-20' },
+ { id: 'LIC-005', name: 'Florida Money Transmitter', jurisdiction: 'Florida', regBody: 'OFR', status: '有效', issueDate: '2025-04-01', expiryDate: '2027-04-01' },
+ { id: 'LIC-006', name: 'Illinois TOMA', jurisdiction: 'Illinois', regBody: 'IDFPR', status: '申请中', issueDate: '-', expiryDate: '-' },
+ { id: 'LIC-007', name: 'Washington Money Transmitter', jurisdiction: 'Washington', regBody: 'DFI', status: '有效', issueDate: '2025-02-15', expiryDate: '2027-02-15' },
+ { id: 'LIC-008', name: 'SEC Broker-Dealer Registration', jurisdiction: 'Federal (US)', regBody: 'SEC / FINRA', status: '申请中', issueDate: '-', expiryDate: '-' },
+ { id: 'LIC-009', name: 'Georgia Money Transmitter', jurisdiction: 'Georgia', regBody: 'DBF', status: '待续期', issueDate: '2024-02-28', expiryDate: '2026-02-28' },
+ { id: 'LIC-010', name: 'Nevada Money Transmitter', jurisdiction: 'Nevada', regBody: 'FID', status: '有效', issueDate: '2025-06-01', expiryDate: '2027-06-01' },
+];
+
+const regulatoryBodies = [
+ { name: 'FinCEN', fullName: 'Financial Crimes Enforcement Network', scope: '联邦反洗钱监管', licenses: 1 },
+ { name: 'SEC', fullName: 'Securities and Exchange Commission', scope: '证券交易监管', licenses: 1 },
+ { name: 'NYDFS', fullName: 'NY Dept. of Financial Services', scope: '纽约州金融服务', licenses: 1 },
+ { name: 'DFPI', fullName: 'CA Dept. of Financial Protection & Innovation', scope: '加州金融保护', licenses: 1 },
+ { name: 'FINRA', fullName: 'Financial Industry Regulatory Authority', scope: '经纪商自律监管', licenses: 1 },
+ { name: 'OCC', fullName: 'Office of the Comptroller of the Currency', scope: '联邦银行监管', licenses: 0 },
+];
+
+const renewalAlerts = [
+ { license: 'Texas Money Transmitter', expiryDate: '2026-03-20', daysRemaining: 38, urgency: 'high' },
+ { license: 'Georgia Money Transmitter', expiryDate: '2026-02-28', daysRemaining: 18, urgency: 'critical' },
+ { license: 'FinCEN MSB Registration', expiryDate: '2026-06-01', daysRemaining: 111, urgency: 'medium' },
+ { license: 'New York BitLicense', expiryDate: '2026-09-15', daysRemaining: 217, urgency: 'low' },
+];
+
+const getLicenseStatusStyle = (status: string) => {
+ switch (status) {
+ case '有效':
+ return { background: 'var(--color-success-light)', color: 'var(--color-success)' };
+ case '申请中':
+ return { background: 'var(--color-info-light)', color: 'var(--color-info)' };
+ case '待续期':
+ return { background: 'var(--color-warning-light)', color: 'var(--color-warning)' };
+ case '即将到期':
+ return { background: 'var(--color-error-light)', color: 'var(--color-error)' };
+ case '已过期':
+ return { background: 'var(--color-gray-100)', color: 'var(--color-error)' };
+ default:
+ return { background: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' };
+ }
+};
+
+const getUrgencyStyle = (urgency: string) => {
+ switch (urgency) {
+ case 'critical':
+ return { background: 'var(--color-error)', color: 'white' };
+ case 'high':
+ return { background: 'var(--color-error-light)', color: 'var(--color-error)' };
+ case 'medium':
+ return { background: 'var(--color-warning-light)', color: 'var(--color-warning)' };
+ case 'low':
+ return { background: 'var(--color-success-light)', color: 'var(--color-success)' };
+ default:
+ return { background: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' };
+ }
+};
+
+export const LicenseManagementPage: React.FC = () => {
+ return (
+
+
+
牌照与监管许可管理
+
+
+
+ {/* Stats */}
+
+ {licenseStats.map(s => (
+
+
{s.label}
+
{s.value}
+
+ ))}
+
+
+ {/* License Table */}
+
+
+
牌照清单
+
+
+
+
+ {['编号', '牌照名称', '司法管辖区', '监管机构', '签发日期', '到期日期', '状态', '操作'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {licenses.map(l => (
+
+ | {l.id} |
+ {l.name} |
+ {l.jurisdiction} |
+
+ {l.regBody}
+ |
+ {l.issueDate} |
+ {l.expiryDate} |
+
+ {l.status}
+ |
+
+
+ |
+
+ ))}
+
+
+
+
+
+ {/* Regulatory Body Mapping */}
+
+
监管机构映射
+ {regulatoryBodies.map((rb, i) => (
+
+
+ {rb.name.slice(0, 3)}
+
+
+
{rb.name}
+
{rb.fullName}
+
{rb.scope}
+
+
0 ? 'var(--color-success-light)' : 'var(--color-gray-100)',
+ color: rb.licenses > 0 ? 'var(--color-success)' : 'var(--color-text-tertiary)',
+ font: 'var(--text-caption)',
+ }}>
+ {rb.licenses > 0 ? `${rb.licenses} 牌照` : '未申请'}
+
+
+ ))}
+
+
+ {/* Renewal Alerts */}
+
+
续期提醒
+ {renewalAlerts.map((alert, i) => (
+
+
+ {alert.license}
+
+ {alert.urgency === 'critical' ? '紧急' : alert.urgency === 'high' ? '高' : alert.urgency === 'medium' ? '中' : '低'}
+
+
+
+ 到期日: {alert.expiryDate}
+
+ 剩余 {alert.daysRemaining} 天
+
+
+ {alert.urgency === 'critical' && (
+
+ )}
+
+ ))}
+ {/* Summary */}
+
+
+ 共 {licenses.filter(l => l.status === '有效').length} 个有效牌照覆盖 {new Set(licenses.filter(l => l.status === '有效').map(l => l.jurisdiction)).size} 个司法管辖区
+
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/compliance/SecFilingPage.tsx b/frontend/admin-web/src/pages/compliance/SecFilingPage.tsx
new file mode 100644
index 0000000..6609da0
--- /dev/null
+++ b/frontend/admin-web/src/pages/compliance/SecFilingPage.tsx
@@ -0,0 +1,205 @@
+import React from 'react';
+
+/**
+ * SEC Filing Management - SEC文件提交与管理
+ *
+ * 管理所有SEC申报文件(10-K, 10-Q, S-1, 8-K等),
+ * 追踪提交状态、审核进度、截止日期,以及自动生成披露文件的状态。
+ */
+
+const filingStats = [
+ { label: '已提交文件数', value: '24', color: 'var(--color-primary)' },
+ { label: '待审核', value: '3', color: 'var(--color-warning)' },
+ { label: '已通过', value: '19', color: 'var(--color-success)' },
+ { label: '距下次截止', value: '18天', color: 'var(--color-error)' },
+];
+
+const secFilings = [
+ { id: 'SEC-001', formType: 'S-1', title: 'IPO注册声明书', filingDate: '2026-01-15', deadline: '2026-02-28', status: '审核中', reviewer: 'SEC Division of Corporation Finance' },
+ { id: 'SEC-002', formType: '10-K', title: '2025年度报告', filingDate: '2026-01-30', deadline: '2026-03-31', status: '已提交', reviewer: 'Internal Audit' },
+ { id: 'SEC-003', formType: '10-Q', title: '2025 Q4季度报告', filingDate: '2026-02-01', deadline: '2026-02-15', status: '已通过', reviewer: 'External Auditor' },
+ { id: 'SEC-004', formType: '8-K', title: '重大事项披露-战略合作', filingDate: '2026-02-05', deadline: '2026-02-09', status: '已通过', reviewer: 'Legal Counsel' },
+ { id: 'SEC-005', formType: 'S-1/A', title: 'S-1修订稿(第2版)', filingDate: '2026-02-08', deadline: '2026-02-28', status: '需修订', reviewer: 'SEC Division of Corporation Finance' },
+ { id: 'SEC-006', formType: '10-Q', title: '2026 Q1季度报告', filingDate: '', deadline: '2026-05-15', status: '待提交', reviewer: '-' },
+];
+
+const timelineEvents = [
+ { date: '2026-02-15', event: '10-Q (Q4 2025) 截止', type: 'deadline', done: true },
+ { date: '2026-02-28', event: 'S-1 注册声明审核回复', type: 'deadline', done: false },
+ { date: '2026-03-15', event: '8-K 材料事件披露窗口', type: 'info', done: false },
+ { date: '2026-03-31', event: '10-K 年度报告截止', type: 'deadline', done: false },
+ { date: '2026-04-15', event: 'Proxy Statement 提交', type: 'deadline', done: false },
+ { date: '2026-05-15', event: '10-Q (Q1 2026) 截止', type: 'deadline', done: false },
+];
+
+const disclosureItems = [
+ { name: '风险因素 (Risk Factors)', status: 'done', lastUpdated: '2026-02-05' },
+ { name: '管理层讨论与分析 (MD&A)', status: 'done', lastUpdated: '2026-02-03' },
+ { name: '财务报表 (Financial Statements)', status: 'progress', lastUpdated: '2026-02-08' },
+ { name: '关联交易披露', status: 'progress', lastUpdated: '2026-02-07' },
+ { name: '高管薪酬披露', status: 'pending', lastUpdated: '-' },
+ { name: '公司治理结构', status: 'done', lastUpdated: '2026-01-28' },
+ { name: '法律诉讼披露', status: 'pending', lastUpdated: '-' },
+];
+
+const getFilingStatusStyle = (status: string) => {
+ switch (status) {
+ case '已通过':
+ return { background: 'var(--color-success-light)', color: 'var(--color-success)' };
+ case '审核中':
+ return { background: 'var(--color-info-light)', color: 'var(--color-info)' };
+ case '已提交':
+ return { background: 'var(--color-primary-light)', color: 'var(--color-primary)' };
+ case '需修订':
+ return { background: 'var(--color-error-light)', color: 'var(--color-error)' };
+ case '待提交':
+ return { background: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' };
+ default:
+ return { background: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' };
+ }
+};
+
+export const SecFilingPage: React.FC = () => {
+ return (
+
+
+
SEC文件管理
+
+
+
+ {/* Stats */}
+
+ {filingStats.map(s => (
+
+
{s.label}
+
{s.value}
+
+ ))}
+
+
+ {/* Filing Table */}
+
+
+
SEC申报文件列表
+
+
+
+
+ {['编号', '表格类型', '标题', '提交日期', '截止日期', '审核方', '状态', '操作'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {secFilings.map(f => (
+
+ | {f.id} |
+
+ {f.formType}
+ |
+ {f.title} |
+ {f.filingDate || '-'} |
+ {f.deadline} |
+ {f.reviewer} |
+
+ {f.status}
+ |
+
+
+ |
+
+ ))}
+
+
+
+
+
+ {/* Filing Timeline */}
+
+
申报日程
+ {timelineEvents.map((evt, i) => (
+
+
+ {evt.done ? '✓' : evt.type === 'deadline' ? '!' : 'i'}
+
+
+
{evt.event}
+
{evt.date}
+
+ {!evt.done && evt.type === 'deadline' && (
+
即将到期
+ )}
+
+ ))}
+
+
+ {/* Auto-generation Status */}
+
+
披露文件自动生成状态
+ {disclosureItems.map((item, i) => (
+
+
+ {item.status === 'done' ? '✓' : item.status === 'progress' ? '...' : '○'}
+
+
{item.name}
+
{item.lastUpdated}
+
+ {item.status === 'done' ? '已完成' : item.status === 'progress' ? '生成中' : '待开始'}
+
+
+ ))}
+ {/* Overall Progress */}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/compliance/SoxCompliancePage.tsx b/frontend/admin-web/src/pages/compliance/SoxCompliancePage.tsx
new file mode 100644
index 0000000..77ae7fa
--- /dev/null
+++ b/frontend/admin-web/src/pages/compliance/SoxCompliancePage.tsx
@@ -0,0 +1,286 @@
+import React from 'react';
+
+/**
+ * SOX Compliance (Sarbanes-Oxley) - SOX合规管理
+ *
+ * 管理SOX 404内部控制合规状态,包括ICFR、ITGC、访问控制、变更管理、运营控制等,
+ * 追踪测试结果、缺陷修复、审计师审核进度。
+ */
+
+const overallScore = 78;
+
+const controlCategories = [
+ {
+ name: '财务报告内控 (ICFR)',
+ description: '确保财务报告的准确性和完整性,包括收入确认、费用分摊、资产估值等关键控制点',
+ controls: [
+ { name: '收入确认控制', result: '通过', lastTest: '2026-01-15', nextTest: '2026-04-15' },
+ { name: '费用审批流程', result: '通过', lastTest: '2026-01-20', nextTest: '2026-04-20' },
+ { name: '期末关账控制', result: '发现缺陷', lastTest: '2026-02-01', nextTest: '2026-03-01' },
+ { name: '合并报表控制', result: '通过', lastTest: '2026-01-25', nextTest: '2026-04-25' },
+ ],
+ },
+ {
+ name: 'IT通用控制 (ITGC)',
+ description: '信息系统通用控制,包括系统开发、程序变更、计算机操作和数据安全',
+ controls: [
+ { name: '系统开发生命周期', result: '通过', lastTest: '2026-01-10', nextTest: '2026-04-10' },
+ { name: '程序变更管理', result: '通过', lastTest: '2026-01-18', nextTest: '2026-04-18' },
+ { name: '数据备份与恢复', result: '发现缺陷', lastTest: '2026-02-03', nextTest: '2026-03-03' },
+ { name: '逻辑安全控制', result: '待测试', lastTest: '-', nextTest: '2026-02-20' },
+ ],
+ },
+ {
+ name: '访问控制',
+ description: '系统与数据访问权限管理,包括用户权限分配、特权账户管理、职责分离',
+ controls: [
+ { name: '用户权限审查', result: '通过', lastTest: '2026-02-01', nextTest: '2026-05-01' },
+ { name: '特权账户管理', result: '通过', lastTest: '2026-01-28', nextTest: '2026-04-28' },
+ { name: '职责分离 (SoD)', result: '发现缺陷', lastTest: '2026-02-05', nextTest: '2026-03-05' },
+ ],
+ },
+ {
+ name: '变更管理',
+ description: '对生产环境变更的审批、测试、部署流程控制',
+ controls: [
+ { name: '变更审批流程', result: '通过', lastTest: '2026-01-22', nextTest: '2026-04-22' },
+ { name: '部署前测试验证', result: '通过', lastTest: '2026-01-30', nextTest: '2026-04-30' },
+ { name: '紧急变更管理', result: '待测试', lastTest: '-', nextTest: '2026-02-25' },
+ ],
+ },
+ {
+ name: '运营控制',
+ description: '日常运营流程控制,包括交易监控、对账、异常处理',
+ controls: [
+ { name: '日终对账', result: '通过', lastTest: '2026-02-08', nextTest: '2026-05-08' },
+ { name: '异常交易监控', result: '通过', lastTest: '2026-02-06', nextTest: '2026-05-06' },
+ { name: '客户资金隔离', result: '通过', lastTest: '2026-02-04', nextTest: '2026-05-04' },
+ ],
+ },
+];
+
+const deficiencies = [
+ { id: 'DEF-001', control: '期末关账控制', category: 'ICFR', severity: '重大缺陷', description: '部分手工调整缺少二级审批', foundDate: '2026-02-01', dueDate: '2026-03-01', status: '整改中', owner: 'CFO办公室' },
+ { id: 'DEF-002', control: '数据备份与恢复', category: 'ITGC', severity: '一般缺陷', description: 'DR演练未按季度执行', foundDate: '2026-02-03', dueDate: '2026-03-15', status: '整改中', owner: 'IT部门' },
+ { id: 'DEF-003', control: '职责分离 (SoD)', category: '访问控制', severity: '重大缺陷', description: '3名用户同时拥有创建与审批权限', foundDate: '2026-02-05', dueDate: '2026-02-20', status: '待整改', owner: '合规部门' },
+];
+
+const auditorReview = [
+ { phase: '审计计划确认', status: 'done', date: '2026-01-05', auditor: 'Deloitte' },
+ { phase: 'Walk-through 测试', status: 'done', date: '2026-01-20', auditor: 'Deloitte' },
+ { phase: '控制有效性测试', status: 'progress', date: '2026-02-10', auditor: 'Deloitte' },
+ { phase: '缺陷评估与分类', status: 'pending', date: '2026-03-01', auditor: 'Deloitte' },
+ { phase: '管理层报告出具', status: 'pending', date: '2026-03-15', auditor: 'Deloitte' },
+ { phase: '最终审计意见', status: 'pending', date: '2026-04-01', auditor: 'Deloitte' },
+];
+
+const getResultStyle = (result: string) => {
+ switch (result) {
+ case '通过':
+ return { background: 'var(--color-success-light)', color: 'var(--color-success)' };
+ case '发现缺陷':
+ return { background: 'var(--color-error-light)', color: 'var(--color-error)' };
+ case '待测试':
+ return { background: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' };
+ default:
+ return { background: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' };
+ }
+};
+
+const getSeverityStyle = (severity: string) => {
+ switch (severity) {
+ case '重大缺陷':
+ return { background: 'var(--color-error-light)', color: 'var(--color-error)' };
+ case '一般缺陷':
+ return { background: 'var(--color-warning-light)', color: 'var(--color-warning)' };
+ case '观察项':
+ return { background: 'var(--color-info-light)', color: 'var(--color-info)' };
+ default:
+ return { background: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' };
+ }
+};
+
+export const SoxCompliancePage: React.FC = () => {
+ const totalControls = controlCategories.reduce((sum, cat) => sum + cat.controls.length, 0);
+ const passedControls = controlCategories.reduce((sum, cat) => sum + cat.controls.filter(c => c.result === '通过').length, 0);
+ const defectControls = controlCategories.reduce((sum, cat) => sum + cat.controls.filter(c => c.result === '发现缺陷').length, 0);
+ const pendingControls = controlCategories.reduce((sum, cat) => sum + cat.controls.filter(c => c.result === '待测试').length, 0);
+
+ return (
+
+
SOX合规管理
+
+ {/* Compliance Score Gauge + Summary Stats */}
+
+ {/* Gauge */}
+
+
整体合规评分
+
= 80 ? 'var(--color-success)' : overallScore >= 60 ? 'var(--color-warning)' : 'var(--color-error)'} ${overallScore * 3.6}deg, var(--color-gray-100) 0deg)`,
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
+ }}>
+
+ = 80 ? 'var(--color-success)' : overallScore >= 60 ? 'var(--color-warning)' : 'var(--color-error)' }}>
+ {overallScore}
+
+
+
+
满分 100
+
+
+ {/* Summary Stats */}
+
+ {[
+ { label: '总控制点', value: String(totalControls), color: 'var(--color-primary)' },
+ { label: '测试通过', value: String(passedControls), color: 'var(--color-success)' },
+ { label: '发现缺陷', value: String(defectControls), color: 'var(--color-error)' },
+ { label: '待测试', value: String(pendingControls), color: 'var(--color-warning)' },
+ ].map(s => (
+
+
{s.label}
+
{s.value}
+
+ ))}
+
+
+
+ {/* Control Categories */}
+
+
+
内部控制类别
+
+ {controlCategories.map((cat, catIdx) => (
+
+ {/* Category Header */}
+
+
{cat.name}
+
{cat.description}
+
+ {/* Controls */}
+
+
+
+ {['控制点', '测试结果', '上次测试', '下次测试'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {cat.controls.map((ctrl, ctrlIdx) => (
+
+ | {ctrl.name} |
+
+ {ctrl.result}
+ |
+ {ctrl.lastTest} |
+ {ctrl.nextTest} |
+
+ ))}
+
+
+
+ ))}
+
+
+
+ {/* Deficiency Tracking */}
+
+
+
缺陷追踪
+
+
+
+
+ {['编号', '控制点', '严重程度', '描述', '整改期限', '状态', '负责方'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {deficiencies.map(d => (
+
+ | {d.id} |
+ {d.control} |
+
+ {d.severity}
+ |
+ {d.description} |
+ {d.dueDate} |
+
+ {d.status}
+ |
+ {d.owner} |
+
+ ))}
+
+
+
+
+ {/* Auditor Review Status */}
+
+
审计师审核进度
+
External Auditor: Deloitte
+ {auditorReview.map((phase, i) => (
+
+
+ {phase.status === 'done' ? '✓' : phase.status === 'progress' ? '...' : '○'}
+
+
+
{phase.phase}
+
{phase.date}
+
+
+ {phase.status === 'done' ? '已完成' : phase.status === 'progress' ? '进行中' : '待开始'}
+
+
+ ))}
+ {/* Progress Bar */}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/compliance/TaxCompliancePage.tsx b/frontend/admin-web/src/pages/compliance/TaxCompliancePage.tsx
new file mode 100644
index 0000000..8a9e450
--- /dev/null
+++ b/frontend/admin-web/src/pages/compliance/TaxCompliancePage.tsx
@@ -0,0 +1,272 @@
+import React from 'react';
+
+/**
+ * Tax Compliance Management - 税务合规管理
+ *
+ * 管理平台在各司法管辖区的税务义务,追踪联邦和州级税务申报,
+ * 包括所得税、销售税、预扣税、交易税等,以及IRS表格提交进度。
+ */
+
+const taxStats = [
+ { label: '应纳税额', value: '$1,245,890', color: 'var(--color-primary)' },
+ { label: '已缴税额', value: '$982,450', color: 'var(--color-success)' },
+ { label: '税务合规率', value: '96.8%', color: 'var(--color-info)' },
+ { label: '待处理事项', value: '5', color: 'var(--color-warning)' },
+];
+
+const taxObligations = [
+ { jurisdiction: 'Federal', taxType: 'Corporate Income Tax', period: 'FY 2025', amount: '$425,000', paid: '$425,000', status: '已缴', dueDate: '2026-04-15' },
+ { jurisdiction: 'Federal', taxType: 'Employment Tax (FICA)', period: 'Q4 2025', amount: '$68,200', paid: '$68,200', status: '已缴', dueDate: '2026-01-31' },
+ { jurisdiction: 'California', taxType: 'State Income Tax', amount: '$187,500', paid: '$187,500', period: 'FY 2025', status: '已缴', dueDate: '2026-04-15' },
+ { jurisdiction: 'California', taxType: 'Sales & Use Tax', amount: '$42,300', paid: '$42,300', period: 'Q4 2025', status: '已缴', dueDate: '2026-01-31' },
+ { jurisdiction: 'New York', taxType: 'State Income Tax', amount: '$156,800', paid: '$120,000', period: 'FY 2025', status: '部分缴纳', dueDate: '2026-04-15' },
+ { jurisdiction: 'New York', taxType: 'Metropolitan Commuter Tax', amount: '$12,400', paid: '$0', period: 'FY 2025', status: '待缴', dueDate: '2026-04-15' },
+ { jurisdiction: 'Texas', taxType: 'Franchise Tax', amount: '$34,600', paid: '$34,600', period: 'FY 2025', status: '已缴', dueDate: '2026-05-15' },
+ { jurisdiction: 'Florida', taxType: 'Sales Tax', amount: '$28,900', paid: '$28,900', period: 'Q4 2025', status: '已缴', dueDate: '2026-01-31' },
+ { jurisdiction: 'Federal', taxType: 'Estimated Tax (Q1 2026)', amount: '$263,190', paid: '$0', period: 'Q1 2026', status: '待缴', dueDate: '2026-04-15' },
+];
+
+const taxTypeBreakdown = [
+ { type: 'Income Tax (所得税)', federal: '$425,000', state: '$344,300', total: '$769,300', percentage: 61.7 },
+ { type: 'Sales/Use Tax (销售税)', federal: '-', state: '$71,200', total: '$71,200', percentage: 5.7 },
+ { type: 'Employment/Withholding Tax (预扣税)', federal: '$68,200', state: '$45,600', total: '$113,800', percentage: 9.1 },
+ { type: 'Transaction Tax (交易税)', federal: '$28,400', state: '$0', total: '$28,400', percentage: 2.3 },
+ { type: 'Estimated Tax (预估税)', federal: '$263,190', state: '$0', total: '$263,190', percentage: 21.2 },
+];
+
+const irsFilings = [
+ { form: 'Form 1120', description: '公司所得税申报', taxYear: '2025', deadline: '2026-04-15', status: '准备中', filedDate: '-' },
+ { form: 'Form 1099-K', description: '支付卡和第三方网络交易', taxYear: '2025', deadline: '2026-01-31', status: '已提交', filedDate: '2026-01-28' },
+ { form: 'Form 1099-MISC', description: '杂项收入(承包商支付)', taxYear: '2025', deadline: '2026-01-31', status: '已提交', filedDate: '2026-01-29' },
+ { form: 'Form 1099-NEC', description: '非雇员报酬', taxYear: '2025', deadline: '2026-01-31', status: '已提交', filedDate: '2026-01-30' },
+ { form: 'Form 941', description: '雇主季度联邦税', taxYear: 'Q4 2025', deadline: '2026-01-31', status: '已提交', filedDate: '2026-01-25' },
+ { form: 'Form W-2', description: '工资与税务声明', taxYear: '2025', deadline: '2026-01-31', status: '已提交', filedDate: '2026-01-27' },
+ { form: 'Form 1042-S', description: '外国人预扣所得', taxYear: '2025', deadline: '2026-03-15', status: '准备中', filedDate: '-' },
+ { form: 'Form 8300', description: '现金支付超$10,000报告', taxYear: '2025', deadline: '交易后15天', status: '按需提交', filedDate: '-' },
+];
+
+const taxDeadlines = [
+ { date: '2026-01-31', event: 'Form 1099-K/1099-MISC/W-2 提交截止', done: true },
+ { date: '2026-01-31', event: 'Q4 Employment Tax (Form 941) 截止', done: true },
+ { date: '2026-03-15', event: 'Form 1042-S 外国人预扣税申报截止', done: false },
+ { date: '2026-04-15', event: 'Form 1120 公司所得税申报截止', done: false },
+ { date: '2026-04-15', event: 'Q1 2026 Estimated Tax Payment', done: false },
+ { date: '2026-04-15', event: 'CA/NY State Income Tax 截止', done: false },
+ { date: '2026-05-15', event: 'Texas Franchise Tax 截止', done: false },
+ { date: '2026-06-15', event: 'Q2 2026 Estimated Tax Payment', done: false },
+];
+
+const getPaymentStatusStyle = (status: string) => {
+ switch (status) {
+ case '已缴':
+ return { background: 'var(--color-success-light)', color: 'var(--color-success)' };
+ case '部分缴纳':
+ return { background: 'var(--color-warning-light)', color: 'var(--color-warning)' };
+ case '待缴':
+ return { background: 'var(--color-error-light)', color: 'var(--color-error)' };
+ default:
+ return { background: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' };
+ }
+};
+
+const getFilingStatusStyle = (status: string) => {
+ switch (status) {
+ case '已提交':
+ return { background: 'var(--color-success-light)', color: 'var(--color-success)' };
+ case '准备中':
+ return { background: 'var(--color-warning-light)', color: 'var(--color-warning)' };
+ case '按需提交':
+ return { background: 'var(--color-info-light)', color: 'var(--color-info)' };
+ case '逾期':
+ return { background: 'var(--color-error-light)', color: 'var(--color-error)' };
+ default:
+ return { background: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' };
+ }
+};
+
+export const TaxCompliancePage: React.FC = () => {
+ return (
+
+
+
税务合规管理
+
+
+
+ {/* Stats */}
+
+ {taxStats.map(s => (
+
+
{s.label}
+
{s.value}
+
+ ))}
+
+
+ {/* Tax Obligations by Jurisdiction */}
+
+
+
各司法管辖区税务义务
+
+
+
+
+ {['管辖区', '税种', '期间', '应缴金额', '已缴金额', '截止日期', '状态'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {taxObligations.map((t, i) => (
+
+ |
+ {t.jurisdiction}
+ |
+ {t.taxType} |
+ {t.period} |
+ {t.amount} |
+ {t.paid} |
+ {t.dueDate} |
+
+ {t.status}
+ |
+
+ ))}
+
+
+
+
+ {/* Tax Type Breakdown + IRS Filings */}
+
+ {/* Tax Type Breakdown */}
+
+
+
税种分类汇总
+
+
+
+
+ {['税种', '联邦', '州级', '合计', '占比'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {taxTypeBreakdown.map((row, i) => (
+
+ | {row.type} |
+ {row.federal} |
+ {row.state} |
+ {row.total} |
+
+
+ |
+
+ ))}
+
+
+
+
+ {/* Tax Calendar / Deadlines */}
+
+
税务日历
+ {taxDeadlines.map((evt, i) => (
+
+
+ {evt.done ? '✓' : '!'}
+
+
+
{evt.event}
+
{evt.date}
+
+
+ {evt.done ? '已完成' : '待处理'}
+
+
+ ))}
+
+
+
+ {/* IRS Filing Tracker */}
+
+
+
IRS表格提交追踪
+
+
+
+
+ {['表格', '说明', '税务年度', '截止日期', '提交日期', '状态'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {irsFilings.map((f, i) => (
+
+ |
+ {f.form}
+ |
+ {f.description} |
+ {f.taxYear} |
+ {f.deadline} |
+ {f.filedDate} |
+
+ {f.status}
+ |
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/coupons/CouponManagementPage.tsx b/frontend/admin-web/src/pages/coupons/CouponManagementPage.tsx
new file mode 100644
index 0000000..318ec2e
--- /dev/null
+++ b/frontend/admin-web/src/pages/coupons/CouponManagementPage.tsx
@@ -0,0 +1,117 @@
+import React, { useState } from 'react';
+
+/**
+ * D2. 券管理 - 平台券审核与管理
+ *
+ * 发行方提交的券审核、已上架券管理、券数据统计
+ */
+
+interface CouponBatch {
+ id: string;
+ issuer: string;
+ name: string;
+ template: string;
+ faceValue: number;
+ quantity: number;
+ sold: number;
+ redeemed: number;
+ status: 'pending' | 'active' | 'suspended' | 'expired';
+ createdAt: string;
+}
+
+const mockCoupons: CouponBatch[] = [
+ { id: 'C001', issuer: 'Starbucks', name: '¥25 礼品卡', template: '礼品卡', faceValue: 25, quantity: 5000, sold: 4200, redeemed: 3300, status: 'active', createdAt: '2026-01-15' },
+ { id: 'C002', issuer: 'Amazon', name: '¥100 购物券', template: '代金券', faceValue: 100, quantity: 2000, sold: 1580, redeemed: 980, status: 'active', createdAt: '2026-01-20' },
+ { id: 'C003', issuer: 'Nike', name: '8折运动券', template: '折扣券', faceValue: 80, quantity: 1000, sold: 0, redeemed: 0, status: 'pending', createdAt: '2026-02-08' },
+ { id: 'C004', issuer: 'Walmart', name: '¥50 生活券', template: '代金券', faceValue: 50, quantity: 3000, sold: 3000, redeemed: 2800, status: 'expired', createdAt: '2025-08-01' },
+];
+
+const statusColors: Record = {
+ pending: 'var(--color-warning)',
+ active: 'var(--color-success)',
+ suspended: 'var(--color-error)',
+ expired: 'var(--color-text-tertiary)',
+};
+const statusLabels: Record = {
+ pending: '待审核',
+ active: '在售中',
+ suspended: '已暂停',
+ expired: '已过期',
+};
+
+export const CouponManagementPage: React.FC = () => {
+ const [filter, setFilter] = useState('all');
+
+ const filtered = filter === 'all' ? mockCoupons : mockCoupons.filter(c => c.status === filter);
+
+ return (
+
+
+
券管理
+
+ {['all', 'pending', 'active', 'suspended', 'expired'].map(f => (
+
+ ))}
+
+
+
+ {/* Coupon Table */}
+
+
+
+
+ {['券ID', '发行方', '券名称', '模板', '面值', '发行量', '已售', '已核销', '状态', '操作'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {filtered.map(coupon => (
+
+ | {coupon.id} |
+ {coupon.issuer} |
+ {coupon.name} |
+ {coupon.template} |
+ ${coupon.faceValue} |
+ {coupon.quantity.toLocaleString()} |
+ {coupon.sold.toLocaleString()} |
+ {coupon.redeemed.toLocaleString()} |
+
+ {statusLabels[coupon.status]}
+ |
+
+ {coupon.status === 'pending' && (
+
+
+
+
+ )}
+ {coupon.status === 'active' && (
+
+ )}
+ |
+
+ ))}
+
+
+
+
+ );
+};
+
+const cellStyle: React.CSSProperties = { padding: '12px 16px', font: 'var(--text-body)', color: 'var(--color-text-primary)' };
+const btnStyle = (color: string): React.CSSProperties => ({
+ padding: '4px 12px', border: `1px solid ${color}`, borderRadius: 'var(--radius-sm)',
+ background: 'transparent', color, cursor: 'pointer', font: 'var(--text-label-sm)',
+});
diff --git a/frontend/admin-web/src/pages/dashboard/DashboardPage.tsx b/frontend/admin-web/src/pages/dashboard/DashboardPage.tsx
new file mode 100644
index 0000000..8ad65e7
--- /dev/null
+++ b/frontend/admin-web/src/pages/dashboard/DashboardPage.tsx
@@ -0,0 +1,210 @@
+import React from 'react';
+
+/**
+ * D1. 平台运营仪表盘
+ *
+ * 平台总交易量/金额、活跃用户数、发行方数量、券流通总量、
+ * 实时交易流、系统健康状态
+ */
+
+interface StatCard {
+ label: string;
+ value: string;
+ change: string;
+ trend: 'up' | 'down';
+ color: string;
+}
+
+const stats: StatCard[] = [
+ { label: '总交易量', value: '156,890', change: '+12.3%', trend: 'up', color: 'var(--color-primary)' },
+ { label: '交易金额', value: '$4,523,456', change: '+8.7%', trend: 'up', color: 'var(--color-success)' },
+ { label: '活跃用户', value: '28,456', change: '+5.2%', trend: 'up', color: 'var(--color-info)' },
+ { label: '发行方数量', value: '342', change: '+15', trend: 'up', color: 'var(--color-warning)' },
+ { label: '券流通总量', value: '1,234,567', change: '-2.1%', trend: 'down', color: 'var(--color-primary-dark)' },
+ { label: '系统健康', value: '99.97%', change: 'Normal', trend: 'up', color: 'var(--color-success)' },
+];
+
+export const DashboardPage: React.FC = () => {
+ return (
+
+
+ 运营总览
+
+
+ {/* Stats Grid */}
+
+ {stats.map(stat => (
+
+
+ {stat.label}
+
+
+ {stat.value}
+
+
+ {stat.change}
+
+
+ ))}
+
+
+ {/* Charts Row */}
+
+ {/* Transaction Volume Chart */}
+
+
交易量趋势
+
+ Recharts / ECharts 折线图
+
+
+
+ {/* Transaction Type Pie */}
+
+
交易类型占比
+
+ 饼图 (一级/二级/核销/转赠)
+
+
+
+
+ {/* Real-time Transaction Feed + System Health */}
+
+ {/* Real-time Feed */}
+
+
+
+
+
+ {['时间', '类型', '订单号', '金额', '状态'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {[
+ { time: '14:32:15', type: '购买', order: 'GNX-20260210-001234', amount: '$21.25', status: '完成' },
+ { time: '14:31:58', type: '核销', order: 'GNX-20260210-001233', amount: '$50.00', status: '完成' },
+ { time: '14:31:42', type: '转售', order: 'GNX-20260210-001232', amount: '$85.00', status: '完成' },
+ { time: '14:31:20', type: '购买', order: 'GNX-20260210-001231', amount: '$42.50', status: '处理中' },
+ { time: '14:30:55', type: '转赠', order: 'GNX-20260210-001230', amount: '$30.00', status: '完成' },
+ ].map((row, i) => (
+
+ | {row.time} |
+ {row.type} |
+ {row.order} |
+ {row.amount} |
+
+
+ {row.status}
+
+ |
+
+ ))}
+
+
+
+
+ {/* System Health */}
+
+
系统健康
+ {[
+ { name: 'API服务', status: 'healthy', latency: '12ms' },
+ { name: '数据库', status: 'healthy', latency: '3ms' },
+ { name: 'Genex Chain', status: 'healthy', latency: '156ms' },
+ { name: '缓存服务', status: 'healthy', latency: '1ms' },
+ { name: '消息队列', status: 'warning', latency: '45ms' },
+ ].map(service => (
+
+
+ {service.name}
+ {service.latency}
+
+ ))}
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/disputes/DisputePage.tsx b/frontend/admin-web/src/pages/disputes/DisputePage.tsx
new file mode 100644
index 0000000..f19add2
--- /dev/null
+++ b/frontend/admin-web/src/pages/disputes/DisputePage.tsx
@@ -0,0 +1,141 @@
+import React from 'react';
+
+/**
+ * D8. 争议处理
+ *
+ * 工单列表(买方申诉/卖方申诉/退款申请)、状态、处理时效
+ * 工单详情:双方信息、链上交易记录(证据)、仲裁操作
+ */
+
+interface Dispute {
+ id: string;
+ type: '买方申诉' | '卖方申诉' | '退款申请';
+ order: string;
+ plaintiff: string;
+ defendant: string;
+ amount: string;
+ status: 'pending' | 'processing' | 'resolved' | 'rejected';
+ createdAt: string;
+ sla: string;
+}
+
+const mockDisputes: Dispute[] = [
+ { id: 'DSP-001', type: '买方申诉', order: 'GNX-20260208-001200', plaintiff: 'U-012', defendant: 'U-045', amount: '$85.00', status: 'pending', createdAt: '2026-02-09', sla: '23h' },
+ { id: 'DSP-002', type: '退款申请', order: 'GNX-20260207-001150', plaintiff: 'U-023', defendant: '-', amount: '$42.50', status: 'processing', createdAt: '2026-02-08', sla: '6h' },
+ { id: 'DSP-003', type: '卖方申诉', order: 'GNX-20260206-001100', plaintiff: 'U-078', defendant: 'U-091', amount: '$120.00', status: 'pending', createdAt: '2026-02-07', sla: '47h' },
+ { id: 'DSP-004', type: '买方申诉', order: 'GNX-20260205-001050', plaintiff: 'U-034', defendant: 'U-056', amount: '$30.00', status: 'resolved', createdAt: '2026-02-05', sla: '-' },
+ { id: 'DSP-005', type: '退款申请', order: 'GNX-20260204-001000', plaintiff: 'U-067', defendant: '-', amount: '$21.25', status: 'rejected', createdAt: '2026-02-04', sla: '-' },
+];
+
+const statusConfig: Record = {
+ pending: { label: '待处理', bg: 'var(--color-warning-light)', color: 'var(--color-warning)' },
+ processing: { label: '处理中', bg: 'var(--color-info-light)', color: 'var(--color-info)' },
+ resolved: { label: '已解决', bg: 'var(--color-success-light)', color: 'var(--color-success)' },
+ rejected: { label: '已驳回', bg: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' },
+};
+
+const typeConfig: Record = {
+ '买方申诉': { bg: 'var(--color-error-light)', color: 'var(--color-error)' },
+ '卖方申诉': { bg: 'var(--color-warning-light)', color: 'var(--color-warning)' },
+ '退款申请': { bg: 'var(--color-info-light)', color: 'var(--color-info)' },
+};
+
+export const DisputePage: React.FC = () => {
+ return (
+
+
+
争议处理
+
+ {/* Stats */}
+ {[
+ { label: '待处理', value: '3', color: 'var(--color-warning)' },
+ { label: '处理中', value: '1', color: 'var(--color-info)' },
+ { label: '今日解决', value: '5', color: 'var(--color-success)' },
+ ].map(s => (
+
+ {s.value}
+ {s.label}
+
+ ))}
+
+
+
+ {/* Disputes Table */}
+
+
+
+
+ {['工单号', '类型', '关联订单', '申诉方', '被诉方', '金额', '状态', '处理时效', '创建时间', '操作'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {mockDisputes.map(d => {
+ const sc = statusConfig[d.status];
+ const tc = typeConfig[d.type];
+ return (
+
+ | {d.id} |
+
+ {d.type}
+ |
+ {d.order} |
+ {d.plaintiff} |
+ {d.defendant} |
+ {d.amount} |
+
+ {sc.label}
+ |
+
+ {d.sla !== '-' ? (
+ 24 ? 'var(--color-error)' : 'var(--color-text-secondary)',
+ }}>{d.sla}
+ ) : (
+ -
+ )}
+ |
+ {d.createdAt} |
+
+
+ |
+
+ );
+ })}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/finance/FinanceManagementPage.tsx b/frontend/admin-web/src/pages/finance/FinanceManagementPage.tsx
new file mode 100644
index 0000000..fcf2c56
--- /dev/null
+++ b/frontend/admin-web/src/pages/finance/FinanceManagementPage.tsx
@@ -0,0 +1,91 @@
+import React from 'react';
+
+/**
+ * D3. 财务管理 - 平台级财务总览
+ *
+ * 平台收入(手续费)、发行方结算、消费者退款、资金池监控
+ */
+
+const financeStats = [
+ { label: '平台手续费收入', value: '$234,567', period: '本月', color: 'var(--color-success)' },
+ { label: '待结算给发行方', value: '$1,456,000', period: '累计', color: 'var(--color-warning)' },
+ { label: '消费者退款', value: '$12,340', period: '本月', color: 'var(--color-error)' },
+ { label: '资金池余额', value: '$8,234,567', period: '实时', color: 'var(--color-primary)' },
+];
+
+const recentSettlements = [
+ { issuer: 'Starbucks', amount: '$45,200', status: '已结算', time: '2026-02-10 14:00' },
+ { issuer: 'Amazon', amount: '$128,000', status: '处理中', time: '2026-02-10 12:00' },
+ { issuer: 'Nike', amount: '$23,500', status: '待结算', time: '2026-02-09' },
+ { issuer: 'Walmart', amount: '$67,800', status: '已结算', time: '2026-02-08' },
+];
+
+export const FinanceManagementPage: React.FC = () => {
+ return (
+
+
财务管理
+
+ {/* Stats */}
+
+ {financeStats.map(s => (
+
+
{s.label}
+
{s.value}
+
{s.period}
+
+ ))}
+
+
+
+ {/* Settlement Queue */}
+
+
结算队列
+
+
+ {['发行方', '金额', '状态', '时间'].map(h => (
+ | {h} |
+ ))}
+
+
+ {recentSettlements.map((s, i) => (
+
+ | {s.issuer} |
+ {s.amount} |
+
+ {s.status}
+ |
+ {s.time} |
+
+ ))}
+
+
+
+
+ {/* Revenue Chart Placeholder */}
+
+
收入趋势
+
+ 月度手续费收入趋势图
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/insurance/InsurancePage.tsx b/frontend/admin-web/src/pages/insurance/InsurancePage.tsx
new file mode 100644
index 0000000..c66e73f
--- /dev/null
+++ b/frontend/admin-web/src/pages/insurance/InsurancePage.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+
+/**
+ * D8. 保险与消费者保护 - 平台保障体系管理
+ *
+ * 消费者保护基金、保险机制、赔付记录、IPO准备度
+ */
+
+const protectionStats = [
+ { label: '消费者保护基金', value: '$2,345,678', color: 'var(--color-success)' },
+ { label: '本月赔付', value: '$12,340', color: 'var(--color-warning)' },
+ { label: '赔付率', value: '0.08%', color: 'var(--color-info)' },
+ { label: 'IPO准备度', value: '72%', color: 'var(--color-primary)' },
+];
+
+const recentClaims = [
+ { id: 'CLM-001', user: 'User#12345', reason: '发行方破产', amount: '$250', status: '已赔付', date: '2026-02-08' },
+ { id: 'CLM-002', user: 'User#23456', reason: '券核销失败', amount: '$100', status: '处理中', date: '2026-02-09' },
+ { id: 'CLM-003', user: 'User#34567', reason: '重复扣款', amount: '$42.50', status: '已赔付', date: '2026-02-07' },
+];
+
+const ipoChecklist = [
+ { item: 'SOX合规审计', status: 'done' },
+ { item: '消费者保护机制', status: 'done' },
+ { item: 'AML/KYC合规体系', status: 'done' },
+ { item: 'SEC披露文件准备', status: 'progress' },
+ { item: '独立审计报告', status: 'progress' },
+ { item: '市场做市商协议', status: 'pending' },
+ { item: '牌照申请', status: 'pending' },
+];
+
+export const InsurancePage: React.FC = () => {
+ return (
+
+
保险与消费者保护
+
+ {/* Stats */}
+
+ {protectionStats.map(s => (
+
+
{s.label}
+
{s.value}
+
+ ))}
+
+
+
+ {/* Claims */}
+
+
最近赔付记录
+
+
+ {['编号', '用户', '原因', '金额', '状态'].map(h => (
+ | {h} |
+ ))}
+
+
+ {recentClaims.map(c => (
+
+ | {c.id} |
+ {c.user} |
+ {c.reason} |
+ {c.amount} |
+
+ {c.status}
+ |
+
+ ))}
+
+
+
+
+ {/* IPO Readiness */}
+
+
IPO准备度检查清单
+ {ipoChecklist.map((item, i) => (
+
+
+ {item.status === 'done' ? '✓' : item.status === 'progress' ? '…' : '○'}
+
+
{item.item}
+
+ {item.status === 'done' ? '已完成' : item.status === 'progress' ? '进行中' : '待开始'}
+
+
+ ))}
+ {/* Progress Bar */}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/issuers/IssuerManagementPage.tsx b/frontend/admin-web/src/pages/issuers/IssuerManagementPage.tsx
new file mode 100644
index 0000000..ade1547
--- /dev/null
+++ b/frontend/admin-web/src/pages/issuers/IssuerManagementPage.tsx
@@ -0,0 +1,203 @@
+import React, { useState } from 'react';
+
+/**
+ * D2. 发行方管理
+ *
+ * 入驻审核列表、发行方详情、券审核列表
+ */
+
+interface Issuer {
+ id: string;
+ name: string;
+ creditRating: string;
+ status: 'pending' | 'approved' | 'rejected';
+ submittedAt: string;
+ couponCount: number;
+ totalVolume: string;
+}
+
+const mockIssuers: Issuer[] = [
+ { id: 'ISS-001', name: 'Starbucks Inc.', creditRating: 'AAA', status: 'approved', submittedAt: '2026-01-15', couponCount: 12, totalVolume: '$128,450' },
+ { id: 'ISS-002', name: 'Amazon Corp.', creditRating: 'AA', status: 'approved', submittedAt: '2026-01-20', couponCount: 8, totalVolume: '$456,000' },
+ { id: 'ISS-003', name: 'NewBrand LLC', creditRating: '-', status: 'pending', submittedAt: '2026-02-09', couponCount: 0, totalVolume: '-' },
+ { id: 'ISS-004', name: 'Target Corp.', creditRating: 'A', status: 'approved', submittedAt: '2026-01-25', couponCount: 5, totalVolume: '$67,200' },
+ { id: 'ISS-005', name: 'FakeStore Inc.', creditRating: '-', status: 'rejected', submittedAt: '2026-02-05', couponCount: 0, totalVolume: '-' },
+];
+
+export const IssuerManagementPage: React.FC = () => {
+ const [tab, setTab] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all');
+
+ const filtered = tab === 'all' ? mockIssuers : mockIssuers.filter(i => i.status === tab);
+
+ const creditColor = (rating: string) => {
+ const map: Record = {
+ 'AAA': 'var(--color-credit-aaa)',
+ 'AA': 'var(--color-credit-aa)',
+ 'A': 'var(--color-credit-a)',
+ 'BBB': 'var(--color-credit-bbb)',
+ };
+ return map[rating] || 'var(--color-text-tertiary)';
+ };
+
+ const statusStyle = (status: string) => {
+ const map: Record = {
+ pending: { bg: 'var(--color-warning-light)', color: 'var(--color-warning)' },
+ approved: { bg: 'var(--color-success-light)', color: 'var(--color-success)' },
+ rejected: { bg: 'var(--color-error-light)', color: 'var(--color-error)' },
+ };
+ return map[status] || { bg: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' };
+ };
+
+ const statusLabel = (status: string) => {
+ const map: Record = { pending: '待审核', approved: '已通过', rejected: '已驳回' };
+ return map[status] || status;
+ };
+
+ return (
+
+
+
发行方管理
+
+
+
+
+
+ {/* Tabs */}
+
+ {(['all', 'pending', 'approved', 'rejected'] as const).map(t => (
+
+ ))}
+
+
+ {/* Table */}
+
+
+
+
+ {['ID', '企业名称', '信用评级', '状态', '提交时间', '券数量', '总发行额', '操作'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {filtered.map(issuer => {
+ const ss = statusStyle(issuer.status);
+ return (
+
+ |
+ {issuer.id}
+ |
+ {issuer.name} |
+
+
+ {issuer.creditRating}
+
+ |
+
+
+ {statusLabel(issuer.status)}
+
+ |
+
+ {issuer.submittedAt}
+ |
+ {issuer.couponCount} |
+
+ {issuer.totalVolume}
+ |
+
+
+ {issuer.status === 'pending' && (
+
+ )}
+ |
+
+ );
+ })}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/merchant/MerchantRedemptionPage.tsx b/frontend/admin-web/src/pages/merchant/MerchantRedemptionPage.tsx
new file mode 100644
index 0000000..61d2e86
--- /dev/null
+++ b/frontend/admin-web/src/pages/merchant/MerchantRedemptionPage.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+
+/**
+ * D6. 商户核销管理 - 平台视角的核销数据
+ *
+ * 核销统计、门店核销排行、异常核销检测
+ */
+
+const redemptionStats = [
+ { label: '今日核销', value: '1,234', change: '+15%', color: 'var(--color-success)' },
+ { label: '今日核销金额', value: '$45,600', change: '+8%', color: 'var(--color-primary)' },
+ { label: '活跃门店', value: '89', change: '+3', color: 'var(--color-info)' },
+ { label: '异常核销', value: '2', change: '需审核', color: 'var(--color-error)' },
+];
+
+const topStores = [
+ { rank: 1, store: 'Starbucks 徐汇店', count: 156, amount: '$3,900' },
+ { rank: 2, store: 'Amazon Locker #A23', count: 98, amount: '$9,800' },
+ { rank: 3, store: 'Nike 南京西路店', count: 67, amount: '$5,360' },
+ { rank: 4, store: 'Walmart 浦东店', count: 45, amount: '$2,250' },
+ { rank: 5, store: 'Target Downtown', count: 34, amount: '$1,020' },
+];
+
+export const MerchantRedemptionPage: React.FC = () => {
+ return (
+
+
商户核销管理
+
+ {/* Stats */}
+
+ {redemptionStats.map(s => (
+
+
{s.label}
+
{s.value}
+
{s.change}
+
+ ))}
+
+
+
+ {/* Top Stores */}
+
+
门店核销排行
+ {topStores.map(s => (
+
+ {s.rank}
+ {s.store}
+ {s.count}笔
+ {s.amount}
+
+ ))}
+
+
+ {/* Realtime Feed */}
+
+
实时核销流
+ {[
+ { store: 'Starbucks 徐汇店', coupon: '¥25 礼品卡', time: '刚刚' },
+ { store: 'Nike 南京西路店', coupon: '¥80 运动券', time: '1分钟前' },
+ { store: 'Amazon Locker #A23', coupon: '¥100 购物券', time: '3分钟前' },
+ { store: 'Starbucks 徐汇店', coupon: '¥25 礼品卡 x2', time: '5分钟前' },
+ { store: 'Walmart 浦东店', coupon: '¥50 生活券', time: '8分钟前' },
+ ].map((r, i) => (
+
+
+
+
{r.store}
+
{r.coupon}
+
+
{r.time}
+
+ ))}
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/reports/ReportsPage.tsx b/frontend/admin-web/src/pages/reports/ReportsPage.tsx
new file mode 100644
index 0000000..a58153b
--- /dev/null
+++ b/frontend/admin-web/src/pages/reports/ReportsPage.tsx
@@ -0,0 +1,108 @@
+import React from 'react';
+
+/**
+ * D5. 报表中心 - 运营报表、合规报表、数据导出
+ *
+ * 日/周/月运营报表、监管合规报表、自定义数据导出
+ * 包括:SOX审计、SEC Filing、税务合规
+ */
+
+const reportCategories = [
+ {
+ title: '运营报表',
+ icon: '📊',
+ reports: [
+ { name: '日度运营报表', desc: '交易量/金额/用户/核销率', status: '已生成', date: '2026-02-10' },
+ { name: '周度运营报表', desc: '周趋势分析', status: '已生成', date: '2026-02-09' },
+ { name: '月度运营报表', desc: '月度综合分析', status: '已生成', date: '2026-01-31' },
+ ],
+ },
+ {
+ title: '合规报表',
+ icon: '📋',
+ reports: [
+ { name: 'SAR可疑活动报告', desc: '本月可疑交易汇总', status: '待审核', date: '2026-02-10' },
+ { name: 'CTR大额交易报告', desc: '>$10,000交易申报', status: '已提交', date: '2026-02-10' },
+ { name: 'OFAC筛查报告', desc: '制裁名单筛查结果', status: '已生成', date: '2026-02-09' },
+ ],
+ },
+ {
+ title: '财务报表',
+ icon: '💰',
+ reports: [
+ { name: '发行方结算报表', desc: '各发行方结算明细', status: '已生成', date: '2026-02-10' },
+ { name: '平台收入报表', desc: '手续费/Breakage收入', status: '已生成', date: '2026-01-31' },
+ { name: '税务合规报表', desc: '1099-K/消费税汇总', status: '待生成', date: '' },
+ ],
+ },
+ {
+ title: '审计报表',
+ icon: '🔍',
+ reports: [
+ { name: 'SOX合规检查', desc: '内部控制审计', status: '已通过', date: '2026-01-15' },
+ { name: 'SEC Filing', desc: '证券类披露(预留)', status: 'N/A', date: '' },
+ { name: '操作审计日志', desc: '管理员操作记录', status: '已生成', date: '2026-02-10' },
+ ],
+ },
+];
+
+const statusStyle = (status: string): React.CSSProperties => {
+ const map: Record = {
+ '已生成': { bg: 'var(--color-success-light)', color: 'var(--color-success)' },
+ '已提交': { bg: 'var(--color-info-light)', color: 'var(--color-info)' },
+ '已通过': { bg: 'var(--color-success-light)', color: 'var(--color-success)' },
+ '待审核': { bg: 'var(--color-warning-light)', color: 'var(--color-warning)' },
+ '待生成': { bg: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' },
+ 'N/A': { bg: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' },
+ };
+ const s = map[status] || map['N/A'];
+ return { padding: '2px 8px', borderRadius: 'var(--radius-full)', background: s.bg, color: s.color, font: 'var(--text-caption)', fontWeight: 600 };
+};
+
+export const ReportsPage: React.FC = () => {
+ return (
+
+
+
报表中心
+
+
+
+
+ {reportCategories.map(cat => (
+
+
+ {cat.icon}{cat.title}
+
+ {cat.reports.map((r, i) => (
+
+
+
+ {r.status}
+ {r.date && {r.date}}
+ {r.status !== 'N/A' && r.status !== '待生成' && (
+
+ )}
+
+
+ ))}
+
+ ))}
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/risk/RiskCenterPage.tsx b/frontend/admin-web/src/pages/risk/RiskCenterPage.tsx
new file mode 100644
index 0000000..e217b02
--- /dev/null
+++ b/frontend/admin-web/src/pages/risk/RiskCenterPage.tsx
@@ -0,0 +1,163 @@
+import React from 'react';
+
+/**
+ * D5. 风控中心
+ *
+ * 风险仪表盘、可疑交易、黑名单管理、OFAC筛查日志
+ */
+
+export const RiskCenterPage: React.FC = () => {
+ return (
+
+
+
风控中心
+
+
+
+ {/* Risk Stats */}
+
+ {[
+ { label: '风险事件', value: '23', color: 'var(--color-error)', bg: 'var(--color-error-light)' },
+ { label: '可疑交易', value: '15', color: 'var(--color-warning)', bg: 'var(--color-warning-light)' },
+ { label: '冻结账户', value: '3', color: 'var(--color-info)', bg: 'var(--color-info-light)' },
+ { label: 'OFAC命中', value: '0', color: 'var(--color-success)', bg: 'var(--color-success-light)' },
+ ].map(s => (
+
+
{s.label}
+
{s.value}
+
+ ))}
+
+
+ {/* AI Risk Alerts */}
+
+
+ 🤖 AI 风险预警
+
+ {[
+ '检测到异常模式:用户U-045在30分钟内完成12笔交易,总金额$4,560,建议人工审核',
+ '可疑关联账户:U-078和U-091 IP地址相同但KYC信息不同,可能存在刷单行为',
+ ].map((alert, i) => (
+
+ {alert}
+
+
+ ))}
+
+
+ {/* Suspicious Transactions Table */}
+
+
+ 可疑交易
+
+
+
+
+ {['交易ID', '用户', '异常类型', '金额', '时间', '风险评分', '操作'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {[
+ { id: 'TXN-8901', user: 'U-045', type: '高频交易', amount: '$4,560', time: '14:15', score: 87 },
+ { id: 'TXN-8900', user: 'U-078', type: '大额单笔', amount: '$8,900', time: '13:45', score: 72 },
+ { id: 'TXN-8899', user: 'U-091', type: '关联账户', amount: '$3,200', time: '12:30', score: 65 },
+ { id: 'TXN-8898', user: 'U-023', type: '异常IP', amount: '$1,500', time: '11:20', score: 58 },
+ ].map(tx => (
+
+ | {tx.id} |
+ {tx.user} |
+
+ {tx.type}
+ |
+ {tx.amount} |
+ {tx.time} |
+
+
+
+ = 80 ? 'var(--color-error)' : tx.score >= 60 ? 'var(--color-warning)' : 'var(--color-info)',
+ borderRadius: 3,
+ }} />
+
+ = 80 ? 'var(--color-error)' : 'var(--color-warning)' }}>
+ {tx.score}
+
+
+ |
+
+
+
+
+ |
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/system/SystemManagementPage.tsx b/frontend/admin-web/src/pages/system/SystemManagementPage.tsx
new file mode 100644
index 0000000..b520191
--- /dev/null
+++ b/frontend/admin-web/src/pages/system/SystemManagementPage.tsx
@@ -0,0 +1,224 @@
+import React, { useState } from 'react';
+
+/**
+ * D7. 系统管理
+ *
+ * 管理员账号(RBAC)、系统配置、合约管理、系统监控
+ */
+
+export const SystemManagementPage: React.FC = () => {
+ const [activeTab, setActiveTab] = useState<'admins' | 'config' | 'contracts' | 'monitor'>('admins');
+
+ return (
+
+
系统管理
+
+ {/* Tabs */}
+
+ {[
+ { key: 'admins', label: '管理员账号' },
+ { key: 'config', label: '系统配置' },
+ { key: 'contracts', label: '合约管理' },
+ { key: 'monitor', label: '系统监控' },
+ ].map(t => (
+
+ ))}
+
+
+ {/* Admin Accounts */}
+ {activeTab === 'admins' && (
+
+
+ 管理员列表
+
+
+
+
+
+ {['账号', '姓名', '角色', '最后登录', '状态', '操作'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {[
+ { account: 'admin@genex.io', name: '超级管理员', role: '超管', lastLogin: '2026-02-10 14:00', active: true },
+ { account: 'ops@genex.io', name: '运营', role: '运营管理', lastLogin: '2026-02-10 09:30', active: true },
+ { account: 'risk@genex.io', name: '风控', role: '风控审核', lastLogin: '2026-02-09 18:00', active: true },
+ { account: 'cs@genex.io', name: '客服', role: '客服处理', lastLogin: '2026-02-08 14:30', active: false },
+ ].map(admin => (
+
+ | {admin.account} |
+ {admin.name} |
+
+ {admin.role}
+ |
+ {admin.lastLogin} |
+
+
+ |
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+ {/* System Config */}
+ {activeTab === 'config' && (
+
+ {[
+ { title: '手续费率设置', items: [{ label: '一级市场手续费', value: '2.5%' }, { label: '二级市场手续费', value: '3.0%' }, { label: '提现手续费', value: '1.0%' }] },
+ { title: 'KYC阈值配置', items: [{ label: 'L0每日限额', value: '$100' }, { label: 'L1每日限额', value: '$1,000' }, { label: 'L2每日限额', value: '$10,000' }] },
+ { title: '交易限额配置', items: [{ label: '单笔最大金额', value: '$50,000' }, { label: '每日最大金额', value: '$100,000' }, { label: '大额交易阈值', value: '$10,000' }] },
+ { title: '系统参数', items: [{ label: 'Utility Track价格上限', value: '≤面值' }, { label: '最大转售次数', value: '5次' }, { label: 'Breakage阈值', value: '3年' }] },
+ ].map(section => (
+
+
+ {section.title}
+
+
+ {section.items.map((item, i) => (
+
0 ? '1px solid var(--color-border-light)' : 'none',
+ }}>
+ {item.label}
+ {item.value}
+
+ ))}
+
+ ))}
+
+ )}
+
+ {/* Contract Management */}
+ {activeTab === 'contracts' && (
+
+
智能合约状态
+ {[
+ { name: 'CouponNFT', address: '0x1234...abcd', version: 'v1.2.0', status: '运行中' },
+ { name: 'Settlement', address: '0x5678...efgh', version: 'v1.1.0', status: '运行中' },
+ { name: 'Marketplace', address: '0x9abc...ijkl', version: 'v1.0.0', status: '运行中' },
+ { name: 'Oracle', address: '0xdef0...mnop', version: 'v1.0.0', status: '运行中' },
+ ].map(c => (
+
+
+
{c.name}
+
{c.address}
+
+
{c.version}
+
{c.status}
+
+ ))}
+
+ )}
+
+ {/* System Monitor */}
+ {activeTab === 'monitor' && (
+
+ {/* Service Health */}
+
+
服务健康检查
+ {[
+ { name: 'API Gateway', status: 'healthy', cpu: '23%', mem: '45%' },
+ { name: 'Auth Service', status: 'healthy', cpu: '12%', mem: '34%' },
+ { name: 'Trading Engine', status: 'healthy', cpu: '56%', mem: '67%' },
+ { name: 'Genex Chain Node', status: 'healthy', cpu: '34%', mem: '78%' },
+ { name: 'Redis Cache', status: 'healthy', cpu: '8%', mem: '52%' },
+ ].map(s => (
+
+
+ {s.name}
+ CPU {s.cpu}
+ MEM {s.mem}
+
+ ))}
+
+
+ {/* API Response Time */}
+
+
API 响应时间
+
+ P50 / P95 / P99 延迟趋势图
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/trading/TradingMonitorPage.tsx b/frontend/admin-web/src/pages/trading/TradingMonitorPage.tsx
new file mode 100644
index 0000000..8b79a5c
--- /dev/null
+++ b/frontend/admin-web/src/pages/trading/TradingMonitorPage.tsx
@@ -0,0 +1,149 @@
+import React from 'react';
+
+/**
+ * D4. 交易监控
+ *
+ * 实时交易流、交易统计、订单管理
+ */
+
+export const TradingMonitorPage: React.FC = () => {
+ return (
+
+
交易监控
+
+ {/* Stats Row */}
+
+ {[
+ { label: '今日交易量', value: '2,456', color: 'var(--color-primary)' },
+ { label: '今日交易额', value: '$156,789', color: 'var(--color-success)' },
+ { label: '平均折扣率', value: '82.3%', color: 'var(--color-info)' },
+ { label: '大额交易', value: '12', color: 'var(--color-warning)' },
+ ].map(s => (
+
+
{s.label}
+
{s.value}
+
+ ))}
+
+
+ {/* Chart Area */}
+
+
+
交易量/金额趋势
+
+ {['1H', '24H', '7D', '30D'].map(p => (
+
+ ))}
+
+
+
+ Recharts 双轴图 (交易量柱状 + 金额折线)
+
+
+
+ {/* Orders Table */}
+
+
+ 订单管理
+
+
+
+
+
+ {['订单号', '类型', '券名称', '买方', '卖方', '金额', '状态', '时间'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {Array.from({ length: 8 }, (_, i) => (
+
+ |
+ GNX-20260210-{String(1200 - i).padStart(6, '0')}
+ |
+
+ {['购买', '转售', '核销', '转赠'][i % 4]}
+ |
+
+ {['星巴克 $25', 'Amazon $100', 'Nike $80', 'Target $30'][i % 4]}
+ |
+ U-{String(i + 1).padStart(3, '0')} |
+
+ {i % 4 === 1 ? `U-${String(10 + i).padStart(3, '0')}` : '-'}
+ |
+
+ ${[21.25, 85.00, 68.00, 24.00][i % 4].toFixed(2)}
+ |
+
+
+ {i < 6 ? '完成' : '争议'}
+
+ |
+
+ 14:{30 + i}
+ |
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/pages/users/UserManagementPage.tsx b/frontend/admin-web/src/pages/users/UserManagementPage.tsx
new file mode 100644
index 0000000..5e492b2
--- /dev/null
+++ b/frontend/admin-web/src/pages/users/UserManagementPage.tsx
@@ -0,0 +1,155 @@
+import React, { useState } from 'react';
+
+/**
+ * D3. 用户管理
+ *
+ * 用户列表(搜索/KYC筛选)、用户详情、KYC审核
+ */
+
+interface User {
+ id: string;
+ phone: string;
+ email: string;
+ kycLevel: number;
+ couponCount: number;
+ totalTraded: string;
+ riskTags: string[];
+ createdAt: string;
+}
+
+const mockUsers: User[] = [
+ { id: 'U-001', phone: '138****1234', email: 'john@mail.com', kycLevel: 2, couponCount: 15, totalTraded: '$2,340', riskTags: [], createdAt: '2026-01-10' },
+ { id: 'U-002', phone: '139****5678', email: 'jane@mail.com', kycLevel: 1, couponCount: 8, totalTraded: '$890', riskTags: ['高频交易'], createdAt: '2026-01-15' },
+ { id: 'U-003', phone: '137****9012', email: 'bob@mail.com', kycLevel: 3, couponCount: 42, totalTraded: '$12,450', riskTags: [], createdAt: '2025-12-01' },
+ { id: 'U-004', phone: '136****3456', email: 'alice@mail.com', kycLevel: 0, couponCount: 0, totalTraded: '-', riskTags: [], createdAt: '2026-02-09' },
+];
+
+export const UserManagementPage: React.FC = () => {
+ const [search, setSearch] = useState('');
+ const [kycFilter, setKycFilter] = useState(null);
+
+ const filtered = mockUsers.filter(u => {
+ if (search && !u.phone.includes(search) && !u.email.includes(search) && !u.id.includes(search)) return false;
+ if (kycFilter !== null && u.kycLevel !== kycFilter) return false;
+ return true;
+ });
+
+ const kycBadge = (level: number) => {
+ const colors = ['var(--color-gray-400)', 'var(--color-info)', 'var(--color-primary)', 'var(--color-success)'];
+ return (
+
+ L{level}
+
+ );
+ };
+
+ return (
+
+
用户管理
+
+ {/* Search + Filters */}
+
+ setSearch(e.target.value)}
+ style={{
+ flex: 1,
+ maxWidth: 360,
+ height: 40,
+ border: '1px solid var(--color-border)',
+ borderRadius: 'var(--radius-sm)',
+ padding: '0 16px',
+ font: 'var(--text-body)',
+ }}
+ />
+ {[null, 0, 1, 2, 3].map(level => (
+
+ ))}
+
+
+ {/* Users Table */}
+
+
+
+
+ {['用户ID', '手机号', '邮箱', 'KYC等级', '持券数', '交易额', '风险标签', '注册时间', '操作'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {filtered.map(user => (
+
+ | {user.id} |
+ {user.phone} |
+ {user.email} |
+ {kycBadge(user.kycLevel)} |
+ {user.couponCount} |
+ {user.totalTraded} |
+
+ {user.riskTags.length > 0
+ ? user.riskTags.map(tag => (
+ {tag}
+ ))
+ : -
+ }
+ |
+ {user.createdAt} |
+
+
+ |
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/frontend/admin-web/src/styles/design-tokens.css b/frontend/admin-web/src/styles/design-tokens.css
new file mode 100644
index 0000000..7452b4f
--- /dev/null
+++ b/frontend/admin-web/src/styles/design-tokens.css
@@ -0,0 +1,107 @@
+/* ============================================================
+ Genex Design System - CSS Design Tokens
+
+ 紫色系科技风,干净清爽型
+ Primary: #6C5CE7 (创新/科技紫)
+ Target: React + Next.js Web管理前端
+ ============================================================ */
+
+:root {
+ /* ---- Primary Purple ---- */
+ --color-primary: #6C5CE7;
+ --color-primary-light: #9B8FFF;
+ --color-primary-dark: #4834D4;
+ --color-primary-surface: #F3F1FF;
+ --color-primary-container: #E8E5FF;
+
+ /* ---- Neutral ---- */
+ --color-gray-50: #F8F9FC;
+ --color-gray-100: #F1F3F8;
+ --color-gray-200: #E4E7F0;
+ --color-gray-300: #CDD2DE;
+ --color-gray-400: #A0A8BE;
+ --color-gray-500: #7A839E;
+ --color-gray-600: #5C6478;
+ --color-gray-700: #3D4459;
+ --color-gray-800: #262B3A;
+ --color-gray-900: #141723;
+
+ /* ---- Semantic ---- */
+ --color-success: #00C48C;
+ --color-success-light: #E6FAF3;
+ --color-warning: #FFAB2E;
+ --color-warning-light: #FFF7E6;
+ --color-error: #FF4757;
+ --color-error-light: #FFF0F0;
+ --color-info: #3B82F6;
+ --color-info-light: #EFF6FF;
+
+ /* ---- Background & Surface ---- */
+ --color-bg: #F8F9FC;
+ --color-surface: #FFFFFF;
+ --color-surface-variant: #F1F3F8;
+
+ /* ---- Text ---- */
+ --color-text-primary: #141723;
+ --color-text-secondary: #5C6478;
+ --color-text-tertiary: #A0A8BE;
+ --color-text-disabled: #CDD2DE;
+ --color-text-on-primary: #FFFFFF;
+ --color-text-link: #6C5CE7;
+
+ /* ---- Border ---- */
+ --color-border: #E4E7F0;
+ --color-border-light: #F1F3F8;
+ --color-border-focus: #6C5CE7;
+
+ /* ---- Credit Rating ---- */
+ --color-credit-aaa: #00C48C;
+ --color-credit-aa: #3B82F6;
+ --color-credit-a: #6C5CE7;
+ --color-credit-bbb: #FFAB2E;
+ --color-credit-bb: #FF6B6B;
+
+ /* ---- Typography ---- */
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ --font-family-mono: 'JetBrains Mono', 'Fira Code', monospace;
+
+ --text-display: 700 32px/1.2 var(--font-family);
+ --text-h1: 700 24px/1.3 var(--font-family);
+ --text-h2: 600 20px/1.35 var(--font-family);
+ --text-h3: 600 16px/1.4 var(--font-family);
+ --text-body-lg: 400 16px/1.5 var(--font-family);
+ --text-body: 400 14px/1.5 var(--font-family);
+ --text-body-sm: 400 12px/1.5 var(--font-family);
+ --text-label-lg: 600 16px/1.4 var(--font-family);
+ --text-label: 500 14px/1.4 var(--font-family);
+ --text-label-sm: 500 12px/1.4 var(--font-family);
+ --text-caption: 400 11px/1.4 var(--font-family);
+
+ /* ---- Spacing ---- */
+ --space-xs: 4px;
+ --space-sm: 8px;
+ --space-md: 12px;
+ --space-lg: 16px;
+ --space-xl: 20px;
+ --space-2xl: 24px;
+ --space-3xl: 32px;
+ --space-4xl: 40px;
+
+ /* ---- Border Radius ---- */
+ --radius-sm: 8px;
+ --radius-md: 12px;
+ --radius-lg: 16px;
+ --radius-xl: 20px;
+ --radius-full: 999px;
+
+ /* ---- Shadow ---- */
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.04);
+ --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06);
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.08);
+ --shadow-primary: 0 4px 16px rgba(108, 92, 231, 0.2);
+
+ /* ---- Sidebar ---- */
+ --sidebar-width: 260px;
+ --sidebar-collapsed-width: 72px;
+ --header-height: 64px;
+}
diff --git a/frontend/miniapp/src/components/ai-guide/index.tsx b/frontend/miniapp/src/components/ai-guide/index.tsx
new file mode 100644
index 0000000..6df3d87
--- /dev/null
+++ b/frontend/miniapp/src/components/ai-guide/index.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+// Taro mini-program component
+
+/**
+ * AI引导组件(小程序/H5版)
+ *
+ * type=recommendation: 首页顶部AI推荐标签条
+ * type=purchase: 新用户购买引导气泡
+ */
+
+interface AiGuideProps {
+ type: 'recommendation' | 'purchase';
+}
+
+const AiGuide: React.FC = ({ type }) => {
+ if (type === 'recommendation') {
+ return (
+
+ {[
+ { id: 1, text: '星巴克 8.5折' },
+ { id: 2, text: 'Nike 限时特价' },
+ { id: 3, text: '新品餐饮券' },
+ { id: 4, text: '高评级推荐' },
+ ].map(s => (
+
+ ✨
+ {s.text}
+
+ ))}
+
+ );
+ }
+
+ // Purchase guide bubble
+ return (
+
+
+ ✨
+
+
+
+ 你好!我是AI助手,可以帮你找到最适合的券。试试搜索"星巴克"?
+
+
+
+ ×
+
+
+ );
+};
+
+export default AiGuide;
+
+/*
+CSS:
+
+.ai-suggest-bar {
+ display: flex; white-space: nowrap;
+ padding: 16rpx 32rpx;
+}
+.ai-tag {
+ display: inline-flex; align-items: center;
+ padding: 8rpx 20rpx; margin-right: 12rpx;
+ background: #F3F1FF; border-radius: 999rpx;
+}
+.ai-tag-icon { font-size: 24rpx; margin-right: 6rpx; }
+.ai-tag-text { font-size: 24rpx; color: #6C5CE7; font-weight: 500; white-space: nowrap; }
+
+.ai-bubble {
+ display: flex; align-items: flex-start;
+ margin: 16rpx 32rpx; padding: 20rpx;
+ background: #F3F1FF; border-radius: 16rpx;
+ border: 1rpx solid rgba(108,92,231,0.15);
+}
+.ai-bubble-avatar {
+ width: 48rpx; height: 48rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ border-radius: 12rpx; display: flex;
+ align-items: center; justify-content: center;
+ flex-shrink: 0;
+}
+.ai-bubble-avatar-icon { font-size: 24rpx; }
+.ai-bubble-content { flex: 1; margin: 0 16rpx; }
+.ai-bubble-text { font-size: 26rpx; color: #5C6478; line-height: 1.5; }
+.ai-bubble-close { padding: 4rpx; }
+.ai-bubble-close-text { font-size: 28rpx; color: #A0A8BE; }
+*/
diff --git a/frontend/miniapp/src/components/coupon-card/index.tsx b/frontend/miniapp/src/components/coupon-card/index.tsx
new file mode 100644
index 0000000..6738712
--- /dev/null
+++ b/frontend/miniapp/src/components/coupon-card/index.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+// Taro mini-program component
+
+/**
+ * 通用券卡片组件(小程序/H5版)
+ *
+ * 券图标 + 品牌 + 名称 + 价格 + 折扣标签
+ * 可复用于首页、搜索结果、我的券等场景
+ */
+
+interface CouponCardProps {
+ brand: string;
+ name: string;
+ price: string;
+ faceValue: string;
+ discount: string;
+ creditRating?: string;
+ onClick?: () => void;
+}
+
+const CouponCard: React.FC = ({
+ brand, name, price, faceValue, discount, creditRating, onClick,
+}) => {
+ return (
+
+
+ 🎫
+
+
+
+ {brand}
+ {creditRating && (
+
+ {creditRating}
+
+ )}
+
+ {name}
+
+ {price}
+ {faceValue}
+
+ {discount}
+
+
+
+
+ );
+};
+
+export default CouponCard;
+
+/*
+CSS:
+
+.coupon-card-component {
+ display: flex; padding: 20rpx;
+ background: white; border-radius: 16rpx;
+ border: 1rpx solid #F1F3F8;
+ margin-bottom: 16rpx;
+}
+.cc-image {
+ width: 160rpx; height: 160rpx;
+ background: #F3F1FF; border-radius: 12rpx;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+}
+.cc-image-icon { font-size: 48rpx; }
+.cc-info {
+ flex: 1; padding-left: 20rpx;
+ display: flex; flex-direction: column; justify-content: space-between;
+}
+.cc-top-row { display: flex; align-items: center; }
+.cc-brand { font-size: 22rpx; color: #A0A8BE; }
+.cc-credit {
+ margin-left: 8rpx; padding: 0 8rpx;
+ background: #EFF6FF; border-radius: 999rpx;
+}
+.cc-credit-text { font-size: 18rpx; color: #3B82F6; font-weight: 600; }
+.cc-name { font-size: 28rpx; font-weight: 500; color: #141723; margin-top: 8rpx; }
+.cc-price-row { display: flex; align-items: flex-end; margin-top: 8rpx; }
+.cc-price { font-size: 32rpx; font-weight: 700; color: #6C5CE7; }
+.cc-face { font-size: 22rpx; color: #A0A8BE; text-decoration: line-through; margin-left: 8rpx; }
+.cc-discount {
+ margin-left: 8rpx; padding: 2rpx 10rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ border-radius: 999rpx;
+}
+.cc-discount-text { color: white; font-size: 20rpx; font-weight: 700; }
+*/
diff --git a/frontend/miniapp/src/components/share-card/index.tsx b/frontend/miniapp/src/components/share-card/index.tsx
new file mode 100644
index 0000000..1f99476
--- /dev/null
+++ b/frontend/miniapp/src/components/share-card/index.tsx
@@ -0,0 +1,106 @@
+import React from 'react';
+// Taro mini-program component
+
+/**
+ * 分享卡片组件
+ *
+ * 用于生成小程序分享图片/卡片
+ * 包含券信息 + 品牌 + 价格 + 小程序码
+ */
+
+interface ShareCardProps {
+ brand: string;
+ name: string;
+ price: string;
+ faceValue: string;
+ discount: string;
+}
+
+const ShareCard: React.FC = ({
+ brand, name, price, faceValue, discount,
+}) => {
+ return (
+
+ {/* Header */}
+
+
+ G
+
+ Genex · {brand}
+
+
+ {/* Coupon Info */}
+
+ {name}
+
+ {price}
+ {faceValue}
+
+ {discount}
+
+
+
+
+ {/* Footer with QR */}
+
+
+ 小程序码
+
+
+ 长按识别小程序码
+ 立即抢购优惠好券
+
+
+
+ );
+};
+
+export default ShareCard;
+
+/*
+CSS:
+
+.share-card {
+ width: 560rpx; background: white;
+ border-radius: 24rpx; overflow: hidden;
+ box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.1);
+}
+.share-header {
+ display: flex; align-items: center;
+ padding: 24rpx 28rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+}
+.share-logo {
+ width: 48rpx; height: 48rpx; background: rgba(255,255,255,0.2);
+ border-radius: 10rpx; display: flex;
+ align-items: center; justify-content: center;
+}
+.share-logo-text { color: white; font-weight: 700; font-size: 24rpx; }
+.share-brand { color: white; font-size: 26rpx; font-weight: 600; margin-left: 12rpx; }
+
+.share-body { padding: 28rpx; }
+.share-name { font-size: 32rpx; font-weight: 600; color: #141723; }
+.share-price-row { display: flex; align-items: flex-end; margin-top: 16rpx; }
+.share-price { font-size: 40rpx; font-weight: 700; color: #6C5CE7; }
+.share-face { font-size: 24rpx; color: #A0A8BE; text-decoration: line-through; margin-left: 12rpx; }
+.share-discount {
+ margin-left: 12rpx; padding: 4rpx 12rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ border-radius: 999rpx;
+}
+.share-discount-text { color: white; font-size: 22rpx; font-weight: 700; }
+
+.share-footer {
+ display: flex; align-items: center;
+ padding: 20rpx 28rpx; border-top: 1rpx solid #F1F3F8;
+}
+.share-qr {
+ width: 80rpx; height: 80rpx; background: #F3F1FF;
+ border-radius: 8rpx; display: flex;
+ align-items: center; justify-content: center;
+}
+.share-qr-placeholder { font-size: 16rpx; color: #A0A8BE; }
+.share-cta { margin-left: 16rpx; }
+.share-cta-title { font-size: 24rpx; font-weight: 600; color: #141723; }
+.share-cta-desc { font-size: 20rpx; color: #A0A8BE; }
+*/
diff --git a/frontend/miniapp/src/i18n/index.ts b/frontend/miniapp/src/i18n/index.ts
new file mode 100644
index 0000000..2b7f2b6
--- /dev/null
+++ b/frontend/miniapp/src/i18n/index.ts
@@ -0,0 +1,532 @@
+/**
+ * Genex 小程序/H5 - i18n 多语言支持
+ *
+ * 支持: zh-CN (默认), en-US, ja-JP
+ * 使用方式: import { t } from '@/i18n'; t('key') 或 t('key', 'en-US')
+ */
+
+export type Locale = 'zh-CN' | 'en-US' | 'ja-JP';
+
+export const defaultLocale: Locale = 'zh-CN';
+
+export const supportedLocales: { value: Locale; label: string }[] = [
+ { value: 'zh-CN', label: '简体中文' },
+ { value: 'en-US', label: 'English' },
+ { value: 'ja-JP', label: '日本語' },
+];
+
+export function t(key: string, locale: Locale = defaultLocale): string {
+ return translations[locale]?.[key] ?? translations['zh-CN']?.[key] ?? key;
+}
+
+const translations: Record> = {
+ 'zh-CN': {
+ // ── Common ──
+ 'app_name': 'Genex',
+ 'confirm': '确认',
+ 'cancel': '取消',
+ 'save': '保存',
+ 'delete': '删除',
+ 'edit': '编辑',
+ 'search': '搜索',
+ 'loading': '加载中...',
+ 'retry': '重试',
+ 'done': '完成',
+ 'next': '下一步',
+ 'back': '返回',
+ 'close': '关闭',
+ 'more': '更多',
+ 'all': '全部',
+ 'share': '分享',
+ 'copy': '复制',
+
+ // ── Tabs ──
+ 'tab_home': '首页',
+ 'tab_categories': '分类',
+ 'tab_coupons': '我的券',
+ 'tab_profile': '我的',
+
+ // ── Home ──
+ 'home_greeting': '你好',
+ 'home_search_hint': '搜索券、品牌...',
+ 'home_recommended': 'AI推荐',
+ 'home_hot': '热门券',
+ 'home_new': '新上架',
+ 'home_categories': '分类浏览',
+ 'home_nearby': '附近好券',
+ 'home_flash_sale': '限时抢购',
+ 'home_banner_more': '查看更多',
+
+ // ── Search ──
+ 'search_placeholder': '搜索券、品牌、类别...',
+ 'search_history': '搜索历史',
+ 'search_clear_history': '清空历史',
+ 'search_hot_keywords': '热门搜索',
+ 'search_no_result': '未找到相关结果',
+ 'search_result_count': '共找到 {count} 个结果',
+
+ // ── Categories ──
+ 'category_all': '全部分类',
+ 'category_food': '餐饮美食',
+ 'category_shopping': '购物百货',
+ 'category_entertainment': '休闲娱乐',
+ 'category_travel': '旅游出行',
+ 'category_education': '教育培训',
+ 'category_health': '健康医疗',
+ 'category_lifestyle': '生活服务',
+ 'category_digital': '数码电器',
+
+ // ── Coupon List ──
+ 'coupon_list_title': '券列表',
+ 'coupon_sort_default': '默认排序',
+ 'coupon_sort_price_asc': '价格从低到高',
+ 'coupon_sort_price_desc': '价格从高到低',
+ 'coupon_sort_discount': '折扣最大',
+ 'coupon_sort_newest': '最新上架',
+ 'coupon_sort_expiring': '即将过期',
+ 'coupon_filter_brand': '品牌筛选',
+ 'coupon_filter_price': '价格范围',
+ 'coupon_filter_discount': '折扣范围',
+
+ // ── Coupon Detail ──
+ 'coupon_detail': '券详情',
+ 'coupon_face_value': '面值',
+ 'coupon_price': '价格',
+ 'coupon_discount': '折扣',
+ 'coupon_valid_until': '有效期至',
+ 'coupon_brand': '品牌',
+ 'coupon_category': '类别',
+ 'coupon_description': '使用说明',
+ 'coupon_terms': '使用条款',
+ 'coupon_buy': '购买',
+ 'coupon_sell': '出售',
+ 'coupon_transfer': '转赠',
+ 'coupon_use': '使用',
+ 'coupon_share': '分享给好友',
+ 'coupon_save_to_wallet': '存入钱包',
+ 'coupon_seller_info': '卖家信息',
+ 'coupon_chain_info': '链上信息',
+
+ // ── My Coupons ──
+ 'my_coupons': '我的券',
+ 'my_coupons_available': '可用',
+ 'my_coupons_used': '已使用',
+ 'my_coupons_expired': '已过期',
+ 'my_coupons_transferred': '已转赠',
+ 'my_coupons_empty': '暂无券',
+ 'my_coupons_empty_hint': '去市场看看吧',
+
+ // ── Orders ──
+ 'order_list': '我的订单',
+ 'order_all': '全部',
+ 'order_pending_payment': '待支付',
+ 'order_pending_delivery': '待发放',
+ 'order_completed': '已完成',
+ 'order_cancelled': '已取消',
+ 'order_detail': '订单详情',
+ 'order_number': '订单号',
+ 'order_time': '下单时间',
+ 'order_pay': '去支付',
+ 'order_cancel': '取消订单',
+ 'order_amount': '订单金额',
+
+ // ── Profile ──
+ 'profile_login': '登录 / 注册',
+ 'profile_login_phone': '手机号登录',
+ 'profile_login_wechat': '微信登录',
+ 'profile_wallet': '我的钱包',
+ 'profile_orders': '我的订单',
+ 'profile_favorites': '我的收藏',
+ 'profile_settings': '设置',
+ 'profile_help': '帮助中心',
+ 'profile_feedback': '意见反馈',
+ 'profile_about': '关于',
+ 'profile_logout': '退出登录',
+ 'profile_kyc': '身份认证',
+
+ // ── Login ──
+ 'login_title': '登录',
+ 'login_phone_placeholder': '请输入手机号',
+ 'login_code_placeholder': '请输入验证码',
+ 'login_send_code': '获取验证码',
+ 'login_resend': '{seconds}秒后重发',
+ 'login_agree_prefix': '我已阅读并同意',
+ 'login_user_agreement': '用户协议',
+ 'login_privacy_policy': '隐私政策',
+ 'login_and': '和',
+
+ // ── Redeem ──
+ 'redeem_title': '使用券',
+ 'redeem_scan_qr': '扫码核销',
+ 'redeem_show_code': '出示券码',
+ 'redeem_input_code': '输入核销码',
+ 'redeem_success': '核销成功',
+ 'redeem_failed': '核销失败',
+
+ // ── Share ──
+ 'share_title': '分享',
+ 'share_wechat': '微信好友',
+ 'share_moments': '朋友圈',
+ 'share_qr_code': '二维码',
+ 'share_copy_link': '复制链接',
+ 'share_save_image': '保存图片',
+ 'share_success': '分享成功',
+ 'share_copied': '链接已复制',
+
+ // ── Download App ──
+ 'download_app_title': '下载 Genex App',
+ 'download_app_desc': '下载App享受更多功能和更好体验',
+ 'download_app_button': '立即下载',
+ 'download_app_ios': 'iOS 下载',
+ 'download_app_android': 'Android 下载',
+ 'download_app_dismiss': '暂不下载',
+
+ // ── Error ──
+ 'error_network': '网络连接失败',
+ 'error_server': '服务器错误',
+ 'error_timeout': '请求超时',
+ 'error_unknown': '未知错误',
+ 'error_not_found': '页面不存在',
+ 'error_unauthorized': '请先登录',
+ },
+
+ 'en-US': {
+ // ── Common ──
+ 'app_name': 'Genex',
+ 'confirm': 'Confirm',
+ 'cancel': 'Cancel',
+ 'save': 'Save',
+ 'delete': 'Delete',
+ 'edit': 'Edit',
+ 'search': 'Search',
+ 'loading': 'Loading...',
+ 'retry': 'Retry',
+ 'done': 'Done',
+ 'next': 'Next',
+ 'back': 'Back',
+ 'close': 'Close',
+ 'more': 'More',
+ 'all': 'All',
+ 'share': 'Share',
+ 'copy': 'Copy',
+
+ // ── Tabs ──
+ 'tab_home': 'Home',
+ 'tab_categories': 'Categories',
+ 'tab_coupons': 'My Coupons',
+ 'tab_profile': 'Profile',
+
+ // ── Home ──
+ 'home_greeting': 'Hello',
+ 'home_search_hint': 'Search coupons, brands...',
+ 'home_recommended': 'AI Picks',
+ 'home_hot': 'Trending',
+ 'home_new': 'New Arrivals',
+ 'home_categories': 'Categories',
+ 'home_nearby': 'Nearby Deals',
+ 'home_flash_sale': 'Flash Sale',
+ 'home_banner_more': 'View More',
+
+ // ── Search ──
+ 'search_placeholder': 'Search coupons, brands, categories...',
+ 'search_history': 'Search History',
+ 'search_clear_history': 'Clear History',
+ 'search_hot_keywords': 'Trending Searches',
+ 'search_no_result': 'No results found',
+ 'search_result_count': '{count} results found',
+
+ // ── Categories ──
+ 'category_all': 'All Categories',
+ 'category_food': 'Food & Dining',
+ 'category_shopping': 'Shopping',
+ 'category_entertainment': 'Entertainment',
+ 'category_travel': 'Travel',
+ 'category_education': 'Education',
+ 'category_health': 'Health',
+ 'category_lifestyle': 'Lifestyle',
+ 'category_digital': 'Electronics',
+
+ // ── Coupon List ──
+ 'coupon_list_title': 'Coupons',
+ 'coupon_sort_default': 'Default',
+ 'coupon_sort_price_asc': 'Price: Low to High',
+ 'coupon_sort_price_desc': 'Price: High to Low',
+ 'coupon_sort_discount': 'Best Discount',
+ 'coupon_sort_newest': 'Newest',
+ 'coupon_sort_expiring': 'Expiring Soon',
+ 'coupon_filter_brand': 'Brand',
+ 'coupon_filter_price': 'Price Range',
+ 'coupon_filter_discount': 'Discount Range',
+
+ // ── Coupon Detail ──
+ 'coupon_detail': 'Coupon Details',
+ 'coupon_face_value': 'Face Value',
+ 'coupon_price': 'Price',
+ 'coupon_discount': 'Discount',
+ 'coupon_valid_until': 'Valid Until',
+ 'coupon_brand': 'Brand',
+ 'coupon_category': 'Category',
+ 'coupon_description': 'Description',
+ 'coupon_terms': 'Terms & Conditions',
+ 'coupon_buy': 'Buy',
+ 'coupon_sell': 'Sell',
+ 'coupon_transfer': 'Gift',
+ 'coupon_use': 'Redeem',
+ 'coupon_share': 'Share with Friends',
+ 'coupon_save_to_wallet': 'Save to Wallet',
+ 'coupon_seller_info': 'Seller Info',
+ 'coupon_chain_info': 'On-chain Info',
+
+ // ── My Coupons ──
+ 'my_coupons': 'My Coupons',
+ 'my_coupons_available': 'Available',
+ 'my_coupons_used': 'Used',
+ 'my_coupons_expired': 'Expired',
+ 'my_coupons_transferred': 'Gifted',
+ 'my_coupons_empty': 'No Coupons',
+ 'my_coupons_empty_hint': 'Browse the marketplace',
+
+ // ── Orders ──
+ 'order_list': 'My Orders',
+ 'order_all': 'All',
+ 'order_pending_payment': 'Pending Payment',
+ 'order_pending_delivery': 'Pending Delivery',
+ 'order_completed': 'Completed',
+ 'order_cancelled': 'Cancelled',
+ 'order_detail': 'Order Details',
+ 'order_number': 'Order Number',
+ 'order_time': 'Order Time',
+ 'order_pay': 'Pay Now',
+ 'order_cancel': 'Cancel Order',
+ 'order_amount': 'Order Amount',
+
+ // ── Profile ──
+ 'profile_login': 'Log In / Sign Up',
+ 'profile_login_phone': 'Phone Login',
+ 'profile_login_wechat': 'WeChat Login',
+ 'profile_wallet': 'My Wallet',
+ 'profile_orders': 'My Orders',
+ 'profile_favorites': 'Favorites',
+ 'profile_settings': 'Settings',
+ 'profile_help': 'Help Center',
+ 'profile_feedback': 'Feedback',
+ 'profile_about': 'About',
+ 'profile_logout': 'Log Out',
+ 'profile_kyc': 'Verification',
+
+ // ── Login ──
+ 'login_title': 'Log In',
+ 'login_phone_placeholder': 'Enter phone number',
+ 'login_code_placeholder': 'Enter verification code',
+ 'login_send_code': 'Send Code',
+ 'login_resend': 'Resend in {seconds}s',
+ 'login_agree_prefix': 'I have read and agree to the',
+ 'login_user_agreement': 'User Agreement',
+ 'login_privacy_policy': 'Privacy Policy',
+ 'login_and': 'and',
+
+ // ── Redeem ──
+ 'redeem_title': 'Redeem Coupon',
+ 'redeem_scan_qr': 'Scan QR Code',
+ 'redeem_show_code': 'Show Code',
+ 'redeem_input_code': 'Enter Code',
+ 'redeem_success': 'Redeemed Successfully',
+ 'redeem_failed': 'Redemption Failed',
+
+ // ── Share ──
+ 'share_title': 'Share',
+ 'share_wechat': 'WeChat',
+ 'share_moments': 'Moments',
+ 'share_qr_code': 'QR Code',
+ 'share_copy_link': 'Copy Link',
+ 'share_save_image': 'Save Image',
+ 'share_success': 'Shared Successfully',
+ 'share_copied': 'Link Copied',
+
+ // ── Download App ──
+ 'download_app_title': 'Download Genex App',
+ 'download_app_desc': 'Download for more features and better experience',
+ 'download_app_button': 'Download Now',
+ 'download_app_ios': 'iOS Download',
+ 'download_app_android': 'Android Download',
+ 'download_app_dismiss': 'Not Now',
+
+ // ── Error ──
+ 'error_network': 'Network Error',
+ 'error_server': 'Server Error',
+ 'error_timeout': 'Request Timeout',
+ 'error_unknown': 'Unknown Error',
+ 'error_not_found': 'Page Not Found',
+ 'error_unauthorized': 'Please Log In',
+ },
+
+ 'ja-JP': {
+ // ── Common ──
+ 'app_name': 'Genex',
+ 'confirm': '確認',
+ 'cancel': 'キャンセル',
+ 'save': '保存',
+ 'delete': '削除',
+ 'edit': '編集',
+ 'search': '検索',
+ 'loading': '読み込み中...',
+ 'retry': 'リトライ',
+ 'done': '完了',
+ 'next': '次へ',
+ 'back': '戻る',
+ 'close': '閉じる',
+ 'more': 'もっと見る',
+ 'all': 'すべて',
+ 'share': 'シェア',
+ 'copy': 'コピー',
+
+ // ── Tabs ──
+ 'tab_home': 'ホーム',
+ 'tab_categories': 'カテゴリー',
+ 'tab_coupons': 'マイクーポン',
+ 'tab_profile': 'マイページ',
+
+ // ── Home ──
+ 'home_greeting': 'こんにちは',
+ 'home_search_hint': 'クーポン、ブランドを検索...',
+ 'home_recommended': 'AIおすすめ',
+ 'home_hot': '人気',
+ 'home_new': '新着',
+ 'home_categories': 'カテゴリー',
+ 'home_nearby': '近くのお得情報',
+ 'home_flash_sale': 'タイムセール',
+ 'home_banner_more': 'もっと見る',
+
+ // ── Search ──
+ 'search_placeholder': 'クーポン、ブランド、カテゴリーを検索...',
+ 'search_history': '検索履歴',
+ 'search_clear_history': '履歴を消去',
+ 'search_hot_keywords': '人気の検索',
+ 'search_no_result': '検索結果が見つかりません',
+ 'search_result_count': '{count}件の結果',
+
+ // ── Categories ──
+ 'category_all': 'すべてのカテゴリー',
+ 'category_food': 'グルメ',
+ 'category_shopping': 'ショッピング',
+ 'category_entertainment': 'エンタメ',
+ 'category_travel': '旅行',
+ 'category_education': '教育',
+ 'category_health': '健康',
+ 'category_lifestyle': 'ライフスタイル',
+ 'category_digital': 'デジタル家電',
+
+ // ── Coupon List ──
+ 'coupon_list_title': 'クーポン一覧',
+ 'coupon_sort_default': 'デフォルト',
+ 'coupon_sort_price_asc': '価格:安い順',
+ 'coupon_sort_price_desc': '価格:高い順',
+ 'coupon_sort_discount': '割引率順',
+ 'coupon_sort_newest': '新着順',
+ 'coupon_sort_expiring': '期限切れ間近',
+ 'coupon_filter_brand': 'ブランド',
+ 'coupon_filter_price': '価格帯',
+ 'coupon_filter_discount': '割引率',
+
+ // ── Coupon Detail ──
+ 'coupon_detail': 'クーポン詳細',
+ 'coupon_face_value': '額面',
+ 'coupon_price': '価格',
+ 'coupon_discount': '割引',
+ 'coupon_valid_until': '有効期限',
+ 'coupon_brand': 'ブランド',
+ 'coupon_category': 'カテゴリー',
+ 'coupon_description': '利用説明',
+ 'coupon_terms': '利用規約',
+ 'coupon_buy': '購入',
+ 'coupon_sell': '売却',
+ 'coupon_transfer': '贈与',
+ 'coupon_use': '使用',
+ 'coupon_share': '友達にシェア',
+ 'coupon_save_to_wallet': 'ウォレットに保存',
+ 'coupon_seller_info': '出品者情報',
+ 'coupon_chain_info': 'オンチェーン情報',
+
+ // ── My Coupons ──
+ 'my_coupons': 'マイクーポン',
+ 'my_coupons_available': '利用可能',
+ 'my_coupons_used': '使用済み',
+ 'my_coupons_expired': '期限切れ',
+ 'my_coupons_transferred': '贈与済み',
+ 'my_coupons_empty': 'クーポンがありません',
+ 'my_coupons_empty_hint': 'マーケットをチェック',
+
+ // ── Orders ──
+ 'order_list': '注文履歴',
+ 'order_all': 'すべて',
+ 'order_pending_payment': '支払い待ち',
+ 'order_pending_delivery': '発行待ち',
+ 'order_completed': '完了',
+ 'order_cancelled': 'キャンセル済み',
+ 'order_detail': '注文詳細',
+ 'order_number': '注文番号',
+ 'order_time': '注文日時',
+ 'order_pay': '支払う',
+ 'order_cancel': 'キャンセル',
+ 'order_amount': '注文金額',
+
+ // ── Profile ──
+ 'profile_login': 'ログイン / 新規登録',
+ 'profile_login_phone': '電話番号ログイン',
+ 'profile_login_wechat': 'WeChatログイン',
+ 'profile_wallet': 'マイウォレット',
+ 'profile_orders': '注文履歴',
+ 'profile_favorites': 'お気に入り',
+ 'profile_settings': '設定',
+ 'profile_help': 'ヘルプ',
+ 'profile_feedback': 'フィードバック',
+ 'profile_about': 'アプリについて',
+ 'profile_logout': 'ログアウト',
+ 'profile_kyc': '本人確認',
+
+ // ── Login ──
+ 'login_title': 'ログイン',
+ 'login_phone_placeholder': '電話番号を入力',
+ 'login_code_placeholder': '認証コードを入力',
+ 'login_send_code': 'コードを送信',
+ 'login_resend': '{seconds}秒後に再送信',
+ 'login_agree_prefix': '以下に同意します:',
+ 'login_user_agreement': '利用規約',
+ 'login_privacy_policy': 'プライバシーポリシー',
+ 'login_and': 'および',
+
+ // ── Redeem ──
+ 'redeem_title': 'クーポンを使用',
+ 'redeem_scan_qr': 'QRコードスキャン',
+ 'redeem_show_code': 'コードを表示',
+ 'redeem_input_code': 'コードを入力',
+ 'redeem_success': '使用完了',
+ 'redeem_failed': '使用失敗',
+
+ // ── Share ──
+ 'share_title': 'シェア',
+ 'share_wechat': 'WeChat',
+ 'share_moments': 'モーメンツ',
+ 'share_qr_code': 'QRコード',
+ 'share_copy_link': 'リンクをコピー',
+ 'share_save_image': '画像を保存',
+ 'share_success': 'シェア完了',
+ 'share_copied': 'リンクをコピーしました',
+
+ // ── Download App ──
+ 'download_app_title': 'Genex Appをダウンロード',
+ 'download_app_desc': 'アプリでより多くの機能をお楽しみください',
+ 'download_app_button': '今すぐダウンロード',
+ 'download_app_ios': 'iOSダウンロード',
+ 'download_app_android': 'Androidダウンロード',
+ 'download_app_dismiss': '後で',
+
+ // ── Error ──
+ 'error_network': 'ネットワークエラー',
+ 'error_server': 'サーバーエラー',
+ 'error_timeout': 'リクエストタイムアウト',
+ 'error_unknown': '不明なエラー',
+ 'error_not_found': 'ページが見つかりません',
+ 'error_unauthorized': 'ログインしてください',
+ },
+};
diff --git a/frontend/miniapp/src/pages/detail/index.tsx b/frontend/miniapp/src/pages/detail/index.tsx
new file mode 100644
index 0000000..76490d4
--- /dev/null
+++ b/frontend/miniapp/src/pages/detail/index.tsx
@@ -0,0 +1,190 @@
+import React from 'react';
+// Taro mini-program - Coupon Detail + Purchase
+
+/**
+ * E1. 小程序核心页面 - 券详情 + 购买
+ *
+ * 同App,微信支付/支付宝支付
+ */
+
+const DetailPage: React.FC = () => {
+ return (
+
+ {/* Coupon Image */}
+
+
+ 🎫
+
+
+
+ {/* Main Info */}
+
+
+ S
+
+ Starbucks
+
+ AAA
+
+
+
+ 星巴克 ¥25 礼品卡
+
+ {/* Price */}
+
+
+ ¥
+ 21.25
+ ¥25
+ 8.5折
+
+ 比面值节省 ¥3.75
+
+
+ {/* Info List */}
+
+ {[
+ { label: '面值', value: '¥25.00' },
+ { label: '有效期', value: '2026/12/31' },
+ { label: '类型', value: '消费券' },
+ { label: '使用门店', value: '全国 12,800+ 门店' },
+ ].map((item, i) => (
+
+ {item.label}
+ {item.value}
+
+ ))}
+
+
+ {/* Rules */}
+
+ 使用说明
+ {[
+ '全国星巴克门店通用',
+ '可转赠给好友',
+ '有效期内随时使用',
+ '不可叠加使用',
+ ].map((rule, i) => (
+
+ •
+ {rule}
+
+ ))}
+
+
+ {/* Utility Track Notice */}
+
+ ✓
+ 您正在购买消费券用于消费
+
+
+
+ {/* Bottom Buy Bar */}
+
+
+ 合计
+ ¥21.25
+
+
+ 立即购买
+
+
+
+ );
+};
+
+export default DetailPage;
+
+/*
+CSS (index.scss):
+
+.detail-page { padding-bottom: 140rpx; background: #F8F9FC; }
+
+.detail-hero { height: 400rpx; }
+.detail-hero-bg {
+ height: 100%; background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ display: flex; align-items: center; justify-content: center;
+}
+.hero-icon { font-size: 120rpx; opacity: 0.3; }
+
+.detail-info { margin-top: -40rpx; position: relative; z-index: 1; padding: 0 32rpx; }
+
+.brand-row {
+ display: flex; align-items: center;
+ background: white; border-radius: 24rpx; padding: 24rpx;
+ box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
+}
+.brand-logo {
+ width: 64rpx; height: 64rpx; border-radius: 12rpx;
+ background: #F1F3F8; display: flex; align-items: center; justify-content: center;
+ font-size: 28rpx; font-weight: 700; color: #6C5CE7;
+}
+.brand-info { margin-left: 16rpx; display: flex; align-items: center; gap: 12rpx; }
+.brand-name { font-size: 28rpx; color: #5C6478; }
+.credit-badge {
+ padding: 2rpx 12rpx; background: rgba(0,196,140,0.1);
+ border: 1rpx solid rgba(0,196,140,0.3); border-radius: 999rpx;
+}
+.credit-text { font-size: 20rpx; color: #00C48C; font-weight: 700; }
+
+.coupon-title {
+ display: block; font-size: 36rpx; font-weight: 600; color: #141723;
+ margin-top: 20rpx; margin-bottom: 16rpx;
+}
+
+.price-card {
+ background: #F3F1FF; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx;
+}
+.price-row { display: flex; align-items: flex-end; }
+.price-symbol { font-size: 28rpx; font-weight: 700; color: #6C5CE7; }
+.price-value { font-size: 52rpx; font-weight: 700; color: #6C5CE7; line-height: 1; }
+.price-original { font-size: 24rpx; color: #A0A8BE; text-decoration: line-through; margin-left: 12rpx; }
+.discount-tag {
+ margin-left: 12rpx; padding: 4rpx 12rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ color: white; font-size: 20rpx; font-weight: 700; border-radius: 999rpx;
+}
+.price-save { font-size: 22rpx; color: #00C48C; margin-top: 8rpx; }
+
+.info-card {
+ background: white; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx;
+ border: 1rpx solid #F1F3F8;
+}
+.info-row {
+ display: flex; justify-content: space-between; padding: 14rpx 0;
+ border-bottom: 1rpx solid #F1F3F8;
+}
+.info-row:last-child { border-bottom: none; }
+.info-label { font-size: 26rpx; color: #5C6478; }
+.info-value { font-size: 26rpx; color: #141723; font-weight: 500; }
+
+.rules-card {
+ background: white; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx;
+ border: 1rpx solid #F1F3F8;
+}
+.rules-title { font-size: 28rpx; font-weight: 500; margin-bottom: 16rpx; }
+.rule-item { display: flex; align-items: center; margin-bottom: 12rpx; }
+.rule-dot { color: #A0A8BE; margin-right: 12rpx; }
+.rule-text { font-size: 24rpx; color: #5C6478; }
+
+.utility-notice {
+ display: flex; align-items: center; padding: 16rpx 24rpx;
+ background: #E6FAF3; border-radius: 12rpx;
+}
+.utility-icon { color: #00C48C; margin-right: 12rpx; font-weight: 700; }
+.utility-text { font-size: 24rpx; color: #3D4459; }
+
+.buy-bar {
+ position: fixed; bottom: 0; left: 0; right: 0;
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 20rpx 32rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+ background: white; border-top: 1rpx solid #F1F3F8;
+}
+.buy-bar-price { display: flex; flex-direction: column; }
+.buy-label { font-size: 22rpx; color: #A0A8BE; }
+.buy-price { font-size: 40rpx; font-weight: 700; color: #6C5CE7; }
+.buy-button {
+ padding: 20rpx 48rpx; background: #6C5CE7; border-radius: 16rpx;
+}
+.buy-button-text { color: white; font-size: 30rpx; font-weight: 600; }
+*/
diff --git a/frontend/miniapp/src/pages/h5-activity/index.tsx b/frontend/miniapp/src/pages/h5-activity/index.tsx
new file mode 100644
index 0000000..e5ffa85
--- /dev/null
+++ b/frontend/miniapp/src/pages/h5-activity/index.tsx
@@ -0,0 +1,551 @@
+import React from 'react';
+// Taro mini-program component
+
+/**
+ * H5 Activity / Campaign Landing Page
+ *
+ * 活动落地页 - 通过微信/社交媒体分享的促销活动页面
+ * 包含:倒计时、优惠券卡片、活动规则、分享栏
+ */
+
+const H5ActivityPage: React.FC = () => {
+ return (
+
+ {/* Hero Banner */}
+
+
+ 🔥
+ 限时活动
+
+ 限时特惠 | 新用户专享
+ 精选大牌优惠券,折扣低至7折起
+
+ {/* Countdown Timer */}
+
+ 距活动结束
+
+
+ 02
+
+ :
+
+ 18
+
+ :
+
+ 45
+
+ :
+
+ 32
+
+
+
+
+
+ {/* Featured Coupon Cards */}
+
+
+ 精选好券
+ 限量抢购,先到先得
+
+
+ {[
+ {
+ brand: 'Starbucks',
+ brandInitial: 'S',
+ name: '星巴克 ¥50 礼品卡',
+ originalPrice: '¥50.00',
+ discountPrice: '¥35.00',
+ discount: '7折',
+ tag: '爆款',
+ },
+ {
+ brand: 'Amazon',
+ brandInitial: 'A',
+ name: 'Amazon ¥200 购物券',
+ originalPrice: '¥200.00',
+ discountPrice: '¥156.00',
+ discount: '7.8折',
+ tag: '热卖',
+ },
+ {
+ brand: 'Nike',
+ brandInitial: 'N',
+ name: 'Nike ¥100 运动券',
+ originalPrice: '¥100.00',
+ discountPrice: '¥72.00',
+ discount: '7.2折',
+ tag: '新品',
+ },
+ ].map((coupon, i) => (
+
+ {/* Discount Badge */}
+
+ {coupon.tag}
+
+
+ {/* Card Top: Brand + Image */}
+
+
+ 🎫
+
+
+ {coupon.discount}
+
+
+
+ {/* Card Body */}
+
+
+
+ {coupon.brandInitial}
+
+ {coupon.brand}
+
+ {coupon.name}
+
+ ¥
+ {coupon.discountPrice.replace('¥', '')}
+ {coupon.originalPrice}
+
+
+
+ {/* Buy Button */}
+
+ 立即抢购
+
+
+ ))}
+
+
+ {/* Activity Rules */}
+
+
+
+ 📋
+
+ 活动规则
+
+
+
+ {[
+ '活动时间:2026年2月10日 - 2026年2月28日',
+ '每位用户限购每种券3张,活动优惠券不与其他优惠叠加使用',
+ '优惠券自购买之日起30天内有效,过期自动作废',
+ '活动券仅限新注册用户首次购买使用',
+ '如遇商品售罄,Genex保留调整活动内容的权利',
+ '退款将原路返回,处理时间为1-3个工作日',
+ '如有疑问请联系客服:support@genex.com',
+ ].map((rule, i) => (
+
+
+ {rule}
+
+ ))}
+
+
+
+ {/* Share Bar */}
+
+
+ 👥
+ 已有 2,386 人参与
+
+
+ 📤
+ 分享给好友
+
+
+
+ {/* Brand Footer */}
+
+
+
+ G
+
+ Genex
+
+ 全球券资产交易平台
+ © 2026 Genex. All rights reserved.
+
+
+ );
+};
+
+export default H5ActivityPage;
+
+/*
+CSS (H5活动页样式 - 对应 index.scss):
+
+.activity-page {
+ min-height: 100vh;
+ background: #F8F9FC;
+ padding-bottom: 180rpx;
+}
+
+/* === Hero Banner === */
+.hero-banner {
+ background: linear-gradient(135deg, #6C5CE7 0%, #9B8FFF 100%);
+ padding: 80rpx 40rpx 60rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border-radius: 0 0 40rpx 40rpx;
+ position: relative;
+ overflow: hidden;
+}
+.hero-banner::after {
+ content: '';
+ position: absolute;
+ width: 400rpx; height: 400rpx;
+ background: rgba(255,255,255,0.06);
+ border-radius: 50%;
+ top: -100rpx; right: -80rpx;
+}
+.hero-badge {
+ display: flex;
+ align-items: center;
+ padding: 8rpx 24rpx;
+ background: rgba(255,255,255,0.2);
+ border-radius: 999rpx;
+ margin-bottom: 24rpx;
+}
+.hero-badge-icon { font-size: 24rpx; margin-right: 8rpx; }
+.hero-badge-text { font-size: 24rpx; color: white; font-weight: 500; }
+
+.hero-title {
+ font-size: 44rpx;
+ font-weight: 700;
+ color: white;
+ text-align: center;
+ letter-spacing: 2rpx;
+}
+.hero-subtitle {
+ font-size: 28rpx;
+ color: rgba(255,255,255,0.8);
+ margin-top: 12rpx;
+ text-align: center;
+}
+
+/* Countdown */
+.countdown-section {
+ margin-top: 40rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+.countdown-label {
+ font-size: 24rpx;
+ color: rgba(255,255,255,0.7);
+ margin-bottom: 16rpx;
+}
+.countdown-timer {
+ display: flex;
+ align-items: center;
+}
+.countdown-block {
+ width: 72rpx; height: 72rpx;
+ background: rgba(0,0,0,0.25);
+ border-radius: 12rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.countdown-block-ms {
+ width: 60rpx; height: 60rpx;
+ background: rgba(0,0,0,0.15);
+ border-radius: 10rpx;
+}
+.countdown-num {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: white;
+ font-family: 'DIN Alternate', 'Roboto Mono', monospace;
+}
+.countdown-num-ms {
+ font-size: 28rpx;
+ font-weight: 700;
+ color: rgba(255,255,255,0.8);
+ font-family: 'DIN Alternate', 'Roboto Mono', monospace;
+}
+.countdown-sep {
+ font-size: 28rpx;
+ font-weight: 700;
+ color: rgba(255,255,255,0.6);
+ margin: 0 8rpx;
+}
+
+/* === Coupon Section === */
+.coupon-section {
+ padding: 32rpx;
+}
+.section-header {
+ margin-bottom: 24rpx;
+}
+.section-title {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #141723;
+ display: block;
+}
+.section-subtitle {
+ font-size: 24rpx;
+ color: #A0A8BE;
+ margin-top: 4rpx;
+ display: block;
+}
+
+.coupon-card {
+ background: white;
+ border-radius: 24rpx;
+ margin-bottom: 24rpx;
+ border: 1rpx solid #F1F3F8;
+ overflow: hidden;
+ position: relative;
+ box-shadow: 0 4rpx 24rpx rgba(0,0,0,0.04);
+}
+.coupon-badge {
+ position: absolute;
+ top: 0; left: 0;
+ background: linear-gradient(135deg, #FF6B6B, #FF8E8E);
+ padding: 6rpx 20rpx 6rpx 16rpx;
+ border-radius: 0 0 16rpx 0;
+ z-index: 2;
+}
+.coupon-badge-text {
+ font-size: 22rpx;
+ font-weight: 700;
+ color: white;
+}
+
+.coupon-card-top {
+ height: 200rpx;
+ background: linear-gradient(135deg, #F3F1FF 0%, #E8E4FF 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+}
+.coupon-image-area {
+ width: 160rpx; height: 160rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.coupon-image-icon { font-size: 80rpx; opacity: 0.5; }
+.coupon-discount-tag {
+ position: absolute;
+ top: 16rpx; right: 16rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ padding: 8rpx 20rpx;
+ border-radius: 999rpx;
+}
+.coupon-discount-text {
+ font-size: 24rpx;
+ font-weight: 700;
+ color: white;
+}
+
+.coupon-card-body {
+ padding: 24rpx 28rpx;
+}
+.coupon-brand-row {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12rpx;
+}
+.coupon-brand-avatar {
+ width: 40rpx; height: 40rpx;
+ background: #F1F3F8;
+ border-radius: 8rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 12rpx;
+}
+.coupon-brand-initial {
+ font-size: 22rpx;
+ font-weight: 700;
+ color: #6C5CE7;
+}
+.coupon-brand-name {
+ font-size: 24rpx;
+ color: #5C6478;
+}
+.coupon-name {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: #141723;
+ display: block;
+ margin-bottom: 16rpx;
+}
+.coupon-price-row {
+ display: flex;
+ align-items: flex-end;
+}
+.coupon-price-symbol {
+ font-size: 24rpx;
+ font-weight: 700;
+ color: #6C5CE7;
+ margin-bottom: 4rpx;
+}
+.coupon-price-value {
+ font-size: 40rpx;
+ font-weight: 700;
+ color: #6C5CE7;
+ line-height: 1;
+ margin-right: 12rpx;
+}
+.coupon-original-price {
+ font-size: 24rpx;
+ color: #A0A8BE;
+ text-decoration: line-through;
+ margin-bottom: 4rpx;
+}
+
+.coupon-buy-btn {
+ margin: 0 28rpx 28rpx;
+ height: 80rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.coupon-buy-text {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: white;
+ letter-spacing: 2rpx;
+}
+
+/* === Rules Section === */
+.rules-section {
+ margin: 0 32rpx;
+ background: white;
+ border-radius: 24rpx;
+ padding: 32rpx;
+ border: 1rpx solid #F1F3F8;
+}
+.rules-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 24rpx;
+ padding-bottom: 20rpx;
+ border-bottom: 1rpx solid #F1F3F8;
+}
+.rules-icon {
+ width: 48rpx; height: 48rpx;
+ background: #F3F1FF;
+ border-radius: 12rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 16rpx;
+}
+.rules-icon-text { font-size: 28rpx; }
+.rules-title {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: #141723;
+}
+
+.rules-list { padding: 0; }
+.rule-item {
+ display: flex;
+ align-items: flex-start;
+ margin-bottom: 20rpx;
+}
+.rule-dot {
+ width: 10rpx; height: 10rpx;
+ background: #6C5CE7;
+ border-radius: 50%;
+ margin-top: 14rpx;
+ margin-right: 16rpx;
+ flex-shrink: 0;
+}
+.rule-text {
+ font-size: 24rpx;
+ color: #5C6478;
+ line-height: 1.6;
+}
+
+/* === Share Bar === */
+.share-bar {
+ position: fixed;
+ bottom: 0; left: 0; right: 0;
+ height: 120rpx;
+ background: white;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 32rpx;
+ border-top: 1rpx solid #F1F3F8;
+ box-shadow: 0 -4rpx 24rpx rgba(0,0,0,0.06);
+ z-index: 100;
+}
+.share-info {
+ display: flex;
+ align-items: center;
+}
+.share-count-icon { font-size: 32rpx; margin-right: 8rpx; }
+.share-count-text {
+ font-size: 24rpx;
+ color: #5C6478;
+}
+.share-btn {
+ display: flex;
+ align-items: center;
+ padding: 16rpx 40rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ border-radius: 999rpx;
+}
+.share-btn-icon { font-size: 28rpx; margin-right: 8rpx; }
+.share-btn-text {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: white;
+}
+
+/* === Brand Footer === */
+.brand-footer {
+ padding: 60rpx 32rpx 40rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+.footer-logo {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12rpx;
+}
+.footer-logo-box {
+ width: 48rpx; height: 48rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ border-radius: 12rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 12rpx;
+}
+.footer-logo-text {
+ font-size: 28rpx;
+ font-weight: 700;
+ color: white;
+}
+.footer-logo-name {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: #141723;
+}
+.footer-slogan {
+ font-size: 22rpx;
+ color: #A0A8BE;
+ margin-bottom: 8rpx;
+}
+.footer-copyright {
+ font-size: 20rpx;
+ color: #C8CDDA;
+}
+*/
diff --git a/frontend/miniapp/src/pages/h5-register/index.tsx b/frontend/miniapp/src/pages/h5-register/index.tsx
new file mode 100644
index 0000000..12a229d
--- /dev/null
+++ b/frontend/miniapp/src/pages/h5-register/index.tsx
@@ -0,0 +1,494 @@
+import React from 'react';
+// Taro mini-program component
+
+/**
+ * H5 Registration Guide Page
+ *
+ * 注册引导页 - 通过外部链接引导新用户注册
+ * 包含:品牌展示、权益介绍、注册表单、社交登录、信任标识
+ */
+
+const H5RegisterPage: React.FC = () => {
+ return (
+
+ {/* Top Branding Section */}
+
+
+
+
+ G
+
+ Genex
+ 全球券资产交易平台
+
+
+ {/* Benefits Section */}
+
+ 为什么选择 Genex?
+
+ {[
+ {
+ icon: '🎫',
+ title: '海量优惠券',
+ desc: '覆盖餐饮、购物、娱乐等20+品类,全球大牌低价好券',
+ },
+ {
+ icon: '🔒',
+ title: '安全交易',
+ desc: '平台担保交易,资金托管机制,保障每一笔交易安全可靠',
+ },
+ {
+ icon: '🤖',
+ title: 'AI智能推荐',
+ desc: '基于您的偏好智能推荐高性价比好券,省时又省钱',
+ },
+ ].map((benefit, i) => (
+
+
+ {benefit.icon}
+
+ {benefit.title}
+ {benefit.desc}
+
+ ))}
+
+
+
+ {/* Registration Form */}
+
+ 创建您的账户
+
+ {/* Phone Input */}
+
+
+ 📱
+
+ +86
+ ▼
+
+
+
+
+
+ {/* SMS Code Input */}
+
+
+
+ 🔑
+
+
+
+ 获取验证码
+
+
+
+
+ {/* Terms Checkbox */}
+
+
+
+
+
+ 我已阅读并同意
+ 《用户协议》
+ 和
+ 《隐私政策》
+
+
+
+ {/* Primary CTA Button */}
+
+ 立即注册
+
+
+ {/* Social Login Divider */}
+
+
+ 其他登录方式
+
+
+
+ {/* WeChat Login */}
+
+ 💬
+ 微信一键登录
+
+
+ {/* Already Have Account */}
+
+ 已有账号?
+ 立即登录
+
+
+
+ {/* Trust Badges */}
+
+
+ {[
+ { icon: '🛡️', label: '安全认证' },
+ { icon: '✅', label: '用户保障' },
+ { icon: '🔐', label: '隐私保护' },
+ ].map((badge, i) => (
+
+ {badge.icon}
+ {badge.label}
+
+ ))}
+
+ 您的信息受到银行级加密保护
+
+
+ );
+};
+
+export default H5RegisterPage;
+
+/*
+CSS (H5注册引导页样式 - 对应 index.scss):
+
+.register-page {
+ min-height: 100vh;
+ background: #F8F9FC;
+}
+
+/* === Branding Section === */
+.branding-section {
+ background: linear-gradient(135deg, #6C5CE7 0%, #9B8FFF 100%);
+ padding: 100rpx 40rpx 80rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: relative;
+ overflow: hidden;
+ border-radius: 0 0 48rpx 48rpx;
+}
+.brand-bg-circle-1 {
+ position: absolute;
+ width: 320rpx; height: 320rpx;
+ background: rgba(255,255,255,0.06);
+ border-radius: 50%;
+ top: -60rpx; right: -40rpx;
+}
+.brand-bg-circle-2 {
+ position: absolute;
+ width: 200rpx; height: 200rpx;
+ background: rgba(255,255,255,0.04);
+ border-radius: 50%;
+ bottom: -20rpx; left: -30rpx;
+}
+.brand-logo-box {
+ width: 120rpx; height: 120rpx;
+ background: rgba(255,255,255,0.2);
+ border-radius: 28rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 20rpx;
+ border: 2rpx solid rgba(255,255,255,0.3);
+ box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.15);
+}
+.brand-logo-letter {
+ font-size: 56rpx;
+ font-weight: 700;
+ color: white;
+}
+.brand-app-name {
+ font-size: 44rpx;
+ font-weight: 700;
+ color: white;
+ letter-spacing: 4rpx;
+}
+.brand-tagline {
+ font-size: 26rpx;
+ color: rgba(255,255,255,0.8);
+ margin-top: 8rpx;
+ letter-spacing: 2rpx;
+}
+
+/* === Benefits Section === */
+.benefits-section {
+ padding: 40rpx 32rpx 0;
+}
+.benefits-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #141723;
+ display: block;
+ margin-bottom: 24rpx;
+}
+.benefits-grid {
+ display: flex;
+ gap: 16rpx;
+}
+.benefit-card {
+ flex: 1;
+ background: white;
+ border-radius: 20rpx;
+ padding: 28rpx 20rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ border: 1rpx solid #F1F3F8;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.03);
+}
+.benefit-icon-box {
+ width: 72rpx; height: 72rpx;
+ background: #F3F1FF;
+ border-radius: 20rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 16rpx;
+}
+.benefit-icon { font-size: 36rpx; }
+.benefit-title {
+ font-size: 24rpx;
+ font-weight: 600;
+ color: #141723;
+ display: block;
+ margin-bottom: 8rpx;
+}
+.benefit-desc {
+ font-size: 20rpx;
+ color: #5C6478;
+ line-height: 1.5;
+}
+
+/* === Form Section === */
+.form-section {
+ padding: 40rpx 32rpx;
+}
+.form-title {
+ font-size: 34rpx;
+ font-weight: 700;
+ color: #141723;
+ display: block;
+ margin-bottom: 32rpx;
+}
+.form-input-group {
+ margin-bottom: 20rpx;
+}
+.form-input-wrap {
+ display: flex;
+ align-items: center;
+ height: 100rpx;
+ background: white;
+ border-radius: 20rpx;
+ padding: 0 28rpx;
+ border: 2rpx solid #F1F3F8;
+}
+.form-input-wrap:focus-within {
+ border-color: #6C5CE7;
+ box-shadow: 0 0 0 4rpx rgba(108,92,231,0.1);
+}
+.form-input-icon {
+ font-size: 32rpx;
+ margin-right: 16rpx;
+ flex-shrink: 0;
+}
+.form-input-prefix {
+ display: flex;
+ align-items: center;
+ padding-right: 16rpx;
+ margin-right: 16rpx;
+ border-right: 1rpx solid #F1F3F8;
+}
+.form-prefix-text {
+ font-size: 28rpx;
+ font-weight: 500;
+ color: #141723;
+}
+.form-prefix-arrow {
+ font-size: 16rpx;
+ color: #A0A8BE;
+ margin-left: 6rpx;
+}
+.form-input {
+ flex: 1;
+ font-size: 28rpx;
+ color: #141723;
+ background: transparent;
+}
+
+.form-code-wrap { padding: 0; }
+.form-code-left {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ padding: 0 28rpx;
+}
+.form-code-btn {
+ height: 100rpx;
+ padding: 0 28rpx;
+ display: flex;
+ align-items: center;
+ border-left: 1rpx solid #F1F3F8;
+}
+.form-code-btn-text {
+ font-size: 26rpx;
+ font-weight: 500;
+ color: #6C5CE7;
+ white-space: nowrap;
+}
+
+/* Terms Checkbox */
+.form-terms {
+ display: flex;
+ align-items: flex-start;
+ margin: 28rpx 0 36rpx;
+}
+.terms-checkbox {
+ width: 36rpx; height: 36rpx;
+ border: 2rpx solid #D0D5E0;
+ border-radius: 8rpx;
+ margin-right: 12rpx;
+ margin-top: 2rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+.terms-checkbox.checked {
+ background: #6C5CE7;
+ border-color: #6C5CE7;
+}
+.terms-checkbox-inner {
+ width: 16rpx; height: 16rpx;
+}
+.terms-text-wrap {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+}
+.terms-text-normal {
+ font-size: 24rpx;
+ color: #5C6478;
+}
+.terms-text-link {
+ font-size: 24rpx;
+ color: #6C5CE7;
+ font-weight: 500;
+}
+
+/* Register Button */
+.register-btn {
+ height: 100rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ border-radius: 999rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 32rpx rgba(108,92,231,0.35);
+}
+.register-btn:active {
+ opacity: 0.9;
+ transform: scale(0.98);
+}
+.register-btn-text {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: white;
+ letter-spacing: 4rpx;
+}
+
+/* Social Divider */
+.social-divider {
+ display: flex;
+ align-items: center;
+ margin: 48rpx 0;
+}
+.social-divider-line {
+ flex: 1;
+ height: 1rpx;
+ background: #E4E7F0;
+}
+.social-divider-text {
+ margin: 0 24rpx;
+ font-size: 24rpx;
+ color: #A0A8BE;
+ white-space: nowrap;
+}
+
+/* WeChat Login */
+.wechat-login-btn {
+ height: 100rpx;
+ background: #07C160;
+ border-radius: 999rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.wechat-login-btn:active {
+ opacity: 0.9;
+}
+.wechat-login-icon {
+ font-size: 36rpx;
+ margin-right: 12rpx;
+}
+.wechat-login-text {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: white;
+}
+
+/* Login Link */
+.login-link-row {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 32rpx;
+}
+.login-link-text {
+ font-size: 26rpx;
+ color: #5C6478;
+}
+.login-link-action {
+ font-size: 26rpx;
+ font-weight: 600;
+ color: #6C5CE7;
+}
+
+/* === Trust Section === */
+.trust-section {
+ padding: 40rpx 32rpx 60rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+.trust-badges {
+ display: flex;
+ justify-content: center;
+ gap: 48rpx;
+ margin-bottom: 20rpx;
+}
+.trust-badge-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+.trust-badge-icon {
+ font-size: 40rpx;
+ margin-bottom: 8rpx;
+}
+.trust-badge-label {
+ font-size: 22rpx;
+ font-weight: 500;
+ color: #5C6478;
+}
+.trust-footer-text {
+ font-size: 20rpx;
+ color: #A0A8BE;
+ margin-top: 8rpx;
+}
+*/
diff --git a/frontend/miniapp/src/pages/h5-share/index.tsx b/frontend/miniapp/src/pages/h5-share/index.tsx
new file mode 100644
index 0000000..94924fc
--- /dev/null
+++ b/frontend/miniapp/src/pages/h5-share/index.tsx
@@ -0,0 +1,308 @@
+import React from 'react';
+
+/**
+ * E2. H5页面 - 券分享页 + 活动落地页 + 注册引导页
+ *
+ * 券信息展示 + 「打开App购买」/「小程序购买」引导
+ */
+
+// === 券分享页 ===
+export const SharePage: React.FC = () => {
+ return (
+
+ {/* Header */}
+
+
+ {/* Coupon Card */}
+
+ {/* Image */}
+
+ 🎫
+
+
+ {/* Info */}
+
+
+
+
+ 星巴克 $25 礼品卡
+
+
+
+
+ $
+ 21.25
+ $25
+ 8.5折
+
+
+ 比面值节省 $3.75
+
+
+
+ {/* Info rows */}
+ {[
+ { label: '有效期', value: '2026/12/31' },
+ { label: '使用门店', value: '全国 12,800+ 门店' },
+ ].map((item, i) => (
+
+ {item.label}
+ {item.value}
+
+ ))}
+
+
+
+ {/* CTA Buttons */}
+
+
+
+
+
+ );
+};
+
+// === 活动落地页 ===
+export const ActivityPage: React.FC = () => {
+ return (
+
+ {/* Activity Banner */}
+
+
新用户专享
+
首单立减 $10,限时优惠
+
+
+ {/* Coupon Grid */}
+
+
活动好券
+
+ {Array.from({ length: 4 }, (_, i) => (
+
+
+ 🎫
+
+
+
品牌 {i + 1}
+
+ ${(i + 1) * 8.5}
+
+
+ ${(i + 1) * 10}
+
+
+
+ ))}
+
+
+ {/* CTA */}
+
+
+
+ );
+};
+
+// === 注册引导页 ===
+export const RegisterGuidePage: React.FC = () => {
+ return (
+
+ {/* Logo */}
+
+ 💎
+
+
+
+ 加入 Genex
+
+
+ 让每一张券都有价值
+ 注册即享首单立减优惠
+
+
+ {/* Features */}
+ {[
+ { icon: '🎫', title: '海量优惠券', desc: '餐饮、购物、娱乐全覆盖' },
+ { icon: '💰', title: '超值折扣', desc: '最低7折起,省钱又省心' },
+ { icon: '🔒', title: '安全交易', desc: '平台担保,放心购买' },
+ ].map((f, i) => (
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+export default SharePage;
diff --git a/frontend/miniapp/src/pages/home/index.tsx b/frontend/miniapp/src/pages/home/index.tsx
new file mode 100644
index 0000000..82b601c
--- /dev/null
+++ b/frontend/miniapp/src/pages/home/index.tsx
@@ -0,0 +1,181 @@
+import React from 'react';
+// Taro mini-program component (WeChat / Alipay)
+
+/**
+ * E1. 小程序核心页面 - 首页
+ *
+ * 消费者端的轻量版:热门券 + 分类入口 + AI推荐
+ * 支持微信支付/支付宝支付
+ */
+
+const HomePage: React.FC = () => {
+ return (
+
+ {/* Search Bar */}
+
+
+ 🔍
+ 搜索券、品牌...
+
+
+
+ {/* Banner */}
+
+ {['新用户专享 - 首单立减¥10', '限时折扣 - 全场低至7折', '热门推荐 - 精选高折扣券'].map((text, i) => (
+
+
+ {text.split(' - ')[0]}
+ {text.split(' - ')[1]}
+
+
+ ))}
+
+
+ {/* Category Grid */}
+
+ {[
+ { name: '餐饮', icon: '🍽️' },
+ { name: '购物', icon: '🛍️' },
+ { name: '娱乐', icon: '🎮' },
+ { name: '出行', icon: '🚗' },
+ { name: '全部', icon: '📋' },
+ ].map(cat => (
+
+ {cat.icon}
+ {cat.name}
+
+ ))}
+
+
+ {/* AI Suggestion (轻量版) */}
+
+ ✨
+
+ AI 推荐
+ 根据你的偏好,发现了高性价比券
+
+ ›
+
+
+ {/* Hot Coupons */}
+
+ 热门好券
+ 更多 ›
+
+
+
+ {[
+ { brand: 'Starbucks', name: '星巴克 ¥25 礼品卡', price: '¥21.25', face: '¥25', discount: '8.5折' },
+ { brand: 'Amazon', name: 'Amazon ¥100 购物券', price: '¥85.00', face: '¥100', discount: '8.5折' },
+ { brand: 'Nike', name: 'Nike ¥80 运动券', price: '¥68.00', face: '¥80', discount: '8.5折' },
+ { brand: 'Target', name: 'Target ¥30 折扣券', price: '¥24.00', face: '¥30', discount: '8折' },
+ ].map((coupon, i) => (
+
+
+ 🎫
+
+
+ {coupon.brand}
+ {coupon.name}
+
+ {coupon.price}
+ {coupon.face}
+ {coupon.discount}
+
+
+
+ ))}
+
+
+ );
+};
+
+export default HomePage;
+
+/*
+CSS (小程序样式 - 对应 index.scss):
+
+.home-page { padding-bottom: 120rpx; background: #F8F9FC; }
+
+.search-bar { padding: 20rpx 32rpx; }
+.search-input {
+ display: flex; align-items: center;
+ height: 72rpx; padding: 0 24rpx;
+ background: #F1F3F8; border-radius: 999rpx;
+ border: 1rpx solid #E4E7F0;
+}
+.search-icon { font-size: 32rpx; margin-right: 12rpx; }
+.search-placeholder { color: #A0A8BE; font-size: 28rpx; }
+
+.banner-swiper { height: 280rpx; margin: 0 32rpx; border-radius: 24rpx; }
+.banner-item {
+ height: 280rpx; border-radius: 24rpx; padding: 32rpx;
+ display: flex; flex-direction: column; justify-content: flex-end;
+}
+.banner-0 { background: linear-gradient(135deg, #6C5CE7, #9B8FFF); }
+.banner-1 { background: linear-gradient(135deg, #00C48C, #00E6A0); }
+.banner-2 { background: linear-gradient(135deg, #4834D4, #6C5CE7); }
+.banner-title { color: white; font-size: 36rpx; font-weight: 700; }
+.banner-subtitle { color: rgba(255,255,255,0.8); font-size: 26rpx; margin-top: 8rpx; }
+
+.category-grid {
+ display: flex; justify-content: space-around; padding: 32rpx;
+}
+.category-item { display: flex; flex-direction: column; align-items: center; }
+.category-icon {
+ width: 80rpx; height: 80rpx;
+ background: #F3F1FF; border-radius: 16rpx;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 36rpx;
+}
+.category-name { font-size: 22rpx; color: #141723; margin-top: 8rpx; }
+
+.ai-suggestion {
+ display: flex; align-items: center;
+ margin: 0 32rpx; padding: 20rpx 24rpx;
+ background: #F3F1FF; border-radius: 16rpx;
+ border: 1rpx solid rgba(108,92,231,0.15);
+}
+.ai-icon { font-size: 36rpx; margin-right: 16rpx; }
+.ai-content { flex: 1; }
+.ai-title { font-size: 24rpx; color: #6C5CE7; font-weight: 500; }
+.ai-text { font-size: 22rpx; color: #5C6478; }
+.ai-arrow { font-size: 32rpx; color: #6C5CE7; }
+
+.section-header {
+ display: flex; justify-content: space-between; align-items: center;
+ padding: 32rpx 32rpx 16rpx;
+}
+.section-title { font-size: 32rpx; font-weight: 600; color: #141723; }
+.section-more { font-size: 24rpx; color: #6C5CE7; }
+
+.coupon-card {
+ display: flex; margin: 0 32rpx 16rpx; padding: 20rpx;
+ background: white; border-radius: 16rpx;
+ border: 1rpx solid #F1F3F8;
+}
+.coupon-image {
+ width: 160rpx; height: 160rpx;
+ background: #F3F1FF; border-radius: 12rpx;
+ display: flex; align-items: center; justify-content: center;
+}
+.coupon-image-icon { font-size: 48rpx; }
+.coupon-info { flex: 1; padding-left: 20rpx; display: flex; flex-direction: column; justify-content: space-between; }
+.coupon-brand { font-size: 22rpx; color: #A0A8BE; }
+.coupon-name { font-size: 28rpx; font-weight: 500; color: #141723; }
+.coupon-price-row { display: flex; align-items: flex-end; }
+.coupon-price { font-size: 32rpx; font-weight: 700; color: #6C5CE7; }
+.coupon-face { font-size: 22rpx; color: #A0A8BE; text-decoration: line-through; margin-left: 8rpx; }
+.coupon-discount {
+ margin-left: 8rpx; padding: 2rpx 10rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ color: white; font-size: 20rpx; font-weight: 700;
+ border-radius: 999rpx;
+}
+*/
diff --git a/frontend/miniapp/src/pages/login/index.tsx b/frontend/miniapp/src/pages/login/index.tsx
new file mode 100644
index 0000000..1398c8f
--- /dev/null
+++ b/frontend/miniapp/src/pages/login/index.tsx
@@ -0,0 +1,142 @@
+import React from 'react';
+// Taro mini-program component
+
+/**
+ * E3. 登录/注册页
+ *
+ * 微信小程序:一键微信登录
+ * H5:手机号+验证码
+ */
+
+const LoginPage: React.FC = () => {
+ return (
+
+ {/* Logo */}
+
+
+ G
+
+ Genex
+ 发现优质好券
+
+
+ {/* WeChat Login (小程序) */}
+
+
+ 💬
+ 微信一键登录
+
+
+
+
+ 或
+
+
+
+ {/* Phone Login (H5) */}
+
+
+ 📱
+
+
+
+
+ 🔒
+
+
+
+ 获取验证码
+
+
+
+ 登录
+
+
+
+ {/* Terms */}
+
+ 登录即表示同意
+ 《用户协议》
+ 和
+ 《隐私政策》
+
+
+
+ );
+};
+
+export default LoginPage;
+
+/*
+CSS:
+
+.login-page {
+ min-height: 100vh; background: white;
+ display: flex; flex-direction: column;
+ padding: 0 64rpx;
+}
+
+.logo-section {
+ display: flex; flex-direction: column; align-items: center;
+ padding-top: 160rpx; padding-bottom: 80rpx;
+}
+.logo-box {
+ width: 120rpx; height: 120rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ border-radius: 28rpx;
+ display: flex; align-items: center; justify-content: center;
+}
+.logo-text { color: white; font-size: 48rpx; font-weight: 700; }
+.app-name { font-size: 40rpx; font-weight: 700; color: #141723; margin-top: 24rpx; }
+.app-slogan { font-size: 26rpx; color: #A0A8BE; margin-top: 8rpx; }
+
+.login-actions { flex: 1; }
+
+.wechat-btn {
+ display: flex; align-items: center; justify-content: center;
+ height: 96rpx; background: #07C160; border-radius: 999rpx;
+}
+.wechat-icon { font-size: 36rpx; margin-right: 12rpx; }
+.wechat-text { color: white; font-size: 30rpx; font-weight: 600; }
+
+.divider {
+ display: flex; align-items: center; margin: 48rpx 0;
+}
+.divider-line { flex: 1; height: 1rpx; background: #E4E7F0; }
+.divider-text { margin: 0 24rpx; font-size: 24rpx; color: #A0A8BE; }
+
+.input-wrap {
+ display: flex; align-items: center;
+ height: 96rpx; background: #F8F9FC;
+ border-radius: 16rpx; padding: 0 24rpx;
+ margin-bottom: 20rpx; border: 1rpx solid #E4E7F0;
+}
+.input-icon { font-size: 32rpx; margin-right: 16rpx; }
+.input-field { flex: 1; font-size: 28rpx; background: transparent; }
+
+.code-wrap { padding: 0; }
+.code-input {
+ flex: 1; display: flex; align-items: center;
+ padding: 0 24rpx;
+}
+.code-btn {
+ padding: 0 28rpx; height: 96rpx;
+ display: flex; align-items: center;
+ border-left: 1rpx solid #E4E7F0;
+}
+.code-btn-text { font-size: 24rpx; color: #6C5CE7; font-weight: 500; }
+
+.login-btn {
+ height: 96rpx; background: #6C5CE7; border-radius: 999rpx;
+ display: flex; align-items: center; justify-content: center;
+ margin-top: 16rpx;
+}
+.login-btn-text { color: white; font-size: 30rpx; font-weight: 600; }
+
+.terms {
+ display: flex; justify-content: center; align-items: center;
+ padding: 48rpx 0; flex-wrap: wrap;
+}
+.terms-text { font-size: 22rpx; color: #A0A8BE; }
+.terms-link { font-size: 22rpx; color: #6C5CE7; }
+*/
diff --git a/frontend/miniapp/src/pages/my-coupons/index.tsx b/frontend/miniapp/src/pages/my-coupons/index.tsx
new file mode 100644
index 0000000..13a9dbf
--- /dev/null
+++ b/frontend/miniapp/src/pages/my-coupons/index.tsx
@@ -0,0 +1,130 @@
+import React from 'react';
+// Taro mini-program component
+
+/**
+ * E2. 我的券 - 消费者持有的券列表
+ *
+ * 按状态分类:可使用 / 已使用 / 已过期
+ * 每张券可操作:使用(核销)、出售、转赠
+ */
+
+const MyCouponsPage: React.FC = () => {
+ const tabs = ['可使用', '已使用', '已过期'];
+ const [activeTab] = React.useState(0);
+
+ return (
+
+ {/* Tabs */}
+
+ {tabs.map((tab, i) => (
+
+ {tab}
+ {i === activeTab && }
+
+ ))}
+
+
+ {/* Coupon List */}
+
+ {[
+ { brand: 'Starbucks', name: '星巴克 ¥25 礼品卡', expiry: '2026-04-15', status: 'active' },
+ { brand: 'Amazon', name: 'Amazon ¥100 购物券', expiry: '2026-03-20', status: 'active' },
+ { brand: 'Nike', name: 'Nike ¥80 运动券', expiry: '2026-05-01', status: 'active' },
+ ].map((coupon, i) => (
+
+
+
+ 🎫
+
+
+
+ {coupon.brand}
+ {coupon.name}
+ 有效期至 {coupon.expiry}
+
+
+
+ 使用
+
+
+ {/* Ticket notch decoration */}
+
+
+
+ ))}
+
+
+ {/* Empty State for other tabs */}
+ {activeTab > 0 && (
+
+ 📭
+ 暂无券
+
+ )}
+
+ );
+};
+
+export default MyCouponsPage;
+
+/*
+CSS (小程序样式 - 对应 index.scss):
+
+.my-coupons-page { background: #F8F9FC; min-height: 100vh; }
+
+.tabs {
+ display: flex; background: white;
+ border-bottom: 1rpx solid #F1F3F8;
+}
+.tab {
+ flex: 1; display: flex; flex-direction: column;
+ align-items: center; padding: 24rpx 0; position: relative;
+}
+.tab-text { font-size: 28rpx; color: #A0A8BE; }
+.tab-text-active { color: #6C5CE7; font-weight: 600; }
+.tab-indicator {
+ position: absolute; bottom: 0; width: 48rpx; height: 4rpx;
+ background: #6C5CE7; border-radius: 999rpx;
+}
+
+.coupon-list { padding: 24rpx 32rpx; }
+
+.my-coupon-card {
+ display: flex; align-items: center; position: relative;
+ background: white; border-radius: 16rpx; padding: 24rpx;
+ margin-bottom: 16rpx; border: 1rpx solid #F1F3F8;
+ overflow: hidden;
+}
+.coupon-left { margin-right: 20rpx; }
+.coupon-icon-wrap {
+ width: 100rpx; height: 100rpx; background: #F3F1FF;
+ border-radius: 12rpx; display: flex;
+ align-items: center; justify-content: center;
+}
+.coupon-icon-text { font-size: 40rpx; }
+.coupon-center { flex: 1; }
+.coupon-brand { font-size: 22rpx; color: #A0A8BE; }
+.coupon-name { font-size: 28rpx; font-weight: 500; color: #141723; margin-top: 4rpx; }
+.coupon-expiry { font-size: 22rpx; color: #A0A8BE; margin-top: 8rpx; }
+.coupon-right { margin-left: 16rpx; }
+.use-btn {
+ padding: 12rpx 28rpx; background: #6C5CE7;
+ border-radius: 999rpx;
+}
+.use-btn-text { color: white; font-size: 24rpx; font-weight: 600; }
+
+.notch {
+ position: absolute; left: 130rpx;
+ width: 20rpx; height: 20rpx; background: #F8F9FC;
+ border-radius: 50%;
+}
+.notch-top { top: -10rpx; }
+.notch-bottom { bottom: -10rpx; }
+
+.empty-state {
+ display: flex; flex-direction: column; align-items: center;
+ padding: 120rpx 0;
+}
+.empty-icon { font-size: 64rpx; }
+.empty-text { font-size: 28rpx; color: #A0A8BE; margin-top: 16rpx; }
+*/
diff --git a/frontend/miniapp/src/pages/orders/index.tsx b/frontend/miniapp/src/pages/orders/index.tsx
new file mode 100644
index 0000000..30d74d2
--- /dev/null
+++ b/frontend/miniapp/src/pages/orders/index.tsx
@@ -0,0 +1 @@
+test
\ No newline at end of file
diff --git a/frontend/miniapp/src/pages/profile/index.tsx b/frontend/miniapp/src/pages/profile/index.tsx
new file mode 100644
index 0000000..e63a6e8
--- /dev/null
+++ b/frontend/miniapp/src/pages/profile/index.tsx
@@ -0,0 +1,151 @@
+import React from 'react';
+// Taro mini-program component
+
+/**
+ * E5. 个人中心
+ *
+ * 用户信息、我的券入口、我的订单、设置、下载App引导
+ */
+
+const ProfilePage: React.FC = () => {
+ return (
+
+ {/* User Header */}
+
+
+ U
+
+
+ User_138****88
+
+ L1 基础认证
+
+
+
+
+ {/* Stats */}
+
+ {[
+ { value: '5', label: '持有券' },
+ { value: '12', label: '已使用' },
+ { value: '3', label: '已过期' },
+ ].map(s => (
+
+ {s.value}
+ {s.label}
+
+ ))}
+
+
+ {/* Menu */}
+
+ {[
+ { icon: '🎫', label: '我的券', path: '/pages/my-coupons/index' },
+ { icon: '📋', label: '我的订单', path: '/pages/orders/index' },
+ { icon: '💳', label: '支付管理', path: '' },
+ { icon: '🔔', label: '消息通知', path: '' },
+ ].map(item => (
+
+ {item.icon}
+ {item.label}
+ ›
+
+ ))}
+
+
+
+ {[
+ { icon: '🌐', label: '语言 / Language', value: '简体中文' },
+ { icon: '💰', label: '货币', value: 'USD' },
+ { icon: '❓', label: '帮助中心', value: '' },
+ { icon: '⚙️', label: '设置', value: '' },
+ ].map(item => (
+
+ {item.icon}
+ {item.label}
+ {item.value ? {item.value} : null}
+ ›
+
+ ))}
+
+
+ {/* Download App Banner */}
+
+
+ 下载 Genex App
+ 解锁二级市场交易、P2P转赠等完整功能
+
+
+ 下载
+
+
+
+ );
+};
+
+export default ProfilePage;
+
+/*
+CSS:
+
+.profile-page { background: #F8F9FC; min-height: 100vh; padding-bottom: 120rpx; }
+
+.profile-header {
+ display: flex; align-items: center;
+ padding: 48rpx 32rpx; background: white;
+}
+.avatar {
+ width: 100rpx; height: 100rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ border-radius: 50%; display: flex;
+ align-items: center; justify-content: center;
+}
+.avatar-text { color: white; font-size: 40rpx; font-weight: 700; }
+.user-info { margin-left: 24rpx; }
+.username { font-size: 32rpx; font-weight: 600; color: #141723; }
+.kyc-badge {
+ display: inline-flex; margin-top: 8rpx;
+ padding: 4rpx 14rpx; background: #E6FAF3;
+ border-radius: 999rpx;
+}
+.kyc-text { font-size: 22rpx; color: #00C48C; font-weight: 500; }
+
+.stats-row {
+ display: flex; background: white; padding: 24rpx 0;
+ border-top: 1rpx solid #F1F3F8;
+}
+.stat-item {
+ flex: 1; display: flex; flex-direction: column;
+ align-items: center;
+}
+.stat-value { font-size: 36rpx; font-weight: 700; color: #6C5CE7; }
+.stat-label { font-size: 22rpx; color: #A0A8BE; margin-top: 4rpx; }
+
+.menu-section {
+ background: white; margin-top: 16rpx;
+}
+.menu-item {
+ display: flex; align-items: center;
+ padding: 28rpx 32rpx;
+ border-bottom: 1rpx solid #F1F3F8;
+}
+.menu-icon { font-size: 36rpx; margin-right: 20rpx; }
+.menu-label { flex: 1; font-size: 28rpx; color: #141723; }
+.menu-value { font-size: 24rpx; color: #A0A8BE; margin-right: 8rpx; }
+.menu-arrow { font-size: 32rpx; color: #CDD2DE; }
+
+.download-banner {
+ display: flex; align-items: center;
+ margin: 32rpx; padding: 28rpx;
+ background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
+ border-radius: 16rpx;
+}
+.download-content { flex: 1; }
+.download-title { font-size: 28rpx; font-weight: 600; color: white; }
+.download-desc { font-size: 22rpx; color: rgba(255,255,255,0.7); margin-top: 4rpx; }
+.download-btn {
+ padding: 12rpx 28rpx; background: white;
+ border-radius: 999rpx;
+}
+.download-btn-text { font-size: 24rpx; color: #6C5CE7; font-weight: 600; }
+*/
diff --git a/frontend/miniapp/src/pages/purchase/index.tsx b/frontend/miniapp/src/pages/purchase/index.tsx
new file mode 100644
index 0000000..2bf0590
--- /dev/null
+++ b/frontend/miniapp/src/pages/purchase/index.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+// Taro mini-program component (WeChat / Alipay)
+
+/**
+ * E1. 小程序核心页面 - 购买页
+ *
+ * Coupon summary card, quantity selector, price calculation,
+ * payment button (微信支付/支付宝/H5支付), order confirmation
+ */
+
+const PurchasePage: React.FC = () => {
+ return (
+
+ {/* Coupon Summary Card */}
+
+
+ 🎫
+
+
+ Starbucks
+ 星巴克 ¥25 礼品卡
+
+ ¥21.25
+ ¥25
+ 8.1折
+
+
+
+
+ );
+};
+
+export default PurchasePage;
diff --git a/frontend/miniapp/src/pages/redeem/index.tsx b/frontend/miniapp/src/pages/redeem/index.tsx
new file mode 100644
index 0000000..9ac2f1f
--- /dev/null
+++ b/frontend/miniapp/src/pages/redeem/index.tsx
@@ -0,0 +1,114 @@
+import React from 'react';
+// Taro mini-program component
+
+/**
+ * E4. 券使用页 - 出示券码给商户扫描
+ *
+ * QR码 + 数字码 + 倒计时 + 亮度最大化
+ */
+
+const RedeemPage: React.FC = () => {
+ return (
+
+ {/* Coupon Info */}
+
+ 星巴克 ¥25 礼品卡
+ 面值 ¥25.00
+
+
+ {/* QR Code */}
+
+
+
+ 📱
+ 二维码
+
+
+
+ {/* Numeric Code */}
+
+ 8429 3751 0062
+
+
+ {/* Countdown */}
+
+ ⏱
+ 有效时间 04:58
+
+
+
+ 刷新券码
+
+
+
+ {/* Hint */}
+
+ ℹ️
+ 请将此码出示给商户扫描,屏幕已自动调至最高亮度
+
+
+ );
+};
+
+export default RedeemPage;
+
+/*
+CSS:
+
+.redeem-page {
+ min-height: 100vh; background: #141723;
+ display: flex; flex-direction: column; align-items: center;
+ padding: 48rpx 32rpx;
+}
+
+.coupon-info {
+ display: flex; flex-direction: column; align-items: center;
+ margin-bottom: 48rpx;
+}
+.coupon-name { font-size: 36rpx; font-weight: 700; color: white; }
+.coupon-value { font-size: 26rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; }
+
+.qr-section {
+ display: flex; flex-direction: column; align-items: center;
+}
+
+.qr-box {
+ width: 420rpx; height: 420rpx; padding: 28rpx;
+ background: white; border-radius: 24rpx;
+}
+.qr-placeholder {
+ width: 100%; height: 100%;
+ border: 2rpx solid #E4E7F0; border-radius: 16rpx;
+ display: flex; flex-direction: column;
+ align-items: center; justify-content: center;
+}
+.qr-icon { font-size: 120rpx; }
+.qr-label { font-size: 24rpx; color: #A0A8BE; margin-top: 12rpx; }
+
+.code-display {
+ margin-top: 32rpx; padding: 16rpx 40rpx;
+ background: rgba(255,255,255,0.1); border-radius: 999rpx;
+}
+.code-text {
+ font-size: 40rpx; font-weight: 700; color: white;
+ letter-spacing: 4rpx; font-family: monospace;
+}
+
+.countdown {
+ display: flex; align-items: center;
+ margin-top: 32rpx;
+}
+.countdown-icon { font-size: 28rpx; margin-right: 8rpx; }
+.countdown-text { font-size: 26rpx; color: rgba(255,255,255,0.5); }
+
+.refresh-btn { margin-top: 16rpx; }
+.refresh-text { font-size: 26rpx; color: #9B8FFF; }
+
+.hint-box {
+ display: flex; align-items: center;
+ margin-top: 64rpx; padding: 20rpx 24rpx;
+ background: rgba(255,255,255,0.08); border-radius: 16rpx;
+}
+.hint-icon { font-size: 28rpx; margin-right: 12rpx; }
+.hint-text { font-size: 22rpx; color: rgba(255,255,255,0.4); flex: 1; }
+*/
diff --git a/frontend/mobile/lib/app/i18n/app_localizations.dart b/frontend/mobile/lib/app/i18n/app_localizations.dart
new file mode 100644
index 0000000..20f07c6
--- /dev/null
+++ b/frontend/mobile/lib/app/i18n/app_localizations.dart
@@ -0,0 +1,302 @@
+/// Genex Mobile App - i18n 多语言支持
+///
+/// 支持语言: zh-CN (默认), en-US, ja-JP
+/// 使用方式: AppLocalizations.of(context).translate('key')
+
+class AppLocalizations {
+ final String locale;
+
+ AppLocalizations(this.locale);
+
+ static AppLocalizations of(dynamic context) {
+ // In production, obtain from InheritedWidget / Provider
+ return AppLocalizations('zh-CN');
+ }
+
+ String translate(String key) {
+ return _localizedValues[locale]?[key] ??
+ _localizedValues['zh-CN']?[key] ??
+ key;
+ }
+
+ // Shorthand
+ String t(String key) => translate(key);
+
+ static const supportedLocales = ['zh-CN', 'en-US', 'ja-JP'];
+
+ static const Map> _localizedValues = {
+ 'zh-CN': _zhCN,
+ 'en-US': _enUS,
+ 'ja-JP': _jaJP,
+ };
+
+ static const Map _zhCN = {
+ // Common
+ 'app_name': 'Genex',
+ 'confirm': '确认',
+ 'cancel': '取消',
+ 'save': '保存',
+ 'delete': '删除',
+ 'edit': '编辑',
+ 'search': '搜索',
+ 'loading': '加载中...',
+ 'retry': '重试',
+ 'done': '完成',
+ 'next': '下一步',
+ 'back': '返回',
+ 'close': '关闭',
+ 'more': '更多',
+ 'all': '全部',
+
+ // Tabs
+ 'tab_home': '首页',
+ 'tab_market': '市场',
+ 'tab_wallet': '钱包',
+ 'tab_profile': '我的',
+
+ // Home
+ 'home_greeting': '你好',
+ 'home_search_hint': '搜索券、品牌...',
+ 'home_recommended': 'AI推荐',
+ 'home_hot': '热门券',
+ 'home_new': '新上架',
+ 'home_categories': '分类浏览',
+
+ // Coupon
+ 'coupon_buy': '购买',
+ 'coupon_sell': '出售',
+ 'coupon_transfer': '转赠',
+ 'coupon_use': '使用',
+ 'coupon_detail': '券详情',
+ 'coupon_face_value': '面值',
+ 'coupon_price': '价格',
+ 'coupon_discount': '折扣',
+ 'coupon_valid_until': '有效期至',
+ 'coupon_brand': '品牌',
+ 'coupon_category': '类别',
+ 'coupon_my_coupons': '我的券',
+ 'coupon_available': '可用',
+ 'coupon_used': '已使用',
+ 'coupon_expired': '已过期',
+
+ // Trading
+ 'trade_buy_order': '买单',
+ 'trade_sell_order': '卖单',
+ 'trade_price_input': '输入价格',
+ 'trade_quantity': '数量',
+ 'trade_total': '合计',
+ 'trade_history': '交易记录',
+ 'trade_pending': '待成交',
+ 'trade_completed': '已完成',
+
+ // Wallet
+ 'wallet_balance': '余额',
+ 'wallet_deposit': '充值',
+ 'wallet_withdraw': '提现',
+ 'wallet_transactions': '交易记录',
+
+ // Profile
+ 'profile_settings': '设置',
+ 'profile_kyc': '身份认证',
+ 'profile_kyc_l0': '未认证',
+ 'profile_kyc_l1': 'L1 基础认证',
+ 'profile_kyc_l2': 'L2 身份认证',
+ 'profile_kyc_l3': 'L3 高级认证',
+ 'profile_language': '语言',
+ 'profile_currency': '货币',
+ 'profile_help': '帮助中心',
+ 'profile_about': '关于',
+ 'profile_logout': '退出登录',
+ 'profile_pro_mode': '高级模式',
+
+ // Payment
+ 'payment_method': '支付方式',
+ 'payment_confirm': '确认支付',
+ 'payment_success': '支付成功',
+
+ // AI
+ 'ai_assistant': 'AI助手',
+ 'ai_ask': '问我任何问题...',
+ 'ai_suggestion': 'AI建议',
+ };
+
+ static const Map _enUS = {
+ // Common
+ 'app_name': 'Genex',
+ 'confirm': 'Confirm',
+ 'cancel': 'Cancel',
+ 'save': 'Save',
+ 'delete': 'Delete',
+ 'edit': 'Edit',
+ 'search': 'Search',
+ 'loading': 'Loading...',
+ 'retry': 'Retry',
+ 'done': 'Done',
+ 'next': 'Next',
+ 'back': 'Back',
+ 'close': 'Close',
+ 'more': 'More',
+ 'all': 'All',
+
+ // Tabs
+ 'tab_home': 'Home',
+ 'tab_market': 'Market',
+ 'tab_wallet': 'Wallet',
+ 'tab_profile': 'Profile',
+
+ // Home
+ 'home_greeting': 'Hello',
+ 'home_search_hint': 'Search coupons, brands...',
+ 'home_recommended': 'AI Picks',
+ 'home_hot': 'Trending',
+ 'home_new': 'New Arrivals',
+ 'home_categories': 'Categories',
+
+ // Coupon
+ 'coupon_buy': 'Buy',
+ 'coupon_sell': 'Sell',
+ 'coupon_transfer': 'Gift',
+ 'coupon_use': 'Redeem',
+ 'coupon_detail': 'Coupon Details',
+ 'coupon_face_value': 'Face Value',
+ 'coupon_price': 'Price',
+ 'coupon_discount': 'Discount',
+ 'coupon_valid_until': 'Valid Until',
+ 'coupon_brand': 'Brand',
+ 'coupon_category': 'Category',
+ 'coupon_my_coupons': 'My Coupons',
+ 'coupon_available': 'Available',
+ 'coupon_used': 'Used',
+ 'coupon_expired': 'Expired',
+
+ // Trading
+ 'trade_buy_order': 'Buy Order',
+ 'trade_sell_order': 'Sell Order',
+ 'trade_price_input': 'Enter Price',
+ 'trade_quantity': 'Quantity',
+ 'trade_total': 'Total',
+ 'trade_history': 'Trade History',
+ 'trade_pending': 'Pending',
+ 'trade_completed': 'Completed',
+
+ // Wallet
+ 'wallet_balance': 'Balance',
+ 'wallet_deposit': 'Deposit',
+ 'wallet_withdraw': 'Withdraw',
+ 'wallet_transactions': 'Transactions',
+
+ // Profile
+ 'profile_settings': 'Settings',
+ 'profile_kyc': 'Verification',
+ 'profile_kyc_l0': 'Unverified',
+ 'profile_kyc_l1': 'L1 Basic',
+ 'profile_kyc_l2': 'L2 Identity',
+ 'profile_kyc_l3': 'L3 Advanced',
+ 'profile_language': 'Language',
+ 'profile_currency': 'Currency',
+ 'profile_help': 'Help Center',
+ 'profile_about': 'About',
+ 'profile_logout': 'Log Out',
+ 'profile_pro_mode': 'Pro Mode',
+
+ // Payment
+ 'payment_method': 'Payment Method',
+ 'payment_confirm': 'Confirm Payment',
+ 'payment_success': 'Payment Successful',
+
+ // AI
+ 'ai_assistant': 'AI Assistant',
+ 'ai_ask': 'Ask me anything...',
+ 'ai_suggestion': 'AI Suggestion',
+ };
+
+ static const Map _jaJP = {
+ // Common
+ 'app_name': 'Genex',
+ 'confirm': '確認',
+ 'cancel': 'キャンセル',
+ 'save': '保存',
+ 'delete': '削除',
+ 'edit': '編集',
+ 'search': '検索',
+ 'loading': '読み込み中...',
+ 'retry': 'リトライ',
+ 'done': '完了',
+ 'next': '次へ',
+ 'back': '戻る',
+ 'close': '閉じる',
+ 'more': 'もっと見る',
+ 'all': 'すべて',
+
+ // Tabs
+ 'tab_home': 'ホーム',
+ 'tab_market': 'マーケット',
+ 'tab_wallet': 'ウォレット',
+ 'tab_profile': 'マイページ',
+
+ // Home
+ 'home_greeting': 'こんにちは',
+ 'home_search_hint': 'クーポン、ブランドを検索...',
+ 'home_recommended': 'AIおすすめ',
+ 'home_hot': '人気',
+ 'home_new': '新着',
+ 'home_categories': 'カテゴリー',
+
+ // Coupon
+ 'coupon_buy': '購入',
+ 'coupon_sell': '売却',
+ 'coupon_transfer': '贈与',
+ 'coupon_use': '使用',
+ 'coupon_detail': 'クーポン詳細',
+ 'coupon_face_value': '額面',
+ 'coupon_price': '価格',
+ 'coupon_discount': '割引',
+ 'coupon_valid_until': '有効期限',
+ 'coupon_brand': 'ブランド',
+ 'coupon_category': 'カテゴリー',
+ 'coupon_my_coupons': 'マイクーポン',
+ 'coupon_available': '利用可能',
+ 'coupon_used': '使用済み',
+ 'coupon_expired': '期限切れ',
+
+ // Trading
+ 'trade_buy_order': '買い注文',
+ 'trade_sell_order': '売り注文',
+ 'trade_price_input': '価格を入力',
+ 'trade_quantity': '数量',
+ 'trade_total': '合計',
+ 'trade_history': '取引履歴',
+ 'trade_pending': '未約定',
+ 'trade_completed': '約定済み',
+
+ // Wallet
+ 'wallet_balance': '残高',
+ 'wallet_deposit': '入金',
+ 'wallet_withdraw': '出金',
+ 'wallet_transactions': '取引履歴',
+
+ // Profile
+ 'profile_settings': '設定',
+ 'profile_kyc': '本人確認',
+ 'profile_kyc_l0': '未確認',
+ 'profile_kyc_l1': 'L1 基本認証',
+ 'profile_kyc_l2': 'L2 身分認証',
+ 'profile_kyc_l3': 'L3 高度認証',
+ 'profile_language': '言語',
+ 'profile_currency': '通貨',
+ 'profile_help': 'ヘルプ',
+ 'profile_about': 'アプリについて',
+ 'profile_logout': 'ログアウト',
+ 'profile_pro_mode': 'プロモード',
+
+ // Payment
+ 'payment_method': '支払い方法',
+ 'payment_confirm': '支払いを確認',
+ 'payment_success': '支払い完了',
+
+ // AI
+ 'ai_assistant': 'AIアシスタント',
+ 'ai_ask': '何でも聞いてください...',
+ 'ai_suggestion': 'AIの提案',
+ };
+}
diff --git a/frontend/mobile/lib/app/main_shell.dart b/frontend/mobile/lib/app/main_shell.dart
new file mode 100644
index 0000000..0825b2c
--- /dev/null
+++ b/frontend/mobile/lib/app/main_shell.dart
@@ -0,0 +1,97 @@
+import 'package:flutter/material.dart';
+import '../app/theme/app_colors.dart';
+import '../app/theme/app_typography.dart';
+import '../features/coupons/presentation/pages/home_page.dart';
+import '../features/coupons/presentation/pages/market_page.dart';
+import '../features/coupons/presentation/pages/my_coupons_page.dart';
+import '../features/message/presentation/pages/message_page.dart';
+import '../features/profile/presentation/pages/profile_page.dart';
+
+/// 消费者App主Shell - Bottom Navigation
+///
+/// Tab: 首页 / 市场 / 我的券 / 消息 / 我的
+class MainShell extends StatefulWidget {
+ const MainShell({super.key});
+
+ @override
+ State createState() => _MainShellState();
+}
+
+class _MainShellState extends State {
+ int _currentIndex = 0;
+
+ final _pages = const [
+ HomePage(),
+ MarketPage(),
+ MyCouponsPage(),
+ MessagePage(),
+ ProfilePage(),
+ ];
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: IndexedStack(
+ index: _currentIndex,
+ children: _pages,
+ ),
+ bottomNavigationBar: Container(
+ decoration: const BoxDecoration(
+ color: AppColors.surface,
+ border: Border(top: BorderSide(color: AppColors.borderLight, width: 0.5)),
+ ),
+ child: NavigationBar(
+ selectedIndex: _currentIndex,
+ onDestinationSelected: (index) => setState(() => _currentIndex = index),
+ destinations: [
+ _buildDestination(Icons.home_rounded, Icons.home_outlined, '首页'),
+ _buildDestination(Icons.storefront_rounded, Icons.storefront_outlined, '市场'),
+ _buildDestination(
+ Icons.confirmation_number_rounded,
+ Icons.confirmation_number_outlined,
+ '我的券',
+ ),
+ _buildBadgeDestination(
+ Icons.notifications_rounded,
+ Icons.notifications_outlined,
+ '消息',
+ 2,
+ ),
+ _buildDestination(Icons.person_rounded, Icons.person_outlined, '我的'),
+ ],
+ ),
+ ),
+ );
+ }
+
+ NavigationDestination _buildDestination(
+ IconData selected,
+ IconData unselected,
+ String label,
+ ) {
+ return NavigationDestination(
+ icon: Icon(unselected),
+ selectedIcon: Icon(selected),
+ label: label,
+ );
+ }
+
+ NavigationDestination _buildBadgeDestination(
+ IconData selected,
+ IconData unselected,
+ String label,
+ int count,
+ ) {
+ return NavigationDestination(
+ icon: Badge(
+ label: Text('$count', style: const TextStyle(fontSize: 10)),
+ child: Icon(unselected),
+ ),
+ selectedIcon: Badge(
+ label: Text('$count', style: const TextStyle(fontSize: 10)),
+ child: Icon(selected),
+ ),
+ label: label,
+ );
+ }
+}
diff --git a/frontend/mobile/lib/app/theme/app_colors.dart b/frontend/mobile/lib/app/theme/app_colors.dart
new file mode 100644
index 0000000..50a833f
--- /dev/null
+++ b/frontend/mobile/lib/app/theme/app_colors.dart
@@ -0,0 +1,123 @@
+import 'package:flutter/material.dart';
+
+/// Genex Design System - Color Tokens
+///
+/// 紫色系科技风,干净清爽型(参考 Stripe/Alipay/Venmo)
+/// Primary: #6C5CE7 (创新/科技紫)
+class AppColors {
+ AppColors._();
+
+ // ============================================================
+ // Primary Purple Palette
+ // ============================================================
+ static const Color primary = Color(0xFF6C5CE7);
+ static const Color primaryLight = Color(0xFF9B8FFF);
+ static const Color primaryDark = Color(0xFF4834D4);
+ static const Color primarySurface = Color(0xFFF3F1FF);
+ static const Color primaryContainer = Color(0xFFE8E5FF);
+
+ // ============================================================
+ // Neutral Palette (Cool Gray)
+ // ============================================================
+ static const Color gray50 = Color(0xFFF8F9FC);
+ static const Color gray100 = Color(0xFFF1F3F8);
+ static const Color gray200 = Color(0xFFE4E7F0);
+ static const Color gray300 = Color(0xFFCDD2DE);
+ static const Color gray400 = Color(0xFFA0A8BE);
+ static const Color gray500 = Color(0xFF7A839E);
+ static const Color gray600 = Color(0xFF5C6478);
+ static const Color gray700 = Color(0xFF3D4459);
+ static const Color gray800 = Color(0xFF262B3A);
+ static const Color gray900 = Color(0xFF141723);
+
+ // ============================================================
+ // Semantic Colors
+ // ============================================================
+ static const Color success = Color(0xFF00C48C);
+ static const Color successLight = Color(0xFFE6FAF3);
+ static const Color warning = Color(0xFFFFAB2E);
+ static const Color warningLight = Color(0xFFFFF7E6);
+ static const Color error = Color(0xFFFF4757);
+ static const Color errorLight = Color(0xFFFFF0F0);
+ static const Color info = Color(0xFF3B82F6);
+ static const Color infoLight = Color(0xFFEFF6FF);
+
+ // ============================================================
+ // Background & Surface
+ // ============================================================
+ static const Color background = Color(0xFFF8F9FC);
+ static const Color surface = Color(0xFFFFFFFF);
+ static const Color surfaceVariant = Color(0xFFF1F3F8);
+ static const Color surfaceElevated = Color(0xFFFFFFFF);
+ static const Color scrim = Color(0x52000000);
+
+ // ============================================================
+ // Text Colors
+ // ============================================================
+ static const Color textPrimary = Color(0xFF141723);
+ static const Color textSecondary = Color(0xFF5C6478);
+ static const Color textTertiary = Color(0xFFA0A8BE);
+ static const Color textDisabled = Color(0xFFCDD2DE);
+ static const Color textOnPrimary = Color(0xFFFFFFFF);
+ static const Color textLink = Color(0xFF6C5CE7);
+
+ // ============================================================
+ // Border Colors
+ // ============================================================
+ static const Color border = Color(0xFFE4E7F0);
+ static const Color borderLight = Color(0xFFF1F3F8);
+ static const Color borderFocus = Color(0xFF6C5CE7);
+
+ // ============================================================
+ // Coupon-specific Colors (券专属)
+ // ============================================================
+ static const Color couponDining = Color(0xFFFF6B6B);
+ static const Color couponShopping = Color(0xFF6C5CE7);
+ static const Color couponEntertainment = Color(0xFFFFAB2E);
+ static const Color couponTravel = Color(0xFF00C48C);
+ static const Color couponOther = Color(0xFF3B82F6);
+
+ // ============================================================
+ // Credit Rating Colors (信用等级)
+ // ============================================================
+ static const Color creditAAA = Color(0xFF00C48C);
+ static const Color creditAA = Color(0xFF3B82F6);
+ static const Color creditA = Color(0xFF6C5CE7);
+ static const Color creditBBB = Color(0xFFFFAB2E);
+ static const Color creditBB = Color(0xFFFF6B6B);
+
+ // ============================================================
+ // Coupon Status Colors (券状态)
+ // ============================================================
+ static const Color statusActive = Color(0xFF00C48C);
+ static const Color statusPending = Color(0xFFFFAB2E);
+ static const Color statusExpired = Color(0xFFA0A8BE);
+ static const Color statusUsed = Color(0xFFCDD2DE);
+
+ // ============================================================
+ // Track Colors (Utility / Securities)
+ // ============================================================
+ static const Color utilityTrack = Color(0xFF00C48C);
+ static const Color securitiesTrack = Color(0xFFFFAB2E);
+
+ // ============================================================
+ // Gradient Definitions
+ // ============================================================
+ static const LinearGradient primaryGradient = LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [Color(0xFF6C5CE7), Color(0xFF9B8FFF)],
+ );
+
+ static const LinearGradient cardGradient = LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [Color(0xFF6C5CE7), Color(0xFF4834D4)],
+ );
+
+ static const LinearGradient successGradient = LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [Color(0xFF00C48C), Color(0xFF00E6A0)],
+ );
+}
diff --git a/frontend/mobile/lib/app/theme/app_spacing.dart b/frontend/mobile/lib/app/theme/app_spacing.dart
new file mode 100644
index 0000000..d9d8970
--- /dev/null
+++ b/frontend/mobile/lib/app/theme/app_spacing.dart
@@ -0,0 +1,116 @@
+import 'package:flutter/material.dart';
+
+/// Genex Design System - Spacing & Layout Tokens
+///
+/// 基于 4px 网格系统,保持一致的留白节奏
+class AppSpacing {
+ AppSpacing._();
+
+ // ============================================================
+ // Base Grid (4px)
+ // ============================================================
+ static const double xs = 4;
+ static const double sm = 8;
+ static const double md = 12;
+ static const double lg = 16;
+ static const double xl = 20;
+ static const double xxl = 24;
+ static const double xxxl = 32;
+ static const double huge = 40;
+ static const double massive = 48;
+ static const double gigantic = 64;
+
+ // ============================================================
+ // Page Padding
+ // ============================================================
+ static const EdgeInsets pagePadding = EdgeInsets.symmetric(horizontal: 20);
+ static const EdgeInsets pageWithTop = EdgeInsets.fromLTRB(20, 16, 20, 0);
+
+ // ============================================================
+ // Card Padding
+ // ============================================================
+ static const EdgeInsets cardPadding = EdgeInsets.all(16);
+ static const EdgeInsets cardPaddingCompact = EdgeInsets.all(12);
+
+ // ============================================================
+ // Section Spacing
+ // ============================================================
+ static const double sectionGap = 24;
+ static const double itemGap = 12;
+ static const double inlineGap = 8;
+
+ // ============================================================
+ // Border Radius
+ // ============================================================
+ static const double radiusSm = 8;
+ static const double radiusMd = 12;
+ static const double radiusLg = 16;
+ static const double radiusXl = 20;
+ static const double radiusFull = 999;
+
+ static final BorderRadius borderRadiusSm = BorderRadius.circular(radiusSm);
+ static final BorderRadius borderRadiusMd = BorderRadius.circular(radiusMd);
+ static final BorderRadius borderRadiusLg = BorderRadius.circular(radiusLg);
+ static final BorderRadius borderRadiusXl = BorderRadius.circular(radiusXl);
+ static final BorderRadius borderRadiusFull = BorderRadius.circular(radiusFull);
+
+ // ============================================================
+ // Elevation / Shadow
+ // ============================================================
+ static const List shadowSm = [
+ BoxShadow(
+ color: Color(0x0A000000),
+ blurRadius: 8,
+ offset: Offset(0, 2),
+ ),
+ ];
+
+ static const List shadowMd = [
+ BoxShadow(
+ color: Color(0x0F000000),
+ blurRadius: 16,
+ offset: Offset(0, 4),
+ ),
+ ];
+
+ static const List shadowLg = [
+ BoxShadow(
+ color: Color(0x14000000),
+ blurRadius: 24,
+ offset: Offset(0, 8),
+ ),
+ ];
+
+ static const List shadowPrimary = [
+ BoxShadow(
+ color: Color(0x336C5CE7),
+ blurRadius: 16,
+ offset: Offset(0, 4),
+ ),
+ ];
+
+ // ============================================================
+ // Animation Durations
+ // ============================================================
+ static const Duration animFast = Duration(milliseconds: 150);
+ static const Duration animNormal = Duration(milliseconds: 250);
+ static const Duration animSlow = Duration(milliseconds: 350);
+
+ // ============================================================
+ // Component Sizes
+ // ============================================================
+ static const double buttonHeight = 52;
+ static const double buttonHeightSm = 40;
+ static const double inputHeight = 52;
+ static const double appBarHeight = 56;
+ static const double bottomNavHeight = 80;
+ static const double tabBarHeight = 44;
+ static const double avatarSm = 32;
+ static const double avatarMd = 40;
+ static const double avatarLg = 56;
+ static const double iconSm = 20;
+ static const double iconMd = 24;
+ static const double iconLg = 28;
+ static const double couponCardHeight = 120;
+ static const double couponCardHeightLg = 160;
+}
diff --git a/frontend/mobile/lib/app/theme/app_theme.dart b/frontend/mobile/lib/app/theme/app_theme.dart
new file mode 100644
index 0000000..a25a041
--- /dev/null
+++ b/frontend/mobile/lib/app/theme/app_theme.dart
@@ -0,0 +1,230 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'app_colors.dart';
+import 'app_typography.dart';
+import 'app_spacing.dart';
+
+/// Genex Material 3 Theme Configuration
+///
+/// 干净清爽型紫色主题,零区块链感知的金融App体验
+class AppTheme {
+ AppTheme._();
+
+ static ThemeData get light => ThemeData(
+ useMaterial3: true,
+ brightness: Brightness.light,
+
+ // Color Scheme
+ colorScheme: const ColorScheme.light(
+ primary: AppColors.primary,
+ primaryContainer: AppColors.primaryContainer,
+ secondary: AppColors.success,
+ secondaryContainer: AppColors.successLight,
+ tertiary: AppColors.info,
+ error: AppColors.error,
+ errorContainer: AppColors.errorLight,
+ surface: AppColors.surface,
+ surfaceContainerHighest: AppColors.surfaceVariant,
+ onPrimary: Colors.white,
+ onSecondary: Colors.white,
+ onSurface: AppColors.textPrimary,
+ onSurfaceVariant: AppColors.textSecondary,
+ outline: AppColors.border,
+ outlineVariant: AppColors.borderLight,
+ scrim: AppColors.scrim,
+ ),
+
+ scaffoldBackgroundColor: AppColors.background,
+
+ // AppBar
+ appBarTheme: const AppBarTheme(
+ elevation: 0,
+ scrolledUnderElevation: 0.5,
+ centerTitle: true,
+ backgroundColor: AppColors.surface,
+ foregroundColor: AppColors.textPrimary,
+ surfaceTintColor: Colors.transparent,
+ systemOverlayStyle: SystemUiOverlayStyle(
+ statusBarColor: Colors.transparent,
+ statusBarIconBrightness: Brightness.dark,
+ statusBarBrightness: Brightness.light,
+ ),
+ titleTextStyle: AppTypography.h3,
+ iconTheme: IconThemeData(color: AppColors.textPrimary, size: 24),
+ ),
+
+ // Bottom Navigation
+ bottomNavigationBarTheme: const BottomNavigationBarThemeData(
+ type: BottomNavigationBarType.fixed,
+ backgroundColor: AppColors.surface,
+ selectedItemColor: AppColors.primary,
+ unselectedItemColor: AppColors.textTertiary,
+ elevation: 0,
+ selectedLabelStyle: TextStyle(fontSize: 11, fontWeight: FontWeight.w600),
+ unselectedLabelStyle: TextStyle(fontSize: 11, fontWeight: FontWeight.w400),
+ ),
+
+ // Navigation Bar (M3)
+ navigationBarTheme: NavigationBarThemeData(
+ backgroundColor: AppColors.surface,
+ indicatorColor: AppColors.primaryContainer,
+ surfaceTintColor: Colors.transparent,
+ elevation: 0,
+ height: AppSpacing.bottomNavHeight,
+ labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
+ iconTheme: WidgetStateProperty.resolveWith((states) {
+ if (states.contains(WidgetState.selected)) {
+ return const IconThemeData(color: AppColors.primary, size: 24);
+ }
+ return const IconThemeData(color: AppColors.textTertiary, size: 24);
+ }),
+ labelTextStyle: WidgetStateProperty.resolveWith((states) {
+ if (states.contains(WidgetState.selected)) {
+ return AppTypography.caption.copyWith(
+ color: AppColors.primary,
+ fontWeight: FontWeight.w600,
+ );
+ }
+ return AppTypography.caption;
+ }),
+ ),
+
+ // Card
+ cardTheme: CardTheme(
+ elevation: 0,
+ color: AppColors.surface,
+ surfaceTintColor: Colors.transparent,
+ shape: RoundedRectangleBorder(
+ borderRadius: AppSpacing.borderRadiusMd,
+ side: const BorderSide(color: AppColors.borderLight, width: 1),
+ ),
+ margin: EdgeInsets.zero,
+ ),
+
+ // Elevated Button
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppColors.primary,
+ foregroundColor: Colors.white,
+ elevation: 0,
+ minimumSize: const Size(double.infinity, AppSpacing.buttonHeight),
+ shape: RoundedRectangleBorder(
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ textStyle: AppTypography.labelLarge,
+ ),
+ ),
+
+ // Outlined Button
+ outlinedButtonTheme: OutlinedButtonThemeData(
+ style: OutlinedButton.styleFrom(
+ foregroundColor: AppColors.primary,
+ side: const BorderSide(color: AppColors.primary, width: 1.5),
+ minimumSize: const Size(double.infinity, AppSpacing.buttonHeight),
+ shape: RoundedRectangleBorder(
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ textStyle: AppTypography.labelLarge,
+ ),
+ ),
+
+ // Text Button
+ textButtonTheme: TextButtonThemeData(
+ style: TextButton.styleFrom(
+ foregroundColor: AppColors.primary,
+ textStyle: AppTypography.labelMedium,
+ ),
+ ),
+
+ // Input Decoration
+ inputDecorationTheme: InputDecorationTheme(
+ filled: true,
+ fillColor: AppColors.gray50,
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
+ border: OutlineInputBorder(
+ borderRadius: AppSpacing.borderRadiusMd,
+ borderSide: BorderSide.none,
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderRadius: AppSpacing.borderRadiusMd,
+ borderSide: const BorderSide(color: AppColors.borderLight, width: 1),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: AppSpacing.borderRadiusMd,
+ borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
+ ),
+ errorBorder: OutlineInputBorder(
+ borderRadius: AppSpacing.borderRadiusMd,
+ borderSide: const BorderSide(color: AppColors.error, width: 1),
+ ),
+ hintStyle: AppTypography.bodyMedium.copyWith(color: AppColors.textTertiary),
+ labelStyle: AppTypography.bodyMedium,
+ errorStyle: AppTypography.caption.copyWith(color: AppColors.error),
+ ),
+
+ // Chip
+ chipTheme: ChipThemeData(
+ backgroundColor: AppColors.gray50,
+ selectedColor: AppColors.primaryContainer,
+ labelStyle: AppTypography.labelSmall,
+ shape: RoundedRectangleBorder(
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ side: const BorderSide(color: AppColors.borderLight),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ ),
+
+ // TabBar
+ tabBarTheme: TabBarTheme(
+ labelColor: AppColors.primary,
+ unselectedLabelColor: AppColors.textTertiary,
+ labelStyle: AppTypography.labelMedium,
+ unselectedLabelStyle: AppTypography.labelMedium,
+ indicatorSize: TabBarIndicatorSize.label,
+ indicator: const UnderlineTabIndicator(
+ borderSide: BorderSide(color: AppColors.primary, width: 2.5),
+ ),
+ dividerColor: Colors.transparent,
+ ),
+
+ // Dialog
+ dialogTheme: DialogTheme(
+ backgroundColor: AppColors.surface,
+ surfaceTintColor: Colors.transparent,
+ shape: RoundedRectangleBorder(
+ borderRadius: AppSpacing.borderRadiusLg,
+ ),
+ titleTextStyle: AppTypography.h2,
+ contentTextStyle: AppTypography.bodyMedium,
+ ),
+
+ // BottomSheet
+ bottomSheetTheme: const BottomSheetThemeData(
+ backgroundColor: AppColors.surface,
+ surfaceTintColor: Colors.transparent,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ showDragHandle: true,
+ dragHandleColor: AppColors.gray300,
+ dragHandleSize: Size(36, 4),
+ ),
+
+ // Divider
+ dividerTheme: const DividerThemeData(
+ color: AppColors.borderLight,
+ thickness: 1,
+ space: 0,
+ ),
+
+ // Snackbar
+ snackBarTheme: SnackBarThemeData(
+ backgroundColor: AppColors.gray800,
+ contentTextStyle: AppTypography.bodyMedium.copyWith(color: Colors.white),
+ behavior: SnackBarBehavior.floating,
+ shape: RoundedRectangleBorder(
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ ),
+ );
+}
diff --git a/frontend/mobile/lib/app/theme/app_typography.dart b/frontend/mobile/lib/app/theme/app_typography.dart
new file mode 100644
index 0000000..8b76cee
--- /dev/null
+++ b/frontend/mobile/lib/app/theme/app_typography.dart
@@ -0,0 +1,159 @@
+import 'package:flutter/material.dart';
+import 'app_colors.dart';
+
+/// Genex Design System - Typography Tokens
+///
+/// 字体层级:清晰、易读、干净
+/// 基于 SF Pro (iOS) / Roboto (Android) 系统字体
+class AppTypography {
+ AppTypography._();
+
+ static const String _fontFamily = 'SF Pro Display';
+ static const String _fontFamilyFallback = 'Roboto';
+
+ // ============================================================
+ // Display - 超大标题(启动页/空状态)
+ // ============================================================
+ static const TextStyle displayLarge = TextStyle(
+ fontSize: 34,
+ fontWeight: FontWeight.w700,
+ height: 1.2,
+ letterSpacing: -0.5,
+ color: AppColors.textPrimary,
+ );
+
+ static const TextStyle displayMedium = TextStyle(
+ fontSize: 28,
+ fontWeight: FontWeight.w700,
+ height: 1.25,
+ letterSpacing: -0.3,
+ color: AppColors.textPrimary,
+ );
+
+ // ============================================================
+ // Heading - 页面标题 / 模块标题
+ // ============================================================
+ static const TextStyle h1 = TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.w700,
+ height: 1.3,
+ color: AppColors.textPrimary,
+ );
+
+ static const TextStyle h2 = TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ height: 1.35,
+ color: AppColors.textPrimary,
+ );
+
+ static const TextStyle h3 = TextStyle(
+ fontSize: 17,
+ fontWeight: FontWeight.w600,
+ height: 1.4,
+ color: AppColors.textPrimary,
+ );
+
+ // ============================================================
+ // Body - 正文
+ // ============================================================
+ static const TextStyle bodyLarge = TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w400,
+ height: 1.5,
+ color: AppColors.textPrimary,
+ );
+
+ static const TextStyle bodyMedium = TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w400,
+ height: 1.5,
+ color: AppColors.textPrimary,
+ );
+
+ static const TextStyle bodySmall = TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w400,
+ height: 1.5,
+ color: AppColors.textSecondary,
+ );
+
+ // ============================================================
+ // Label - 按钮文字/标签/Tab
+ // ============================================================
+ static const TextStyle labelLarge = TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ height: 1.4,
+ letterSpacing: 0.2,
+ color: AppColors.textPrimary,
+ );
+
+ static const TextStyle labelMedium = TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ height: 1.4,
+ letterSpacing: 0.1,
+ color: AppColors.textPrimary,
+ );
+
+ static const TextStyle labelSmall = TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.4,
+ letterSpacing: 0.2,
+ color: AppColors.textSecondary,
+ );
+
+ // ============================================================
+ // Caption - 辅助文字
+ // ============================================================
+ static const TextStyle caption = TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w400,
+ height: 1.4,
+ color: AppColors.textTertiary,
+ );
+
+ // ============================================================
+ // Price - 价格专用
+ // ============================================================
+ static const TextStyle priceLarge = TextStyle(
+ fontSize: 28,
+ fontWeight: FontWeight.w700,
+ height: 1.2,
+ color: AppColors.primary,
+ );
+
+ static const TextStyle priceMedium = TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.w700,
+ height: 1.2,
+ color: AppColors.primary,
+ );
+
+ static const TextStyle priceSmall = TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ height: 1.2,
+ color: AppColors.primary,
+ );
+
+ static const TextStyle priceOriginal = TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ height: 1.2,
+ color: AppColors.textTertiary,
+ decoration: TextDecoration.lineThrough,
+ );
+
+ // ============================================================
+ // Discount Badge - 折扣标签
+ // ============================================================
+ static const TextStyle discountBadge = TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w700,
+ height: 1.0,
+ color: Colors.white,
+ );
+}
diff --git a/frontend/mobile/lib/features/ai_agent/presentation/pages/agent_chat_page.dart b/frontend/mobile/lib/features/ai_agent/presentation/pages/agent_chat_page.dart
new file mode 100644
index 0000000..c179647
--- /dev/null
+++ b/frontend/mobile/lib/features/ai_agent/presentation/pages/agent_chat_page.dart
@@ -0,0 +1,171 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// AI Agent 全屏对话页面(消费者端)
+///
+/// 场景:智能推券、比价分析、组合建议、投资教育
+class AgentChatPage extends StatefulWidget {
+ const AgentChatPage({super.key});
+
+ @override
+ State createState() => _AgentChatPageState();
+}
+
+class _AgentChatPageState extends State {
+ final _controller = TextEditingController();
+ final _scrollController = ScrollController();
+ final List<_Msg> _messages = [
+ _Msg(true, '你好!我是 Genex AI 助手,可以帮你发现高性价比好券、比价分析、组合推荐。试试问我:'),
+ ];
+ final _suggestions = ['推荐适合我的券', '星巴克券值不值得买?', '帮我做比价分析', '我的券快到期了怎么办?'];
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(Icons.auto_awesome_rounded, color: AppColors.primary, size: 20),
+ SizedBox(width: 8),
+ Text('AI 助手'),
+ ],
+ ),
+ actions: [
+ IconButton(icon: const Icon(Icons.more_horiz_rounded), onPressed: () {}),
+ ],
+ ),
+ body: Column(
+ children: [
+ Expanded(
+ child: ListView.builder(
+ controller: _scrollController,
+ padding: const EdgeInsets.all(16),
+ itemCount: _messages.length,
+ itemBuilder: (context, i) => _buildBubble(_messages[i]),
+ ),
+ ),
+
+ // Suggestion Chips
+ if (_messages.length <= 2)
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
+ child: Row(
+ children: _suggestions.map((s) => Padding(
+ padding: const EdgeInsets.only(right: 8),
+ child: ActionChip(
+ label: Text(s, style: const TextStyle(fontSize: 12)),
+ onPressed: () => _send(s),
+ backgroundColor: AppColors.primarySurface,
+ side: BorderSide.none,
+ ),
+ )).toList(),
+ ),
+ ),
+
+ // Input
+ Container(
+ padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
+ decoration: const BoxDecoration(
+ color: AppColors.surface,
+ border: Border(top: BorderSide(color: AppColors.borderLight)),
+ ),
+ child: Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: _controller,
+ decoration: InputDecoration(
+ hintText: '问我任何关于券的问题...',
+ border: OutlineInputBorder(
+ borderRadius: AppSpacing.borderRadiusFull,
+ borderSide: const BorderSide(color: AppColors.borderLight),
+ ),
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ ),
+ onSubmitted: _send,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Container(
+ decoration: const BoxDecoration(color: AppColors.primary, shape: BoxShape.circle),
+ child: IconButton(
+ icon: const Icon(Icons.send_rounded, color: Colors.white, size: 20),
+ onPressed: () => _send(_controller.text),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildBubble(_Msg msg) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 16),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: msg.isAi ? MainAxisAlignment.start : MainAxisAlignment.end,
+ children: [
+ if (msg.isAi) ...[
+ Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 16),
+ ),
+ const SizedBox(width: 8),
+ ],
+ Flexible(
+ child: Container(
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: msg.isAi ? AppColors.gray50 : AppColors.primary,
+ borderRadius: BorderRadius.circular(16).copyWith(
+ topLeft: msg.isAi ? const Radius.circular(4) : null,
+ topRight: !msg.isAi ? const Radius.circular(4) : null,
+ ),
+ ),
+ child: Text(
+ msg.text,
+ style: AppTypography.bodyMedium.copyWith(
+ color: msg.isAi ? AppColors.textPrimary : Colors.white,
+ height: 1.5,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _send(String text) {
+ if (text.trim().isEmpty) return;
+ setState(() {
+ _messages.add(_Msg(false, text));
+ _controller.clear();
+ });
+ Future.delayed(const Duration(milliseconds: 800), () {
+ if (mounted) {
+ setState(() {
+ _messages.add(_Msg(true, '根据您的偏好和消费习惯,推荐以下高性价比券:\n\n1. 星巴克 \$25 礼品卡 - 当前售价 \$21.25(8.5折),信用AAA\n2. Amazon \$100 购物券 - 当前售价 \$85(8.5折),信用AA\n\n这两张券的折扣率在同类中最优,且发行方信用等级高。'));
+ });
+ }
+ });
+ }
+}
+
+class _Msg {
+ final bool isAi;
+ final String text;
+ _Msg(this.isAi, this.text);
+}
diff --git a/frontend/mobile/lib/features/ai_agent/presentation/widgets/ai_fab.dart b/frontend/mobile/lib/features/ai_agent/presentation/widgets/ai_fab.dart
new file mode 100644
index 0000000..6119fb2
--- /dev/null
+++ b/frontend/mobile/lib/features/ai_agent/presentation/widgets/ai_fab.dart
@@ -0,0 +1,276 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// AI Agent 悬浮入口按钮
+///
+/// 右下角悬浮,显示未读建议数量红点
+/// 点击展开对话面板,长按显示快捷操作
+class AiFab extends StatelessWidget {
+ final int unreadCount;
+ final VoidCallback onTap;
+ final VoidCallback? onLongPress;
+
+ const AiFab({
+ super.key,
+ this.unreadCount = 0,
+ required this.onTap,
+ this.onLongPress,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap,
+ onLongPress: onLongPress,
+ child: Container(
+ width: 56,
+ height: 56,
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ shape: BoxShape.circle,
+ boxShadow: AppSpacing.shadowPrimary,
+ ),
+ child: Stack(
+ children: [
+ const Center(
+ child: Icon(
+ Icons.auto_awesome_rounded,
+ color: Colors.white,
+ size: 26,
+ ),
+ ),
+ if (unreadCount > 0)
+ Positioned(
+ top: 4,
+ right: 4,
+ child: Container(
+ padding: const EdgeInsets.all(4),
+ decoration: const BoxDecoration(
+ color: AppColors.error,
+ shape: BoxShape.circle,
+ ),
+ constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
+ child: Text(
+ unreadCount > 99 ? '99+' : '$unreadCount',
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 9,
+ fontWeight: FontWeight.w600,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// AI Agent 对话面板
+///
+/// 底部Sheet展开,支持文字/语音输入,流式输出
+class AiChatPanel extends StatelessWidget {
+ const AiChatPanel({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return DraggableScrollableSheet(
+ initialChildSize: 0.65,
+ minChildSize: 0.3,
+ maxChildSize: 0.9,
+ builder: (context, scrollController) {
+ return Container(
+ decoration: const BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: Column(
+ children: [
+ // Drag Handle
+ Container(
+ margin: const EdgeInsets.only(top: 8),
+ width: 36,
+ height: 4,
+ decoration: BoxDecoration(
+ color: AppColors.gray300,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ ),
+
+ // Header
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 12, 20, 8),
+ child: Row(
+ children: [
+ Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Icon(
+ Icons.auto_awesome_rounded,
+ color: Colors.white,
+ size: 18,
+ ),
+ ),
+ const SizedBox(width: 10),
+ Text('AI 助手', style: AppTypography.h3),
+ const Spacer(),
+ IconButton(
+ icon: const Icon(Icons.close_rounded, size: 22),
+ onPressed: () => Navigator.of(context).pop(),
+ color: AppColors.textSecondary,
+ ),
+ ],
+ ),
+ ),
+
+ const Divider(),
+
+ // Chat Content
+ Expanded(
+ child: ListView(
+ controller: scrollController,
+ padding: const EdgeInsets.all(20),
+ children: [
+ _buildAiMessage(
+ '你好!我是 Genex AI 助手,可以帮你管理券资产、查找优惠、分析价格。有什么需要帮助的吗?',
+ ),
+ const SizedBox(height: 12),
+ _buildSuggestionChips(),
+ ],
+ ),
+ ),
+
+ // Input Bar
+ Container(
+ padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
+ decoration: const BoxDecoration(
+ color: AppColors.surface,
+ border: Border(top: BorderSide(color: AppColors.borderLight)),
+ ),
+ child: Row(
+ children: [
+ Expanded(
+ child: Container(
+ height: 44,
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusFull,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ const SizedBox(width: 16),
+ Expanded(
+ child: TextField(
+ decoration: InputDecoration(
+ hintText: '输入消息...',
+ hintStyle: AppTypography.bodyMedium
+ .copyWith(color: AppColors.textTertiary),
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.zero,
+ isDense: true,
+ ),
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.mic_rounded, size: 20),
+ onPressed: () {},
+ color: AppColors.textTertiary,
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(
+ minWidth: 36,
+ minHeight: 36,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(
+ Icons.arrow_upward_rounded,
+ color: Colors.white,
+ size: 20,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ Widget _buildAiMessage(String text) {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ width: 28,
+ height: 28,
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 14),
+ ),
+ const SizedBox(width: 10),
+ Flexible(
+ child: Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(4),
+ topRight: Radius.circular(12),
+ bottomLeft: Radius.circular(12),
+ bottomRight: Radius.circular(12),
+ ),
+ ),
+ child: Text(text, style: AppTypography.bodyMedium),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildSuggestionChips() {
+ final suggestions = [
+ '帮我找高折扣券',
+ '我的券快到期了吗?',
+ '推荐今日好券',
+ '分析我的券资产',
+ ];
+
+ return Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: suggestions.map((s) {
+ return ActionChip(
+ label: Text(s, style: AppTypography.labelSmall.copyWith(color: AppColors.primary)),
+ onPressed: () {},
+ backgroundColor: AppColors.primarySurface,
+ side: BorderSide(color: AppColors.primary.withValues(alpha: 0.2)),
+ shape: RoundedRectangleBorder(borderRadius: AppSpacing.borderRadiusFull),
+ );
+ }).toList(),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/auth/presentation/pages/forgot_password_page.dart b/frontend/mobile/lib/features/auth/presentation/pages/forgot_password_page.dart
new file mode 100644
index 0000000..0c3c0fd
--- /dev/null
+++ b/frontend/mobile/lib/features/auth/presentation/pages/forgot_password_page.dart
@@ -0,0 +1,9 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/genex_button.dart';
+
+/// A1. 忘记密码 - 手机号/邮箱验证 → 输入验证码 → 设置新密码 → 成功
+///
+/// 分步骤流程:Step1 输入账号 → Step2 验证码 → Step3 新密码 → Step4 成功
diff --git a/frontend/mobile/lib/features/auth/presentation/pages/login_page.dart b/frontend/mobile/lib/features/auth/presentation/pages/login_page.dart
new file mode 100644
index 0000000..3d88395
--- /dev/null
+++ b/frontend/mobile/lib/features/auth/presentation/pages/login_page.dart
@@ -0,0 +1,213 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/genex_button.dart';
+
+/// A1. 登录页 - 手机号/邮箱+密码 / 验证码快捷登录
+class LoginPage extends StatefulWidget {
+ const LoginPage({super.key});
+
+ @override
+ State createState() => _LoginPageState();
+}
+
+class _LoginPageState extends State with SingleTickerProviderStateMixin {
+ late TabController _tabController;
+ final _phoneController = TextEditingController();
+ final _passwordController = TextEditingController();
+ final _codeController = TextEditingController();
+ bool _obscurePassword = true;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = TabController(length: 2, vsync: this);
+ }
+
+ @override
+ void dispose() {
+ _tabController.dispose();
+ _phoneController.dispose();
+ _passwordController.dispose();
+ _codeController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: AppSpacing.pagePadding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 16),
+ Text('欢迎回来', style: AppTypography.displayMedium),
+ const SizedBox(height: 8),
+ Text(
+ '登录 Genex 管理你的券资产',
+ style: AppTypography.bodyLarge.copyWith(color: AppColors.textSecondary),
+ ),
+ const SizedBox(height: 32),
+
+ // Tab: Password / SMS Code
+ Container(
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: TabBar(
+ controller: _tabController,
+ indicator: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusFull,
+ boxShadow: AppSpacing.shadowSm,
+ ),
+ indicatorSize: TabBarIndicatorSize.tab,
+ dividerColor: Colors.transparent,
+ labelColor: AppColors.textPrimary,
+ unselectedLabelColor: AppColors.textTertiary,
+ labelStyle: AppTypography.labelMedium,
+ tabs: const [
+ Tab(text: '密码登录'),
+ Tab(text: '验证码登录'),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ Expanded(
+ child: TabBarView(
+ controller: _tabController,
+ children: [
+ _buildPasswordLogin(),
+ _buildCodeLogin(),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildPasswordLogin() {
+ return Column(
+ children: [
+ // Phone/Email Input
+ TextField(
+ controller: _phoneController,
+ keyboardType: TextInputType.phone,
+ decoration: const InputDecoration(
+ hintText: '手机号或邮箱',
+ prefixIcon: Icon(Icons.person_outline_rounded, color: AppColors.textTertiary),
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Password Input
+ TextField(
+ controller: _passwordController,
+ obscureText: _obscurePassword,
+ decoration: InputDecoration(
+ hintText: '密码',
+ prefixIcon: const Icon(Icons.lock_outline_rounded, color: AppColors.textTertiary),
+ suffixIcon: IconButton(
+ icon: Icon(
+ _obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined,
+ color: AppColors.textTertiary,
+ size: 20,
+ ),
+ onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
+ ),
+ ),
+ ),
+ const SizedBox(height: 12),
+
+ // Forgot Password
+ Align(
+ alignment: Alignment.centerRight,
+ child: GestureDetector(
+ onTap: () {
+ // Navigator: → ForgotPasswordPage
+ },
+ child: Text('忘记密码?', style: AppTypography.labelSmall.copyWith(
+ color: AppColors.primary,
+ )),
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Login Button
+ GenexButton(
+ label: '登录',
+ onPressed: () {
+ // Auth: login with password
+ },
+ ),
+ ],
+ );
+ }
+
+ Widget _buildCodeLogin() {
+ return Column(
+ children: [
+ // Phone Input
+ TextField(
+ keyboardType: TextInputType.phone,
+ decoration: const InputDecoration(
+ hintText: '手机号',
+ prefixIcon: Icon(Icons.phone_android_rounded, color: AppColors.textTertiary),
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Code Input + Send Button
+ Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: _codeController,
+ keyboardType: TextInputType.number,
+ decoration: const InputDecoration(
+ hintText: '验证码',
+ prefixIcon: Icon(Icons.shield_outlined, color: AppColors.textTertiary),
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ SizedBox(
+ height: AppSpacing.inputHeight,
+ child: GenexButton(
+ label: '获取验证码',
+ variant: GenexButtonVariant.secondary,
+ size: GenexButtonSize.medium,
+ fullWidth: false,
+ onPressed: () {
+ // SMS: send verification code
+ },
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 24),
+
+ GenexButton(
+ label: '登录',
+ onPressed: () {
+ // Auth: login with SMS code
+ },
+ ),
+ ],
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/auth/presentation/pages/register_page.dart b/frontend/mobile/lib/features/auth/presentation/pages/register_page.dart
new file mode 100644
index 0000000..d2f0343
--- /dev/null
+++ b/frontend/mobile/lib/features/auth/presentation/pages/register_page.dart
@@ -0,0 +1,286 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/genex_button.dart';
+
+/// A1. 手机号注册页
+///
+/// 手机号输入、获取验证码、设置密码、用户协议勾选
+/// 注册成功后后台静默创建MPC钱包
+class RegisterPage extends StatefulWidget {
+ final bool isEmail;
+
+ const RegisterPage({super.key, this.isEmail = false});
+
+ @override
+ State createState() => _RegisterPageState();
+}
+
+class _RegisterPageState extends State {
+ final _accountController = TextEditingController();
+ final _codeController = TextEditingController();
+ final _passwordController = TextEditingController();
+ bool _obscurePassword = true;
+ bool _agreeTerms = false;
+
+ @override
+ void dispose() {
+ _accountController.dispose();
+ _codeController.dispose();
+ _passwordController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ),
+ body: SafeArea(
+ child: SingleChildScrollView(
+ padding: AppSpacing.pagePadding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 16),
+ Text('创建账号', style: AppTypography.displayMedium),
+ const SizedBox(height: 8),
+ Text(
+ widget.isEmail ? '使用邮箱注册 Genex 账号' : '使用手机号注册 Genex 账号',
+ style: AppTypography.bodyLarge.copyWith(color: AppColors.textSecondary),
+ ),
+ const SizedBox(height: 40),
+
+ // Step indicator
+ _buildStepIndicator(),
+ const SizedBox(height: 32),
+
+ // Account Input (Phone/Email)
+ Text(
+ widget.isEmail ? '邮箱地址' : '手机号',
+ style: AppTypography.labelMedium,
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _accountController,
+ keyboardType:
+ widget.isEmail ? TextInputType.emailAddress : TextInputType.phone,
+ decoration: InputDecoration(
+ hintText: widget.isEmail ? '请输入邮箱地址' : '请输入手机号',
+ prefixIcon: Icon(
+ widget.isEmail ? Icons.email_outlined : Icons.phone_android_rounded,
+ color: AppColors.textTertiary,
+ ),
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Verification Code
+ Text('验证码', style: AppTypography.labelMedium),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: _codeController,
+ keyboardType: TextInputType.number,
+ maxLength: 6,
+ decoration: const InputDecoration(
+ hintText: '请输入6位验证码',
+ counterText: '',
+ prefixIcon: Icon(Icons.shield_outlined, color: AppColors.textTertiary),
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ SizedBox(
+ height: AppSpacing.inputHeight,
+ child: GenexButton(
+ label: '获取验证码',
+ variant: GenexButtonVariant.secondary,
+ size: GenexButtonSize.medium,
+ fullWidth: false,
+ onPressed: () {},
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 20),
+
+ // Password
+ Text('设置密码', style: AppTypography.labelMedium),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _passwordController,
+ obscureText: _obscurePassword,
+ decoration: InputDecoration(
+ hintText: '8-20位,含字母和数字',
+ prefixIcon: const Icon(Icons.lock_outline_rounded, color: AppColors.textTertiary),
+ suffixIcon: IconButton(
+ icon: Icon(
+ _obscurePassword
+ ? Icons.visibility_off_outlined
+ : Icons.visibility_outlined,
+ color: AppColors.textTertiary,
+ size: 20,
+ ),
+ onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
+ ),
+ ),
+ ),
+ const SizedBox(height: 8),
+ _buildPasswordStrength(),
+ const SizedBox(height: 32),
+
+ // Terms Agreement
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: 20,
+ height: 20,
+ child: Checkbox(
+ value: _agreeTerms,
+ onChanged: (v) => setState(() => _agreeTerms = v ?? false),
+ activeColor: AppColors.primary,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: GestureDetector(
+ onTap: () => setState(() => _agreeTerms = !_agreeTerms),
+ child: RichText(
+ text: TextSpan(
+ style: AppTypography.bodySmall,
+ children: [
+ const TextSpan(text: '我已阅读并同意 '),
+ TextSpan(
+ text: '《用户协议》',
+ style: AppTypography.bodySmall.copyWith(color: AppColors.primary),
+ ),
+ const TextSpan(text: ' 和 '),
+ TextSpan(
+ text: '《隐私政策》',
+ style: AppTypography.bodySmall.copyWith(color: AppColors.primary),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 32),
+
+ // Register Button
+ GenexButton(
+ label: '注册',
+ onPressed: _agreeTerms ? () {
+ // Auth: register → silent MPC wallet creation
+ } : null,
+ ),
+ const SizedBox(height: 40),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildStepIndicator() {
+ return Row(
+ children: [
+ _buildStep(1, '验证', true),
+ _buildStepLine(true),
+ _buildStep(2, '设密码', true),
+ _buildStepLine(false),
+ _buildStep(3, '完成', false),
+ ],
+ );
+ }
+
+ Widget _buildStep(int number, String label, bool active) {
+ return Column(
+ children: [
+ Container(
+ width: 28,
+ height: 28,
+ decoration: BoxDecoration(
+ color: active ? AppColors.primary : AppColors.gray200,
+ shape: BoxShape.circle,
+ ),
+ child: Center(
+ child: Text(
+ '$number',
+ style: TextStyle(
+ color: active ? Colors.white : AppColors.textTertiary,
+ fontSize: 13,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ label,
+ style: AppTypography.caption.copyWith(
+ color: active ? AppColors.primary : AppColors.textTertiary,
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildStepLine(bool active) {
+ return Expanded(
+ child: Container(
+ height: 2,
+ margin: const EdgeInsets.only(bottom: 18),
+ color: active ? AppColors.primary : AppColors.gray200,
+ ),
+ );
+ }
+
+ Widget _buildPasswordStrength() {
+ final password = _passwordController.text;
+ final hasLength = password.length >= 8;
+ final hasLetter = RegExp(r'[a-zA-Z]').hasMatch(password);
+ final hasDigit = RegExp(r'\d').hasMatch(password);
+
+ return Row(
+ children: [
+ _buildCheck('8位以上', hasLength),
+ const SizedBox(width: 16),
+ _buildCheck('含字母', hasLetter),
+ const SizedBox(width: 16),
+ _buildCheck('含数字', hasDigit),
+ ],
+ );
+ }
+
+ Widget _buildCheck(String label, bool passed) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ passed ? Icons.check_circle_rounded : Icons.circle_outlined,
+ size: 14,
+ color: passed ? AppColors.success : AppColors.textTertiary,
+ ),
+ const SizedBox(width: 4),
+ Text(
+ label,
+ style: AppTypography.caption.copyWith(
+ color: passed ? AppColors.success : AppColors.textTertiary,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/auth/presentation/pages/welcome_page.dart b/frontend/mobile/lib/features/auth/presentation/pages/welcome_page.dart
new file mode 100644
index 0000000..aa44f48
--- /dev/null
+++ b/frontend/mobile/lib/features/auth/presentation/pages/welcome_page.dart
@@ -0,0 +1,180 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/genex_button.dart';
+
+/// A1. 欢迎页 - 品牌展示 + 注册/登录入口
+///
+/// 品牌Logo、Slogan、手机号注册、邮箱注册、社交登录入口(Google/Apple)
+class WelcomePage extends StatelessWidget {
+ const WelcomePage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: Column(
+ children: [
+ const Spacer(flex: 2),
+
+ // Brand Logo
+ Container(
+ width: 80,
+ height: 80,
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusXl,
+ boxShadow: AppSpacing.shadowPrimary,
+ ),
+ child: const Icon(
+ Icons.diamond_rounded,
+ color: Colors.white,
+ size: 40,
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Brand Name
+ Text(
+ 'Genex',
+ style: AppTypography.displayLarge.copyWith(
+ color: AppColors.primary,
+ letterSpacing: 2,
+ ),
+ ),
+ const SizedBox(height: 8),
+
+ // Slogan
+ Text(
+ '让每一张券都有价值',
+ style: AppTypography.bodyLarge.copyWith(
+ color: AppColors.textSecondary,
+ ),
+ ),
+
+ const Spacer(flex: 3),
+
+ // Phone Register
+ GenexButton(
+ label: '手机号注册',
+ icon: Icons.phone_android_rounded,
+ onPressed: () {
+ // Navigator: → PhoneRegisterPage
+ },
+ ),
+ const SizedBox(height: 12),
+
+ // Email Register
+ GenexButton(
+ label: '邮箱注册',
+ icon: Icons.email_outlined,
+ variant: GenexButtonVariant.outline,
+ onPressed: () {
+ // Navigator: → EmailRegisterPage
+ },
+ ),
+ const SizedBox(height: 24),
+
+ // Social Login Divider
+ Row(
+ children: [
+ const Expanded(child: Divider(color: AppColors.border)),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Text('其他方式登录', style: AppTypography.caption),
+ ),
+ const Expanded(child: Divider(color: AppColors.border)),
+ ],
+ ),
+ const SizedBox(height: 16),
+
+ // Social Login Buttons
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ _SocialLoginButton(
+ icon: Icons.g_mobiledata_rounded,
+ label: 'Google',
+ onTap: () {},
+ ),
+ const SizedBox(width: 24),
+ _SocialLoginButton(
+ icon: Icons.apple_rounded,
+ label: 'Apple',
+ onTap: () {},
+ ),
+ ],
+ ),
+ const SizedBox(height: 32),
+
+ // Already have account
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text('已有账号?', style: AppTypography.bodyMedium.copyWith(
+ color: AppColors.textSecondary,
+ )),
+ GestureDetector(
+ onTap: () {
+ // Navigator: → LoginPage
+ },
+ child: Text('登录', style: AppTypography.labelMedium.copyWith(
+ color: AppColors.primary,
+ )),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+
+ // Terms
+ Text(
+ '注册即表示同意《用户协议》和《隐私政策》',
+ style: AppTypography.caption.copyWith(fontSize: 10),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 16),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _SocialLoginButton extends StatelessWidget {
+ final IconData icon;
+ final String label;
+ final VoidCallback onTap;
+
+ const _SocialLoginButton({
+ required this.icon,
+ required this.label,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Column(
+ children: [
+ Container(
+ width: 52,
+ height: 52,
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ shape: BoxShape.circle,
+ border: Border.all(color: AppColors.border),
+ ),
+ child: Icon(icon, size: 28, color: AppColors.textPrimary),
+ ),
+ const SizedBox(height: 6),
+ Text(label, style: AppTypography.caption),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/coupons/presentation/pages/coupon_detail_page.dart b/frontend/mobile/lib/features/coupons/presentation/pages/coupon_detail_page.dart
new file mode 100644
index 0000000..33e03bc
--- /dev/null
+++ b/frontend/mobile/lib/features/coupons/presentation/pages/coupon_detail_page.dart
@@ -0,0 +1,418 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/price_tag.dart';
+import '../../../../shared/widgets/credit_badge.dart';
+import '../../../../shared/widgets/genex_button.dart';
+
+/// A2. 券详情页
+///
+/// 券图片、品牌Logo、面值、当前价格、折扣率、有效期、使用条件、
+/// 使用门店、发行方信用等级、立即购买、加入收藏、价格走势图、同类券推荐
+class CouponDetailPage extends StatelessWidget {
+ const CouponDetailPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: CustomScrollView(
+ slivers: [
+ // Hero Image + AppBar
+ SliverAppBar(
+ expandedHeight: 220,
+ pinned: true,
+ backgroundColor: AppColors.surface,
+ leading: _buildBackButton(context),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.share_rounded, size: 22),
+ onPressed: () {},
+ style: IconButton.styleFrom(
+ backgroundColor: Colors.black26,
+ foregroundColor: Colors.white,
+ ),
+ ),
+ const SizedBox(width: 8),
+ ],
+ flexibleSpace: FlexibleSpaceBar(
+ background: Container(
+ decoration: const BoxDecoration(gradient: AppColors.primaryGradient),
+ child: const Center(
+ child: Icon(
+ Icons.confirmation_number_rounded,
+ size: 64,
+ color: Colors.white24,
+ ),
+ ),
+ ),
+ ),
+ ),
+
+ SliverToBoxAdapter(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Brand + Title + Rating
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ // Brand logo placeholder
+ Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: AppColors.gray100,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Icon(Icons.store_rounded,
+ color: AppColors.textTertiary, size: 20),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Starbucks', style: AppTypography.bodySmall),
+ Text('星巴克 \$25 礼品卡', style: AppTypography.h2),
+ ],
+ ),
+ ),
+ const CreditBadge(rating: 'AAA', size: CreditBadgeSize.large),
+ ],
+ ),
+ ],
+ ),
+ ),
+
+ // Price Section
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const PriceTag(
+ currentPrice: 21.25,
+ faceValue: 25.0,
+ size: PriceTagSize.large,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '比面值节省 \$3.75',
+ style: AppTypography.bodySmall.copyWith(color: AppColors.success),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // Info Cards
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
+ child: _buildInfoSection(),
+ ),
+
+ // Usage Rules
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
+ child: _buildUsageRules(),
+ ),
+
+ // Available Stores
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
+ child: _buildStores(),
+ ),
+
+ // Price Trend (Optional)
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
+ child: _buildPriceTrend(),
+ ),
+
+ // Similar Coupons
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
+ child: Text('同类券推荐', style: AppTypography.h3),
+ ),
+ SizedBox(
+ height: 180,
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.fromLTRB(20, 12, 20, 12),
+ itemCount: 5,
+ separatorBuilder: (_, __) => const SizedBox(width: 12),
+ itemBuilder: (context, index) => _buildSimilarCard(index),
+ ),
+ ),
+
+ const SizedBox(height: 120),
+ ],
+ ),
+ ),
+ ],
+ ),
+
+ // Bottom Buy Bar
+ bottomNavigationBar: Container(
+ padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
+ decoration: const BoxDecoration(
+ color: AppColors.surface,
+ border: Border(top: BorderSide(color: AppColors.borderLight)),
+ ),
+ child: Row(
+ children: [
+ // Favorite
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(Icons.favorite_border_rounded, color: AppColors.textTertiary, size: 22),
+ Text('收藏', style: AppTypography.caption),
+ ],
+ ),
+ const SizedBox(width: 24),
+
+ // Buy Button
+ Expanded(
+ child: GenexButton(
+ label: '立即购买 \$21.25',
+ onPressed: () {
+ // Navigator: → OrderConfirmPage
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildBackButton(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(8),
+ child: IconButton(
+ icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 18),
+ onPressed: () => Navigator.of(context).pop(),
+ style: IconButton.styleFrom(
+ backgroundColor: Colors.black26,
+ foregroundColor: Colors.white,
+ ),
+ ),
+ );
+ }
+
+ Widget _buildInfoSection() {
+ final items = [
+ ('面值', '\$25.00'),
+ ('有效期', '2026/12/31'),
+ ('类型', '消费券'),
+ ('发行方', 'Starbucks Inc.'),
+ ];
+
+ return Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: items.asMap().entries.map((entry) {
+ final isLast = entry.key == items.length - 1;
+ return Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(entry.value.$1, style: AppTypography.bodyMedium.copyWith(
+ color: AppColors.textSecondary,
+ )),
+ Text(entry.value.$2, style: AppTypography.labelMedium),
+ ],
+ ),
+ if (!isLast) const Padding(
+ padding: EdgeInsets.symmetric(vertical: 10),
+ child: Divider(),
+ ),
+ ],
+ );
+ }).toList(),
+ ),
+ );
+ }
+
+ Widget _buildUsageRules() {
+ return Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('使用说明', style: AppTypography.labelMedium),
+ const SizedBox(height: 12),
+ _buildRuleItem(Icons.check_circle_outline, '全国星巴克门店通用'),
+ _buildRuleItem(Icons.check_circle_outline, '可转赠给好友'),
+ _buildRuleItem(Icons.check_circle_outline, '有效期内随时使用'),
+ _buildRuleItem(Icons.info_outline_rounded, '不可叠加使用'),
+ _buildRuleItem(Icons.info_outline_rounded, '不可兑换现金'),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildRuleItem(IconData icon, String text) {
+ final isPositive = icon == Icons.check_circle_outline;
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: Row(
+ children: [
+ Icon(icon, size: 16, color: isPositive ? AppColors.success : AppColors.textTertiary),
+ const SizedBox(width: 8),
+ Text(text, style: AppTypography.bodySmall),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildStores() {
+ return Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('使用门店', style: AppTypography.labelMedium),
+ Text('全国 12,800+ 门店', style: AppTypography.caption.copyWith(
+ color: AppColors.primary,
+ )),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Text(
+ '支持全国所有星巴克直营门店使用',
+ style: AppTypography.bodySmall,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildPriceTrend() {
+ return Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('价格走势', style: AppTypography.labelMedium),
+ Text('近30天', style: AppTypography.caption),
+ ],
+ ),
+ const SizedBox(height: 16),
+ // Placeholder for price chart
+ Container(
+ height: 120,
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Center(
+ child: Text(
+ '价格走势图 (fl_chart)',
+ style: AppTypography.bodySmall.copyWith(color: AppColors.textTertiary),
+ ),
+ ),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ _buildTrendStat('最高', '\$22.50', AppColors.error),
+ _buildTrendStat('最低', '\$20.00', AppColors.success),
+ _buildTrendStat('均价', '\$21.10', AppColors.textSecondary),
+ _buildTrendStat('历史成交', '1,234笔', AppColors.primary),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTrendStat(String label, String value, Color color) {
+ return Column(
+ children: [
+ Text(value, style: AppTypography.labelSmall.copyWith(color: color)),
+ const SizedBox(height: 2),
+ Text(label, style: AppTypography.caption),
+ ],
+ );
+ }
+
+ Widget _buildSimilarCard(int index) {
+ return Container(
+ width: 130,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ height: 80,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
+ ),
+ child: Center(
+ child: Icon(Icons.confirmation_number_outlined,
+ color: AppColors.primary.withValues(alpha: 0.3), size: 28),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(8),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('品牌 ${index + 1}', style: AppTypography.caption),
+ const SizedBox(height: 2),
+ Text('\$${(index + 1) * 8}.50',
+ style: AppTypography.priceSmall.copyWith(fontSize: 14)),
+ Text('\$${(index + 1) * 10}', style: AppTypography.priceOriginal.copyWith(fontSize: 10)),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/coupons/presentation/pages/home_page.dart b/frontend/mobile/lib/features/coupons/presentation/pages/home_page.dart
new file mode 100644
index 0000000..ee0fdd9
--- /dev/null
+++ b/frontend/mobile/lib/features/coupons/presentation/pages/home_page.dart
@@ -0,0 +1,285 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/coupon_card.dart';
+import '../../../ai_agent/presentation/widgets/ai_fab.dart';
+
+/// A2. 首页 - 搜索栏 + Banner + 热门分类 + 精选券 + AI Agent
+///
+/// Tab导航:首页/市场/我的券/消息/我的
+class HomePage extends StatelessWidget {
+ const HomePage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Stack(
+ children: [
+ CustomScrollView(
+ slivers: [
+ // Floating App Bar
+ SliverAppBar(
+ floating: true,
+ pinned: false,
+ backgroundColor: AppColors.background,
+ elevation: 0,
+ toolbarHeight: 60,
+ title: _buildSearchBar(),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.qr_code_scanner_rounded, size: 24),
+ onPressed: () {},
+ color: AppColors.textPrimary,
+ ),
+ ],
+ ),
+
+ // Banner Carousel
+ SliverToBoxAdapter(child: _buildBanner()),
+
+ // Category Grid
+ SliverToBoxAdapter(child: _buildCategoryGrid()),
+
+ // AI Smart Suggestions
+ SliverToBoxAdapter(child: _buildAiSuggestions()),
+
+ // Section: Featured Coupons
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(20, 24, 20, 12),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('精选好券', style: AppTypography.h2),
+ GestureDetector(
+ onTap: () {},
+ child: Text('查看全部', style: AppTypography.labelSmall.copyWith(
+ color: AppColors.primary,
+ )),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // Coupon List
+ SliverPadding(
+ padding: AppSpacing.pagePadding,
+ sliver: SliverList(
+ delegate: SliverChildBuilderDelegate(
+ (context, index) => Padding(
+ padding: const EdgeInsets.only(bottom: 12),
+ child: CouponCard(
+ brandName: _mockBrands[index % _mockBrands.length],
+ couponName: _mockNames[index % _mockNames.length],
+ faceValue: _mockFaceValues[index % _mockFaceValues.length],
+ currentPrice: _mockPrices[index % _mockPrices.length],
+ creditRating: _mockRatings[index % _mockRatings.length],
+ expiryDate: DateTime.now().add(Duration(days: (index + 1) * 5)),
+ onTap: () {
+ // Navigator: → CouponDetailPage
+ },
+ ),
+ ),
+ childCount: 10,
+ ),
+ ),
+ ),
+
+ const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
+ ],
+ ),
+
+ // AI FAB
+ Positioned(
+ right: 20,
+ bottom: 100,
+ child: AiFab(
+ unreadCount: 3,
+ onTap: () {
+ // Show AI Chat Panel
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSearchBar() {
+ return GestureDetector(
+ onTap: () {
+ // Navigator: → SearchPage
+ },
+ child: Container(
+ height: 40,
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusFull,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ const SizedBox(width: 14),
+ const Icon(Icons.search_rounded, size: 20, color: AppColors.textTertiary),
+ const SizedBox(width: 8),
+ Text(
+ '搜索券、品牌、分类...',
+ style: AppTypography.bodyMedium.copyWith(color: AppColors.textTertiary),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildBanner() {
+ return Container(
+ height: 160,
+ margin: const EdgeInsets.fromLTRB(20, 8, 20, 0),
+ child: PageView.builder(
+ itemCount: 3,
+ itemBuilder: (context, index) {
+ final colors = [
+ AppColors.primaryGradient,
+ AppColors.successGradient,
+ AppColors.cardGradient,
+ ];
+ final titles = ['新用户专享', '限时折扣', '热门推荐'];
+ final subtitles = ['首单立减 \$10', '全场低至7折', '精选高折扣券'];
+
+ return Container(
+ margin: const EdgeInsets.symmetric(horizontal: 4),
+ decoration: BoxDecoration(
+ gradient: colors[index],
+ borderRadius: AppSpacing.borderRadiusLg,
+ ),
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Text(
+ titles[index],
+ style: AppTypography.h1.copyWith(color: Colors.white),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ subtitles[index],
+ style: AppTypography.bodyMedium.copyWith(
+ color: Colors.white.withValues(alpha: 0.8),
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ );
+ }
+
+ Widget _buildCategoryGrid() {
+ final categories = [
+ ('餐饮', Icons.restaurant_rounded, AppColors.couponDining),
+ ('购物', Icons.shopping_bag_rounded, AppColors.couponShopping),
+ ('娱乐', Icons.sports_esports_rounded, AppColors.couponEntertainment),
+ ('出行', Icons.directions_car_rounded, AppColors.couponTravel),
+ ('生活', Icons.home_rounded, AppColors.couponOther),
+ ('品牌', Icons.storefront_rounded, AppColors.primary),
+ ('折扣', Icons.local_offer_rounded, AppColors.error),
+ ('全部', Icons.grid_view_rounded, AppColors.textSecondary),
+ ];
+
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
+ child: GridView.builder(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 4,
+ mainAxisSpacing: 8,
+ crossAxisSpacing: 8,
+ childAspectRatio: 0.85,
+ ),
+ itemCount: categories.length,
+ itemBuilder: (context, index) {
+ final (name, icon, color) = categories[index];
+ return GestureDetector(
+ onTap: () {},
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Icon(icon, color: color, size: 24),
+ ),
+ const SizedBox(height: 6),
+ Text(name, style: AppTypography.caption.copyWith(
+ color: AppColors.textPrimary,
+ fontWeight: FontWeight.w500,
+ )),
+ ],
+ ),
+ );
+ },
+ ),
+ );
+ }
+
+ Widget _buildAiSuggestions() {
+ return Container(
+ margin: const EdgeInsets.fromLTRB(20, 16, 20, 0),
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.primary.withValues(alpha: 0.15)),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 16),
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('AI 推荐', style: AppTypography.labelSmall.copyWith(
+ color: AppColors.primary,
+ )),
+ const SizedBox(height: 2),
+ Text(
+ '根据你的偏好,发现了3张高性价比券',
+ style: AppTypography.bodySmall,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ ),
+ const Icon(Icons.chevron_right_rounded, color: AppColors.primary, size: 20),
+ ],
+ ),
+ );
+ }
+}
+
+// Mock data
+const _mockBrands = ['Starbucks', 'Amazon', 'Walmart', 'Target', 'Nike'];
+const _mockNames = ['星巴克 \$25 礼品卡', 'Amazon \$100 购物券', 'Walmart \$50 生活券', 'Target \$30 折扣券', 'Nike \$80 运动券'];
+const _mockFaceValues = [25.0, 100.0, 50.0, 30.0, 80.0];
+const _mockPrices = [21.25, 85.0, 42.5, 24.0, 68.0];
+const _mockRatings = ['AAA', 'AA', 'AAA', 'A', 'AA'];
diff --git a/frontend/mobile/lib/features/coupons/presentation/pages/market_page.dart b/frontend/mobile/lib/features/coupons/presentation/pages/market_page.dart
new file mode 100644
index 0000000..531e498
--- /dev/null
+++ b/frontend/mobile/lib/features/coupons/presentation/pages/market_page.dart
@@ -0,0 +1,181 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/coupon_card.dart';
+
+/// A2. 市场(交易大厅)- 一级市场/二级市场 Tab切换
+///
+/// 分类筛选、排序、券卡片列表
+class MarketPage extends StatefulWidget {
+ const MarketPage({super.key});
+
+ @override
+ State createState() => _MarketPageState();
+}
+
+class _MarketPageState extends State with SingleTickerProviderStateMixin {
+ late TabController _tabController;
+ String _sortBy = 'discount';
+ String? _selectedCategory;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = TabController(length: 2, vsync: this);
+ }
+
+ @override
+ void dispose() {
+ _tabController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('交易市场'),
+ bottom: PreferredSize(
+ preferredSize: const Size.fromHeight(AppSpacing.tabBarHeight),
+ child: Container(
+ margin: const EdgeInsets.symmetric(horizontal: 20),
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: TabBar(
+ controller: _tabController,
+ indicator: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusFull,
+ boxShadow: AppSpacing.shadowSm,
+ ),
+ indicatorSize: TabBarIndicatorSize.tab,
+ dividerColor: Colors.transparent,
+ labelColor: AppColors.textPrimary,
+ unselectedLabelColor: AppColors.textTertiary,
+ tabs: const [
+ Tab(text: '一级市场(全新)'),
+ Tab(text: '二级市场(转售)'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ body: Column(
+ children: [
+ const SizedBox(height: 12),
+
+ // Filters
+ _buildFilters(),
+ const SizedBox(height: 8),
+
+ // Sort Bar
+ _buildSortBar(),
+
+ // Coupon List
+ Expanded(
+ child: TabBarView(
+ controller: _tabController,
+ children: [
+ _buildCouponList(isPrimary: true),
+ _buildCouponList(isPrimary: false),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildFilters() {
+ final categories = ['全部', '餐饮', '购物', '娱乐', '出行', '生活'];
+
+ return SizedBox(
+ height: 36,
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ itemCount: categories.length,
+ separatorBuilder: (_, __) => const SizedBox(width: 8),
+ itemBuilder: (context, index) {
+ final cat = categories[index];
+ final isSelected = _selectedCategory == cat || (index == 0 && _selectedCategory == null);
+ return GestureDetector(
+ onTap: () => setState(() => _selectedCategory = index == 0 ? null : cat),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ decoration: BoxDecoration(
+ color: isSelected ? AppColors.primary : AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusFull,
+ border: isSelected ? null : Border.all(color: AppColors.borderLight),
+ ),
+ alignment: Alignment.center,
+ child: Text(
+ cat,
+ style: AppTypography.labelSmall.copyWith(
+ color: isSelected ? Colors.white : AppColors.textSecondary,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ }
+
+ Widget _buildSortBar() {
+ final sortOptions = [
+ ('discount', '折扣率'),
+ ('price_asc', '价格↑'),
+ ('price_desc', '价格↓'),
+ ('expiry', '到期时间'),
+ ];
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
+ child: Row(
+ children: sortOptions.map((option) {
+ final isSelected = _sortBy == option.$1;
+ return Padding(
+ padding: const EdgeInsets.only(right: 16),
+ child: GestureDetector(
+ onTap: () => setState(() => _sortBy = option.$1),
+ child: Text(
+ option.$2,
+ style: AppTypography.labelSmall.copyWith(
+ color: isSelected ? AppColors.primary : AppColors.textTertiary,
+ fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
+ ),
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ );
+ }
+
+ Widget _buildCouponList({required bool isPrimary}) {
+ return ListView.separated(
+ padding: const EdgeInsets.fromLTRB(20, 4, 20, 100),
+ itemCount: 15,
+ separatorBuilder: (_, __) => const SizedBox(height: 12),
+ itemBuilder: (context, index) {
+ return CouponCard(
+ brandName: 'Brand ${index + 1}',
+ couponName: isPrimary
+ ? '全新 \$${(index + 1) * 20} 礼品卡'
+ : '转售 \$${(index + 1) * 15} 优惠券',
+ faceValue: (index + 1) * 20.0,
+ currentPrice: (index + 1) * 20.0 * 0.85,
+ creditRating: index % 3 == 0 ? 'AAA' : (index % 3 == 1 ? 'AA' : 'A'),
+ expiryDate: DateTime.now().add(Duration(days: (index + 1) * 10)),
+ onTap: () {
+ // Navigator: → CouponDetailPage
+ },
+ );
+ },
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/coupons/presentation/pages/my_coupon_detail_page.dart b/frontend/mobile/lib/features/coupons/presentation/pages/my_coupon_detail_page.dart
new file mode 100644
index 0000000..36a774c
--- /dev/null
+++ b/frontend/mobile/lib/features/coupons/presentation/pages/my_coupon_detail_page.dart
@@ -0,0 +1,275 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/genex_button.dart';
+import '../../../../shared/widgets/status_tag.dart';
+
+/// A4. 券详情(持有券)- QR码/条形码 + 转赠/出售/提取
+///
+/// 券二维码/条形码(核销用)、券信息、使用说明、
+/// 「转赠」「出售」「使用说明」按钮
+class MyCouponDetailPage extends StatelessWidget {
+ const MyCouponDetailPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ title: const Text('券详情'),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.more_horiz_rounded),
+ onPressed: () => _showMoreOptions(context),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: AppSpacing.pagePadding,
+ child: Column(
+ children: [
+ const SizedBox(height: 16),
+
+ // QR Code Card
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(24),
+ decoration: BoxDecoration(
+ gradient: AppColors.cardGradient,
+ borderRadius: AppSpacing.borderRadiusLg,
+ boxShadow: AppSpacing.shadowPrimary,
+ ),
+ child: Column(
+ children: [
+ // Brand + Status
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Starbucks', style: AppTypography.bodySmall.copyWith(
+ color: Colors.white70,
+ )),
+ Text('星巴克 \$25 礼品卡', style: AppTypography.h2.copyWith(
+ color: Colors.white,
+ )),
+ ],
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
+ decoration: BoxDecoration(
+ color: Colors.white24,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text('可使用', style: AppTypography.caption.copyWith(
+ color: Colors.white,
+ fontWeight: FontWeight.w600,
+ )),
+ ),
+ ],
+ ),
+ const SizedBox(height: 24),
+
+ // QR Code area
+ Container(
+ width: 200,
+ height: 200,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.qr_code_rounded, size: 140,
+ color: AppColors.textPrimary),
+ const SizedBox(height: 8),
+ Text('GNX-STB-A1B2C3D4',
+ style: AppTypography.caption.copyWith(
+ letterSpacing: 1.5,
+ fontWeight: FontWeight.w600,
+ )),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Instructions
+ Text(
+ '出示此二维码给商户扫描核销',
+ style: AppTypography.bodySmall.copyWith(color: Colors.white70),
+ ),
+
+ const SizedBox(height: 8),
+
+ // Barcode toggle
+ TextButton.icon(
+ onPressed: () {},
+ icon: const Icon(Icons.view_headline_rounded, size: 18,
+ color: Colors.white70),
+ label: Text('切换条形码', style: AppTypography.labelSmall.copyWith(
+ color: Colors.white70,
+ )),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Face Value + Expiry
+ Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: [
+ _infoRow('面值', '\$25.00'),
+ const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
+ _infoRow('购买价格', '\$21.25'),
+ const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
+ _infoRow('有效期', '2026/12/31'),
+ const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
+ _infoRow('订单号', 'GNX-20260209-001234'),
+ const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
+ _infoRow('剩余可转售次数', '3次'),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Action Buttons
+ Row(
+ children: [
+ Expanded(
+ child: GenexButton(
+ label: '转赠',
+ icon: Icons.card_giftcard_rounded,
+ variant: GenexButtonVariant.secondary,
+ onPressed: () {
+ // Navigator: → TransferPage
+ },
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: GenexButton(
+ label: '出售',
+ icon: Icons.sell_rounded,
+ variant: GenexButtonVariant.outline,
+ onPressed: () {
+ // Navigator: → SellPage
+ },
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+
+ // Usage Rules
+ Container(
+ width: double.infinity,
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('使用说明', style: AppTypography.labelMedium),
+ const SizedBox(height: 12),
+ _ruleItem('全国星巴克门店通用'),
+ _ruleItem('请在有效期内使用'),
+ _ruleItem('每次消费仅可使用一张'),
+ _ruleItem('不可兑换现金'),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 80),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _infoRow(String label, String value) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(label, style: AppTypography.bodyMedium.copyWith(
+ color: AppColors.textSecondary,
+ )),
+ Text(value, style: AppTypography.labelMedium),
+ ],
+ );
+ }
+
+ Widget _ruleItem(String text) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: Row(
+ children: [
+ Container(
+ width: 4, height: 4,
+ decoration: const BoxDecoration(
+ color: AppColors.textTertiary,
+ shape: BoxShape.circle,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(text, style: AppTypography.bodySmall),
+ ],
+ ),
+ );
+ }
+
+ void _showMoreOptions(BuildContext context) {
+ showModalBottomSheet(
+ context: context,
+ builder: (ctx) => Container(
+ padding: const EdgeInsets.fromLTRB(20, 8, 20, 40),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 36, height: 4,
+ decoration: BoxDecoration(
+ color: AppColors.gray300,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ ),
+ const SizedBox(height: 16),
+ _optionTile(Icons.wallet_rounded, '提取到外部钱包', '需KYC L2+认证', () {}),
+ const Divider(),
+ _optionTile(Icons.receipt_long_rounded, '查看交易记录', '', () {}),
+ const Divider(),
+ _optionTile(Icons.help_outline_rounded, '使用帮助', '', () {}),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _optionTile(IconData icon, String title, String subtitle, VoidCallback onTap) {
+ return ListTile(
+ leading: Icon(icon, color: AppColors.textPrimary),
+ title: Text(title, style: AppTypography.labelMedium),
+ subtitle: subtitle.isNotEmpty
+ ? Text(subtitle, style: AppTypography.caption)
+ : null,
+ trailing: const Icon(Icons.chevron_right_rounded, color: AppColors.textTertiary),
+ onTap: onTap,
+ contentPadding: EdgeInsets.zero,
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/coupons/presentation/pages/my_coupons_page.dart b/frontend/mobile/lib/features/coupons/presentation/pages/my_coupons_page.dart
new file mode 100644
index 0000000..77a13f9
--- /dev/null
+++ b/frontend/mobile/lib/features/coupons/presentation/pages/my_coupons_page.dart
@@ -0,0 +1,249 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/coupon_card.dart';
+import '../../../../shared/widgets/status_tag.dart';
+import '../../../../shared/widgets/empty_state.dart';
+
+/// A4. 我的券列表
+///
+/// 券卡片(图片+品牌+面值+状态+到期倒计时)
+/// 筛选(全部/可使用/待核销/已过期)、批量操作
+class MyCouponsPage extends StatefulWidget {
+ const MyCouponsPage({super.key});
+
+ @override
+ State createState() => _MyCouponsPageState();
+}
+
+class _MyCouponsPageState extends State
+ with SingleTickerProviderStateMixin {
+ late TabController _tabController;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = TabController(length: 4, vsync: this);
+ }
+
+ @override
+ void dispose() {
+ _tabController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('我的券'),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.sort_rounded, size: 22),
+ onPressed: () {},
+ ),
+ ],
+ bottom: TabBar(
+ controller: _tabController,
+ tabs: const [
+ Tab(text: '全部'),
+ Tab(text: '可使用'),
+ Tab(text: '待核销'),
+ Tab(text: '已过期'),
+ ],
+ ),
+ ),
+ body: TabBarView(
+ controller: _tabController,
+ children: [
+ _buildCouponList(null),
+ _buildCouponList(CouponStatus.active),
+ _buildCouponList(CouponStatus.pending),
+ _buildCouponList(CouponStatus.expired),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildCouponList(CouponStatus? filter) {
+ // Example: show empty state for expired tab
+ if (filter == CouponStatus.expired) {
+ return EmptyState.noCoupons(
+ onBrowse: () {
+ // Navigator: → MarketPage
+ },
+ );
+ }
+
+ return ListView.separated(
+ padding: const EdgeInsets.fromLTRB(20, 16, 20, 100),
+ itemCount: 5,
+ separatorBuilder: (_, __) => const SizedBox(height: 12),
+ itemBuilder: (context, index) {
+ return _MyCouponCard(
+ brandName: ['Starbucks', 'Amazon', 'Target', 'Nike', 'Walmart'][index],
+ couponName: [
+ '星巴克 \$25 礼品卡',
+ 'Amazon \$100 购物券',
+ 'Target \$30 折扣券',
+ 'Nike \$80 运动券',
+ 'Walmart \$50 生活券',
+ ][index],
+ faceValue: [25.0, 100.0, 30.0, 80.0, 50.0][index],
+ status: filter ?? CouponStatus.active,
+ expiryDate: DateTime.now().add(Duration(days: (index + 1) * 7)),
+ onTap: () {
+ // Navigator: → MyCouponDetailPage
+ },
+ );
+ },
+ );
+ }
+}
+
+/// 我的券卡片 - 带状态和操作入口
+class _MyCouponCard extends StatelessWidget {
+ final String brandName;
+ final String couponName;
+ final double faceValue;
+ final CouponStatus status;
+ final DateTime expiryDate;
+ final VoidCallback? onTap;
+
+ const _MyCouponCard({
+ required this.brandName,
+ required this.couponName,
+ required this.faceValue,
+ required this.status,
+ required this.expiryDate,
+ this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ boxShadow: AppSpacing.shadowSm,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ // Coupon image placeholder
+ Container(
+ width: 56,
+ height: 56,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Icon(
+ Icons.confirmation_number_outlined,
+ color: AppColors.primary.withValues(alpha: 0.4),
+ size: 24,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(brandName, style: AppTypography.caption),
+ Text(couponName, style: AppTypography.labelMedium),
+ const SizedBox(height: 4),
+ Row(
+ children: [
+ Text('面值 \$${faceValue.toStringAsFixed(0)}',
+ style: AppTypography.bodySmall),
+ const SizedBox(width: 8),
+ _statusWidget,
+ ],
+ ),
+ ],
+ ),
+ ),
+ const Icon(Icons.chevron_right_rounded,
+ color: AppColors.textTertiary, size: 20),
+ ],
+ ),
+
+ // Expiry + Quick Actions
+ const SizedBox(height: 12),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Row(
+ children: [
+ Icon(Icons.access_time_rounded, size: 14, color: _expiryColor),
+ const SizedBox(width: 4),
+ Text(
+ _expiryText,
+ style: AppTypography.caption.copyWith(color: _expiryColor),
+ ),
+ const Spacer(),
+ if (status == CouponStatus.active) ...[
+ _quickAction('转赠', Icons.card_giftcard_rounded),
+ const SizedBox(width: 12),
+ _quickAction('出售', Icons.sell_rounded),
+ ],
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget get _statusWidget {
+ switch (status) {
+ case CouponStatus.active:
+ return StatusTags.active();
+ case CouponStatus.pending:
+ return StatusTags.pending();
+ case CouponStatus.expired:
+ return StatusTags.expired();
+ case CouponStatus.used:
+ return StatusTags.used();
+ }
+ }
+
+ Widget _quickAction(String label, IconData icon) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(icon, size: 14, color: AppColors.primary),
+ const SizedBox(width: 3),
+ Text(label, style: AppTypography.caption.copyWith(
+ color: AppColors.primary,
+ fontWeight: FontWeight.w500,
+ )),
+ ],
+ );
+ }
+
+ String get _expiryText {
+ final days = expiryDate.difference(DateTime.now()).inDays;
+ if (days < 0) return '已过期';
+ if (days == 0) return '今天到期';
+ if (days <= 7) return '$days天后到期';
+ return '${expiryDate.year}/${expiryDate.month}/${expiryDate.day}到期';
+ }
+
+ Color get _expiryColor {
+ final days = expiryDate.difference(DateTime.now()).inDays;
+ if (days <= 3) return AppColors.error;
+ if (days <= 7) return AppColors.warning;
+ return AppColors.textTertiary;
+ }
+}
diff --git a/frontend/mobile/lib/features/coupons/presentation/pages/order_confirm_page.dart b/frontend/mobile/lib/features/coupons/presentation/pages/order_confirm_page.dart
new file mode 100644
index 0000000..cc65e03
--- /dev/null
+++ b/frontend/mobile/lib/features/coupons/presentation/pages/order_confirm_page.dart
@@ -0,0 +1,373 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/genex_button.dart';
+
+/// A3. 确认订单 + 支付页面
+///
+/// 券信息摘要、数量选择、价格计算、支付方式选择
+/// 支付成功:成功动画、订单号、「查看我的券」「继续逛」
+class OrderConfirmPage extends StatefulWidget {
+ const OrderConfirmPage({super.key});
+
+ @override
+ State createState() => _OrderConfirmPageState();
+}
+
+class _OrderConfirmPageState extends State {
+ int _quantity = 1;
+ int _selectedPayment = 0;
+
+ double get _unitPrice => 21.25;
+ double get _totalPrice => _unitPrice * _quantity;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ title: const Text('确认订单'),
+ ),
+ body: SingleChildScrollView(
+ padding: AppSpacing.pagePadding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 16),
+
+ // Coupon Info Summary
+ _buildCouponSummary(),
+ const SizedBox(height: 16),
+
+ // Quantity Selector
+ _buildQuantitySelector(),
+ const SizedBox(height: 16),
+
+ // Payment Method
+ _buildPaymentMethods(),
+ const SizedBox(height: 16),
+
+ // Price Breakdown
+ _buildPriceBreakdown(),
+ const SizedBox(height: 16),
+
+ // Utility Track Notice
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: AppColors.successLight,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Row(
+ children: [
+ Icon(Icons.verified_user_rounded, size: 16,
+ color: AppColors.utilityTrack),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ '您正在购买消费券用于消费',
+ style: AppTypography.bodySmall.copyWith(
+ color: AppColors.gray700,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 100),
+ ],
+ ),
+ ),
+
+ // Bottom Pay Bar
+ bottomNavigationBar: Container(
+ padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
+ decoration: const BoxDecoration(
+ color: AppColors.surface,
+ border: Border(top: BorderSide(color: AppColors.borderLight)),
+ ),
+ child: Row(
+ children: [
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('合计', style: AppTypography.caption),
+ Text(
+ '\$${_totalPrice.toStringAsFixed(2)}',
+ style: AppTypography.priceMedium,
+ ),
+ ],
+ ),
+ const SizedBox(width: 20),
+ Expanded(
+ child: GenexButton(
+ label: '确认支付',
+ onPressed: () => _showPaymentAuth(context),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildCouponSummary() {
+ return Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 60,
+ height: 60,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Icon(Icons.confirmation_number_outlined,
+ color: AppColors.primary.withValues(alpha: 0.4), size: 28),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Starbucks', style: AppTypography.caption),
+ Text('星巴克 \$25 礼品卡', style: AppTypography.labelMedium),
+ const SizedBox(height: 4),
+ Row(
+ children: [
+ Text(
+ '\$21.25',
+ style: AppTypography.priceSmall.copyWith(fontSize: 15),
+ ),
+ const SizedBox(width: 6),
+ Text('\$25', style: AppTypography.priceOriginal),
+ const SizedBox(width: 6),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text('8.5折', style: AppTypography.discountBadge),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildQuantitySelector() {
+ return Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('购买数量', style: AppTypography.labelMedium),
+ Row(
+ children: [
+ _buildQtyButton(Icons.remove_rounded, () {
+ if (_quantity > 1) setState(() => _quantity--);
+ }),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Text('$_quantity', style: AppTypography.h3),
+ ),
+ _buildQtyButton(Icons.add_rounded, () {
+ if (_quantity < 10) setState(() => _quantity++);
+ }),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildQtyButton(IconData icon, VoidCallback onTap) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusSm,
+ border: Border.all(color: AppColors.border),
+ ),
+ child: Icon(icon, size: 18, color: AppColors.textPrimary),
+ ),
+ );
+ }
+
+ Widget _buildPaymentMethods() {
+ final methods = [
+ ('银行卡/信用卡', Icons.credit_card_rounded),
+ ('Apple Pay', Icons.apple_rounded),
+ ];
+
+ return Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('支付方式', style: AppTypography.labelMedium),
+ const SizedBox(height: 12),
+ ...methods.asMap().entries.map((entry) {
+ final isSelected = _selectedPayment == entry.key;
+ return GestureDetector(
+ onTap: () => setState(() => _selectedPayment = entry.key),
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ decoration: BoxDecoration(
+ border: entry.key < methods.length - 1
+ ? const Border(bottom: BorderSide(color: AppColors.borderLight))
+ : null,
+ ),
+ child: Row(
+ children: [
+ Icon(entry.value.$2, size: 24, color: AppColors.textPrimary),
+ const SizedBox(width: 12),
+ Text(entry.value.$1, style: AppTypography.bodyMedium),
+ const Spacer(),
+ Container(
+ width: 20,
+ height: 20,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ border: Border.all(
+ color: isSelected ? AppColors.primary : AppColors.border,
+ width: isSelected ? 6 : 1.5,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildPriceBreakdown() {
+ return Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: [
+ _priceRow('单价', '\$${_unitPrice.toStringAsFixed(2)}'),
+ const SizedBox(height: 8),
+ _priceRow('数量', '×$_quantity'),
+ const Padding(
+ padding: EdgeInsets.symmetric(vertical: 10),
+ child: Divider(),
+ ),
+ _priceRow(
+ '合计',
+ '\$${_totalPrice.toStringAsFixed(2)}',
+ valueStyle: AppTypography.priceMedium,
+ ),
+ const SizedBox(height: 4),
+ Align(
+ alignment: Alignment.centerRight,
+ child: Text(
+ '比面值节省 \$${(25.0 * _quantity - _totalPrice).toStringAsFixed(2)}',
+ style: AppTypography.caption.copyWith(color: AppColors.success),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _priceRow(String label, String value, {TextStyle? valueStyle}) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(label, style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)),
+ Text(value, style: valueStyle ?? AppTypography.labelMedium),
+ ],
+ );
+ }
+
+ void _showPaymentAuth(BuildContext context) {
+ showModalBottomSheet(
+ context: context,
+ builder: (ctx) => Container(
+ padding: const EdgeInsets.fromLTRB(20, 0, 20, 40),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const SizedBox(height: 8),
+ Container(
+ width: 36, height: 4,
+ decoration: BoxDecoration(
+ color: AppColors.gray300,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ ),
+ const SizedBox(height: 20),
+ Text('确认支付', style: AppTypography.h2),
+ const SizedBox(height: 8),
+ Text(
+ '\$${_totalPrice.toStringAsFixed(2)}',
+ style: AppTypography.priceLarge.copyWith(fontSize: 36),
+ ),
+ const SizedBox(height: 4),
+ Text('星巴克 \$25 礼品卡 × $_quantity',
+ style: AppTypography.bodySmall),
+ const SizedBox(height: 32),
+ // Biometric / Password
+ Container(
+ width: 64, height: 64,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(Icons.fingerprint_rounded,
+ size: 36, color: AppColors.primary),
+ ),
+ const SizedBox(height: 12),
+ Text('请验证指纹或面容以完成支付', style: AppTypography.bodySmall),
+ const SizedBox(height: 24),
+ GenexButton(
+ label: '使用密码支付',
+ variant: GenexButtonVariant.text,
+ onPressed: () {},
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/coupons/presentation/pages/payment_page.dart b/frontend/mobile/lib/features/coupons/presentation/pages/payment_page.dart
new file mode 100644
index 0000000..4688e06
--- /dev/null
+++ b/frontend/mobile/lib/features/coupons/presentation/pages/payment_page.dart
@@ -0,0 +1,144 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// A6. 支付方式选择页
+///
+/// 选择支付方式:信用卡/借记卡、Apple Pay、Google Pay
+/// 后端自动完成:法币→稳定币→链上原子交换(消费者无感知)
+class PaymentPage extends StatefulWidget {
+ const PaymentPage({super.key});
+
+ @override
+ State createState() => _PaymentPageState();
+}
+
+class _PaymentPageState extends State {
+ int _selectedMethod = 0;
+
+ final _methods = const [
+ _PaymentMethod('Visa •••• 4242', Icons.credit_card_rounded, 'visa'),
+ _PaymentMethod('Apple Pay', Icons.apple_rounded, 'apple_pay'),
+ _PaymentMethod('Google Pay', Icons.account_balance_wallet_rounded, 'google_pay'),
+ _PaymentMethod('银行转账', Icons.account_balance_rounded, 'bank'),
+ ];
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('选择支付方式')),
+ body: Column(
+ children: [
+ // Order Summary
+ Container(
+ margin: const EdgeInsets.all(20),
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Icon(Icons.confirmation_number_rounded, color: AppColors.primary),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('星巴克 \$25 礼品卡', style: AppTypography.labelMedium),
+ const SizedBox(height: 2),
+ Text('面值 \$25.00', style: AppTypography.bodySmall),
+ ],
+ ),
+ ),
+ Text('\$21.25', style: AppTypography.priceMedium),
+ ],
+ ),
+ ),
+
+ // Payment Methods
+ Expanded(
+ child: ListView.builder(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ itemCount: _methods.length,
+ itemBuilder: (context, index) {
+ final method = _methods[index];
+ final isSelected = _selectedMethod == index;
+ return GestureDetector(
+ onTap: () => setState(() => _selectedMethod = index),
+ child: Container(
+ margin: const EdgeInsets.only(bottom: 10),
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(
+ color: isSelected ? AppColors.primary : AppColors.borderLight,
+ width: isSelected ? 1.5 : 1,
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(method.icon, color: isSelected ? AppColors.primary : AppColors.textSecondary, size: 24),
+ const SizedBox(width: 14),
+ Expanded(
+ child: Text(method.name, style: AppTypography.labelMedium),
+ ),
+ if (isSelected)
+ const Icon(Icons.check_circle_rounded, color: AppColors.primary, size: 22),
+ ],
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+
+ // Add new method
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: TextButton.icon(
+ onPressed: () {},
+ icon: const Icon(Icons.add_rounded),
+ label: const Text('添加新支付方式'),
+ ),
+ ),
+
+ // Pay Button
+ Container(
+ padding: const EdgeInsets.all(20),
+ child: SizedBox(
+ width: double.infinity,
+ height: AppSpacing.buttonHeight,
+ child: ElevatedButton(
+ onPressed: () {
+ // 后端自动完成法币→稳定币→链上原子交换
+ Navigator.pushNamed(context, '/payment-success');
+ },
+ child: const Text('确认支付 \$21.25'),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _PaymentMethod {
+ final String name;
+ final IconData icon;
+ final String type;
+
+ const _PaymentMethod(this.name, this.icon, this.type);
+}
diff --git a/frontend/mobile/lib/features/coupons/presentation/pages/payment_success_page.dart b/frontend/mobile/lib/features/coupons/presentation/pages/payment_success_page.dart
new file mode 100644
index 0000000..935ed2d
--- /dev/null
+++ b/frontend/mobile/lib/features/coupons/presentation/pages/payment_success_page.dart
@@ -0,0 +1,126 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/genex_button.dart';
+
+/// A3. 支付成功页
+///
+/// 成功动画、订单号、「查看我的券」「继续逛」
+class PaymentSuccessPage extends StatelessWidget {
+ final String orderNumber;
+ final double amount;
+ final String couponName;
+
+ const PaymentSuccessPage({
+ super.key,
+ this.orderNumber = 'GNX-20260209-001234',
+ this.amount = 21.25,
+ this.couponName = '星巴克 \$25 礼品卡',
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: SafeArea(
+ child: Padding(
+ padding: AppSpacing.pagePadding,
+ child: Column(
+ children: [
+ const Spacer(flex: 2),
+
+ // Success Icon
+ Container(
+ width: 88,
+ height: 88,
+ decoration: const BoxDecoration(
+ gradient: AppColors.successGradient,
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(
+ Icons.check_rounded,
+ color: Colors.white,
+ size: 44,
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ Text('支付成功', style: AppTypography.h1),
+ const SizedBox(height: 8),
+ Text(
+ '券已到账,可在「我的券」中查看',
+ style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary),
+ ),
+ const SizedBox(height: 32),
+
+ // Order Info Card
+ Container(
+ width: double.infinity,
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: [
+ _infoRow('券名称', couponName),
+ const SizedBox(height: 10),
+ const Divider(),
+ const SizedBox(height: 10),
+ _infoRow('支付金额', '\$$amount'),
+ const SizedBox(height: 10),
+ const Divider(),
+ const SizedBox(height: 10),
+ _infoRow('订单号', orderNumber),
+ const SizedBox(height: 10),
+ const Divider(),
+ const SizedBox(height: 10),
+ _infoRow('支付时间', '2026-02-09 14:32:15'),
+ ],
+ ),
+ ),
+
+ const Spacer(flex: 3),
+
+ // Actions
+ GenexButton(
+ label: '查看我的券',
+ onPressed: () {
+ // Navigator: → MyCouponsPage
+ },
+ ),
+ const SizedBox(height: 12),
+ GenexButton(
+ label: '继续逛',
+ variant: GenexButtonVariant.outline,
+ onPressed: () {
+ // Navigator: → HomePage (popUntil)
+ },
+ ),
+ const SizedBox(height: 24),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _infoRow(String label, String value) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(label, style: AppTypography.bodyMedium.copyWith(
+ color: AppColors.textSecondary,
+ )),
+ Flexible(
+ child: Text(
+ value,
+ style: AppTypography.labelMedium,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/coupons/presentation/pages/redeem_qr_page.dart b/frontend/mobile/lib/features/coupons/presentation/pages/redeem_qr_page.dart
new file mode 100644
index 0000000..6ba8a49
--- /dev/null
+++ b/frontend/mobile/lib/features/coupons/presentation/pages/redeem_qr_page.dart
@@ -0,0 +1,146 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// A8. 出示券码页面(消费者端)
+///
+/// QR码 + 数字券码 + 倒计时 + 自动调高亮度
+/// 商户扫码核销
+class RedeemQrPage extends StatefulWidget {
+ const RedeemQrPage({super.key});
+
+ @override
+ State createState() => _RedeemQrPageState();
+}
+
+class _RedeemQrPageState extends State {
+ int _remainingSeconds = 300; // 5 minutes
+
+ @override
+ void initState() {
+ super.initState();
+ _startCountdown();
+ }
+
+ void _startCountdown() {
+ Future.doWhile(() async {
+ await Future.delayed(const Duration(seconds: 1));
+ if (!mounted) return false;
+ setState(() => _remainingSeconds--);
+ return _remainingSeconds > 0;
+ });
+ }
+
+ String get _formattedTime {
+ final min = _remainingSeconds ~/ 60;
+ final sec = _remainingSeconds % 60;
+ return '${min.toString().padLeft(2, '0')}:${sec.toString().padLeft(2, '0')}';
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: AppColors.gray900,
+ appBar: AppBar(
+ backgroundColor: AppColors.gray900,
+ foregroundColor: Colors.white,
+ title: const Text('出示券码'),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ // Coupon Info
+ Text('星巴克 \$25 礼品卡', style: AppTypography.h2.copyWith(color: Colors.white)),
+ const SizedBox(height: 4),
+ Text('面值 \$25.00', style: AppTypography.bodyMedium.copyWith(color: Colors.white60)),
+ const SizedBox(height: 32),
+
+ // QR Code Area
+ Container(
+ width: 240,
+ height: 240,
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: AppSpacing.borderRadiusLg,
+ ),
+ child: Container(
+ decoration: BoxDecoration(
+ border: Border.all(color: AppColors.gray200),
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Center(
+ child: Icon(Icons.qr_code_2_rounded, size: 160, color: AppColors.gray900),
+ ),
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Numeric Code
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text(
+ '8429 3751 0062',
+ style: AppTypography.h1.copyWith(
+ color: Colors.white,
+ letterSpacing: 2,
+ fontFamily: 'monospace',
+ ),
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Countdown
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.timer_outlined, color: Colors.white54, size: 18),
+ const SizedBox(width: 6),
+ Text(
+ '有效时间 $_formattedTime',
+ style: AppTypography.bodyMedium.copyWith(color: Colors.white54),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ TextButton(
+ onPressed: () {
+ setState(() => _remainingSeconds = 300);
+ },
+ child: Text('刷新券码', style: AppTypography.labelMedium.copyWith(color: AppColors.primaryLight)),
+ ),
+ const SizedBox(height: 40),
+
+ // Hint
+ Container(
+ margin: const EdgeInsets.symmetric(horizontal: 40),
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.08),
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.info_outline_rounded, color: Colors.white38, size: 18),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Text(
+ '请将此码出示给商户扫描,屏幕已自动调至最高亮度',
+ style: AppTypography.caption.copyWith(color: Colors.white54),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/coupons/presentation/pages/search_page.dart b/frontend/mobile/lib/features/coupons/presentation/pages/search_page.dart
new file mode 100644
index 0000000..dae5fdd
--- /dev/null
+++ b/frontend/mobile/lib/features/coupons/presentation/pages/search_page.dart
@@ -0,0 +1,148 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/coupon_card.dart';
+
+/// A3. 搜索页 - 搜索券、品牌、分类
+///
+/// 热门搜索标签 + 搜索历史 + 实时搜索结果
+class SearchPage extends StatefulWidget {
+ const SearchPage({super.key});
+
+ @override
+ State createState() => _SearchPageState();
+}
+
+class _SearchPageState extends State {
+ final _searchController = TextEditingController();
+ bool _hasInput = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ titleSpacing: 0,
+ title: _buildSearchInput(),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('取消'),
+ ),
+ ],
+ ),
+ body: _hasInput ? _buildSearchResults() : _buildSearchSuggestions(),
+ );
+ }
+
+ Widget _buildSearchInput() {
+ return Container(
+ height: 40,
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusFull,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: TextField(
+ controller: _searchController,
+ autofocus: true,
+ decoration: const InputDecoration(
+ hintText: '搜索券、品牌、分类...',
+ prefixIcon: Icon(Icons.search_rounded, size: 20),
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.symmetric(vertical: 10),
+ ),
+ onChanged: (v) => setState(() => _hasInput = v.isNotEmpty),
+ ),
+ );
+ }
+
+ Widget _buildSearchSuggestions() {
+ return SingleChildScrollView(
+ padding: AppSpacing.pagePadding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 20),
+ // Hot Tags
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('热门搜索', style: AppTypography.h3),
+ GestureDetector(onTap: () {}, child: const Icon(Icons.refresh_rounded, size: 18, color: AppColors.textTertiary)),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: ['星巴克', 'Amazon', '餐饮券', '折扣券', '旅游', 'Nike'].map((tag) {
+ return GestureDetector(
+ onTap: () {
+ _searchController.text = tag;
+ setState(() => _hasInput = true);
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusFull,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Text(tag, style: AppTypography.labelSmall),
+ ),
+ );
+ }).toList(),
+ ),
+ const SizedBox(height: 32),
+
+ // History
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('搜索历史', style: AppTypography.h3),
+ GestureDetector(
+ onTap: () {},
+ child: Text('清空', style: AppTypography.labelSmall.copyWith(color: AppColors.textTertiary)),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ ...['星巴克 礼品卡', 'Nike 运动券', '餐饮 折扣'].map((h) {
+ return ListTile(
+ contentPadding: EdgeInsets.zero,
+ leading: const Icon(Icons.history_rounded, size: 18, color: AppColors.textTertiary),
+ title: Text(h, style: AppTypography.bodyMedium),
+ trailing: const Icon(Icons.north_west_rounded, size: 16, color: AppColors.textTertiary),
+ onTap: () {
+ _searchController.text = h;
+ setState(() => _hasInput = true);
+ },
+ );
+ }),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSearchResults() {
+ return ListView.builder(
+ padding: AppSpacing.pagePadding,
+ itemCount: 5,
+ itemBuilder: (context, index) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 12),
+ child: CouponCard(
+ brandName: ['Starbucks', 'Amazon', 'Walmart', 'Target', 'Nike'][index],
+ couponName: ['星巴克 \$25 礼品卡', 'Amazon \$100 购物券', 'Walmart \$50 生活券', 'Target \$30 折扣券', 'Nike \$80 运动券'][index],
+ faceValue: [25.0, 100.0, 50.0, 30.0, 80.0][index],
+ currentPrice: [21.25, 85.0, 42.5, 24.0, 68.0][index],
+ creditRating: 'AAA',
+ expiryDate: DateTime.now().add(Duration(days: (index + 1) * 10)),
+ onTap: () {},
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/issuer/presentation/pages/issuer_main_page.dart b/frontend/mobile/lib/features/issuer/presentation/pages/issuer_main_page.dart
new file mode 100644
index 0000000..24d784d
--- /dev/null
+++ b/frontend/mobile/lib/features/issuer/presentation/pages/issuer_main_page.dart
@@ -0,0 +1,732 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/genex_button.dart';
+import '../../../../shared/widgets/credit_badge.dart';
+import '../../../ai_agent/presentation/widgets/ai_fab.dart';
+
+/// C. 发行方管理后台App - 主页 + 底部导航
+///
+/// Tab: 发券中心 / 核销管理 / 财务 / 数据 / 更多
+class IssuerMainPage extends StatefulWidget {
+ const IssuerMainPage({super.key});
+
+ @override
+ State createState() => _IssuerMainPageState();
+}
+
+class _IssuerMainPageState extends State {
+ int _currentIndex = 0;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: IndexedStack(
+ index: _currentIndex,
+ children: const [
+ _IssuerDashboard(),
+ _CouponCenter(),
+ _RedeemManagement(),
+ _FinancePage(),
+ _IssuerMore(),
+ ],
+ ),
+ bottomNavigationBar: NavigationBar(
+ selectedIndex: _currentIndex,
+ onDestinationSelected: (i) => setState(() => _currentIndex = i),
+ destinations: const [
+ NavigationDestination(icon: Icon(Icons.dashboard_outlined),
+ selectedIcon: Icon(Icons.dashboard_rounded), label: '总览'),
+ NavigationDestination(icon: Icon(Icons.add_card_outlined),
+ selectedIcon: Icon(Icons.add_card_rounded), label: '发券'),
+ NavigationDestination(icon: Icon(Icons.fact_check_outlined),
+ selectedIcon: Icon(Icons.fact_check_rounded), label: '核销'),
+ NavigationDestination(icon: Icon(Icons.account_balance_outlined),
+ selectedIcon: Icon(Icons.account_balance_rounded), label: '财务'),
+ NavigationDestination(icon: Icon(Icons.more_horiz_rounded),
+ selectedIcon: Icon(Icons.more_horiz_rounded), label: '更多'),
+ ],
+ ),
+ floatingActionButton: AiFab(
+ unreadCount: 2,
+ onTap: () {},
+ ),
+ );
+ }
+}
+
+/// C5. 发行方仪表盘 - 数据概览
+class _IssuerDashboard extends StatelessWidget {
+ const _IssuerDashboard();
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('发行方管理'),
+ actions: [
+ IconButton(icon: const Icon(Icons.notifications_outlined), onPressed: () {}),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: AppSpacing.pagePadding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 12),
+
+ // Company Card
+ Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ gradient: AppColors.cardGradient,
+ borderRadius: AppSpacing.borderRadiusLg,
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 48, height: 48,
+ decoration: BoxDecoration(
+ color: Colors.white24,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Icon(Icons.business_rounded, color: Colors.white, size: 26),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Starbucks Inc.', style: AppTypography.h2.copyWith(color: Colors.white)),
+ const SizedBox(height: 4),
+ Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ color: Colors.white24,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text('AAA', style: AppTypography.caption.copyWith(
+ color: Colors.white, fontWeight: FontWeight.w700,
+ )),
+ ),
+ const SizedBox(width: 8),
+ Text('已认证发行方', style: AppTypography.bodySmall.copyWith(
+ color: Colors.white70,
+ )),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // AI Suggestion Card
+ Container(
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.primary.withValues(alpha: 0.15)),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 28, height: 28,
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 14),
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Text(
+ 'AI建议:当前市场需求旺盛,建议增发 \$50 面值礼品卡',
+ style: AppTypography.bodySmall,
+ maxLines: 2,
+ ),
+ ),
+ const Icon(Icons.chevron_right_rounded, color: AppColors.primary, size: 18),
+ ],
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Stats Grid
+ _buildStatsGrid(),
+ const SizedBox(height: 24),
+
+ // Quick Actions
+ Text('快捷操作', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ _quickAction(Icons.add_card_rounded, '创建券', AppColors.primary),
+ const SizedBox(width: 12),
+ _quickAction(Icons.people_outline_rounded, '门店管理', AppColors.info),
+ const SizedBox(width: 12),
+ _quickAction(Icons.analytics_outlined, '销售分析', AppColors.success),
+ const SizedBox(width: 12),
+ _quickAction(Icons.download_rounded, '对账单', AppColors.warning),
+ ],
+ ),
+ const SizedBox(height: 24),
+
+ // Recent Coupons
+ Text('我的券', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ ...List.generate(3, (i) => _couponItem(i)),
+
+ const SizedBox(height: 80),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildStatsGrid() {
+ final stats = [
+ ('发行总量', '12,800', AppColors.primary),
+ ('已售出', '9,650', AppColors.success),
+ ('已核销', '6,240', AppColors.info),
+ ('核销率', '64.7%', AppColors.warning),
+ ];
+
+ return GridView.count(
+ crossAxisCount: 2,
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ mainAxisSpacing: 12,
+ crossAxisSpacing: 12,
+ childAspectRatio: 1.8,
+ children: stats.map((s) => Container(
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(s.$1, style: AppTypography.caption),
+ Text(s.$2, style: AppTypography.h1.copyWith(color: s.$3)),
+ ],
+ ),
+ )).toList(),
+ );
+ }
+
+ Widget _quickAction(IconData icon, String label, Color color) {
+ return Expanded(
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 14),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.08),
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Column(
+ children: [
+ Icon(icon, color: color, size: 24),
+ const SizedBox(height: 6),
+ Text(label, style: AppTypography.caption.copyWith(
+ color: color, fontWeight: FontWeight.w500,
+ )),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _couponItem(int index) {
+ final names = ['\$25 礼品卡', '\$50 满减券', '\$10 折扣券'];
+ final statuses = ['已上架', '审核中', '已售罄'];
+ final colors = [AppColors.success, AppColors.warning, AppColors.textTertiary];
+
+ return Container(
+ margin: const EdgeInsets.only(bottom: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 40, height: 40,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Icon(Icons.confirmation_number_outlined,
+ color: AppColors.primary.withValues(alpha: 0.4), size: 20),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(names[index], style: AppTypography.labelMedium),
+ Text('发行 1,000 / 已售 ${[850, 0, 500][index]}',
+ style: AppTypography.caption),
+ ],
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ decoration: BoxDecoration(
+ color: colors[index].withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text(statuses[index], style: AppTypography.caption.copyWith(
+ color: colors[index], fontWeight: FontWeight.w500,
+ )),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+/// C2. 发券中心
+class _CouponCenter extends StatelessWidget {
+ const _CouponCenter();
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('发券中心')),
+ body: SingleChildScrollView(
+ padding: AppSpacing.pagePadding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 16),
+
+ // Template Selection
+ Text('选择券模板', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ GridView.count(
+ crossAxisCount: 2,
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ mainAxisSpacing: 12,
+ crossAxisSpacing: 12,
+ childAspectRatio: 1.2,
+ children: [
+ _templateCard('满减券', Icons.local_offer_rounded, AppColors.couponDining),
+ _templateCard('折扣券', Icons.percent_rounded, AppColors.couponShopping),
+ _templateCard('礼品卡', Icons.card_giftcard_rounded, AppColors.couponEntertainment),
+ _templateCard('储值券', Icons.account_balance_wallet_rounded, AppColors.couponTravel),
+ ],
+ ),
+ const SizedBox(height: 24),
+
+ // My Coupons Management List
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('券管理', style: AppTypography.h3),
+ TextButton(onPressed: () {}, child: const Text('查看全部')),
+ ],
+ ),
+ ...List.generate(5, (i) {
+ final statusColors = [AppColors.success, AppColors.warning, AppColors.success, AppColors.textTertiary, AppColors.error];
+ final statuses = ['已上架', '审核中', '已上架', '已下架', '已售罄'];
+ return ListTile(
+ contentPadding: EdgeInsets.zero,
+ leading: Container(
+ width: 40, height: 40,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Icon(Icons.confirmation_number_outlined,
+ color: AppColors.primary, size: 20),
+ ),
+ title: Text('券活动 ${i + 1}', style: AppTypography.labelMedium),
+ subtitle: Text('已售 ${(i + 1) * 120} / ${(i + 1) * 200}',
+ style: AppTypography.caption),
+ trailing: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ decoration: BoxDecoration(
+ color: statusColors[i].withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text(statuses[i], style: AppTypography.caption.copyWith(
+ color: statusColors[i], fontWeight: FontWeight.w500,
+ )),
+ ),
+ );
+ }),
+
+ const SizedBox(height: 80),
+ ],
+ ),
+ ),
+ floatingActionButton: FloatingActionButton.extended(
+ onPressed: () {
+ // Navigator: → CreateCouponPage
+ },
+ backgroundColor: AppColors.primary,
+ foregroundColor: Colors.white,
+ icon: const Icon(Icons.add_rounded),
+ label: const Text('创建新券'),
+ ),
+ );
+ }
+
+ Widget _templateCard(String name, IconData icon, Color color) {
+ return Container(
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.08),
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: color.withValues(alpha: 0.2)),
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(icon, color: color, size: 32),
+ const SizedBox(height: 8),
+ Text(name, style: AppTypography.labelMedium.copyWith(color: color)),
+ ],
+ ),
+ );
+ }
+}
+
+/// C3. 核销管理
+class _RedeemManagement extends StatelessWidget {
+ const _RedeemManagement();
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('核销管理')),
+ body: SingleChildScrollView(
+ padding: AppSpacing.pagePadding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 16),
+
+ // Stats
+ Row(
+ children: [
+ _stat('今日', '156笔', AppColors.primary),
+ const SizedBox(width: 12),
+ _stat('本周', '892笔', AppColors.success),
+ const SizedBox(width: 12),
+ _stat('本月', '3,450笔', AppColors.info),
+ ],
+ ),
+ const SizedBox(height: 24),
+
+ // Trend Chart placeholder
+ Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Center(
+ child: Text('核销趋势图 (fl_chart)',
+ style: AppTypography.bodySmall.copyWith(color: AppColors.textTertiary)),
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Store Management
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('门店管理', style: AppTypography.h3),
+ TextButton(onPressed: () {}, child: const Text('全部门店')),
+ ],
+ ),
+ const SizedBox(height: 8),
+ ...List.generate(3, (i) => Container(
+ margin: const EdgeInsets.only(bottom: 8),
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.store_rounded, color: AppColors.primary),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(['总部', '朝阳门店', '国贸门店'][i],
+ style: AppTypography.labelMedium),
+ Text('今日 ${[56, 23, 18][i]} 笔', style: AppTypography.caption),
+ ],
+ ),
+ ),
+ Text('${[3, 2, 1][i]} 名员工', style: AppTypography.caption),
+ ],
+ ),
+ )),
+ const SizedBox(height: 80),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _stat(String label, String value, Color color) {
+ return Expanded(
+ child: Container(
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.08),
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Column(
+ children: [
+ Text(value, style: AppTypography.h3.copyWith(color: color)),
+ const SizedBox(height: 4),
+ Text(label, style: AppTypography.caption),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// C4. 财务管理
+class _FinancePage extends StatelessWidget {
+ const _FinancePage();
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('财务管理')),
+ body: SingleChildScrollView(
+ padding: AppSpacing.pagePadding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 16),
+
+ // Revenue Card
+ Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusLg,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('总销售额', style: AppTypography.bodySmall.copyWith(
+ color: Colors.white70,
+ )),
+ const SizedBox(height: 4),
+ Text('\$128,450.00', style: AppTypography.displayLarge.copyWith(
+ color: Colors.white, fontSize: 32,
+ )),
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ _revenueItem('已到账', '\$98,200'),
+ const SizedBox(width: 24),
+ _revenueItem('待结算', '\$24,250'),
+ const SizedBox(width: 24),
+ _revenueItem('Breakage', '\$6,000'),
+ ],
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Quick actions
+ Row(
+ children: [
+ Expanded(
+ child: GenexButton(
+ label: '提现',
+ icon: Icons.account_balance_rounded,
+ onPressed: () {},
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: GenexButton(
+ label: '对账报表',
+ icon: Icons.receipt_long_rounded,
+ variant: GenexButtonVariant.outline,
+ onPressed: () {},
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 24),
+
+ // Settlement details
+ Text('结算明细', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ ...List.generate(5, (i) => Container(
+ margin: const EdgeInsets.only(bottom: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 36, height: 36,
+ decoration: BoxDecoration(
+ color: AppColors.successLight,
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(Icons.arrow_downward_rounded,
+ color: AppColors.success, size: 18),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('核销结算 - \$25券 × ${(i + 1) * 5}笔',
+ style: AppTypography.labelSmall),
+ Text('02/${10 - i}', style: AppTypography.caption),
+ ],
+ ),
+ ),
+ Text('+\$${(i + 1) * 125}.00', style: AppTypography.labelMedium.copyWith(
+ color: AppColors.success,
+ )),
+ ],
+ ),
+ )),
+
+ const SizedBox(height: 80),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _revenueItem(String label, String value) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(label, style: AppTypography.caption.copyWith(color: Colors.white54)),
+ const SizedBox(height: 2),
+ Text(value, style: AppTypography.labelMedium.copyWith(color: Colors.white)),
+ ],
+ );
+ }
+}
+
+/// C6. 更多 (信用等级、额度、设置)
+class _IssuerMore extends StatelessWidget {
+ const _IssuerMore();
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('更多')),
+ body: ListView(
+ padding: AppSpacing.pagePadding,
+ children: [
+ const SizedBox(height: 16),
+
+ // Credit & Quota Card
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.verified_rounded, color: AppColors.creditAAA),
+ const SizedBox(width: 8),
+ Text('信用等级', style: AppTypography.labelMedium),
+ const Spacer(),
+ const CreditBadge(rating: 'AAA', size: CreditBadgeSize.large),
+ ],
+ ),
+ const Divider(height: 24),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('发行额度', style: AppTypography.caption),
+ Text('\$500,000', style: AppTypography.h2.copyWith(color: AppColors.primary)),
+ ],
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text('已用额度', style: AppTypography.caption),
+ Text('\$128,450', style: AppTypography.h3),
+ ],
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ ClipRRect(
+ borderRadius: AppSpacing.borderRadiusFull,
+ child: LinearProgressIndicator(
+ value: 128450 / 500000,
+ backgroundColor: AppColors.gray100,
+ valueColor: const AlwaysStoppedAnimation(AppColors.primary),
+ minHeight: 8,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Menu items
+ _menuItem(Icons.bar_chart_rounded, '数据中心', '发行量/销量/兑付率'),
+ _menuItem(Icons.people_rounded, '用户画像', '购买用户分布分析'),
+ _menuItem(Icons.shield_outlined, '信用详情', '评分详情与提升建议'),
+ _menuItem(Icons.history_rounded, '额度变动', '历史额度调整记录'),
+ _menuItem(Icons.business_rounded, '企业信息', '营业执照/联系人'),
+ _menuItem(Icons.settings_outlined, '设置', '通知/安全/语言'),
+ _menuItem(Icons.help_outline_rounded, '帮助中心', '常见问题与客服'),
+ ],
+ ),
+ );
+ }
+
+ Widget _menuItem(IconData icon, String title, String subtitle) {
+ return Container(
+ margin: const EdgeInsets.only(bottom: 2),
+ child: ListTile(
+ contentPadding: const EdgeInsets.symmetric(horizontal: 4),
+ leading: Icon(icon, color: AppColors.textPrimary, size: 22),
+ title: Text(title, style: AppTypography.bodyMedium),
+ subtitle: Text(subtitle, style: AppTypography.caption),
+ trailing: const Icon(Icons.chevron_right_rounded, color: AppColors.textTertiary, size: 20),
+ onTap: () {},
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/merchant/presentation/pages/merchant_ai_assistant_page.dart b/frontend/mobile/lib/features/merchant/presentation/pages/merchant_ai_assistant_page.dart
new file mode 100644
index 0000000..50fcaab
--- /dev/null
+++ b/frontend/mobile/lib/features/merchant/presentation/pages/merchant_ai_assistant_page.dart
@@ -0,0 +1,930 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// 商户端 AI 助手页面
+///
+/// 核销辅助:异常券识别、核销建议
+/// 客流预测:基于历史数据的每日/每周客流预测
+/// 营销建议:促销活动建议、热门券品类推荐
+/// 异常预警:可疑券检测、高频核销预警
+class MerchantAiAssistantPage extends StatefulWidget {
+ const MerchantAiAssistantPage({super.key});
+
+ @override
+ State createState() =>
+ _MerchantAiAssistantPageState();
+}
+
+class _MerchantAiAssistantPageState extends State
+ with SingleTickerProviderStateMixin {
+ late TabController _tabController;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = TabController(length: 3, vsync: this);
+ }
+
+ @override
+ void dispose() {
+ _tabController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('AI 助手'),
+ bottom: TabBar(
+ controller: _tabController,
+ tabs: const [
+ Tab(text: '核销辅助'),
+ Tab(text: '客流预测'),
+ Tab(text: '异常预警'),
+ ],
+ labelColor: AppColors.primary,
+ unselectedLabelColor: AppColors.textTertiary,
+ indicatorColor: AppColors.primary,
+ labelStyle: AppTypography.labelMedium,
+ ),
+ ),
+ body: TabBarView(
+ controller: _tabController,
+ children: [
+ _buildRedeemAssistTab(),
+ _buildTrafficPredictionTab(),
+ _buildAnomalyAlertTab(),
+ ],
+ ),
+ );
+ }
+
+ // ========================================
+ // Tab 1: 核销辅助
+ // ========================================
+ Widget _buildRedeemAssistTab() {
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // AI Quick Actions
+ _buildAiQuickActions(),
+ const SizedBox(height: 20),
+
+ // Redeem Tips
+ _buildRedeemTips(),
+ const SizedBox(height: 20),
+
+ // Hot Coupons Today
+ _buildHotCouponsToday(),
+ const SizedBox(height: 20),
+
+ // Marketing Suggestions
+ _buildMarketingSuggestions(),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildAiQuickActions() {
+ final actions = [
+ ('验券真伪', Icons.verified_user_rounded, AppColors.success),
+ ('查券状态', Icons.search_rounded, AppColors.info),
+ ('批量核销', Icons.playlist_add_check_rounded, AppColors.primary),
+ ('问题反馈', Icons.feedback_rounded, AppColors.warning),
+ ];
+
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Container(
+ width: 28,
+ height: 28,
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.2),
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child:
+ const Center(child: Text('✨', style: TextStyle(fontSize: 16))),
+ ),
+ const SizedBox(width: 10),
+ const Text(
+ 'AI 快捷操作',
+ style: TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ color: Colors.white),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: actions.map((a) {
+ final (label, icon, _) = a;
+ return Expanded(
+ child: GestureDetector(
+ onTap: () {},
+ child: Column(
+ children: [
+ Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.15),
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Icon(icon, color: Colors.white, size: 22),
+ ),
+ const SizedBox(height: 6),
+ Text(
+ label,
+ style: TextStyle(
+ fontSize: 11,
+ color: Colors.white.withValues(alpha: 0.9),
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildRedeemTips() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.lightbulb_outline_rounded,
+ color: AppColors.warning, size: 20),
+ const SizedBox(width: 8),
+ Text('核销提示', style: AppTypography.labelLarge),
+ ],
+ ),
+ const SizedBox(height: 12),
+ _buildTipItem(
+ '星巴克 \$25 礼品卡有批次更新',
+ '新批次(#B2026-03)已上线,请注意核验二维码格式',
+ AppColors.info,
+ ),
+ _buildTipItem(
+ '午间高峰期即将到来',
+ '预计 11:30-13:00 核销量将达峰值 ~15笔/小时',
+ AppColors.warning,
+ ),
+ _buildTipItem(
+ '本店暂不支持 Nike 体验券',
+ '该券仅限旗舰店核销,请引导顾客至正确门店',
+ AppColors.error,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTipItem(String title, String desc, Color color) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ width: 6,
+ height: 6,
+ margin: const EdgeInsets.only(top: 6),
+ decoration: BoxDecoration(color: color, shape: BoxShape.circle),
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title,
+ style: AppTypography.labelMedium
+ .copyWith(fontSize: 13)),
+ const SizedBox(height: 2),
+ Text(desc,
+ style: AppTypography.bodySmall
+ .copyWith(color: AppColors.textSecondary)),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildHotCouponsToday() {
+ final hotCoupons = [
+ ('星巴克 \$25 礼品卡', 12, AppColors.couponDining),
+ ('Amazon \$50 购物券', 8, AppColors.couponShopping),
+ ('电影票 \$12', 5, AppColors.couponEntertainment),
+ ];
+
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.local_fire_department_rounded,
+ color: AppColors.error, size: 20),
+ const SizedBox(width: 8),
+ Text('今日热门核销', style: AppTypography.labelLarge),
+ ],
+ ),
+ const SizedBox(height: 12),
+ ...hotCoupons.map((c) {
+ final (name, count, color) = c;
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(
+ children: [
+ Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Icon(Icons.confirmation_number_rounded,
+ color: color, size: 18),
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Text(name, style: AppTypography.bodyMedium)),
+ Container(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text(
+ '$count笔',
+ style: AppTypography.labelSmall
+ .copyWith(color: AppColors.primary),
+ ),
+ ),
+ ],
+ ),
+ );
+ }),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildMarketingSuggestions() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.auto_awesome_rounded,
+ color: AppColors.primary, size: 20),
+ const SizedBox(width: 8),
+ Text('AI 营销建议', style: AppTypography.labelLarge),
+ ],
+ ),
+ const SizedBox(height: 12),
+ _buildSuggestionItem(
+ '推荐搭配销售',
+ '购买咖啡券的顾客同时对糕点券感兴趣,建议推荐组合',
+ Icons.restaurant_rounded,
+ ),
+ _buildSuggestionItem(
+ '周末促销建议',
+ '历史数据显示周六核销量+30%,建议推出周末限时活动',
+ Icons.campaign_rounded,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSuggestionItem(String title, String desc, IconData icon) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(icon, color: AppColors.primary, size: 18),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title,
+ style: const TextStyle(
+ fontSize: 13, fontWeight: FontWeight.w600)),
+ const SizedBox(height: 2),
+ Text(desc,
+ style: AppTypography.bodySmall
+ .copyWith(color: AppColors.textSecondary)),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ // ========================================
+ // Tab 2: 客流预测
+ // ========================================
+ Widget _buildTrafficPredictionTab() {
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Today's Prediction
+ _buildTodayPrediction(),
+ const SizedBox(height: 20),
+
+ // Hourly Breakdown
+ _buildHourlyBreakdown(),
+ const SizedBox(height: 20),
+
+ // Weekly Forecast
+ _buildWeeklyForecast(),
+ const SizedBox(height: 20),
+
+ // Staffing Suggestion
+ _buildStaffingSuggestion(),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTodayPrediction() {
+ return Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ gradient: AppColors.cardGradient,
+ borderRadius: AppSpacing.borderRadiusLg,
+ ),
+ child: Column(
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.insights_rounded, color: Colors.white, size: 22),
+ SizedBox(width: 10),
+ Text(
+ '今日客流预测',
+ style: TextStyle(
+ fontSize: 17,
+ fontWeight: FontWeight.w700,
+ color: Colors.white),
+ ),
+ ],
+ ),
+ const SizedBox(height: 20),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _predictionStat('预计核销', '45笔'),
+ _predictionStat('高峰时段', '11:30-13:00'),
+ _predictionStat('预计收入', '\$892'),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.15),
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Row(
+ children: [
+ const Text('✨',
+ style: TextStyle(fontSize: 14)),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ '较上周同期增长12%,建议午间增加1名收银员',
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.white.withValues(alpha: 0.9),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _predictionStat(String label, String value) {
+ return Column(
+ children: [
+ Text(
+ value,
+ style: const TextStyle(
+ fontSize: 18, fontWeight: FontWeight.w700, color: Colors.white),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ label,
+ style: TextStyle(
+ fontSize: 11, color: Colors.white.withValues(alpha: 0.7)),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildHourlyBreakdown() {
+ final hours = [
+ ('9:00', 3),
+ ('10:00', 5),
+ ('11:00', 8),
+ ('12:00', 12),
+ ('13:00', 9),
+ ('14:00', 4),
+ ('15:00', 3),
+ ('16:00', 2),
+ ('17:00', 5),
+ ('18:00', 7),
+ ];
+ final maxCount = 12;
+
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('分时段预测', style: AppTypography.labelLarge),
+ const SizedBox(height: 16),
+ ...hours.map((h) {
+ final (time, count) = h;
+ final pct = count / maxCount;
+ final isPeak = count >= 8;
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 4),
+ child: Row(
+ children: [
+ SizedBox(
+ width: 44,
+ child: Text(time,
+ style: AppTypography.caption
+ .copyWith(fontFamily: 'monospace')),
+ ),
+ Expanded(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(3),
+ child: LinearProgressIndicator(
+ value: pct,
+ backgroundColor: AppColors.gray100,
+ valueColor: AlwaysStoppedAnimation(
+ isPeak ? AppColors.primary : AppColors.primaryLight,
+ ),
+ minHeight: 16,
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ SizedBox(
+ width: 30,
+ child: Text(
+ '$count笔',
+ style: TextStyle(
+ fontSize: 11,
+ fontWeight: isPeak ? FontWeight.w600 : FontWeight.w400,
+ color: isPeak ? AppColors.primary : AppColors.textSecondary,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildWeeklyForecast() {
+ final days = [
+ ('周一', 38, false),
+ ('周二', 42, false),
+ ('周三', 45, true),
+ ('周四', 40, false),
+ ('周五', 52, false),
+ ('周六', 68, false),
+ ('周日', 55, false),
+ ];
+
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('本周预测', style: AppTypography.labelLarge),
+ const SizedBox(height: 16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: days.map((d) {
+ final (day, count, isToday) = d;
+ final heightPct = count / 68;
+ return Column(
+ children: [
+ Text(
+ '$count',
+ style: TextStyle(
+ fontSize: 10,
+ fontWeight: FontWeight.w600,
+ color: isToday ? AppColors.primary : AppColors.textTertiary,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Container(
+ width: 28,
+ height: 80 * heightPct,
+ decoration: BoxDecoration(
+ color: isToday ? AppColors.primary : AppColors.primarySurface,
+ borderRadius:
+ const BorderRadius.vertical(top: Radius.circular(4)),
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ day,
+ style: TextStyle(
+ fontSize: 11,
+ fontWeight: isToday ? FontWeight.w600 : FontWeight.w400,
+ color: isToday ? AppColors.primary : AppColors.textSecondary,
+ ),
+ ),
+ ],
+ );
+ }).toList(),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildStaffingSuggestion() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.people_alt_rounded,
+ color: AppColors.primary, size: 20),
+ const SizedBox(width: 8),
+ Text('排班建议', style: AppTypography.labelLarge),
+ ],
+ ),
+ const SizedBox(height: 12),
+ _staffRow('上午 (9:00-13:00)', '建议 2 人', '含午间高峰'),
+ _staffRow('下午 (13:00-17:00)', '建议 1 人', '客流较少'),
+ _staffRow('傍晚 (17:00-21:00)', '建议 2 人', '下班高峰'),
+ ],
+ ),
+ );
+ }
+
+ Widget _staffRow(String period, String suggestion, String reason) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(
+ children: [
+ Expanded(
+ flex: 3,
+ child: Text(period, style: AppTypography.bodySmall),
+ ),
+ Expanded(
+ flex: 2,
+ child: Text(suggestion,
+ style: AppTypography.labelSmall
+ .copyWith(color: AppColors.primary)),
+ ),
+ Expanded(
+ flex: 2,
+ child: Text(reason, style: AppTypography.caption),
+ ),
+ ],
+ ),
+ );
+ }
+
+ // ========================================
+ // Tab 3: 异常预警
+ // ========================================
+ Widget _buildAnomalyAlertTab() {
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Alert Summary
+ _buildAlertSummary(),
+ const SizedBox(height: 20),
+
+ // Active Alerts
+ _buildActiveAlerts(),
+ const SizedBox(height: 20),
+
+ // Suspicious Patterns
+ _buildSuspiciousPatterns(),
+ const SizedBox(height: 20),
+
+ // Recent Resolved
+ _buildResolvedAlerts(),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildAlertSummary() {
+ return Row(
+ children: [
+ _alertStatCard('待处理', '2', AppColors.error),
+ const SizedBox(width: 12),
+ _alertStatCard('今日已处理', '5', AppColors.success),
+ const SizedBox(width: 12),
+ _alertStatCard('风险指数', '低', AppColors.info),
+ ],
+ );
+ }
+
+ Widget _alertStatCard(String label, String value, Color color) {
+ return Expanded(
+ child: Container(
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.08),
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: color.withValues(alpha: 0.2)),
+ ),
+ child: Column(
+ children: [
+ Text(value,
+ style: TextStyle(
+ fontSize: 22, fontWeight: FontWeight.w700, color: color)),
+ const SizedBox(height: 2),
+ Text(label, style: AppTypography.caption.copyWith(color: color)),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildActiveAlerts() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.error.withValues(alpha: 0.3)),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.warning_amber_rounded,
+ color: AppColors.error, size: 20),
+ const SizedBox(width: 8),
+ Text('活跃预警',
+ style:
+ AppTypography.labelLarge.copyWith(color: AppColors.error)),
+ ],
+ ),
+ const SizedBox(height: 12),
+ _alertItem(
+ '高频核销检测',
+ '用户#78901 在 5 分钟内尝试核销 3 张同品牌券',
+ '2 分钟前',
+ AppColors.error,
+ Icons.speed_rounded,
+ ),
+ const Divider(height: 20),
+ _alertItem(
+ '疑似伪造券码',
+ '券码 GNX-FAKE-001 格式异常,不在系统记录中',
+ '15 分钟前',
+ AppColors.warning,
+ Icons.gpp_bad_rounded,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _alertItem(
+ String title, String desc, String time, Color color, IconData icon) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 4),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ width: 36,
+ height: 36,
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Icon(icon, color: color, size: 18),
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title, style: AppTypography.labelMedium),
+ const SizedBox(height: 2),
+ Text(desc, style: AppTypography.bodySmall),
+ const SizedBox(height: 4),
+ Text(time, style: AppTypography.caption),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSuspiciousPatterns() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.pattern_rounded,
+ color: AppColors.warning, size: 20),
+ const SizedBox(width: 8),
+ Text('可疑模式检测', style: AppTypography.labelLarge),
+ ],
+ ),
+ const SizedBox(height: 12),
+ _patternItem(
+ '同一用户连续核销', '3次/5分钟 (阈值: 2次/5分钟)', 0.8, AppColors.error),
+ const SizedBox(height: 10),
+ _patternItem(
+ '非营业时间核销尝试', '0次/本周', 0.0, AppColors.success),
+ const SizedBox(height: 10),
+ _patternItem(
+ '过期券核销尝试', '2次/今日', 0.4, AppColors.warning),
+ ],
+ ),
+ );
+ }
+
+ Widget _patternItem(
+ String label, String detail, double severity, Color color) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(label, style: AppTypography.labelMedium.copyWith(fontSize: 13)),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text(
+ severity > 0.6
+ ? '异常'
+ : severity > 0.2
+ ? '注意'
+ : '正常',
+ style: TextStyle(
+ fontSize: 10, fontWeight: FontWeight.w600, color: color),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 4),
+ Text(detail, style: AppTypography.caption),
+ const SizedBox(height: 6),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(3),
+ child: LinearProgressIndicator(
+ value: severity,
+ backgroundColor: AppColors.gray100,
+ valueColor: AlwaysStoppedAnimation(color),
+ minHeight: 4,
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildResolvedAlerts() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('今日已处理', style: AppTypography.labelLarge),
+ const SizedBox(height: 12),
+ _resolvedItem('过期券核销拦截', '系统自动拦截', '10:24'),
+ _resolvedItem('重复核销拦截', '同一券码二次扫描', '11:05'),
+ _resolvedItem('非本店券提醒', '引导至正确门店', '12:30'),
+ _resolvedItem('余额不足核销', '告知顾客充值', '13:15'),
+ _resolvedItem('系统超时重试', '网络恢复后自动完成', '14:02'),
+ ],
+ ),
+ );
+ }
+
+ Widget _resolvedItem(String title, String desc, String time) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(
+ children: [
+ const Icon(Icons.check_circle_rounded,
+ color: AppColors.success, size: 16),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title, style: AppTypography.bodyMedium),
+ Text(desc, style: AppTypography.caption),
+ ],
+ ),
+ ),
+ Text(time,
+ style: AppTypography.caption
+ .copyWith(fontFamily: 'monospace')),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/merchant/presentation/pages/merchant_home_page.dart b/frontend/mobile/lib/features/merchant/presentation/pages/merchant_home_page.dart
new file mode 100644
index 0000000..0f9b40b
--- /dev/null
+++ b/frontend/mobile/lib/features/merchant/presentation/pages/merchant_home_page.dart
@@ -0,0 +1,669 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/genex_button.dart';
+
+/// B. 商户核销端 - 主界面
+///
+/// B1: 员工登录(手机号+门店选择)
+/// B2: 扫码核销(主页面)、券信息确认、核销成功、手动输码、离线提示
+/// B3: 核销记录列表、待同步队列
+/// B4: 门店仪表盘(店长权限)
+class MerchantHomePage extends StatelessWidget {
+ const MerchantHomePage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: SafeArea(
+ child: Column(
+ children: [
+ // Header
+ _buildHeader(),
+
+ // Network Status
+ _buildNetworkStatus(isOnline: true),
+
+ // Main Scanner Area
+ Expanded(child: _buildScannerArea(context)),
+
+ // Bottom Actions
+ _buildBottomActions(context),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeader() {
+ return Container(
+ padding: const EdgeInsets.fromLTRB(20, 12, 20, 12),
+ child: Row(
+ children: [
+ Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Icon(Icons.store_rounded, color: AppColors.primary, size: 22),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('星巴克 朝阳门店', style: AppTypography.labelMedium),
+ Text('收银员 - 张三', style: AppTypography.caption),
+ ],
+ ),
+ ),
+ // Today's stats
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ decoration: BoxDecoration(
+ color: AppColors.successLight,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(Icons.check_circle_rounded, size: 14, color: AppColors.success),
+ const SizedBox(width: 4),
+ Text('今日 23 笔', style: AppTypography.labelSmall.copyWith(
+ color: AppColors.success,
+ )),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildNetworkStatus({required bool isOnline}) {
+ return Container(
+ margin: const EdgeInsets.symmetric(horizontal: 20),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ decoration: BoxDecoration(
+ color: isOnline ? AppColors.successLight : AppColors.warningLight,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 8, height: 8,
+ decoration: BoxDecoration(
+ color: isOnline ? AppColors.success : AppColors.warning,
+ shape: BoxShape.circle,
+ ),
+ ),
+ const SizedBox(width: 6),
+ Text(
+ isOnline ? '在线模式' : '离线模式 - 待同步 3 笔',
+ style: AppTypography.caption.copyWith(
+ color: isOnline ? AppColors.success : AppColors.warning,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildScannerArea(BuildContext context) {
+ return Container(
+ margin: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: AppColors.gray900,
+ borderRadius: AppSpacing.borderRadiusXl,
+ ),
+ child: Stack(
+ children: [
+ // Camera placeholder
+ Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // Scan frame
+ Container(
+ width: 240,
+ height: 240,
+ decoration: BoxDecoration(
+ border: Border.all(color: AppColors.primary, width: 2),
+ borderRadius: AppSpacing.borderRadiusLg,
+ ),
+ child: Stack(
+ children: [
+ // Corner markers
+ ..._buildCornerMarkers(),
+ // Center icon
+ Center(
+ child: Icon(
+ Icons.qr_code_scanner_rounded,
+ size: 48,
+ color: Colors.white.withValues(alpha: 0.5),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 20),
+ Text(
+ '将券二维码对准扫描框',
+ style: AppTypography.bodyMedium.copyWith(color: Colors.white70),
+ ),
+ ],
+ ),
+ ),
+
+ // Flashlight toggle
+ Positioned(
+ bottom: 20,
+ left: 0,
+ right: 0,
+ child: Center(
+ child: GestureDetector(
+ onTap: () {},
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ color: Colors.white12,
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(Icons.flashlight_on_rounded,
+ color: Colors.white70, size: 22),
+ ),
+ const SizedBox(height: 4),
+ Text('手电筒', style: AppTypography.caption.copyWith(color: Colors.white54)),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ List _buildCornerMarkers() {
+ const size = 24.0;
+ const thickness = 3.0;
+ const color = AppColors.primary;
+ const radius = Radius.circular(4);
+
+ return [
+ // Top-left
+ Positioned(
+ top: 0, left: 0,
+ child: Container(
+ width: size, height: thickness,
+ decoration: const BoxDecoration(color: color, borderRadius: BorderRadius.only(topLeft: radius)),
+ ),
+ ),
+ Positioned(
+ top: 0, left: 0,
+ child: Container(width: thickness, height: size, color: color),
+ ),
+ // Top-right
+ Positioned(
+ top: 0, right: 0,
+ child: Container(width: size, height: thickness, color: color),
+ ),
+ Positioned(
+ top: 0, right: 0,
+ child: Container(width: thickness, height: size, color: color),
+ ),
+ // Bottom-left
+ Positioned(
+ bottom: 0, left: 0,
+ child: Container(width: size, height: thickness, color: color),
+ ),
+ Positioned(
+ bottom: 0, left: 0,
+ child: Container(width: thickness, height: size, color: color),
+ ),
+ // Bottom-right
+ Positioned(
+ bottom: 0, right: 0,
+ child: Container(width: size, height: thickness, color: color),
+ ),
+ Positioned(
+ bottom: 0, right: 0,
+ child: Container(width: thickness, height: size, color: color),
+ ),
+ ];
+ }
+
+ Widget _buildBottomActions(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.fromLTRB(20, 12, 20, 16),
+ child: Row(
+ children: [
+ _bottomAction(Icons.keyboard_rounded, '手动输码', () {
+ _showManualInput(context);
+ }),
+ const SizedBox(width: 16),
+ _bottomAction(Icons.history_rounded, '核销记录', () {
+ // Navigator: → RedeemHistoryPage
+ }),
+ const SizedBox(width: 16),
+ _bottomAction(Icons.bar_chart_rounded, '门店数据', () {
+ // Navigator: → StoreDashboardPage
+ }),
+ ],
+ ),
+ );
+ }
+
+ Widget _bottomAction(IconData icon, String label, VoidCallback onTap) {
+ return Expanded(
+ child: GestureDetector(
+ onTap: onTap,
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: [
+ Icon(icon, color: AppColors.primary, size: 24),
+ const SizedBox(height: 4),
+ Text(label, style: AppTypography.caption.copyWith(
+ color: AppColors.textPrimary,
+ fontWeight: FontWeight.w500,
+ )),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _showManualInput(BuildContext context) {
+ showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ builder: (ctx) => Padding(
+ padding: EdgeInsets.only(
+ bottom: MediaQuery.of(ctx).viewInsets.bottom,
+ ),
+ child: Container(
+ padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 36, height: 4,
+ decoration: BoxDecoration(
+ color: AppColors.gray300,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ ),
+ const SizedBox(height: 20),
+ Text('手动输入券码', style: AppTypography.h2),
+ const SizedBox(height: 16),
+ TextField(
+ autofocus: true,
+ decoration: const InputDecoration(
+ hintText: '请输入券码',
+ prefixIcon: Icon(Icons.confirmation_number_outlined,
+ color: AppColors.textTertiary),
+ ),
+ textCapitalization: TextCapitalization.characters,
+ ),
+ const SizedBox(height: 16),
+ GenexButton(
+ label: '查询',
+ onPressed: () {},
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// B2. 核销确认弹窗
+class RedeemConfirmSheet extends StatelessWidget {
+ const RedeemConfirmSheet({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.fromLTRB(20, 8, 20, 32),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 36, height: 4,
+ decoration: BoxDecoration(
+ color: AppColors.gray300,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Consumer Avatar + Info
+ Row(
+ children: [
+ Container(
+ width: 48, height: 48,
+ decoration: const BoxDecoration(
+ color: AppColors.primarySurface,
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(Icons.person_rounded, color: AppColors.primary),
+ ),
+ const SizedBox(width: 12),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('用户昵称', style: AppTypography.labelMedium),
+ Text('消费者', style: AppTypography.caption),
+ ],
+ ),
+ ],
+ ),
+ const SizedBox(height: 20),
+
+ // Coupon Info
+ Container(
+ width: double.infinity,
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Column(
+ children: [
+ _row('券名称', '星巴克 \$25 礼品卡'),
+ const SizedBox(height: 8),
+ _row('面值', '\$25.00'),
+ const SizedBox(height: 8),
+ _row('有效期', '2026/12/31'),
+ const SizedBox(height: 8),
+ _row('使用条件', '无最低消费'),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ GenexButton(
+ label: '确认核销',
+ onPressed: () {
+ Navigator.of(context).pop();
+ // Show success
+ },
+ ),
+ const SizedBox(height: 8),
+ GenexButton(
+ label: '取消',
+ variant: GenexButtonVariant.text,
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _row(String label, String value) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(label, style: AppTypography.bodySmall.copyWith(color: AppColors.textSecondary)),
+ Text(value, style: AppTypography.labelSmall),
+ ],
+ );
+ }
+}
+
+/// B2. 核销成功页
+class RedeemSuccessSheet extends StatelessWidget {
+ const RedeemSuccessSheet({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.fromLTRB(20, 8, 20, 32),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 36, height: 4,
+ decoration: BoxDecoration(
+ color: AppColors.gray300,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ ),
+ const SizedBox(height: 32),
+ Container(
+ width: 72, height: 72,
+ decoration: const BoxDecoration(
+ gradient: AppColors.successGradient,
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(Icons.check_rounded, color: Colors.white, size: 36),
+ ),
+ const SizedBox(height: 16),
+ Text('核销成功', style: AppTypography.h1),
+ const SizedBox(height: 8),
+ Text('星巴克 \$25 礼品卡', style: AppTypography.bodyMedium.copyWith(
+ color: AppColors.textSecondary,
+ )),
+ const SizedBox(height: 32),
+ GenexButton(
+ label: '继续核销',
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+/// B3. 核销记录页
+class RedeemHistoryPage extends StatelessWidget {
+ const RedeemHistoryPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ title: const Text('核销记录'),
+ actions: [
+ TextButton(
+ onPressed: () {},
+ child: Text('今日', style: AppTypography.labelSmall.copyWith(
+ color: AppColors.primary,
+ )),
+ ),
+ ],
+ ),
+ body: ListView.separated(
+ padding: const EdgeInsets.all(20),
+ itemCount: 8,
+ separatorBuilder: (_, __) => const SizedBox(height: 8),
+ itemBuilder: (context, index) {
+ final isSync = index < 6;
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 36, height: 36,
+ decoration: BoxDecoration(
+ color: isSync ? AppColors.successLight : AppColors.warningLight,
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ isSync ? Icons.check_rounded : Icons.sync_rounded,
+ size: 18,
+ color: isSync ? AppColors.success : AppColors.warning,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('品牌 ${index + 1} \$${(index + 1) * 10} 券',
+ style: AppTypography.labelSmall),
+ Text('核销员: 张三 · 14:${30 + index}',
+ style: AppTypography.caption),
+ ],
+ ),
+ ),
+ Text(
+ isSync ? '已同步' : '待同步',
+ style: AppTypography.caption.copyWith(
+ color: isSync ? AppColors.success : AppColors.warning,
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
+
+/// B4. 门店仪表盘
+class StoreDashboardPage extends StatelessWidget {
+ const StoreDashboardPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ title: const Text('门店数据'),
+ ),
+ body: SingleChildScrollView(
+ padding: AppSpacing.pagePadding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 16),
+
+ // Today Stats
+ Row(
+ children: [
+ _statCard('今日核销', '23笔', Icons.check_circle_rounded, AppColors.success),
+ const SizedBox(width: 12),
+ _statCard('核销金额', '\$1,456', Icons.attach_money_rounded, AppColors.primary),
+ ],
+ ),
+ const SizedBox(height: 24),
+
+ // Weekly Trend (placeholder)
+ Text('本周趋势', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Center(
+ child: Text('周核销趋势图 (fl_chart)',
+ style: AppTypography.bodySmall.copyWith(color: AppColors.textTertiary)),
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Staff Ranking
+ Text('核销员排行', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ ...List.generate(3, (index) {
+ final names = ['张三', '李四', '王五'];
+ final counts = [12, 8, 3];
+ return Container(
+ margin: const EdgeInsets.only(bottom: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 28, height: 28,
+ decoration: BoxDecoration(
+ color: index == 0
+ ? AppColors.primary
+ : AppColors.gray200,
+ shape: BoxShape.circle,
+ ),
+ child: Center(
+ child: Text('${index + 1}', style: TextStyle(
+ color: index == 0 ? Colors.white : AppColors.textSecondary,
+ fontSize: 13,
+ fontWeight: FontWeight.w600,
+ )),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Text(names[index], style: AppTypography.labelMedium),
+ const Spacer(),
+ Text('${counts[index]}笔', style: AppTypography.bodyMedium.copyWith(
+ color: AppColors.primary,
+ )),
+ ],
+ ),
+ );
+ }),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _statCard(String label, String value, IconData icon, Color color) {
+ return Expanded(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ boxShadow: AppSpacing.shadowSm,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(icon, color: color, size: 24),
+ const SizedBox(height: 12),
+ Text(value, style: AppTypography.h1.copyWith(color: color)),
+ const SizedBox(height: 4),
+ Text(label, style: AppTypography.caption),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/message/presentation/pages/message_detail_page.dart b/frontend/mobile/lib/features/message/presentation/pages/message_detail_page.dart
new file mode 100644
index 0000000..1815d49
--- /dev/null
+++ b/frontend/mobile/lib/features/message/presentation/pages/message_detail_page.dart
@@ -0,0 +1,146 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// 消息详情页面
+///
+/// 查看单条通知的详细内容
+/// 类型:交易通知、到期提醒、系统通知、活动推送
+class MessageDetailPage extends StatelessWidget {
+ final String title;
+ final String type;
+
+ const MessageDetailPage({
+ super.key,
+ this.title = '交易成功通知',
+ this.type = 'transaction',
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('消息详情')),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Icon & Type
+ Row(
+ children: [
+ Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ color: _typeColor.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Icon(_typeIcon, color: _typeColor, size: 22),
+ ),
+ const SizedBox(width: 12),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
+ decoration: BoxDecoration(
+ color: _typeColor.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text(_typeLabel, style: TextStyle(fontSize: 11, color: _typeColor, fontWeight: FontWeight.w600)),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+
+ // Title
+ Text(title, style: AppTypography.h1),
+ const SizedBox(height: 8),
+ Text('2026年2月10日 14:32', style: AppTypography.bodySmall),
+ const SizedBox(height: 24),
+
+ // Content
+ Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: const Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '您成功购买了 星巴克 \$25 礼品卡,支付金额 \$21.25。',
+ style: TextStyle(fontSize: 15, height: 1.6),
+ ),
+ SizedBox(height: 16),
+ _DetailRow('券名称', '星巴克 \$25 礼品卡'),
+ _DetailRow('面值', '\$25.00'),
+ _DetailRow('支付金额', '\$21.25'),
+ _DetailRow('订单号', 'GNX20260210001'),
+ _DetailRow('支付方式', 'Visa •••• 4242'),
+ ],
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Actions
+ SizedBox(
+ width: double.infinity,
+ child: OutlinedButton(
+ onPressed: () {},
+ child: const Text('查看券详情'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Color get _typeColor {
+ switch (type) {
+ case 'transaction': return AppColors.success;
+ case 'expiry': return AppColors.warning;
+ case 'system': return AppColors.info;
+ default: return AppColors.primary;
+ }
+ }
+
+ IconData get _typeIcon {
+ switch (type) {
+ case 'transaction': return Icons.receipt_long_rounded;
+ case 'expiry': return Icons.timer_rounded;
+ case 'system': return Icons.settings_rounded;
+ default: return Icons.campaign_rounded;
+ }
+ }
+
+ String get _typeLabel {
+ switch (type) {
+ case 'transaction': return '交易通知';
+ case 'expiry': return '到期提醒';
+ case 'system': return '系统通知';
+ default: return '活动推送';
+ }
+ }
+}
+
+class _DetailRow extends StatelessWidget {
+ final String label;
+ final String value;
+ const _DetailRow(this.label, this.value);
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(label, style: AppTypography.bodySmall),
+ Text(value, style: AppTypography.labelMedium),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/message/presentation/pages/message_page.dart b/frontend/mobile/lib/features/message/presentation/pages/message_page.dart
new file mode 100644
index 0000000..25b9fa9
--- /dev/null
+++ b/frontend/mobile/lib/features/message/presentation/pages/message_page.dart
@@ -0,0 +1,215 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/empty_state.dart';
+
+/// A8. 消息模块
+///
+/// 交易通知、系统公告、券到期提醒、价格提醒
+/// 分类Tab + 消息详情
+class MessagePage extends StatefulWidget {
+ const MessagePage({super.key});
+
+ @override
+ State createState() => _MessagePageState();
+}
+
+class _MessagePageState extends State
+ with SingleTickerProviderStateMixin {
+ late TabController _tabController;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = TabController(length: 4, vsync: this);
+ }
+
+ @override
+ void dispose() {
+ _tabController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('消息'),
+ actions: [
+ TextButton(
+ onPressed: () {},
+ child: Text('全部已读', style: AppTypography.labelSmall.copyWith(
+ color: AppColors.primary,
+ )),
+ ),
+ ],
+ bottom: TabBar(
+ controller: _tabController,
+ tabs: const [
+ Tab(text: '全部'),
+ Tab(text: '交易'),
+ Tab(text: '到期'),
+ Tab(text: '公告'),
+ ],
+ ),
+ ),
+ body: TabBarView(
+ controller: _tabController,
+ children: [
+ _buildMessageList(all: true),
+ _buildMessageList(type: MessageType.transaction),
+ _buildMessageList(type: MessageType.expiry),
+ _buildMessageList(type: MessageType.announcement),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildMessageList({bool all = false, MessageType? type}) {
+ if (type == MessageType.announcement) {
+ return EmptyState.noMessages();
+ }
+
+ final messages = _mockMessages
+ .where((m) => all || m.type == type)
+ .toList();
+
+ return ListView.separated(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ itemCount: messages.length,
+ separatorBuilder: (_, __) => const Divider(indent: 76),
+ itemBuilder: (context, index) {
+ final msg = messages[index];
+ return _buildMessageItem(msg);
+ },
+ );
+ }
+
+ Widget _buildMessageItem(_MockMessage msg) {
+ return ListTile(
+ contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
+ leading: Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ color: _iconBgColor(msg.type),
+ shape: BoxShape.circle,
+ ),
+ child: Icon(_iconData(msg.type), size: 22, color: _iconColor(msg.type)),
+ ),
+ title: Row(
+ children: [
+ Expanded(
+ child: Text(msg.title, style: AppTypography.labelMedium.copyWith(
+ fontWeight: msg.isRead ? FontWeight.w400 : FontWeight.w600,
+ )),
+ ),
+ Text(msg.time, style: AppTypography.caption),
+ ],
+ ),
+ subtitle: Padding(
+ padding: const EdgeInsets.only(top: 4),
+ child: Text(
+ msg.body,
+ style: AppTypography.bodySmall,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ trailing: !msg.isRead
+ ? Container(
+ width: 8,
+ height: 8,
+ decoration: const BoxDecoration(
+ color: AppColors.primary,
+ shape: BoxShape.circle,
+ ),
+ )
+ : null,
+ onTap: () {
+ // Navigator: → MessageDetailPage (with associated action)
+ },
+ );
+ }
+
+ IconData _iconData(MessageType type) {
+ switch (type) {
+ case MessageType.transaction:
+ return Icons.swap_horiz_rounded;
+ case MessageType.expiry:
+ return Icons.access_time_rounded;
+ case MessageType.price:
+ return Icons.trending_up_rounded;
+ case MessageType.announcement:
+ return Icons.campaign_rounded;
+ }
+ }
+
+ Color _iconColor(MessageType type) {
+ switch (type) {
+ case MessageType.transaction:
+ return AppColors.primary;
+ case MessageType.expiry:
+ return AppColors.warning;
+ case MessageType.price:
+ return AppColors.success;
+ case MessageType.announcement:
+ return AppColors.info;
+ }
+ }
+
+ Color _iconBgColor(MessageType type) {
+ return _iconColor(type).withValues(alpha: 0.1);
+ }
+}
+
+enum MessageType { transaction, expiry, price, announcement }
+
+class _MockMessage {
+ final String title;
+ final String body;
+ final String time;
+ final MessageType type;
+ final bool isRead;
+
+ const _MockMessage(this.title, this.body, this.time, this.type, this.isRead);
+}
+
+const _mockMessages = [
+ _MockMessage(
+ '购买成功',
+ '您已成功购买 星巴克 \$25 礼品卡,共花费 \$21.25',
+ '14:32',
+ MessageType.transaction,
+ false,
+ ),
+ _MockMessage(
+ '券即将到期',
+ '您持有的 Target \$30 折扣券 将于3天后到期,请及时使用',
+ '10:15',
+ MessageType.expiry,
+ false,
+ ),
+ _MockMessage(
+ '价格提醒',
+ '您关注的 Amazon \$100 购物券 当前价格已降至 \$82,低于您设定的提醒价格',
+ '昨天',
+ MessageType.price,
+ true,
+ ),
+ _MockMessage(
+ '出售成交',
+ '您挂单出售的 Nike \$80 运动券 已成功售出,收入 \$68.00',
+ '02/07',
+ MessageType.transaction,
+ true,
+ ),
+ _MockMessage(
+ '核销成功',
+ 'Walmart \$50 生活券 已在门店核销成功',
+ '02/06',
+ MessageType.transaction,
+ true,
+ ),
+];
diff --git a/frontend/mobile/lib/features/profile/presentation/pages/kyc_page.dart b/frontend/mobile/lib/features/profile/presentation/pages/kyc_page.dart
new file mode 100644
index 0000000..56236ba
--- /dev/null
+++ b/frontend/mobile/lib/features/profile/presentation/pages/kyc_page.dart
@@ -0,0 +1,167 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// KYC认证页面
+///
+/// 分级认证:L0(无认证) → L1(手机+邮箱) → L2(身份证) → L3(高级验证)
+/// 每级解锁不同额度和功能
+class KycPage extends StatelessWidget {
+ const KycPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('身份认证')),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ children: [
+ // Current Level
+ _buildCurrentLevel(),
+ const SizedBox(height: 24),
+
+ // KYC Levels
+ _buildLevel(
+ 'L1 基础认证',
+ '手机号 + 邮箱验证',
+ ['每日购买限额 \$500', '可购买券、出示核销'],
+ true,
+ AppColors.success,
+ ),
+ _buildLevel(
+ 'L2 身份认证',
+ '身份证/护照验证',
+ ['每日购买限额 \$5,000', '解锁二级市场交易、P2P转赠'],
+ false,
+ AppColors.info,
+ ),
+ _buildLevel(
+ 'L3 高级认证',
+ '视频面审 + 地址证明',
+ ['无限额', '解锁大额交易、提现无限制'],
+ false,
+ AppColors.primary,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildCurrentLevel() {
+ return Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusLg,
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 56,
+ height: 56,
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.2),
+ borderRadius: BorderRadius.circular(14),
+ ),
+ child: const Icon(Icons.verified_user_rounded, color: Colors.white, size: 28),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('当前认证等级', style: AppTypography.bodySmall.copyWith(color: Colors.white70)),
+ const SizedBox(height: 4),
+ Text('L1 基础认证', style: AppTypography.h1.copyWith(color: Colors.white)),
+ const SizedBox(height: 4),
+ Text('每日购买限额 \$500', style: AppTypography.bodySmall.copyWith(color: Colors.white60)),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildLevel(
+ String title,
+ String requirement,
+ List benefits,
+ bool completed,
+ Color color,
+ ) {
+ return Container(
+ margin: const EdgeInsets.only(bottom: 12),
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: completed ? color.withValues(alpha: 0.3) : AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Icon(
+ completed ? Icons.check_circle_rounded : Icons.lock_outlined,
+ color: color,
+ size: 18,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title, style: AppTypography.labelLarge),
+ Text(requirement, style: AppTypography.caption),
+ ],
+ ),
+ ),
+ if (completed)
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
+ decoration: BoxDecoration(
+ color: AppColors.successLight,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text('已完成', style: AppTypography.caption.copyWith(color: AppColors.success)),
+ )
+ else
+ ElevatedButton(
+ onPressed: () {},
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ minimumSize: Size.zero,
+ ),
+ child: const Text('去认证', style: TextStyle(fontSize: 13)),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ ...benefits.map((b) => Padding(
+ padding: const EdgeInsets.only(bottom: 4),
+ child: Row(
+ children: [
+ Icon(Icons.check_rounded, size: 14, color: completed ? color : AppColors.textTertiary),
+ const SizedBox(width: 6),
+ Text(b, style: AppTypography.bodySmall),
+ ],
+ ),
+ )),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/profile/presentation/pages/payment_management_page.dart b/frontend/mobile/lib/features/profile/presentation/pages/payment_management_page.dart
new file mode 100644
index 0000000..00e8daa
--- /dev/null
+++ b/frontend/mobile/lib/features/profile/presentation/pages/payment_management_page.dart
@@ -0,0 +1,136 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// 支付管理页面
+///
+/// 管理银行卡、信用卡、支付密码
+class PaymentManagementPage extends StatelessWidget {
+ const PaymentManagementPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('支付管理')),
+ body: ListView(
+ padding: const EdgeInsets.all(20),
+ children: [
+ Text('我的银行卡', style: AppTypography.h3),
+ const SizedBox(height: 12),
+
+ // Card List
+ _buildCard('Visa', '•••• •••• •••• 4242', 'CREDIT', AppColors.primary),
+ const SizedBox(height: 10),
+ _buildCard('Mastercard', '•••• •••• •••• 8888', 'DEBIT', AppColors.info),
+ const SizedBox(height: 16),
+
+ // Add Card
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ border: Border.all(color: AppColors.border, style: BorderStyle.solid),
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: const Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.add_circle_outline_rounded, color: AppColors.primary),
+ SizedBox(width: 8),
+ Text('添加新银行卡', style: TextStyle(color: AppColors.primary, fontWeight: FontWeight.w600)),
+ ],
+ ),
+ ),
+ const SizedBox(height: 32),
+
+ // Bank Account
+ Text('银行账户(提现用)', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.account_balance_rounded, color: AppColors.textSecondary),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Bank of America', style: AppTypography.labelMedium),
+ Text('•••• 6789 · 储蓄账户', style: AppTypography.caption),
+ ],
+ ),
+ ),
+ const Icon(Icons.chevron_right_rounded, color: AppColors.textTertiary),
+ ],
+ ),
+ ),
+ const SizedBox(height: 32),
+
+ // Payment Security
+ Text('支付安全', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ _buildSettingTile('支付密码', '已设置', Icons.password_rounded),
+ _buildSettingTile('指纹/面容支付', '已开启', Icons.fingerprint_rounded),
+ _buildSettingTile('免密支付', '单笔≤\$10', Icons.flash_on_rounded),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildCard(String brand, String number, String type, Color color) {
+ return Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [color, color.withValues(alpha: 0.7)],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ borderRadius: AppSpacing.borderRadiusLg,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(brand, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 16)),
+ Text(type, style: TextStyle(color: Colors.white.withValues(alpha: 0.7), fontSize: 11)),
+ ],
+ ),
+ const SizedBox(height: 20),
+ Text(number, style: const TextStyle(color: Colors.white, fontSize: 18, letterSpacing: 2)),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSettingTile(String title, String value, IconData icon) {
+ return Container(
+ margin: const EdgeInsets.only(bottom: 8),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: ListTile(
+ leading: Icon(icon, color: AppColors.textSecondary, size: 22),
+ title: Text(title, style: AppTypography.bodyMedium),
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(value, style: AppTypography.caption),
+ const SizedBox(width: 4),
+ const Icon(Icons.chevron_right_rounded, size: 18, color: AppColors.textTertiary),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/profile/presentation/pages/pro_mode_page.dart b/frontend/mobile/lib/features/profile/presentation/pages/pro_mode_page.dart
new file mode 100644
index 0000000..b5b3971
--- /dev/null
+++ b/frontend/mobile/lib/features/profile/presentation/pages/pro_mode_page.dart
@@ -0,0 +1,456 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// 高级模式(Pro Mode)设置页
+///
+/// KYC L2+ 用户可开启,展示链上信息:
+/// - WalletConnect 连接外部钱包
+/// - 链上地址展示
+/// - 交易Hash查看器
+/// - 链上资产同步
+/// 注:默认关闭,普通用户完全无感知区块链
+class ProModePage extends StatefulWidget {
+ const ProModePage({super.key});
+
+ @override
+ State createState() => _ProModePageState();
+}
+
+class _ProModePageState extends State {
+ bool _proModeEnabled = false;
+ bool _showChainAddress = false;
+ bool _showTxHash = false;
+ bool _walletConnected = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('高级模式')),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Pro Mode Toggle
+ _buildProModeCard(),
+ const SizedBox(height: 20),
+
+ if (_proModeEnabled) ...[
+ // WalletConnect
+ _buildWalletConnectCard(),
+ const SizedBox(height: 16),
+
+ // Chain Address Display
+ _buildChainSettingsCard(),
+ const SizedBox(height: 16),
+
+ // Transaction Explorer
+ _buildTxExplorerCard(),
+ const SizedBox(height: 16),
+
+ // Chain Assets
+ _buildChainAssetsCard(),
+ const SizedBox(height: 16),
+
+ // Track Selection
+ _buildTrackCard(),
+ ],
+
+ if (!_proModeEnabled) _buildProModeInfo(),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildProModeCard() {
+ return Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ gradient: _proModeEnabled ? AppColors.cardGradient : null,
+ color: _proModeEnabled ? null : AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusLg,
+ border: _proModeEnabled ? null : Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(
+ Icons.code_rounded,
+ color: _proModeEnabled ? Colors.white : AppColors.primary,
+ size: 24,
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '高级模式 (Pro)',
+ style: TextStyle(
+ fontSize: 17,
+ fontWeight: FontWeight.w700,
+ color: _proModeEnabled ? Colors.white : AppColors.textPrimary,
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ '开启后可查看链上信息和连接外部钱包',
+ style: TextStyle(
+ fontSize: 12,
+ color: _proModeEnabled ? Colors.white70 : AppColors.textSecondary,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Switch(
+ value: _proModeEnabled,
+ onChanged: (v) => setState(() => _proModeEnabled = v),
+ activeColor: Colors.white,
+ activeTrackColor: Colors.white24,
+ ),
+ ],
+ ),
+ if (_proModeEnabled) ...[
+ const SizedBox(height: 12),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.15),
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: const Text(
+ '需要 KYC L2 及以上认证',
+ style: TextStyle(fontSize: 11, color: Colors.white70),
+ ),
+ ),
+ ],
+ ],
+ ),
+ );
+ }
+
+ Widget _buildWalletConnectCard() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.account_balance_wallet_rounded, color: AppColors.primary, size: 20),
+ const SizedBox(width: 8),
+ Text('WalletConnect', style: AppTypography.labelLarge),
+ const Spacer(),
+ if (_walletConnected)
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
+ decoration: BoxDecoration(
+ color: AppColors.successLight,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: const Text('已连接', style: TextStyle(fontSize: 11, color: AppColors.success, fontWeight: FontWeight.w600)),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ if (_walletConnected) ...[
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Row(
+ children: [
+ const CircleAvatar(radius: 16, backgroundColor: AppColors.primaryContainer, child: Text('M', style: TextStyle(fontSize: 14, color: AppColors.primary))),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('MetaMask', style: AppTypography.labelMedium),
+ Text('0x7a3b...c4f2', style: AppTypography.caption.copyWith(fontFamily: 'monospace')),
+ ],
+ ),
+ ),
+ TextButton(
+ onPressed: () => setState(() => _walletConnected = false),
+ child: const Text('断开', style: TextStyle(color: AppColors.error, fontSize: 13)),
+ ),
+ ],
+ ),
+ ),
+ ] else
+ SizedBox(
+ width: double.infinity,
+ child: OutlinedButton.icon(
+ onPressed: () => setState(() => _walletConnected = true),
+ icon: const Icon(Icons.link_rounded, size: 18),
+ label: const Text('连接外部钱包'),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '连接外部钱包后可将平台资产提取至自有地址',
+ style: AppTypography.caption,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildChainSettingsCard() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: [
+ SwitchListTile(
+ title: Text('显示链上地址', style: AppTypography.labelMedium),
+ subtitle: Text('在券详情中展示合约地址', style: AppTypography.caption),
+ value: _showChainAddress,
+ onChanged: (v) => setState(() => _showChainAddress = v),
+ activeColor: AppColors.primary,
+ contentPadding: EdgeInsets.zero,
+ ),
+ const Divider(height: 1),
+ SwitchListTile(
+ title: Text('显示交易Hash', style: AppTypography.labelMedium),
+ subtitle: Text('在交易记录中展示链上Hash', style: AppTypography.caption),
+ value: _showTxHash,
+ onChanged: (v) => setState(() => _showTxHash = v),
+ activeColor: AppColors.primary,
+ contentPadding: EdgeInsets.zero,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTxExplorerCard() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.explore_rounded, color: AppColors.primary, size: 20),
+ const SizedBox(width: 8),
+ Text('交易浏览器', style: AppTypography.labelLarge),
+ ],
+ ),
+ const SizedBox(height: 12),
+ _buildTxItem('购买 星巴克 \$25 礼品卡', '0xabc1...def3', '已确认', AppColors.success),
+ _buildTxItem('出售 Amazon \$100 券', '0x789a...bc12', '已确认', AppColors.success),
+ _buildTxItem('转赠给 Alice', '0xdef4...5678', '确认中', AppColors.warning),
+ const SizedBox(height: 8),
+ Center(
+ child: TextButton(
+ onPressed: () {},
+ child: const Text('查看全部链上交易'),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTxItem(String title, String hash, String status, Color color) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: Row(
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title, style: AppTypography.bodyMedium),
+ Text(hash, style: AppTypography.caption.copyWith(fontFamily: 'monospace', color: AppColors.textLink)),
+ ],
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text(status, style: TextStyle(fontSize: 11, color: color, fontWeight: FontWeight.w600)),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildChainAssetsCard() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.token_rounded, color: AppColors.primary, size: 20),
+ const SizedBox(width: 8),
+ Text('链上资产', style: AppTypography.labelLarge),
+ ],
+ ),
+ const SizedBox(height: 12),
+ _buildAssetRow('平台托管钱包', '0x1234...abcd', '5 张券'),
+ if (_walletConnected) _buildAssetRow('外部钱包 (MetaMask)', '0x7a3b...c4f2', '0 张券'),
+ const SizedBox(height: 12),
+ if (_walletConnected)
+ SizedBox(
+ width: double.infinity,
+ child: OutlinedButton(
+ onPressed: () {},
+ child: const Text('提取至外部钱包'),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildAssetRow(String label, String address, String count) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(label, style: AppTypography.labelMedium),
+ Text(address, style: AppTypography.caption.copyWith(fontFamily: 'monospace')),
+ ],
+ ),
+ ),
+ Text(count, style: AppTypography.labelMedium.copyWith(color: AppColors.primary)),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTrackCard() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.swap_horiz_rounded, color: AppColors.primary, size: 20),
+ const SizedBox(width: 8),
+ Text('交易轨道', style: AppTypography.labelLarge),
+ ],
+ ),
+ const SizedBox(height: 12),
+ _buildTrackOption('Utility Track', '券有效期≤12个月,无需证券牌照', AppColors.success, true),
+ const SizedBox(height: 8),
+ _buildTrackOption('Securities Track', '长期投资型券产品(即将推出)', AppColors.warning, false),
+ const SizedBox(height: 8),
+ Text(
+ '当前MVP版本仅支持Utility Track',
+ style: AppTypography.caption.copyWith(color: AppColors.textTertiary),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTrackOption(String name, String desc, Color color, bool active) {
+ return Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: active ? color.withValues(alpha: 0.05) : AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusSm,
+ border: Border.all(color: active ? color : AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 10,
+ height: 10,
+ decoration: BoxDecoration(
+ color: active ? color : AppColors.textTertiary,
+ shape: BoxShape.circle,
+ ),
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(name, style: AppTypography.labelMedium),
+ Text(desc, style: AppTypography.caption),
+ ],
+ ),
+ ),
+ if (active) Icon(Icons.check_circle_rounded, color: color, size: 20),
+ if (!active) Text('敬请期待', style: AppTypography.caption.copyWith(color: AppColors.textTertiary)),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildProModeInfo() {
+ return Container(
+ margin: const EdgeInsets.only(top: 20),
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: [
+ const Icon(Icons.info_outline_rounded, color: AppColors.textTertiary, size: 40),
+ const SizedBox(height: 12),
+ Text('什么是高级模式?', style: AppTypography.h3),
+ const SizedBox(height: 8),
+ Text(
+ '高级模式面向有区块链经验的用户,开启后可以:\n'
+ '• 连接外部钱包(MetaMask等)\n'
+ '• 查看链上地址和交易Hash\n'
+ '• 将资产提取至自有钱包\n'
+ '• 查看底层链上数据\n\n'
+ '需要完成 KYC L2 认证后方可开启。',
+ style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary, height: 1.6),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile/lib/features/profile/presentation/pages/profile_page.dart
new file mode 100644
index 0000000..69bdb91
--- /dev/null
+++ b/frontend/mobile/lib/features/profile/presentation/pages/profile_page.dart
@@ -0,0 +1,220 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/kyc_badge.dart';
+
+/// A7. 个人中心
+///
+/// 头像、昵称、KYC等级标识、信用积分
+/// KYC认证、支付管理、设置、Pro模式
+class ProfilePage extends StatelessWidget {
+ const ProfilePage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: CustomScrollView(
+ slivers: [
+ // Profile Header
+ SliverToBoxAdapter(child: _buildProfileHeader()),
+
+ // Quick Stats
+ SliverToBoxAdapter(child: _buildQuickStats()),
+
+ // Menu Sections
+ SliverToBoxAdapter(child: _buildMenuSection('账户', [
+ _MenuItem(Icons.verified_user_outlined, 'KYC 认证', '已完成 L1 认证', true),
+ _MenuItem(Icons.credit_card_rounded, '支付管理', '已绑定 2 张卡', true),
+ _MenuItem(Icons.account_balance_wallet_outlined, '我的余额', '\$1,234.56', true),
+ ])),
+
+ SliverToBoxAdapter(child: _buildMenuSection('交易', [
+ _MenuItem(Icons.receipt_long_rounded, '交易记录', '', true),
+ _MenuItem(Icons.storefront_rounded, '我的挂单', '2笔出售中', true),
+ _MenuItem(Icons.favorite_border_rounded, '我的收藏', '', true),
+ ])),
+
+ SliverToBoxAdapter(child: _buildMenuSection('设置', [
+ _MenuItem(Icons.notifications_outlined, '通知设置', '', true),
+ _MenuItem(Icons.language_rounded, '语言', '简体中文', true),
+ _MenuItem(Icons.shield_outlined, '安全设置', '', true),
+ _MenuItem(Icons.tune_rounded, '高级设置', 'Pro模式', true),
+ _MenuItem(Icons.info_outline_rounded, '关于 Genex', 'v1.0.0', true),
+ ])),
+
+ // Logout
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(20, 8, 20, 40),
+ child: TextButton(
+ onPressed: () {},
+ child: Text('退出登录', style: AppTypography.labelMedium.copyWith(
+ color: AppColors.error,
+ )),
+ ),
+ ),
+ ),
+
+ const SliverPadding(padding: EdgeInsets.only(bottom: 80)),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildProfileHeader() {
+ return Container(
+ padding: const EdgeInsets.fromLTRB(20, 60, 20, 24),
+ decoration: const BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ ),
+ child: Row(
+ children: [
+ // Avatar
+ Container(
+ width: 64,
+ height: 64,
+ decoration: BoxDecoration(
+ color: Colors.white24,
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.white38, width: 2),
+ ),
+ child: const Icon(Icons.person_rounded, color: Colors.white, size: 32),
+ ),
+ const SizedBox(width: 16),
+
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text('用户昵称', style: AppTypography.h2.copyWith(color: Colors.white)),
+ const SizedBox(width: 8),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ color: Colors.white24,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(Icons.shield_rounded, size: 10, color: Colors.white),
+ const SizedBox(width: 2),
+ Text('L1', style: AppTypography.caption.copyWith(
+ color: Colors.white,
+ fontWeight: FontWeight.w600,
+ )),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 4),
+ Text('信用积分: 750', style: AppTypography.bodySmall.copyWith(
+ color: Colors.white70,
+ )),
+ ],
+ ),
+ ),
+
+ // Settings icon
+ IconButton(
+ icon: const Icon(Icons.settings_outlined, color: Colors.white),
+ onPressed: () {},
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildQuickStats() {
+ final stats = [
+ ('持券', '12'),
+ ('交易', '28'),
+ ('节省', '\$156'),
+ ('信用', '750'),
+ ];
+
+ return Container(
+ margin: const EdgeInsets.fromLTRB(20, 16, 20, 8),
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ boxShadow: AppSpacing.shadowSm,
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: stats.map((stat) {
+ return Column(
+ children: [
+ Text(stat.$2, style: AppTypography.h2.copyWith(color: AppColors.primary)),
+ const SizedBox(height: 4),
+ Text(stat.$1, style: AppTypography.caption),
+ ],
+ );
+ }).toList(),
+ ),
+ );
+ }
+
+ Widget _buildMenuSection(String title, List<_MenuItem> items) {
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title, style: AppTypography.labelSmall.copyWith(color: AppColors.textTertiary)),
+ const SizedBox(height: 8),
+ Container(
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: items.asMap().entries.map((entry) {
+ final item = entry.value;
+ final isLast = entry.key == items.length - 1;
+ return Column(
+ children: [
+ ListTile(
+ leading: Icon(item.icon, color: AppColors.textPrimary, size: 22),
+ title: Text(item.title, style: AppTypography.bodyMedium),
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (item.subtitle.isNotEmpty)
+ Text(item.subtitle, style: AppTypography.caption),
+ if (item.hasArrow) ...[
+ const SizedBox(width: 4),
+ const Icon(Icons.chevron_right_rounded,
+ color: AppColors.textTertiary, size: 20),
+ ],
+ ],
+ ),
+ onTap: () {},
+ ),
+ if (!isLast)
+ const Divider(indent: 56, height: 1),
+ ],
+ );
+ }).toList(),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _MenuItem {
+ final IconData icon;
+ final String title;
+ final String subtitle;
+ final bool hasArrow;
+
+ const _MenuItem(this.icon, this.title, this.subtitle, this.hasArrow);
+}
diff --git a/frontend/mobile/lib/features/profile/presentation/pages/settings_page.dart b/frontend/mobile/lib/features/profile/presentation/pages/settings_page.dart
new file mode 100644
index 0000000..490c4d2
--- /dev/null
+++ b/frontend/mobile/lib/features/profile/presentation/pages/settings_page.dart
@@ -0,0 +1,107 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+
+/// 设置页面
+///
+/// 账号安全、通知、支付管理、语言、关于
+class SettingsPage extends StatelessWidget {
+ const SettingsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('设置')),
+ body: ListView(
+ children: [
+ // Account & Security
+ _buildSection('账号与安全', [
+ _buildTile('手机号', subtitle: '138****8888', icon: Icons.phone_rounded),
+ _buildTile('邮箱', subtitle: 'u***@email.com', icon: Icons.email_rounded),
+ _buildTile('修改密码', icon: Icons.lock_rounded),
+ _buildTile('身份认证', subtitle: 'L1 基础认证', icon: Icons.verified_user_rounded, onTap: () {}),
+ ]),
+
+ // Payment
+ _buildSection('支付管理', [
+ _buildTile('支付方式', subtitle: 'Visa •••• 4242', icon: Icons.credit_card_rounded),
+ _buildTile('银行账户', subtitle: 'BoA •••• 6789', icon: Icons.account_balance_rounded),
+ _buildTile('支付密码', icon: Icons.password_rounded),
+ ]),
+
+ // Notifications
+ _buildSection('通知设置', [
+ _buildSwitchTile('交易通知', true),
+ _buildSwitchTile('到期提醒', true),
+ _buildSwitchTile('行情变动', false),
+ _buildSwitchTile('营销推送', false),
+ ]),
+
+ // General
+ _buildSection('通用', [
+ _buildTile('语言', subtitle: '简体中文', icon: Icons.language_rounded),
+ _buildTile('货币', subtitle: 'USD', icon: Icons.attach_money_rounded),
+ _buildTile('清除缓存', icon: Icons.cleaning_services_rounded),
+ ]),
+
+ // About
+ _buildSection('关于', [
+ _buildTile('版本', subtitle: 'v1.0.0', icon: Icons.info_outline_rounded),
+ _buildTile('用户协议', icon: Icons.description_rounded),
+ _buildTile('隐私政策', icon: Icons.privacy_tip_rounded),
+ _buildTile('帮助中心', icon: Icons.help_outline_rounded),
+ ]),
+
+ // Logout
+ Padding(
+ padding: const EdgeInsets.all(20),
+ child: OutlinedButton(
+ onPressed: () {},
+ style: OutlinedButton.styleFrom(
+ foregroundColor: AppColors.error,
+ side: const BorderSide(color: AppColors.error),
+ minimumSize: const Size(double.infinity, 48),
+ ),
+ child: const Text('退出登录'),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSection(String title, List children) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
+ child: Text(title, style: AppTypography.labelSmall),
+ ),
+ Container(
+ color: AppColors.surface,
+ child: Column(children: children),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildTile(String title, {String? subtitle, IconData? icon, VoidCallback? onTap}) {
+ return ListTile(
+ leading: icon != null ? Icon(icon, size: 22, color: AppColors.textSecondary) : null,
+ title: Text(title, style: AppTypography.bodyMedium),
+ subtitle: subtitle != null ? Text(subtitle, style: AppTypography.caption) : null,
+ trailing: const Icon(Icons.chevron_right_rounded, size: 20, color: AppColors.textTertiary),
+ onTap: onTap ?? () {},
+ );
+ }
+
+ Widget _buildSwitchTile(String title, bool value) {
+ return SwitchListTile(
+ title: Text(title, style: AppTypography.bodyMedium),
+ value: value,
+ onChanged: (_) {},
+ activeColor: AppColors.primary,
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/trading/presentation/pages/sell_order_page.dart b/frontend/mobile/lib/features/trading/presentation/pages/sell_order_page.dart
new file mode 100644
index 0000000..e43f91c
--- /dev/null
+++ b/frontend/mobile/lib/features/trading/presentation/pages/sell_order_page.dart
@@ -0,0 +1,189 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// A10. 挂单出售页面
+///
+/// 消费者将持有的券挂到二级市场出售
+/// 自定义定价 + AI推荐价格 + 手续费预览
+class SellOrderPage extends StatefulWidget {
+ const SellOrderPage({super.key});
+
+ @override
+ State createState() => _SellOrderPageState();
+}
+
+class _SellOrderPageState extends State {
+ final _priceController = TextEditingController(text: '22.50');
+ double _faceValue = 25.0;
+
+ double get _price => double.tryParse(_priceController.text) ?? 0;
+ double get _discount => _faceValue > 0 ? _price / _faceValue * 100 : 0;
+ double get _fee => _price * 0.015; // 1.5% 手续费
+ double get _receive => _price - _fee;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('挂单出售')),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Coupon Info
+ Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 56,
+ height: 56,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: const Icon(Icons.confirmation_number_rounded, color: AppColors.primary, size: 28),
+ ),
+ const SizedBox(width: 14),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('星巴克 \$25 礼品卡', style: AppTypography.labelLarge),
+ const SizedBox(height: 4),
+ Text('面值 \$$_faceValue · 信用 AAA', style: AppTypography.bodySmall),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Price Input
+ Text('设定售价', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ TextField(
+ controller: _priceController,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
+ decoration: const InputDecoration(
+ prefixText: '\$ ',
+ labelText: '售价',
+ suffixText: 'USD',
+ ),
+ style: AppTypography.priceLarge,
+ onChanged: (_) => setState(() {}),
+ ),
+ const SizedBox(height: 8),
+
+ // AI Suggestion
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.auto_awesome_rounded, color: AppColors.primary, size: 16),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ 'AI建议售价:\$22.50(9折),此价格成交概率最高',
+ style: AppTypography.caption.copyWith(color: AppColors.primary),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Fee Breakdown
+ Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: [
+ _buildRow('售价', '\$${_price.toStringAsFixed(2)}'),
+ _buildRow('折扣率', '${_discount.toStringAsFixed(1)}%'),
+ _buildRow('平台手续费 (1.5%)', '-\$${_fee.toStringAsFixed(2)}'),
+ const Divider(height: 24),
+ _buildRow('预计到账', '\$${_receive.toStringAsFixed(2)}', isBold: true),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Market Info
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: AppColors.infoLight,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.info_outline_rounded, color: AppColors.info, size: 16),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ '当前市场均价 \$22.80 · 最近24小时成交 42 笔',
+ style: AppTypography.caption.copyWith(color: AppColors.info),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ bottomNavigationBar: Container(
+ padding: const EdgeInsets.all(20),
+ child: SizedBox(
+ height: AppSpacing.buttonHeight,
+ child: ElevatedButton(
+ onPressed: () => _confirmSell(context),
+ child: const Text('确认挂单'),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildRow(String label, String value, {bool isBold = false}) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(label, style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)),
+ Text(value, style: isBold ? AppTypography.priceSmall : AppTypography.labelMedium),
+ ],
+ ),
+ );
+ }
+
+ void _confirmSell(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (ctx) => AlertDialog(
+ title: const Text('挂单成功'),
+ content: const Text('您的券已挂到市场,当有买家下单时将自动成交。'),
+ actions: [
+ TextButton(onPressed: () { Navigator.pop(ctx); Navigator.pop(context); }, child: const Text('确定')),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/trading/presentation/pages/trading_page.dart b/frontend/mobile/lib/features/trading/presentation/pages/trading_page.dart
new file mode 100644
index 0000000..0d3605e
--- /dev/null
+++ b/frontend/mobile/lib/features/trading/presentation/pages/trading_page.dart
@@ -0,0 +1,203 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/status_tag.dart';
+import '../../../../shared/widgets/empty_state.dart';
+
+/// A5. 交易模块(二级市场)
+///
+/// 我的挂单、我的交易记录
+class TradingPage extends StatefulWidget {
+ const TradingPage({super.key});
+
+ @override
+ State createState() => _TradingPageState();
+}
+
+class _TradingPageState extends State
+ with SingleTickerProviderStateMixin {
+ late TabController _tabController;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = TabController(length: 2, vsync: this);
+ }
+
+ @override
+ void dispose() {
+ _tabController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('我的交易'),
+ bottom: TabBar(
+ controller: _tabController,
+ tabs: const [
+ Tab(text: '我的挂单'),
+ Tab(text: '交易记录'),
+ ],
+ ),
+ ),
+ body: TabBarView(
+ controller: _tabController,
+ children: [
+ _buildMyListings(),
+ _buildTransactionHistory(),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildMyListings() {
+ return ListView.separated(
+ padding: const EdgeInsets.fromLTRB(20, 16, 20, 100),
+ itemCount: 3,
+ separatorBuilder: (_, __) => const SizedBox(height: 12),
+ itemBuilder: (context, index) {
+ final statuses = [
+ StatusTags.onSale(),
+ StatusTags.completed(),
+ StatusTags.cancelled(),
+ ];
+ return Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Icon(Icons.confirmation_number_outlined,
+ color: AppColors.primary.withValues(alpha: 0.4), size: 22),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(['星巴克 \$25', 'Amazon \$50', 'Nike \$80'][index],
+ style: AppTypography.labelMedium),
+ const SizedBox(height: 4),
+ Row(
+ children: [
+ Text('挂单价 ', style: AppTypography.caption),
+ Text('\$${[21.25, 42.50, 68.00][index]}',
+ style: AppTypography.priceSmall.copyWith(fontSize: 14)),
+ ],
+ ),
+ ],
+ ),
+ ),
+ statuses[index],
+ ],
+ ),
+ const SizedBox(height: 12),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('挂单时间: 2026/02/${9 - index}',
+ style: AppTypography.caption),
+ if (index == 0)
+ GestureDetector(
+ onTap: () {},
+ child: Text('撤单', style: AppTypography.labelSmall.copyWith(
+ color: AppColors.error,
+ )),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ Widget _buildTransactionHistory() {
+ return ListView.separated(
+ padding: const EdgeInsets.fromLTRB(20, 16, 20, 100),
+ itemCount: 6,
+ separatorBuilder: (_, __) => const SizedBox(height: 8),
+ itemBuilder: (context, index) {
+ final isBuy = index % 2 == 0;
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 36,
+ height: 36,
+ decoration: BoxDecoration(
+ color: isBuy ? AppColors.successLight : AppColors.errorLight,
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ isBuy ? Icons.arrow_downward_rounded : Icons.arrow_upward_rounded,
+ size: 18,
+ color: isBuy ? AppColors.success : AppColors.error,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ isBuy ? '买入' : '卖出',
+ style: AppTypography.labelMedium,
+ ),
+ Text(
+ '品牌 ${index + 1} 礼品卡',
+ style: AppTypography.caption,
+ ),
+ ],
+ ),
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text(
+ '${isBuy ? "-" : "+"}\$${(index + 1) * 15}.00',
+ style: AppTypography.labelMedium.copyWith(
+ color: isBuy ? AppColors.textPrimary : AppColors.success,
+ ),
+ ),
+ Text('02/${10 - index} 14:${30 + index}',
+ style: AppTypography.caption),
+ ],
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/trading/presentation/pages/transfer_page.dart b/frontend/mobile/lib/features/trading/presentation/pages/transfer_page.dart
new file mode 100644
index 0000000..be60098
--- /dev/null
+++ b/frontend/mobile/lib/features/trading/presentation/pages/transfer_page.dart
@@ -0,0 +1,211 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// A9. P2P转赠页面
+///
+/// 选择好友 → 确认转赠 → 转赠成功
+/// 零区块链术语:使用"转赠"而非"转移NFT"
+class TransferPage extends StatefulWidget {
+ const TransferPage({super.key});
+
+ @override
+ State createState() => _TransferPageState();
+}
+
+class _TransferPageState extends State {
+ final _searchController = TextEditingController();
+ String? _selectedFriend;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('转赠给好友')),
+ body: Column(
+ children: [
+ // Coupon Info
+ Container(
+ margin: const EdgeInsets.all(20),
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Icon(Icons.card_giftcard_rounded, color: AppColors.primary),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('星巴克 \$25 礼品卡', style: AppTypography.labelMedium),
+ Text('面值 \$25.00', style: AppTypography.bodySmall),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // Search Friend
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: TextField(
+ controller: _searchController,
+ decoration: const InputDecoration(
+ hintText: '搜索好友(手机号/用户名)',
+ prefixIcon: Icon(Icons.search_rounded),
+ ),
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Friends List
+ Expanded(
+ child: ListView(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ children: [
+ _buildFriendTile('Alice', 'alice@example.com', 'A'),
+ _buildFriendTile('Bob', 'bob@example.com', 'B'),
+ _buildFriendTile('Charlie', 'charlie@example.com', 'C'),
+ _buildFriendTile('Diana', 'diana@example.com', 'D'),
+ ],
+ ),
+ ),
+
+ // Transfer Button
+ Container(
+ padding: const EdgeInsets.all(20),
+ child: SizedBox(
+ width: double.infinity,
+ height: AppSpacing.buttonHeight,
+ child: ElevatedButton(
+ onPressed: _selectedFriend != null ? () => _showConfirm(context) : null,
+ child: Text(_selectedFriend != null ? '转赠给 $_selectedFriend' : '请选择好友'),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildFriendTile(String name, String email, String avatar) {
+ final isSelected = _selectedFriend == name;
+ return GestureDetector(
+ onTap: () => setState(() => _selectedFriend = name),
+ child: Container(
+ margin: const EdgeInsets.only(bottom: 8),
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(
+ color: isSelected ? AppColors.primary : AppColors.borderLight,
+ width: isSelected ? 1.5 : 1,
+ ),
+ ),
+ child: Row(
+ children: [
+ CircleAvatar(
+ radius: 20,
+ backgroundColor: AppColors.primaryContainer,
+ child: Text(avatar, style: const TextStyle(color: AppColors.primary, fontWeight: FontWeight.w600)),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(name, style: AppTypography.labelMedium),
+ Text(email, style: AppTypography.caption),
+ ],
+ ),
+ ),
+ if (isSelected) const Icon(Icons.check_circle_rounded, color: AppColors.primary, size: 22),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showConfirm(BuildContext context) {
+ showModalBottomSheet(
+ context: context,
+ builder: (ctx) => Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(Icons.card_giftcard_rounded, color: AppColors.primary, size: 48),
+ const SizedBox(height: 16),
+ Text('确认转赠', style: AppTypography.h2),
+ const SizedBox(height: 8),
+ Text('将 星巴克 \$25 礼品卡 转赠给 $_selectedFriend?', style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)),
+ const SizedBox(height: 8),
+ Text('转赠后您将不再持有此券', style: AppTypography.caption.copyWith(color: AppColors.warning)),
+ const SizedBox(height: 24),
+ Row(
+ children: [
+ Expanded(
+ child: OutlinedButton(
+ onPressed: () => Navigator.pop(ctx),
+ child: const Text('取消'),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: ElevatedButton(
+ onPressed: () {
+ Navigator.pop(ctx);
+ _showSuccess(context);
+ },
+ child: const Text('确认转赠'),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showSuccess(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (ctx) => AlertDialog(
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(Icons.check_circle_rounded, color: AppColors.success, size: 56),
+ const SizedBox(height: 16),
+ Text('转赠成功', style: AppTypography.h2),
+ const SizedBox(height: 8),
+ Text('$_selectedFriend 已收到您的券', style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.pop(ctx);
+ Navigator.pop(context);
+ },
+ child: const Text('确定'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/wallet/presentation/pages/deposit_page.dart b/frontend/mobile/lib/features/wallet/presentation/pages/deposit_page.dart
new file mode 100644
index 0000000..adc19ed
--- /dev/null
+++ b/frontend/mobile/lib/features/wallet/presentation/pages/deposit_page.dart
@@ -0,0 +1,141 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// 充值页面
+///
+/// 法币充值到平台账户余额
+/// 支付方式:银行卡、Apple Pay、Google Pay
+class DepositPage extends StatefulWidget {
+ const DepositPage({super.key});
+
+ @override
+ State createState() => _DepositPageState();
+}
+
+class _DepositPageState extends State {
+ final _amountController = TextEditingController();
+ final _presets = [50, 100, 200, 500];
+ int? _selectedPreset;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('充值')),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Current Balance
+ Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusLg,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('当前余额', style: AppTypography.bodySmall.copyWith(color: Colors.white70)),
+ const SizedBox(height: 4),
+ Text('\$128.50', style: AppTypography.displayLarge.copyWith(color: Colors.white)),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ Text('充值金额', style: AppTypography.h3),
+ const SizedBox(height: 12),
+
+ // Preset Amounts
+ Row(
+ children: _presets.map((amount) {
+ final isSelected = _selectedPreset == amount;
+ return Expanded(
+ child: GestureDetector(
+ onTap: () {
+ setState(() {
+ _selectedPreset = amount;
+ _amountController.text = amount.toString();
+ });
+ },
+ child: Container(
+ margin: const EdgeInsets.only(right: 8),
+ padding: const EdgeInsets.symmetric(vertical: 14),
+ decoration: BoxDecoration(
+ color: isSelected ? AppColors.primaryContainer : AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(
+ color: isSelected ? AppColors.primary : AppColors.border,
+ ),
+ ),
+ child: Center(
+ child: Text(
+ '\$$amount',
+ style: AppTypography.labelMedium.copyWith(
+ color: isSelected ? AppColors.primary : AppColors.textPrimary,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ const SizedBox(height: 16),
+
+ // Custom Amount
+ TextField(
+ controller: _amountController,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
+ decoration: const InputDecoration(
+ labelText: '自定义金额',
+ prefixText: '\$ ',
+ ),
+ onChanged: (_) => setState(() => _selectedPreset = null),
+ ),
+ const SizedBox(height: 24),
+
+ // Payment Method
+ Text('支付方式', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ _buildPaymentOption('Visa •••• 4242', Icons.credit_card_rounded, true),
+ _buildPaymentOption('Apple Pay', Icons.apple_rounded, false),
+ ],
+ ),
+ ),
+ bottomNavigationBar: Container(
+ padding: const EdgeInsets.all(20),
+ child: SizedBox(
+ height: AppSpacing.buttonHeight,
+ child: ElevatedButton(
+ onPressed: _amountController.text.isNotEmpty ? () {} : null,
+ child: Text('充值 \$${_amountController.text.isNotEmpty ? _amountController.text : '0'}'),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildPaymentOption(String name, IconData icon, bool selected) {
+ return Container(
+ margin: const EdgeInsets.only(bottom: 8),
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: selected ? AppColors.primary : AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ Icon(icon, color: AppColors.textSecondary),
+ const SizedBox(width: 12),
+ Expanded(child: Text(name, style: AppTypography.labelMedium)),
+ if (selected) const Icon(Icons.check_circle_rounded, color: AppColors.primary, size: 20),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/wallet/presentation/pages/transaction_records_page.dart b/frontend/mobile/lib/features/wallet/presentation/pages/transaction_records_page.dart
new file mode 100644
index 0000000..6df4cfc
--- /dev/null
+++ b/frontend/mobile/lib/features/wallet/presentation/pages/transaction_records_page.dart
@@ -0,0 +1,132 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// 交易记录页面
+///
+/// 所有交易明细:购买、出售、转赠、充值、提现
+/// 按时间/类型筛选
+class TransactionRecordsPage extends StatelessWidget {
+ const TransactionRecordsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return DefaultTabController(
+ length: 4,
+ child: Scaffold(
+ appBar: AppBar(
+ title: const Text('交易记录'),
+ bottom: const TabBar(
+ isScrollable: true,
+ tabs: [
+ Tab(text: '全部'),
+ Tab(text: '购买'),
+ Tab(text: '出售'),
+ Tab(text: '转赠'),
+ ],
+ ),
+ ),
+ body: TabBarView(
+ children: [
+ _buildList(_allRecords),
+ _buildList(_allRecords.where((r) => r.type == '购买').toList()),
+ _buildList(_allRecords.where((r) => r.type == '出售').toList()),
+ _buildList(_allRecords.where((r) => r.type == '转赠').toList()),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildList(List<_TxRecord> records) {
+ if (records.isEmpty) {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.receipt_long_rounded, size: 48, color: AppColors.textTertiary),
+ const SizedBox(height: 12),
+ Text('暂无记录', style: AppTypography.bodyMedium.copyWith(color: AppColors.textTertiary)),
+ ],
+ ),
+ );
+ }
+
+ return ListView.separated(
+ padding: const EdgeInsets.all(20),
+ itemCount: records.length,
+ separatorBuilder: (_, __) => const Divider(height: 1),
+ itemBuilder: (context, index) {
+ final r = records[index];
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ child: Row(
+ children: [
+ Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: r.color.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Icon(r.icon, color: r.color, size: 20),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(r.title, style: AppTypography.labelMedium),
+ const SizedBox(height: 2),
+ Text(r.subtitle, style: AppTypography.caption),
+ ],
+ ),
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text(
+ r.amount,
+ style: AppTypography.labelMedium.copyWith(
+ color: r.amount.startsWith('+') ? AppColors.success : AppColors.textPrimary,
+ ),
+ ),
+ Text(r.time, style: AppTypography.caption),
+ ],
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+}
+
+class _TxRecord {
+ final String type;
+ final String title;
+ final String subtitle;
+ final String amount;
+ final String time;
+ final IconData icon;
+ final Color color;
+
+ const _TxRecord({
+ required this.type,
+ required this.title,
+ required this.subtitle,
+ required this.amount,
+ required this.time,
+ required this.icon,
+ required this.color,
+ });
+}
+
+const _allRecords = [
+ _TxRecord(type: '购买', title: '购买 星巴克 \$25 礼品卡', subtitle: '订单号 GNX20260210001', amount: '-\$21.25', time: '今天 14:32', icon: Icons.shopping_cart_rounded, color: AppColors.primary),
+ _TxRecord(type: '出售', title: '出售 Amazon \$100 购物券', subtitle: '订单号 GNX20260210002', amount: '+\$92.00', time: '今天 12:15', icon: Icons.sell_rounded, color: AppColors.success),
+ _TxRecord(type: '转赠', title: '转赠给 Alice', subtitle: 'Nike \$80 运动券', amount: '\$0', time: '昨天 18:45', icon: Icons.card_giftcard_rounded, color: AppColors.info),
+ _TxRecord(type: '购买', title: '购买 Target \$30 折扣券', subtitle: '订单号 GNX20260209001', amount: '-\$24.00', time: '昨天 10:20', icon: Icons.shopping_cart_rounded, color: AppColors.primary),
+ _TxRecord(type: '出售', title: '出售 Walmart \$50 生活券', subtitle: '订单号 GNX20260208003', amount: '+\$46.50', time: '2天前', icon: Icons.sell_rounded, color: AppColors.success),
+];
diff --git a/frontend/mobile/lib/features/wallet/presentation/pages/wallet_page.dart b/frontend/mobile/lib/features/wallet/presentation/pages/wallet_page.dart
new file mode 100644
index 0000000..e85e476
--- /dev/null
+++ b/frontend/mobile/lib/features/wallet/presentation/pages/wallet_page.dart
@@ -0,0 +1,187 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+import '../../../../shared/widgets/genex_button.dart';
+
+/// A6. 账户模块 - 我的余额
+///
+/// 总余额(美元显示)、可提现金额、冻结金额、充值/提现
+/// 交易记录时间线
+class WalletPage extends StatelessWidget {
+ const WalletPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('我的余额'),
+ ),
+ body: SingleChildScrollView(
+ child: Column(
+ children: [
+ // Balance Card
+ _buildBalanceCard(),
+
+ // Quick Actions
+ Padding(
+ padding: AppSpacing.pagePadding,
+ child: Row(
+ children: [
+ Expanded(
+ child: GenexButton(
+ label: '充值',
+ icon: Icons.add_rounded,
+ variant: GenexButtonVariant.primary,
+ onPressed: () {},
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: GenexButton(
+ label: '提现',
+ icon: Icons.account_balance_rounded,
+ variant: GenexButtonVariant.outline,
+ onPressed: () {},
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Transaction History
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('交易记录', style: AppTypography.h3),
+ GestureDetector(
+ onTap: () {},
+ child: Row(
+ children: [
+ Text('筛选', style: AppTypography.labelSmall.copyWith(
+ color: AppColors.textTertiary,
+ )),
+ const Icon(Icons.filter_list_rounded, size: 16,
+ color: AppColors.textTertiary),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 12),
+
+ // Transaction List
+ _buildTransactionList(),
+
+ const SizedBox(height: 80),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildBalanceCard() {
+ return Container(
+ margin: const EdgeInsets.fromLTRB(20, 16, 20, 16),
+ padding: const EdgeInsets.all(24),
+ decoration: BoxDecoration(
+ gradient: AppColors.cardGradient,
+ borderRadius: AppSpacing.borderRadiusLg,
+ boxShadow: AppSpacing.shadowPrimary,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('总余额', style: AppTypography.bodySmall.copyWith(
+ color: Colors.white70,
+ )),
+ const SizedBox(height: 8),
+ Text(
+ '\$1,234.56',
+ style: AppTypography.displayLarge.copyWith(
+ color: Colors.white,
+ fontSize: 36,
+ ),
+ ),
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ _balanceItem('可提现', '\$1,034.56'),
+ const SizedBox(width: 32),
+ _balanceItem('冻结中', '\$200.00'),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _balanceItem(String label, String value) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(label, style: AppTypography.caption.copyWith(color: Colors.white54)),
+ const SizedBox(height: 4),
+ Text(value, style: AppTypography.labelMedium.copyWith(color: Colors.white)),
+ ],
+ );
+ }
+
+ Widget _buildTransactionList() {
+ final transactions = [
+ ('买入 星巴克 \$25 礼品卡', '-\$21.25', Icons.shopping_cart_rounded, AppColors.textPrimary, '今天 14:32'),
+ ('卖出 Amazon \$50 购物券', '+\$42.50', Icons.sell_rounded, AppColors.success, '今天 10:15'),
+ ('充值', '+\$500.00', Icons.add_circle_outline_rounded, AppColors.info, '昨天 09:20'),
+ ('转赠 Target 券', '-\$30.00', Icons.card_giftcard_rounded, AppColors.textPrimary, '02/07 16:45'),
+ ('核销 Nike 运动券', '使用', Icons.check_circle_outline_rounded, AppColors.success, '02/06 12:00'),
+ ('提现', '-\$200.00', Icons.account_balance_rounded, AppColors.textPrimary, '02/05 08:30'),
+ ];
+
+ return ListView.separated(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ itemCount: transactions.length,
+ separatorBuilder: (_, __) => const Divider(indent: 56),
+ itemBuilder: (context, index) {
+ final (title, amount, icon, color, time) = transactions[index];
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: Row(
+ children: [
+ Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ shape: BoxShape.circle,
+ ),
+ child: Icon(icon, size: 20, color: color),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title, style: AppTypography.labelMedium),
+ Text(time, style: AppTypography.caption),
+ ],
+ ),
+ ),
+ Text(
+ amount,
+ style: AppTypography.labelMedium.copyWith(
+ color: amount.startsWith('+') ? AppColors.success : AppColors.textPrimary,
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/frontend/mobile/lib/features/wallet/presentation/pages/withdraw_page.dart b/frontend/mobile/lib/features/wallet/presentation/pages/withdraw_page.dart
new file mode 100644
index 0000000..37b9885
--- /dev/null
+++ b/frontend/mobile/lib/features/wallet/presentation/pages/withdraw_page.dart
@@ -0,0 +1,144 @@
+import 'package:flutter/material.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../../app/theme/app_typography.dart';
+import '../../../../app/theme/app_spacing.dart';
+
+/// 提现页面
+///
+/// 将平台余额提现到银行账户
+/// 展示可提现余额、手续费、到账时间
+class WithdrawPage extends StatefulWidget {
+ const WithdrawPage({super.key});
+
+ @override
+ State createState() => _WithdrawPageState();
+}
+
+class _WithdrawPageState extends State {
+ final _amountController = TextEditingController();
+ double _balance = 128.50;
+
+ double get _amount => double.tryParse(_amountController.text) ?? 0;
+ double get _fee => _amount * 0.005; // 0.5%
+ double get _receive => _amount - _fee;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('提现')),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Balance
+ Text('可提现余额', style: AppTypography.bodySmall),
+ const SizedBox(height: 4),
+ Text('\$${_balance.toStringAsFixed(2)}', style: AppTypography.displayMedium),
+ const SizedBox(height: 24),
+
+ // Amount Input
+ Text('提现金额', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ TextField(
+ controller: _amountController,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
+ decoration: InputDecoration(
+ prefixText: '\$ ',
+ suffixIcon: TextButton(
+ onPressed: () {
+ _amountController.text = _balance.toStringAsFixed(2);
+ setState(() {});
+ },
+ child: const Text('全部'),
+ ),
+ ),
+ style: AppTypography.priceLarge,
+ onChanged: (_) => setState(() {}),
+ ),
+ const SizedBox(height: 24),
+
+ // Withdraw To
+ Text('提现到', style: AppTypography.h3),
+ const SizedBox(height: 12),
+ Container(
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.primary),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.account_balance_rounded, color: AppColors.primary),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Bank of America •••• 6789', style: AppTypography.labelMedium),
+ Text('储蓄账户', style: AppTypography.caption),
+ ],
+ ),
+ ),
+ const Icon(Icons.check_circle_rounded, color: AppColors.primary, size: 20),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Fee Details
+ if (_amount > 0) ...[
+ Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Column(
+ children: [
+ _buildRow('提现金额', '\$${_amount.toStringAsFixed(2)}'),
+ _buildRow('手续费 (0.5%)', '-\$${_fee.toStringAsFixed(2)}'),
+ const Divider(height: 16),
+ _buildRow('实际到账', '\$${_receive.toStringAsFixed(2)}', bold: true),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ const Icon(Icons.schedule_rounded, size: 14, color: AppColors.textTertiary),
+ const SizedBox(width: 4),
+ Text('预计 1-2 个工作日到账', style: AppTypography.caption),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ bottomNavigationBar: Container(
+ padding: const EdgeInsets.all(20),
+ child: SizedBox(
+ height: AppSpacing.buttonHeight,
+ child: ElevatedButton(
+ onPressed: _amount > 0 && _amount <= _balance ? () {} : null,
+ child: Text('确认提现 \$${_amount.toStringAsFixed(2)}'),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildRow(String label, String value, {bool bold = false}) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 4),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(label, style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)),
+ Text(value, style: bold ? AppTypography.priceSmall : AppTypography.labelMedium),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/shared/widgets/ai_confirm_dialog.dart b/frontend/mobile/lib/shared/widgets/ai_confirm_dialog.dart
new file mode 100644
index 0000000..8a6eb06
--- /dev/null
+++ b/frontend/mobile/lib/shared/widgets/ai_confirm_dialog.dart
@@ -0,0 +1,317 @@
+import 'package:flutter/material.dart';
+import '../../app/theme/app_colors.dart';
+import '../../app/theme/app_typography.dart';
+import '../../app/theme/app_spacing.dart';
+import 'genex_button.dart';
+
+/// AI操作确认弹窗组件
+///
+/// AI Agent 执行操作前的二次确认弹窗
+/// 场景:AI帮你出售、AI帮你购买、AI帮你转赠 等需要确认的代理操作
+///
+/// 展示内容:
+/// - AI建议的操作描述
+/// - 操作详情(金额/数量/对象等)
+/// - 风险提示
+/// - 确认/取消按钮
+class AiConfirmDialog extends StatelessWidget {
+ final String actionTitle;
+ final String actionDescription;
+ final List details;
+ final String? riskWarning;
+ final String confirmText;
+ final String? cancelText;
+ final VoidCallback onConfirm;
+ final VoidCallback? onCancel;
+ final AiConfirmLevel level;
+
+ const AiConfirmDialog({
+ super.key,
+ required this.actionTitle,
+ required this.actionDescription,
+ required this.details,
+ this.riskWarning,
+ this.confirmText = '确认执行',
+ this.cancelText = '取消',
+ required this.onConfirm,
+ this.onCancel,
+ this.level = AiConfirmLevel.normal,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Dialog(
+ backgroundColor: Colors.transparent,
+ insetPadding: const EdgeInsets.symmetric(horizontal: 24),
+ child: Container(
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusLg,
+ boxShadow: const [
+ BoxShadow(
+ color: Color(0x1A000000),
+ blurRadius: 24,
+ offset: Offset(0, 8),
+ ),
+ ],
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // Header with AI icon
+ _buildHeader(),
+
+ // Body
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Action Description
+ Text(
+ actionDescription,
+ style: AppTypography.bodyMedium.copyWith(
+ color: AppColors.textSecondary,
+ height: 1.5,
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Detail Items
+ Container(
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Column(
+ children: details.asMap().entries.map((entry) {
+ final isLast = entry.key == details.length - 1;
+ final detail = entry.value;
+ return Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ detail.label,
+ style: AppTypography.bodySmall.copyWith(
+ color: AppColors.textSecondary,
+ ),
+ ),
+ Text(
+ detail.value,
+ style: detail.isHighlight
+ ? AppTypography.labelMedium.copyWith(
+ color: AppColors.primary,
+ )
+ : AppTypography.labelMedium,
+ ),
+ ],
+ ),
+ if (!isLast) ...[
+ const SizedBox(height: 10),
+ Divider(color: AppColors.gray200, height: 1),
+ const SizedBox(height: 10),
+ ],
+ ],
+ );
+ }).toList(),
+ ),
+ ),
+
+ // Risk Warning
+ if (riskWarning != null) ...[
+ const SizedBox(height: 12),
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: level == AiConfirmLevel.high
+ ? AppColors.errorLight
+ : AppColors.warningLight,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(
+ level == AiConfirmLevel.high
+ ? Icons.warning_amber_rounded
+ : Icons.info_outline_rounded,
+ size: 16,
+ color: level == AiConfirmLevel.high
+ ? AppColors.error
+ : AppColors.warning,
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ riskWarning!,
+ style: AppTypography.bodySmall.copyWith(
+ color: AppColors.gray700,
+ height: 1.4,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+
+ const SizedBox(height: 20),
+
+ // Buttons
+ GenexButton(
+ label: confirmText,
+ onPressed: () {
+ Navigator.of(context).pop(true);
+ onConfirm();
+ },
+ ),
+ const SizedBox(height: 8),
+ GenexButton(
+ label: cancelText ?? '取消',
+ variant: GenexButtonVariant.text,
+ onPressed: () {
+ Navigator.of(context).pop(false);
+ onCancel?.call();
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeader() {
+ return Container(
+ padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
+ child: Row(
+ children: [
+ // AI Avatar
+ Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: const Center(
+ child: Text(
+ '✨',
+ style: TextStyle(fontSize: 20),
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('AI助手请求确认', style: AppTypography.labelMedium),
+ const SizedBox(height: 2),
+ Text(
+ actionTitle,
+ style: AppTypography.h3.copyWith(color: AppColors.primary),
+ ),
+ ],
+ ),
+ ),
+ // Level indicator
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
+ decoration: BoxDecoration(
+ color: _levelColor.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text(
+ _levelText,
+ style: TextStyle(
+ fontSize: 10,
+ fontWeight: FontWeight.w600,
+ color: _levelColor,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Color get _levelColor {
+ switch (level) {
+ case AiConfirmLevel.low:
+ return AppColors.success;
+ case AiConfirmLevel.normal:
+ return AppColors.warning;
+ case AiConfirmLevel.high:
+ return AppColors.error;
+ }
+ }
+
+ String get _levelText {
+ switch (level) {
+ case AiConfirmLevel.low:
+ return '低风险';
+ case AiConfirmLevel.normal:
+ return '需确认';
+ case AiConfirmLevel.high:
+ return '高风险';
+ }
+ }
+
+ /// 显示AI确认弹窗的便捷方法
+ static Future show(
+ BuildContext context, {
+ required String actionTitle,
+ required String actionDescription,
+ required List details,
+ String? riskWarning,
+ String confirmText = '确认执行',
+ String? cancelText,
+ AiConfirmLevel level = AiConfirmLevel.normal,
+ }) {
+ return showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (ctx) => AiConfirmDialog(
+ actionTitle: actionTitle,
+ actionDescription: actionDescription,
+ details: details,
+ riskWarning: riskWarning,
+ confirmText: confirmText,
+ cancelText: cancelText,
+ level: level,
+ onConfirm: () {},
+ ),
+ );
+ }
+}
+
+/// AI确认弹窗的详情项
+class AiConfirmDetail {
+ final String label;
+ final String value;
+ final bool isHighlight;
+
+ const AiConfirmDetail({
+ required this.label,
+ required this.value,
+ this.isHighlight = false,
+ });
+}
+
+/// AI操作风险等级
+enum AiConfirmLevel {
+ /// 低风险:查看信息、获取建议等
+ low,
+
+ /// 需确认:购买、出售、转赠等涉及资产操作
+ normal,
+
+ /// 高风险:大额操作、提现到外部钱包等
+ high,
+}
diff --git a/frontend/mobile/lib/shared/widgets/confirm_sheet.dart b/frontend/mobile/lib/shared/widgets/confirm_sheet.dart
new file mode 100644
index 0000000..c028e87
--- /dev/null
+++ b/frontend/mobile/lib/shared/widgets/confirm_sheet.dart
@@ -0,0 +1,187 @@
+import 'package:flutter/material.dart';
+import '../../app/theme/app_colors.dart';
+import '../../app/theme/app_typography.dart';
+import '../../app/theme/app_spacing.dart';
+import 'genex_button.dart';
+
+/// 底部确认Sheet组件
+///
+/// 支付确认、转赠确认等操作确认
+/// 使用场景:购买、转赠、出售、提现等需要确认的操作
+class ConfirmSheet extends StatelessWidget {
+ final String title;
+ final List items;
+ final String confirmText;
+ final String? cancelText;
+ final VoidCallback onConfirm;
+ final VoidCallback? onCancel;
+ final Widget? header;
+ final String? warning;
+
+ const ConfirmSheet({
+ super.key,
+ required this.title,
+ required this.items,
+ required this.confirmText,
+ this.cancelText,
+ required this.onConfirm,
+ this.onCancel,
+ this.header,
+ this.warning,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
+ decoration: const BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Drag Handle
+ Center(
+ child: Container(
+ margin: const EdgeInsets.only(top: 8, bottom: 16),
+ width: 36,
+ height: 4,
+ decoration: BoxDecoration(
+ color: AppColors.gray300,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ ),
+ ),
+
+ // Title
+ Text(title, style: AppTypography.h2),
+ const SizedBox(height: 20),
+
+ // Optional Header (e.g., coupon card preview)
+ if (header != null) ...[
+ header!,
+ const SizedBox(height: 16),
+ ],
+
+ // Detail Items
+ Container(
+ padding: AppSpacing.cardPadding,
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ child: Column(
+ children: items.asMap().entries.map((entry) {
+ final isLast = entry.key == items.length - 1;
+ final item = entry.value;
+ return Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(item.label, style: AppTypography.bodyMedium.copyWith(
+ color: AppColors.textSecondary,
+ )),
+ Text(
+ item.value,
+ style: item.isHighlight
+ ? AppTypography.labelMedium.copyWith(color: AppColors.primary)
+ : AppTypography.labelMedium,
+ ),
+ ],
+ ),
+ if (!isLast) ...[
+ const SizedBox(height: 12),
+ const Divider(),
+ const SizedBox(height: 12),
+ ],
+ ],
+ );
+ }).toList(),
+ ),
+ ),
+
+ // Warning
+ if (warning != null) ...[
+ const SizedBox(height: 12),
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: AppColors.warningLight,
+ borderRadius: AppSpacing.borderRadiusSm,
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.info_outline_rounded, size: 16, color: AppColors.warning),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ warning!,
+ style: AppTypography.bodySmall.copyWith(color: AppColors.gray700),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+
+ const SizedBox(height: 24),
+
+ // Buttons
+ GenexButton(
+ label: confirmText,
+ onPressed: onConfirm,
+ ),
+ if (cancelText != null) ...[
+ const SizedBox(height: 8),
+ GenexButton(
+ label: cancelText!,
+ variant: GenexButtonVariant.text,
+ onPressed: onCancel ?? () => Navigator.of(context).pop(),
+ ),
+ ],
+ ],
+ ),
+ );
+ }
+
+ static Future show(
+ BuildContext context, {
+ required String title,
+ required List items,
+ required String confirmText,
+ String? cancelText,
+ Widget? header,
+ String? warning,
+ }) {
+ return showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ backgroundColor: Colors.transparent,
+ builder: (ctx) => ConfirmSheet(
+ title: title,
+ items: items,
+ confirmText: confirmText,
+ cancelText: cancelText,
+ header: header,
+ warning: warning,
+ onConfirm: () => Navigator.of(ctx).pop(true),
+ onCancel: () => Navigator.of(ctx).pop(false),
+ ),
+ );
+ }
+}
+
+class ConfirmSheetItem {
+ final String label;
+ final String value;
+ final bool isHighlight;
+
+ const ConfirmSheetItem({
+ required this.label,
+ required this.value,
+ this.isHighlight = false,
+ });
+}
diff --git a/frontend/mobile/lib/shared/widgets/coupon_card.dart b/frontend/mobile/lib/shared/widgets/coupon_card.dart
new file mode 100644
index 0000000..ebe5447
--- /dev/null
+++ b/frontend/mobile/lib/shared/widgets/coupon_card.dart
@@ -0,0 +1,344 @@
+import 'package:flutter/material.dart';
+import '../../app/theme/app_colors.dart';
+import '../../app/theme/app_typography.dart';
+import '../../app/theme/app_spacing.dart';
+
+/// 券卡片组件 - 全端通用核心组件
+///
+/// 展示:券封面图 + 品牌 + 面值 + 折扣率 + 到期时间
+/// 使用场景:首页推荐、市场列表、我的券列表、搜索结果
+class CouponCard extends StatelessWidget {
+ final String brandName;
+ final String couponName;
+ final double faceValue;
+ final double currentPrice;
+ final String? imageUrl;
+ final String? brandLogoUrl;
+ final DateTime? expiryDate;
+ final String? creditRating;
+ final CouponStatus status;
+ final CouponCardStyle style;
+ final VoidCallback? onTap;
+
+ const CouponCard({
+ super.key,
+ required this.brandName,
+ required this.couponName,
+ required this.faceValue,
+ required this.currentPrice,
+ this.imageUrl,
+ this.brandLogoUrl,
+ this.expiryDate,
+ this.creditRating,
+ this.status = CouponStatus.active,
+ this.style = CouponCardStyle.list,
+ this.onTap,
+ });
+
+ double get discountRate => currentPrice / faceValue;
+ String get discountText => '${(discountRate * 10).toStringAsFixed(1)}折';
+
+ @override
+ Widget build(BuildContext context) {
+ return style == CouponCardStyle.grid ? _buildGridCard() : _buildListCard();
+ }
+
+ Widget _buildListCard() {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ height: AppSpacing.couponCardHeight,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ boxShadow: AppSpacing.shadowSm,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ // Left: Coupon Image with Ticket Notch
+ _buildCouponImage(width: 110, height: AppSpacing.couponCardHeight),
+
+ // Ticket Divider (锯齿分割线)
+ _buildTicketDivider(),
+
+ // Right: Info
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ // Brand + Name
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ brandName,
+ style: AppTypography.caption.copyWith(
+ color: AppColors.textTertiary,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 2),
+ Text(
+ couponName,
+ style: AppTypography.labelMedium,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+
+ // Price + Discount
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text(
+ '\$${currentPrice.toStringAsFixed(2)}',
+ style: AppTypography.priceSmall,
+ ),
+ const SizedBox(width: 6),
+ Text(
+ '\$${faceValue.toStringAsFixed(0)}',
+ style: AppTypography.priceOriginal,
+ ),
+ const Spacer(),
+ _buildDiscountBadge(),
+ ],
+ ),
+
+ // Expiry + Status
+ Row(
+ children: [
+ if (expiryDate != null) ...[
+ Icon(Icons.access_time_rounded, size: 12, color: _expiryColor),
+ const SizedBox(width: 3),
+ Text(
+ _expiryText,
+ style: AppTypography.caption.copyWith(color: _expiryColor),
+ ),
+ ],
+ const Spacer(),
+ if (creditRating != null) _buildCreditBadge(),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildGridCard() {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ boxShadow: AppSpacing.shadowSm,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Image
+ _buildCouponImage(width: double.infinity, height: 100),
+
+ // Info
+ Padding(
+ padding: const EdgeInsets.all(10),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ couponName,
+ style: AppTypography.labelSmall,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 4),
+ Row(
+ children: [
+ Text(
+ '\$${currentPrice.toStringAsFixed(2)}',
+ style: AppTypography.priceSmall.copyWith(fontSize: 15),
+ ),
+ const SizedBox(width: 4),
+ _buildDiscountBadge(),
+ ],
+ ),
+ const SizedBox(height: 4),
+ Text(
+ brandName,
+ style: AppTypography.caption,
+ maxLines: 1,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildCouponImage({required double width, required double height}) {
+ return ClipRRect(
+ borderRadius: BorderRadius.only(
+ topLeft: const Radius.circular(12),
+ bottomLeft: style == CouponCardStyle.list
+ ? const Radius.circular(12)
+ : Radius.zero,
+ topRight: style == CouponCardStyle.grid
+ ? const Radius.circular(12)
+ : Radius.zero,
+ ),
+ child: Container(
+ width: width,
+ height: height,
+ color: AppColors.primarySurface,
+ child: imageUrl != null
+ ? Image.network(imageUrl!, fit: BoxFit.cover)
+ : Center(
+ child: Icon(
+ Icons.confirmation_number_outlined,
+ size: 32,
+ color: AppColors.primary.withValues(alpha: 0.4),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildTicketDivider() {
+ return SizedBox(
+ width: 16,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ _buildNotch(isTop: true),
+ CustomPaint(
+ size: const Size(1, 80),
+ painter: _DashedLinePainter(color: AppColors.border),
+ ),
+ _buildNotch(isTop: false),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildNotch({required bool isTop}) {
+ return Container(
+ width: 16,
+ height: 8,
+ decoration: BoxDecoration(
+ color: AppColors.background,
+ borderRadius: BorderRadius.vertical(
+ top: isTop ? Radius.zero : const Radius.circular(8),
+ bottom: isTop ? const Radius.circular(8) : Radius.zero,
+ ),
+ ),
+ );
+ }
+
+ Widget _buildDiscountBadge() {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text(discountText, style: AppTypography.discountBadge),
+ );
+ }
+
+ Widget _buildCreditBadge() {
+ final color = _creditColor(creditRating!);
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusFull,
+ border: Border.all(color: color.withValues(alpha: 0.3), width: 0.5),
+ ),
+ child: Text(
+ creditRating!,
+ style: AppTypography.caption.copyWith(
+ color: color,
+ fontSize: 10,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ );
+ }
+
+ Color _creditColor(String rating) {
+ switch (rating) {
+ case 'AAA':
+ return AppColors.creditAAA;
+ case 'AA':
+ return AppColors.creditAA;
+ case 'A':
+ return AppColors.creditA;
+ case 'BBB':
+ return AppColors.creditBBB;
+ default:
+ return AppColors.creditBB;
+ }
+ }
+
+ String get _expiryText {
+ if (expiryDate == null) return '';
+ final days = expiryDate!.difference(DateTime.now()).inDays;
+ if (days < 0) return '已过期';
+ if (days == 0) return '今天到期';
+ if (days <= 3) return '$days天后到期';
+ if (days <= 30) return '$days天';
+ return '${expiryDate!.month}/${expiryDate!.day}到期';
+ }
+
+ Color get _expiryColor {
+ if (expiryDate == null) return AppColors.textTertiary;
+ final days = expiryDate!.difference(DateTime.now()).inDays;
+ if (days <= 3) return AppColors.error;
+ if (days <= 7) return AppColors.warning;
+ return AppColors.textTertiary;
+ }
+}
+
+/// 虚线画笔
+class _DashedLinePainter extends CustomPainter {
+ final Color color;
+ _DashedLinePainter({required this.color});
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final paint = Paint()
+ ..color = color
+ ..strokeWidth = 1;
+ const dashHeight = 4.0;
+ const gapHeight = 3.0;
+ double startY = 0;
+ while (startY < size.height) {
+ canvas.drawLine(
+ Offset(0, startY),
+ Offset(0, startY + dashHeight),
+ paint,
+ );
+ startY += dashHeight + gapHeight;
+ }
+ }
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
+}
+
+enum CouponStatus { active, pending, expired, used }
+enum CouponCardStyle { list, grid }
diff --git a/frontend/mobile/lib/shared/widgets/credit_badge.dart b/frontend/mobile/lib/shared/widgets/credit_badge.dart
new file mode 100644
index 0000000..a734b51
--- /dev/null
+++ b/frontend/mobile/lib/shared/widgets/credit_badge.dart
@@ -0,0 +1,81 @@
+import 'package:flutter/material.dart';
+import '../../app/theme/app_colors.dart';
+import '../../app/theme/app_typography.dart';
+import '../../app/theme/app_spacing.dart';
+
+/// 信用等级徽章组件
+///
+/// AAA/AA/A/BBB/BB 颜色标识
+/// 使用场景:券详情、发行方信息、发行方列表
+class CreditBadge extends StatelessWidget {
+ final String rating;
+ final CreditBadgeSize size;
+
+ const CreditBadge({
+ super.key,
+ required this.rating,
+ this.size = CreditBadgeSize.medium,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: EdgeInsets.symmetric(
+ horizontal: size == CreditBadgeSize.large ? 10 : 6,
+ vertical: size == CreditBadgeSize.large ? 4 : 2,
+ ),
+ decoration: BoxDecoration(
+ color: _color.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusFull,
+ border: Border.all(
+ color: _color.withValues(alpha: 0.3),
+ width: 1,
+ ),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.verified_rounded,
+ size: size == CreditBadgeSize.large ? 14 : 10,
+ color: _color,
+ ),
+ SizedBox(width: size == CreditBadgeSize.large ? 4 : 2),
+ Text(
+ rating,
+ style: _textStyle,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Color get _color {
+ switch (rating.toUpperCase()) {
+ case 'AAA':
+ return AppColors.creditAAA;
+ case 'AA':
+ return AppColors.creditAA;
+ case 'A':
+ return AppColors.creditA;
+ case 'BBB':
+ return AppColors.creditBBB;
+ case 'BB':
+ default:
+ return AppColors.creditBB;
+ }
+ }
+
+ TextStyle get _textStyle {
+ final baseStyle = size == CreditBadgeSize.large
+ ? AppTypography.labelSmall
+ : AppTypography.caption;
+ return baseStyle.copyWith(
+ color: _color,
+ fontWeight: FontWeight.w700,
+ fontSize: size == CreditBadgeSize.large ? 13 : 10,
+ );
+ }
+}
+
+enum CreditBadgeSize { small, medium, large }
diff --git a/frontend/mobile/lib/shared/widgets/empty_state.dart b/frontend/mobile/lib/shared/widgets/empty_state.dart
new file mode 100644
index 0000000..c4c4425
--- /dev/null
+++ b/frontend/mobile/lib/shared/widgets/empty_state.dart
@@ -0,0 +1,104 @@
+import 'package:flutter/material.dart';
+import '../../app/theme/app_colors.dart';
+import '../../app/theme/app_typography.dart';
+import '../../app/theme/app_spacing.dart';
+
+/// 空状态页组件
+///
+/// 各场景的空状态插画 + 引导操作
+/// 使用场景:列表为空、搜索无结果、网络错误
+class EmptyState extends StatelessWidget {
+ final IconData icon;
+ final String title;
+ final String? subtitle;
+ final String? actionText;
+ final VoidCallback? onAction;
+
+ const EmptyState({
+ super.key,
+ required this.icon,
+ required this.title,
+ this.subtitle,
+ this.actionText,
+ this.onAction,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 60),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 80,
+ height: 80,
+ decoration: BoxDecoration(
+ color: AppColors.primarySurface,
+ shape: BoxShape.circle,
+ ),
+ child: Icon(icon, size: 36, color: AppColors.primary.withValues(alpha: 0.5)),
+ ),
+ const SizedBox(height: AppSpacing.xxl),
+ Text(
+ title,
+ style: AppTypography.h3.copyWith(color: AppColors.textSecondary),
+ textAlign: TextAlign.center,
+ ),
+ if (subtitle != null) ...[
+ const SizedBox(height: AppSpacing.sm),
+ Text(
+ subtitle!,
+ style: AppTypography.bodySmall,
+ textAlign: TextAlign.center,
+ ),
+ ],
+ if (actionText != null && onAction != null) ...[
+ const SizedBox(height: AppSpacing.xxl),
+ ElevatedButton(
+ onPressed: onAction,
+ child: Text(actionText!),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+
+ // 快捷工厂方法
+ factory EmptyState.noCoupons({VoidCallback? onBrowse}) => EmptyState(
+ icon: Icons.confirmation_number_outlined,
+ title: '还没有券',
+ subtitle: '去市场看看有什么好券吧',
+ actionText: '去逛逛',
+ onAction: onBrowse,
+ );
+
+ factory EmptyState.noOrders() => const EmptyState(
+ icon: Icons.receipt_long_outlined,
+ title: '暂无交易记录',
+ subtitle: '完成首笔交易后这里会显示记录',
+ );
+
+ factory EmptyState.noResults() => const EmptyState(
+ icon: Icons.search_off_rounded,
+ title: '没有找到结果',
+ subtitle: '换个关键词试试',
+ );
+
+ factory EmptyState.noMessages() => const EmptyState(
+ icon: Icons.notifications_none_rounded,
+ title: '暂无消息',
+ subtitle: '交易通知和系统公告会显示在这里',
+ );
+
+ factory EmptyState.networkError({VoidCallback? onRetry}) => EmptyState(
+ icon: Icons.wifi_off_rounded,
+ title: '网络连接失败',
+ subtitle: '请检查网络设置后重试',
+ actionText: '重试',
+ onAction: onRetry,
+ );
+}
diff --git a/frontend/mobile/lib/shared/widgets/genex_button.dart b/frontend/mobile/lib/shared/widgets/genex_button.dart
new file mode 100644
index 0000000..ed46431
--- /dev/null
+++ b/frontend/mobile/lib/shared/widgets/genex_button.dart
@@ -0,0 +1,163 @@
+import 'package:flutter/material.dart';
+import '../../app/theme/app_colors.dart';
+import '../../app/theme/app_typography.dart';
+import '../../app/theme/app_spacing.dart';
+
+/// Genex 按钮组件
+///
+/// 统一的按钮样式,支持 primary/secondary/outline/text 4种变体
+class GenexButton extends StatelessWidget {
+ final String label;
+ final VoidCallback? onPressed;
+ final GenexButtonVariant variant;
+ final GenexButtonSize size;
+ final IconData? icon;
+ final bool isLoading;
+ final bool fullWidth;
+
+ const GenexButton({
+ super.key,
+ required this.label,
+ this.onPressed,
+ this.variant = GenexButtonVariant.primary,
+ this.size = GenexButtonSize.large,
+ this.icon,
+ this.isLoading = false,
+ this.fullWidth = true,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final child = Row(
+ mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (isLoading) ...[
+ SizedBox(
+ width: 18,
+ height: 18,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(_foregroundColor),
+ ),
+ ),
+ const SizedBox(width: 8),
+ ],
+ if (icon != null && !isLoading) ...[
+ Icon(icon, size: 20),
+ const SizedBox(width: 6),
+ ],
+ Text(label),
+ ],
+ );
+
+ final effectiveOnPressed = isLoading ? null : onPressed;
+
+ switch (variant) {
+ case GenexButtonVariant.primary:
+ return SizedBox(
+ width: fullWidth ? double.infinity : null,
+ height: _height,
+ child: ElevatedButton(
+ onPressed: effectiveOnPressed,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppColors.primary,
+ foregroundColor: Colors.white,
+ disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.4),
+ disabledForegroundColor: Colors.white70,
+ elevation: 0,
+ shape: RoundedRectangleBorder(
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ textStyle: _textStyle,
+ ),
+ child: child,
+ ),
+ );
+
+ case GenexButtonVariant.secondary:
+ return SizedBox(
+ width: fullWidth ? double.infinity : null,
+ height: _height,
+ child: ElevatedButton(
+ onPressed: effectiveOnPressed,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppColors.primarySurface,
+ foregroundColor: AppColors.primary,
+ elevation: 0,
+ shape: RoundedRectangleBorder(
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ textStyle: _textStyle,
+ ),
+ child: child,
+ ),
+ );
+
+ case GenexButtonVariant.outline:
+ return SizedBox(
+ width: fullWidth ? double.infinity : null,
+ height: _height,
+ child: OutlinedButton(
+ onPressed: effectiveOnPressed,
+ style: OutlinedButton.styleFrom(
+ foregroundColor: AppColors.primary,
+ side: const BorderSide(color: AppColors.primary, width: 1.5),
+ shape: RoundedRectangleBorder(
+ borderRadius: AppSpacing.borderRadiusMd,
+ ),
+ textStyle: _textStyle,
+ ),
+ child: child,
+ ),
+ );
+
+ case GenexButtonVariant.text:
+ return SizedBox(
+ height: _height,
+ child: TextButton(
+ onPressed: effectiveOnPressed,
+ style: TextButton.styleFrom(
+ foregroundColor: AppColors.primary,
+ textStyle: _textStyle,
+ ),
+ child: child,
+ ),
+ );
+ }
+ }
+
+ double get _height {
+ switch (size) {
+ case GenexButtonSize.large:
+ return AppSpacing.buttonHeight;
+ case GenexButtonSize.medium:
+ return AppSpacing.buttonHeightSm;
+ case GenexButtonSize.small:
+ return 32;
+ }
+ }
+
+ TextStyle get _textStyle {
+ switch (size) {
+ case GenexButtonSize.large:
+ return AppTypography.labelLarge;
+ case GenexButtonSize.medium:
+ return AppTypography.labelMedium;
+ case GenexButtonSize.small:
+ return AppTypography.labelSmall;
+ }
+ }
+
+ Color get _foregroundColor {
+ switch (variant) {
+ case GenexButtonVariant.primary:
+ return Colors.white;
+ default:
+ return AppColors.primary;
+ }
+ }
+}
+
+enum GenexButtonVariant { primary, secondary, outline, text }
+enum GenexButtonSize { large, medium, small }
diff --git a/frontend/mobile/lib/shared/widgets/kyc_badge.dart b/frontend/mobile/lib/shared/widgets/kyc_badge.dart
new file mode 100644
index 0000000..9121866
--- /dev/null
+++ b/frontend/mobile/lib/shared/widgets/kyc_badge.dart
@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import '../../app/theme/app_colors.dart';
+import '../../app/theme/app_typography.dart';
+import '../../app/theme/app_spacing.dart';
+
+/// KYC等级标识组件
+///
+/// L0/L1/L2/L3 徽章
+/// 使用场景:个人中心、用户详情
+class KycBadge extends StatelessWidget {
+ final int level;
+ final bool showLabel;
+
+ const KycBadge({
+ super.key,
+ required this.level,
+ this.showLabel = true,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
+ decoration: BoxDecoration(
+ color: _color.withValues(alpha: 0.1),
+ borderRadius: AppSpacing.borderRadiusFull,
+ border: Border.all(color: _color.withValues(alpha: 0.2)),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(Icons.shield_rounded, size: 12, color: _color),
+ const SizedBox(width: 3),
+ Text(
+ showLabel ? 'L$level 认证' : 'L$level',
+ style: AppTypography.caption.copyWith(
+ color: _color,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Color get _color {
+ switch (level) {
+ case 0:
+ return AppColors.gray400;
+ case 1:
+ return AppColors.info;
+ case 2:
+ return AppColors.primary;
+ case 3:
+ return AppColors.success;
+ default:
+ return AppColors.gray400;
+ }
+ }
+}
diff --git a/frontend/mobile/lib/shared/widgets/price_tag.dart b/frontend/mobile/lib/shared/widgets/price_tag.dart
new file mode 100644
index 0000000..6f30a9d
--- /dev/null
+++ b/frontend/mobile/lib/shared/widgets/price_tag.dart
@@ -0,0 +1,101 @@
+import 'package:flutter/material.dart';
+import '../../app/theme/app_colors.dart';
+import '../../app/theme/app_typography.dart';
+import '../../app/theme/app_spacing.dart';
+
+/// 价格标签组件
+///
+/// 当前价格(大字)+ 原价删除线 + 折扣标签
+/// 使用场景:券详情页、确认订单、我的券
+class PriceTag extends StatelessWidget {
+ final double currentPrice;
+ final double faceValue;
+ final PriceTagSize size;
+ final bool showDiscount;
+
+ const PriceTag({
+ super.key,
+ required this.currentPrice,
+ required this.faceValue,
+ this.size = PriceTagSize.medium,
+ this.showDiscount = true,
+ });
+
+ double get discountRate => currentPrice / faceValue;
+ String get discountText => '${(discountRate * 10).toStringAsFixed(1)}折';
+ double get savedAmount => faceValue - currentPrice;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // Dollar sign
+ Text(
+ '\$',
+ style: _priceStyle.copyWith(fontSize: _priceStyle.fontSize! * 0.6),
+ ),
+ // Current price
+ Text(
+ currentPrice.toStringAsFixed(2),
+ style: _priceStyle,
+ ),
+ const SizedBox(width: 8),
+ // Original price (strikethrough)
+ if (currentPrice < faceValue)
+ Text(
+ '\$${faceValue.toStringAsFixed(0)}',
+ style: _originalStyle,
+ ),
+ if (showDiscount && currentPrice < faceValue) ...[
+ const SizedBox(width: 8),
+ _buildDiscountChip(),
+ ],
+ ],
+ );
+ }
+
+ Widget _buildDiscountChip() {
+ return Container(
+ padding: EdgeInsets.symmetric(
+ horizontal: size == PriceTagSize.large ? 8 : 6,
+ vertical: size == PriceTagSize.large ? 3 : 2,
+ ),
+ decoration: BoxDecoration(
+ gradient: AppColors.primaryGradient,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text(
+ discountText,
+ style: AppTypography.discountBadge.copyWith(
+ fontSize: size == PriceTagSize.large ? 13 : 11,
+ ),
+ ),
+ );
+ }
+
+ TextStyle get _priceStyle {
+ switch (size) {
+ case PriceTagSize.large:
+ return AppTypography.priceLarge;
+ case PriceTagSize.medium:
+ return AppTypography.priceMedium;
+ case PriceTagSize.small:
+ return AppTypography.priceSmall;
+ }
+ }
+
+ TextStyle get _originalStyle {
+ switch (size) {
+ case PriceTagSize.large:
+ return AppTypography.priceOriginal.copyWith(fontSize: 16);
+ case PriceTagSize.medium:
+ return AppTypography.priceOriginal;
+ case PriceTagSize.small:
+ return AppTypography.priceOriginal.copyWith(fontSize: 11);
+ }
+ }
+}
+
+enum PriceTagSize { large, medium, small }
diff --git a/frontend/mobile/lib/shared/widgets/skeleton_loader.dart b/frontend/mobile/lib/shared/widgets/skeleton_loader.dart
new file mode 100644
index 0000000..48163e7
--- /dev/null
+++ b/frontend/mobile/lib/shared/widgets/skeleton_loader.dart
@@ -0,0 +1,121 @@
+import 'package:flutter/material.dart';
+import '../../app/theme/app_colors.dart';
+import '../../app/theme/app_spacing.dart';
+
+/// 骨架屏加载组件
+///
+/// 列表加载、详情加载的骨架占位
+class SkeletonLoader extends StatefulWidget {
+ final double width;
+ final double height;
+ final double borderRadius;
+
+ const SkeletonLoader({
+ super.key,
+ this.width = double.infinity,
+ required this.height,
+ this.borderRadius = 8,
+ });
+
+ @override
+ State createState() => _SkeletonLoaderState();
+}
+
+class _SkeletonLoaderState extends State
+ with SingleTickerProviderStateMixin {
+ late AnimationController _controller;
+ late Animation _animation;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = AnimationController(
+ duration: const Duration(milliseconds: 1500),
+ vsync: this,
+ )..repeat();
+ _animation = Tween(begin: -1.0, end: 2.0).animate(
+ CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
+ );
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedBuilder(
+ animation: _animation,
+ builder: (context, child) {
+ return Container(
+ width: widget.width,
+ height: widget.height,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(widget.borderRadius),
+ gradient: LinearGradient(
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ stops: [
+ (_animation.value - 0.3).clamp(0.0, 1.0),
+ _animation.value.clamp(0.0, 1.0),
+ (_animation.value + 0.3).clamp(0.0, 1.0),
+ ],
+ colors: const [
+ AppColors.gray100,
+ AppColors.gray50,
+ AppColors.gray100,
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
+
+/// 券卡片骨架屏
+class CouponCardSkeleton extends StatelessWidget {
+ const CouponCardSkeleton({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: AppSpacing.couponCardHeight,
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: AppSpacing.borderRadiusMd,
+ border: Border.all(color: AppColors.borderLight),
+ ),
+ child: Row(
+ children: [
+ const SkeletonLoader(width: 110, height: double.infinity, borderRadius: 12),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SkeletonLoader(width: 60, height: 10, borderRadius: 4),
+ const SizedBox(height: 6),
+ SkeletonLoader(width: 140, height: 14, borderRadius: 4),
+ ],
+ ),
+ SkeletonLoader(width: 100, height: 16, borderRadius: 4),
+ SkeletonLoader(width: 80, height: 10, borderRadius: 4),
+ ],
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/mobile/lib/shared/widgets/status_tag.dart b/frontend/mobile/lib/shared/widgets/status_tag.dart
new file mode 100644
index 0000000..e620e83
--- /dev/null
+++ b/frontend/mobile/lib/shared/widgets/status_tag.dart
@@ -0,0 +1,85 @@
+import 'package:flutter/material.dart';
+import '../../app/theme/app_colors.dart';
+import '../../app/theme/app_typography.dart';
+import '../../app/theme/app_spacing.dart';
+
+/// 状态标签组件
+///
+/// 处理中/已完成/已取消/退款中 颜色标签
+/// 使用场景:订单列表、我的券、交易记录
+class StatusTag extends StatelessWidget {
+ final String label;
+ final StatusType type;
+
+ const StatusTag({
+ super.key,
+ required this.label,
+ required this.type,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
+ decoration: BoxDecoration(
+ color: _bgColor,
+ borderRadius: AppSpacing.borderRadiusFull,
+ ),
+ child: Text(
+ label,
+ style: AppTypography.caption.copyWith(
+ color: _textColor,
+ fontWeight: FontWeight.w500,
+ fontSize: 11,
+ ),
+ ),
+ );
+ }
+
+ Color get _bgColor {
+ switch (type) {
+ case StatusType.success:
+ return AppColors.successLight;
+ case StatusType.pending:
+ return AppColors.warningLight;
+ case StatusType.error:
+ return AppColors.errorLight;
+ case StatusType.info:
+ return AppColors.infoLight;
+ case StatusType.neutral:
+ return AppColors.gray100;
+ }
+ }
+
+ Color get _textColor {
+ switch (type) {
+ case StatusType.success:
+ return AppColors.success;
+ case StatusType.pending:
+ return AppColors.warning;
+ case StatusType.error:
+ return AppColors.error;
+ case StatusType.info:
+ return AppColors.info;
+ case StatusType.neutral:
+ return AppColors.textSecondary;
+ }
+ }
+}
+
+enum StatusType { success, pending, error, info, neutral }
+
+/// 快捷构造
+class StatusTags {
+ StatusTags._();
+
+ static StatusTag active() => const StatusTag(label: '可使用', type: StatusType.success);
+ static StatusTag pending() => const StatusTag(label: '待核销', type: StatusType.pending);
+ static StatusTag expired() => const StatusTag(label: '已过期', type: StatusType.neutral);
+ static StatusTag used() => const StatusTag(label: '已使用', type: StatusType.neutral);
+ static StatusTag processing() => const StatusTag(label: '处理中', type: StatusType.info);
+ static StatusTag completed() => const StatusTag(label: '已完成', type: StatusType.success);
+ static StatusTag cancelled() => const StatusTag(label: '已取消', type: StatusType.neutral);
+ static StatusTag refunding() => const StatusTag(label: '退款中', type: StatusType.pending);
+ static StatusTag onSale() => const StatusTag(label: '出售中', type: StatusType.info);
+}