diff --git a/docs/guides/03-Web管理前端开发指南.md b/docs/guides/03-Web管理前端开发指南.md index 23b5c3d..6f5045d 100644 --- a/docs/guides/03-Web管理前端开发指南.md +++ b/docs/guides/03-Web管理前端开发指南.md @@ -694,7 +694,261 @@ export function MarketMakerMonitorPage() { --- -*文档版本: v2.0* +## 18. Nasdaq上市合规管理 + +### 18.1 SEC Filing管理 + +```typescript +// src/features/compliance/sec-filing.ts +interface SecFiling { + type: '10-K' | '10-Q' | '8-K' | 'S-1' | 'Form ATS' | 'Form ATS-N'; + status: 'drafting' | 'internal_review' | 'legal_review' | 'filed' | 'accepted'; + period?: string; // 报告期间(如 "FY2027", "Q1 2027") + dueDate: Date; + filedDate?: Date; + preparedBy: string; + reviewedBy: string[]; + secEdgarLink?: string; // SEC EDGAR提交链接 + attachments: FilingAttachment[]; +} + +interface FilingSchedule { + annual: { type: '10-K', deadline: '年度结束后60天(加速申报人)' }; + quarterly: { type: '10-Q', deadline: '季度结束后40天' }; + materialEvent: { type: '8-K', deadline: '事件发生后4个工作日' }; +} + +// SEC Filing管理页面 +export function SecFilingManagementPage() { + return ( +
+

SEC Filing管理

+ + {/* Filing日历 */} + + openFilingDetail(filing)} + /> + + + {/* 10-K/10-Q编制追踪 */} + + }, + { key: 'dueDate', title: '截止日期', render: (d) => }, + { key: 'preparedBy', title: '编制人' }, + { key: 'actions', title: '操作', render: (_, row) => ( + <> + + + + )}, + ]} + /> + + + {/* 8-K重大事件快速申报 */} + + + + +
+ ); +} +``` + +### 18.2 牌照与注册管理 + +```typescript +// src/features/compliance/license-management.ts +interface LicenseRecord { + type: 'MSB' | 'MTL' | 'BitLicense' | 'DFAL' | 'Broker-Dealer' | 'Form ATS'; + jurisdiction: string; // 联邦 / 州名 + regulatoryBody: string; // FinCEN / NYDFS / SEC等 + status: 'active' | 'pending' | 'expired' | 'not_applied'; + obtainedDate?: Date; + expiryDate?: Date; + renewalDate?: Date; + applicationRef?: string; + notes: string; +} + +// 牌照看板 +export function LicenseDashboardPage() { + return ( +
+

牌照与注册状态

+ + {/* 联邦级牌照 */} + + + + + {/* 州级MTL地图 */} + + openStateDetail(state)} + /> + + + + {/* 到期提醒 */} + + + +
+ ); +} +``` + +### 18.3 商业保险管理 + +```typescript +// src/features/compliance/insurance-management.ts +interface InsurancePolicy { + type: 'D&O' | 'E&O' | 'CYBER' | 'FIDELITY' | 'GL'; + name: string; + carrier: string; + policyNumber: string; + coverageAmount: number; + annualPremium: number; + effectiveDate: Date; + expiryDate: Date; + status: 'active' | 'expiring_soon' | 'expired' | 'not_purchased'; + isAdequate: boolean; // 覆盖额度是否满足要求 +} + +// 保险管理页面 +export function InsuranceManagementPage() { + return ( +
+

商业保险管理

+ + {/* 保险概览卡片 */} + + {insurancePolicies.map(policy => ( + + + + ))} + + + {/* 保险充足性分析 */} + + + + + {/* 到期/续期日历 */} + + + +
+ ); +} +``` + +### 18.4 IPO准备清单 + +```typescript +// src/features/compliance/ipo-readiness.ts +interface IpoChecklistItem { + category: 'legal' | 'financial' | 'compliance' | 'governance' | 'insurance' | 'technical'; + item: string; + status: 'completed' | 'in_progress' | 'not_started' | 'blocked'; + owner: string; + targetDate: Date; + dependencies: string[]; + notes: string; +} + +// IPO准备看板 +export function IpoReadinessPage() { + const categories = [ + { + name: '法律与牌照', + items: [ + { item: 'MSB注册(FinCEN Form 107)', status: 'completed' }, + { item: '州级MTL(49州+DC)', status: 'in_progress' }, + { item: 'NY BitLicense', status: 'in_progress' }, + { item: '券法律属性意见书', status: 'not_started' }, + { item: 'GNX代币法律分类', status: 'not_started' }, + ], + }, + { + name: '财务与审计', + items: [ + { item: '聘请PCAOB注册审计师', status: 'not_started' }, + { item: '2年+经审计GAAP财报', status: 'not_started' }, + { item: 'FASB ASU 2023-08数字资产会计', status: 'not_started' }, + { item: 'ASC 606收入确认(Breakage/递延负债)', status: 'in_progress' }, + { item: '链上数据与GAAP对账机制', status: 'in_progress' }, + ], + }, + { + name: 'SOX合规', + items: [ + { item: 'Section 302 CEO/CFO认证流程', status: 'in_progress' }, + { item: 'Section 404(a) 内部控制评估', status: 'not_started' }, + { item: 'Section 404(b) 外部审计师审计', status: 'not_started' }, + { item: '智能合约升级审计追踪', status: 'completed' }, + ], + }, + { + name: '公司治理', + items: [ + { item: '独立董事(多数席位)', status: 'not_started' }, + { item: '审计委员会(全部独立董事)', status: 'not_started' }, + { item: '薪酬委员会', status: 'not_started' }, + { item: 'SEC注册声明(Form S-1)', status: 'not_started' }, + ], + }, + { + name: '商业保险', + items: [ + { item: 'D&O保险(匹配市值)', status: 'not_started' }, + { item: 'E&O专业责任险', status: 'not_started' }, + { item: '网络安全险(覆盖数字资产托管)', status: 'not_started' }, + { item: '忠诚保证金', status: 'not_started' }, + ], + }, + ]; + + return ( +
+

IPO准备清单

+ + {categories.map(cat => ( + + + + ))} + +
+ ); +} +``` + +--- + +*文档版本: v2.1* *基于: Genex 券交易平台 - 软件需求规格说明书 v4.1* *技术栈: React 18 + TypeScript 5 + Next.js 14 + Zustand + Redux Toolkit* -*更新: 补充数据报表/用户行为分析/券类别分析/1099税务/FATCA/虚假宣传监控/SOX审计/财务管理/争议仲裁/Web核销后台/做市商管理* +*更新: v2.1补充Nasdaq上市合规管理(SEC Filing/牌照管理/商业保险/IPO准备清单)* diff --git a/docs/guides/05-后端开发指南.md b/docs/guides/05-后端开发指南.md index 0ecb8bb..4ecd5b3 100644 --- a/docs/guides/05-后端开发指南.md +++ b/docs/guides/05-后端开发指南.md @@ -1460,7 +1460,740 @@ export class ReconciliationService { --- -*文档版本: v2.0* +## 32. 会计处理(ASC 606 / GAAP) + +### 32.1 收入确认框架(ASC 606五步法) + +```typescript +// clearing-service/src/domain/services/accounting.service.ts + +/** + * ASC 606 五步法收入确认: + * 1. 识别合同 → 券发行/交易成交 + * 2. 识别履约义务 → 券兑付义务(发行方)/ 平台服务义务 + * 3. 确定交易价格 → 发行价/成交价 + * 4. 分配交易价格 → 各履约义务 + * 5. 确认收入 → 履约义务满足时 + */ +export class AccountingService { + /** + * 券发行时:发行方收到资金 → 记入递延负债(Deferred Revenue) + * 券核销时:履约义务满足 → 递延负债转为已确认收入 + * 券过期时:Breakage收入确认(ASC 606-10-55-48~51) + */ + async recordIssuance(event: CouponIssuedEvent): Promise { + const entries: JournalEntry[] = []; + + // 发行方视角:收到现金,形成递延负债 + entries.push({ + date: event.issuedAt, + debit: { account: 'cash', amount: event.totalSalesAmount }, + credit: { account: 'deferred_revenue', amount: event.totalSalesAmount }, + memo: `券发行 Batch#${event.batchId},发行方${event.issuerId}`, + reference: event.txHash, + }); + + // 平台视角:发行服务费收入(立即确认,平台履约义务已完成) + const issuanceFee = event.totalSalesAmount * event.feeRate; + entries.push({ + date: event.issuedAt, + debit: { account: 'accounts_receivable_issuer', amount: issuanceFee }, + credit: { account: 'revenue_issuance_fee', amount: issuanceFee }, + memo: `发行服务费 ${event.feeRate * 100}%`, + reference: event.txHash, + }); + + await this.journalRepo.batchSave(entries); + return entries; + } + + async recordRedemption(event: CouponRedeemedEvent): Promise { + const entries: JournalEntry[] = []; + + // 发行方视角:券核销 → 递延负债转为已确认收入 + entries.push({ + date: event.redeemedAt, + debit: { account: 'deferred_revenue', amount: event.faceValue }, + credit: { account: 'revenue_earned', amount: event.faceValue }, + memo: `券核销 Token#${event.tokenId}`, + reference: event.txHash, + }); + + await this.journalRepo.batchSave(entries); + return entries; + } + + async recordTrade(event: TradeSettledEvent): Promise { + const entries: JournalEntry[] = []; + + // 平台视角:交易手续费收入(成交即确认) + entries.push({ + date: event.settledAt, + debit: { account: 'cash_stablecoin', amount: event.buyerFee + event.sellerFee }, + credit: { account: 'revenue_trading_fee', amount: event.buyerFee + event.sellerFee }, + memo: `交易手续费 Order#${event.orderId}`, + reference: event.txHash, + }); + + await this.journalRepo.batchSave(entries); + return entries; + } +} +``` + +### 32.2 Breakage收入确认(ASC 606-10-55-48~51) + +```typescript +// clearing-service/src/domain/services/breakage-accounting.service.ts +export class BreakageAccountingService { + /** + * ASC 606 Breakage收入确认规则: + * - 如果发行方预期部分券不会被兑付(Breakage),且Breakage金额可合理估计 + * → 在券生命周期内按比例确认Breakage收入(proportional method) + * - 如果Breakage不可合理估计 + * → 在券到期/权利失效时一次性确认(remote method) + * + * 平台策略:初期采用remote method(到期时确认),积累足够历史数据后切换proportional method + */ + private readonly RECOGNITION_METHOD: 'proportional' | 'remote' = 'remote'; + + async processBreakageAccounting(coupon: ExpiredCoupon): Promise { + const entries: JournalEntry[] = []; + + if (this.RECOGNITION_METHOD === 'remote') { + // Remote method: 到期时一次性确认 + // 发行方侧:递延负债 → Breakage收入 + entries.push({ + date: coupon.expiryDate, + debit: { account: 'deferred_revenue', amount: coupon.faceValue }, + credit: { account: 'revenue_breakage', amount: coupon.faceValue }, + memo: `Breakage收入确认(remote method)Token#${coupon.tokenId}`, + }); + + // 平台侧:Breakage分润(按协议比例) + const platformShare = coupon.faceValue * this.PLATFORM_BREAKAGE_SHARE; + entries.push({ + date: coupon.expiryDate, + debit: { account: 'accounts_receivable_breakage', amount: platformShare }, + credit: { account: 'revenue_breakage_share', amount: platformShare }, + memo: `平台Breakage分润 ${this.PLATFORM_BREAKAGE_SHARE * 100}%`, + }); + } + + await this.journalRepo.batchSave(entries); + return entries; + } + + /** + * 递延负债余额报表(监管/审计用) + * 展示平台上所有未兑付券的递延负债总额 + */ + async getDeferredRevenueReport(asOfDate: Date): Promise { + const outstandingCoupons = await this.couponRepo.findOutstanding(asOfDate); + const totalDeferred = outstandingCoupons.reduce((sum, c) => sum + c.faceValue, 0); + const byIssuer = this.groupByIssuer(outstandingCoupons); + + return { + asOfDate, + totalDeferredRevenue: totalDeferred, + byIssuer, + byMaturity: this.groupByMaturityBucket(outstandingCoupons), + recognitionMethod: this.RECOGNITION_METHOD, + }; + } +} + +// 会计科目表(Chart of Accounts) +const CHART_OF_ACCOUNTS = { + // 资产类 + cash: '1001 现金及等价物', + cash_stablecoin: '1002 稳定币资产(USDC/USDT)', + accounts_receivable_issuer: '1101 应收账款-发行方', + accounts_receivable_breakage: '1102 应收账款-Breakage分润', + // 负债类 + deferred_revenue: '2001 递延收入(券未兑付负债)', + user_deposits: '2002 用户托管资金', + guarantee_funds_held: '2003 发行方保障资金(代管)', + // 收入类 + revenue_trading_fee: '4001 交易手续费收入', + revenue_issuance_fee: '4002 发行服务费收入', + revenue_breakage_share: '4003 Breakage分润收入', + revenue_vas: '4004 增值服务收入', + revenue_earned: '4005 已确认收入(发行方侧)', + revenue_breakage: '4006 Breakage收入(发行方侧)', +}; +``` + +### 32.3 链上数据与GAAP对账 + +```typescript +// 链上资产余额 vs GAAP账面价值对账 +export class ChainGaapReconciliationService { + async reconcile(period: AccountingPeriod): Promise { + // 1. 链上数据汇总 + const chainData = { + totalCouponsOutstanding: await this.chainClient.getTotalOutstandingCoupons(), + totalStablecoinHeld: await this.chainClient.getTotalStablecoinBalance(), + totalGuaranteeFunds: await this.chainClient.getTotalGuaranteeFunds(), + }; + + // 2. GAAP账面数据 + const gaapData = { + deferredRevenue: await this.accountingRepo.getDeferredRevenue(period.endDate), + userDeposits: await this.accountingRepo.getUserDeposits(period.endDate), + guaranteeFunds: await this.accountingRepo.getGuaranteeFunds(period.endDate), + }; + + // 3. 差异分析 + const discrepancies = this.computeDiscrepancies(chainData, gaapData); + + return { + period, + chainData, + gaapData, + discrepancies, + isClean: discrepancies.length === 0, + auditorNotes: discrepancies.map(d => this.generateAuditNote(d)), + }; + } +} +``` + +--- + +## 33. 消费者保护法合规(FTC / UDAAP / CARD Act) + +### 33.1 FTC Act Section 5 / Dodd-Frank UDAAP 规则引擎 + +```typescript +// compliance-service/src/domain/services/consumer-protection.service.ts +export class ConsumerProtectionService { + /** + * FTC Act Section 5: 禁止不公平或欺骗性商业行为 + * Dodd-Frank UDAAP: 禁止Unfair, Deceptive, or Abusive Acts or Practices + * + * 平台作为券交易市场,对发行方的券描述负审核责任 + */ + private readonly RULES: ConsumerProtectionRule[] = [ + // 券信息披露完整性检查 + { + name: 'disclosure_completeness', + check: (coupon: CouponListing) => { + const required = ['faceValue', 'expiryDate', 'useConditions', 'issuerCreditRating']; + const missing = required.filter(f => !coupon[f]); + return missing.length === 0 + ? { pass: true } + : { pass: false, violation: `缺少必要披露: ${missing.join(',')}` }; + }, + }, + // 定价不得误导(折扣率标注准确性) + { + name: 'pricing_accuracy', + check: (coupon: CouponListing) => { + const actualDiscount = 1 - coupon.currentPrice / coupon.faceValue; + const labeledDiscount = coupon.displayedDiscount; + return Math.abs(actualDiscount - labeledDiscount) <= 0.01 + ? { pass: true } + : { pass: false, violation: `标注折扣${labeledDiscount}与实际折扣${actualDiscount}不符` }; + }, + }, + // 发行方信用等级标注准确性 + { + name: 'credit_rating_accuracy', + check: (coupon: CouponListing) => { + const currentRating = coupon.issuer.creditRating; + const displayedRating = coupon.displayedCreditRating; + return currentRating === displayedRating + ? { pass: true } + : { pass: false, violation: `展示信用等级${displayedRating}与实际${currentRating}不符` }; + }, + }, + // 退款政策透明化 + { + name: 'refund_policy_disclosure', + check: (coupon: CouponListing) => { + return coupon.refundPolicy && coupon.refundPolicy.isVisible + ? { pass: true } + : { pass: false, violation: '退款政策未明确展示' }; + }, + }, + ]; + + async auditListing(coupon: CouponListing): Promise { + const violations = this.RULES + .map(rule => ({ rule: rule.name, result: rule.check(coupon) })) + .filter(r => !r.result.pass); + + if (violations.length > 0) { + await this.alertService.sendViolationAlert(coupon.id, violations); + } + + return { couponId: coupon.id, pass: violations.length === 0, violations }; + } + + // AI辅助虚假宣传检测(批量扫描) + @Cron('0 6 * * *') // 每日凌晨6点 + async batchScanListings(): Promise { + const activeListings = await this.couponRepo.findActiveListings(); + for (const listing of activeListings) { + const result = await this.auditListing(listing); + if (!result.pass) { + await this.createEnforcementCase(listing, result.violations); + } + } + } +} +``` + +### 33.2 CARD Act 有效期冲突检测 + +```typescript +// compliance-service/src/domain/services/card-act.service.ts +export class CardActComplianceService { + /** + * CARD Act (Credit CARD Act of 2009) 礼品卡条款: + * - 有效期不得少于5年 + * - 不得收取休眠费(dormancy/inactivity fees) + * + * 冲突:Utility Track强制有效期≤12个月(SEC合规) + * 解决:论证"消费型券≠Gift Card"(券是发行方预付债务工具,非零售商礼品卡) + * + * 本服务实现分类检测:如果券可能被认定为Gift Card,则触发合规审查 + */ + async checkCardActApplicability(coupon: CouponTemplate): Promise { + // Gift Card特征匹配 + const giftCardIndicators = { + hasFixedFaceValue: coupon.faceValue > 0 && !coupon.isPercentageDiscount, + isOpenLoop: !coupon.allowedStores || coupon.allowedStores.length === 0, + isStoreBranded: coupon.allowedStores && coupon.allowedStores.length > 0, + labeledAsGiftCard: /gift\s*card|礼品卡|礼券/i.test(coupon.name + coupon.description), + isPreloaded: coupon.couponCategory === 'stored_value', + }; + + const isLikelyGiftCard = giftCardIndicators.labeledAsGiftCard || + (giftCardIndicators.hasFixedFaceValue && giftCardIndicators.isPreloaded); + + if (isLikelyGiftCard && coupon.couponType === 'utility') { + // 冲突检测:Utility Track ≤12个月 vs CARD Act ≥5年 + const expiryDays = differenceInDays(coupon.expiryDate, coupon.issuedDate); + if (expiryDays < 365 * 5) { + return { + isConflict: true, + couponId: coupon.id, + conflictType: 'CARD_ACT_VS_UTILITY_TRACK', + detail: `券"${coupon.name}"可能被认定为Gift Card(CARD Act要求≥5年有效期),但Utility Track限制≤12个月`, + recommendation: [ + '1. 修改券名称/描述,避免使用"礼品卡/Gift Card"字样', + '2. 或在法律意见书中论证该券为"预付债务工具"而非"零售礼品卡"', + '3. 或将该券移出Utility Track,按CARD Act规则设置≥5年有效期', + ], + requiresLegalReview: true, + }; + } + } + + // 休眠费检查(平台不收取,但检查发行方设置) + if (coupon.inactivityFee && coupon.inactivityFee > 0) { + return { + isConflict: true, + couponId: coupon.id, + conflictType: 'CARD_ACT_DORMANCY_FEE', + detail: 'CARD Act禁止对礼品卡/预付卡收取休眠费', + recommendation: ['移除休眠费设置'], + requiresLegalReview: false, + }; + } + + return { isConflict: false, couponId: coupon.id }; + } + + // 发行审核时自动触发 + async preIssuanceCardActCheck(template: CouponTemplate): Promise { + const result = await this.checkCardActApplicability(template); + if (result.isConflict) { + // 标记为需要法律审查,暂停发行审批 + await this.issuerRepo.flagForLegalReview(template.id, result); + throw new ComplianceException(`CARD Act合规冲突: ${result.detail}`); + } + } +} +``` + +### 33.3 GENIUS Act 稳定币合规 + +```typescript +// compliance-service/src/domain/services/genius-act.service.ts +export class GeniusActComplianceService { + /** + * GENIUS Act (2025年签署) — 稳定币监管法: + * 平台策略:使用第三方合规稳定币(USDC/USDT),不自行发行 + * 合规责任:确保出入金通道对接的稳定币发行方持有合规牌照 + */ + private readonly APPROVED_STABLECOINS: StablecoinInfo[] = [ + { + symbol: 'USDC', issuer: 'Circle', license: 'NY BitLicense + State MTLs', + reserveAudit: 'Deloitte (monthly attestation)', compliant: true, + }, + { + symbol: 'USDT', issuer: 'Tether', license: 'Various', + reserveAudit: 'BDO Italia', compliant: true, + }, + { + symbol: 'PYUSD', issuer: 'PayPal/Paxos', license: 'NYDFS regulated', + reserveAudit: 'Withum', compliant: true, + }, + ]; + + async validateStablecoinCompliance(stablecoinAddress: string): Promise { + const info = this.APPROVED_STABLECOINS.find(s => + s.contractAddress === stablecoinAddress + ); + if (!info || !info.compliant) { + throw new ComplianceException('不支持的或不合规的稳定币'); + } + return true; + } + + // 季度审查:更新稳定币合规状态 + @Cron('0 0 1 */3 *') // 每季度1号 + async quarterlyStablecoinReview(): Promise { + for (const coin of this.APPROVED_STABLECOINS) { + const latestAttestation = await this.fetchLatestReserveAttestation(coin.issuer); + coin.compliant = latestAttestation.isClean; + if (!coin.compliant) { + await this.alertService.sendCriticalAlert( + `稳定币${coin.symbol}储备审计异常,考虑暂停出入金通道` + ); + } + } + } +} +``` + +--- + +## 34. 州级合规路由(MTL / BitLicense / DFAL) + +```typescript +// compliance-service/src/domain/services/state-compliance.service.ts +export class StateComplianceService { + /** + * 美国49州+DC各自有独立的Money Transmitter License (MTL)要求 + * 不同州的交易限额、KYC要求、报告义务不同 + * 本服务根据用户所在州动态应用对应合规规则 + */ + private readonly STATE_RULES: Record = { + NY: { + state: 'New York', + license: 'BitLicense (NYDFS)', + status: 'required', + additionalKyc: true, // NY要求增强KYC + maxSingleTx: 10000, // 单笔上限(USD) + reportingThreshold: 3000, // 报告阈值 + custodyRequirements: 'NYDFS托管规则:客户资产必须隔离托管', + restrictions: ['不允许匿名交易', '需额外的NY居民声明'], + }, + CA: { + state: 'California', + license: 'DFAL (2026年7月生效)', + status: 'pending_regulation', + additionalKyc: false, + maxSingleTx: null, // 无额外限制 + reportingThreshold: 3000, + custodyRequirements: 'DFAL框架下的托管要求(待细则)', + restrictions: ['CCPA增强隐私要求'], + }, + TX: { + state: 'Texas', + license: 'MTL (Texas Dept of Banking)', + status: 'required', + additionalKyc: false, + maxSingleTx: null, + reportingThreshold: 3000, + custodyRequirements: '标准MTL托管要求', + restrictions: [], + }, + FL: { + state: 'Florida', + license: 'MTL (OFR)', + status: 'required', + additionalKyc: false, + maxSingleTx: null, + reportingThreshold: 3000, + custodyRequirements: '标准MTL托管要求', + restrictions: [], + }, + // ... 其他州配置(配置化管理,通过Admin后台维护) + }; + + // 默认规则(适用于未单独配置的州) + private readonly DEFAULT_RULE: StateComplianceRule = { + state: 'Default', + license: 'MTL (standard)', + status: 'required', + additionalKyc: false, + maxSingleTx: null, + reportingThreshold: 3000, + custodyRequirements: '标准MTL托管要求', + restrictions: [], + }; + + // 受限州(暂不服务,直到获得相应牌照) + private readonly RESTRICTED_STATES = ['NY', 'HI']; // NY需BitLicense,HI暂不服务 + + async checkStateCompliance(userId: string, transaction: TransactionRequest): Promise { + const user = await this.userRepo.findById(userId); + const state = user.residenceState; + + // 1. 检查是否为受限州 + if (this.RESTRICTED_STATES.includes(state)) { + const hasLicense = await this.licenseRepo.hasActiveLicense(state); + if (!hasLicense) { + return { + allowed: false, + reason: `暂不支持${state}州用户交易,待取得${this.STATE_RULES[state]?.license || 'MTL'}牌照`, + }; + } + } + + // 2. 获取州规则 + const rule = this.STATE_RULES[state] || this.DEFAULT_RULE; + + // 3. 单笔限额检查 + if (rule.maxSingleTx && transaction.amount > rule.maxSingleTx) { + return { + allowed: false, + reason: `${state}州单笔交易上限$${rule.maxSingleTx}`, + }; + } + + // 4. 增强KYC检查(如NY) + if (rule.additionalKyc && user.kycLevel < KycLevel.L2) { + return { + allowed: false, + reason: `${state}州要求KYC L2+`, + action: 'upgrade_kyc', + }; + } + + // 5. 州级报告义务 + if (transaction.amount >= rule.reportingThreshold) { + await this.stateReportingService.queueReport(state, transaction); + } + + return { allowed: true, appliedRule: rule }; + } + + // 牌照状态管理 + async getLicenseStatus(): Promise { + const allStates = Object.keys(this.STATE_RULES); + const statuses = await Promise.all( + allStates.map(async (state) => ({ + state, + required: this.STATE_RULES[state].license, + obtained: await this.licenseRepo.hasActiveLicense(state), + expiryDate: await this.licenseRepo.getLicenseExpiry(state), + renewalDue: await this.licenseRepo.isRenewalDue(state), + })) + ); + return { statuses, restrictedStates: this.RESTRICTED_STATES }; + } +} +``` + +--- + +## 35. SEC证券合规(Phase 4预留) + +```typescript +// compliance-service/src/domain/services/securities-compliance.service.ts + +/** + * Securities Track合规服务(Phase 4启用) + * + * 前提条件: + * 1. 券的法律属性意见书已出具 + * 2. 如需Broker-Dealer注册,已完成SEC注册 + * 3. 如需ATS注册,已提交Form ATS + * + * 豁免路径(SRS 1.5-1.6): + * - Reg D (Rule 506(b/c)): 面向合格投资者,无公开募集,无发行上限 + * - Reg A+ (Tier 2): 面向所有投资者,年发行上限$75M,需SEC审查 + * - Reg CF: 众筹豁免,年上限$5M,需通过注册融资门户 + */ +export class SecuritiesComplianceService { + private readonly PHASE4_ENABLED = false; // Phase 4前保持关闭 + + // Reg D 506(c) 合格投资者验证 + async verifyAccreditedInvestor(userId: string): Promise { + if (!this.PHASE4_ENABLED) throw new Error('Securities Track not enabled'); + + const user = await this.userRepo.findById(userId); + + // SEC Rule 501(a) 合格投资者标准 + const criteria = { + // 个人:年收入>$200K(或与配偶合计>$300K)连续2年 + incomeTest: user.annualIncome >= 200000 || user.jointIncome >= 300000, + // 个人:净资产>$1M(不含主要住所) + netWorthTest: user.netWorthExcludingPrimaryResidence >= 1000000, + // 专业认证:Series 7/65/82等 + professionalTest: user.hasFinancialLicense, + // 实体:资产>$5M + entityTest: user.isEntity && user.entityAssets >= 5000000, + }; + + const isAccredited = criteria.incomeTest || criteria.netWorthTest || + criteria.professionalTest || criteria.entityTest; + + return { + userId, + isAccredited, + verificationMethod: 'self_certification', // 初期自认证,后期第三方验证 + verifiedAt: new Date(), + validUntil: addMonths(new Date(), 12), // 12个月有效 + criteria, + }; + } + + // Reg A+ 投资限额检查 + async checkRegAInvestmentLimit(userId: string, amount: number): Promise { + if (!this.PHASE4_ENABLED) throw new Error('Securities Track not enabled'); + + const user = await this.userRepo.findById(userId); + + // Reg A+ Tier 2: 非合格投资者年投资上限 = max(年收入, 净资产) × 10% + if (!user.isAccredited) { + const limit = Math.max(user.annualIncome, user.netWorth) * 0.10; + const ytdInvestment = await this.investmentRepo.getYtdTotal(userId); + return (ytdInvestment + amount) <= limit; + } + + return true; // 合格投资者无限额 + } + + // Form ATS报告义务 + async generateAtsReport(quarter: string): Promise { + if (!this.PHASE4_ENABLED) throw new Error('Securities Track not enabled'); + + return { + quarter, + totalSecuritiesTraded: await this.tradeRepo.getSecuritiesVolume(quarter), + uniqueParticipants: await this.tradeRepo.getUniqueSecuritiesTraders(quarter), + topSecurities: await this.tradeRepo.getTopSecuritiesByVolume(quarter, 10), + // SEC Form ATS-N 披露要求 + atsOperations: { + orderTypes: ['limit', 'market'], + matchingLogic: 'price-time priority', + feeSchedule: this.getSecuritiesFeeSchedule(), + }, + }; + } +} +``` + +--- + +## 36. 商业保险需求跟踪 + +```typescript +// compliance-service/src/domain/services/insurance-tracking.service.ts + +/** + * 商业保险管理(Nasdaq上市审计要求) + * IPO前12个月完成所有保险采购 + */ +export class InsuranceTrackingService { + private readonly REQUIRED_POLICIES: InsurancePolicyRequirement[] = [ + { + type: 'D&O', + name: '董事及高管责任险 (Directors & Officers)', + description: '董事/高管因管理决策被诉的法律费用与赔偿', + minCoverage: 'match_market_cap', // 覆盖额度匹配公司市值 + requiredBy: 'IPO前12个月', + status: 'not_purchased', + carriers: ['AIG', 'Chubb', 'Berkshire Hathaway'], + }, + { + type: 'E&O', + name: '专业责任险 (Errors & Omissions)', + description: '平台服务失误导致用户损失(信用评级错误、交易撮合问题)', + minCoverage: 10_000_000, // $10M + requiredBy: 'IPO前12个月', + status: 'not_purchased', + carriers: ['Lloyd\'s', 'AIG', 'Travelers'], + }, + { + type: 'CYBER', + name: '网络安全险 (Cyber Liability)', + description: '数据泄露、黑客攻击、勒索软件损失与响应成本', + minCoverage: 50_000_000, // $50M(覆盖MPC托管的全部用户资产) + requiredBy: 'IPO前12个月', + status: 'not_purchased', + carriers: ['Coalition', 'Beazley', 'AIG'], + notes: '需覆盖数字资产托管风险,保费较高', + }, + { + type: 'FIDELITY', + name: '忠诚保证金 (Fidelity Bond)', + description: '内部员工欺诈、盗窃数字资产', + minCoverage: 5_000_000, // $5M + requiredBy: 'IPO前12个月', + status: 'not_purchased', + carriers: ['Travelers', 'Hartford', 'CNA'], + }, + { + type: 'GL', + name: '商业综合险 (General Liability)', + description: '一般商业责任', + minCoverage: 2_000_000, // $2M + requiredBy: 'IPO前12个月', + status: 'not_purchased', + carriers: ['Progressive', 'Hartford'], + }, + ]; + + async getInsuranceStatus(): Promise { + const policies = await this.policyRepo.findAll(); + return { + requiredPolicies: this.REQUIRED_POLICIES.map(req => { + const purchased = policies.find(p => p.type === req.type); + return { + ...req, + status: purchased ? 'active' : 'not_purchased', + policyNumber: purchased?.policyNumber, + effectiveDate: purchased?.effectiveDate, + expiryDate: purchased?.expiryDate, + coverageAmount: purchased?.coverageAmount, + annualPremium: purchased?.annualPremium, + isAdequate: purchased + ? this.isCoverageAdequate(req, purchased) + : false, + }; + }), + allRequirementsMet: this.REQUIRED_POLICIES.every( + req => policies.some(p => p.type === req.type && p.status === 'active') + ), + }; + } + + // 保险到期提醒 + @Cron('0 9 * * 1') // 每周一上午9点 + async checkExpiringPolicies(): Promise { + const policies = await this.policyRepo.findExpiringWithin(90); // 90天内到期 + for (const policy of policies) { + await this.alertService.sendAlert({ + type: 'insurance_expiring', + severity: 'high', + message: `${policy.name}将于${policy.expiryDate}到期,请及时续期`, + }); + } + } +} +``` + +--- + +*文档版本: v2.1* *基于: Genex 券交易平台 - 软件需求规格说明书 v4.1* *技术栈: NestJS + Go + Kong + PostgreSQL + Kafka + Redis* -*更新: 补充手续费/Breakage/退款/做市商/定价引擎/AI-ML/AML/OFAC/Travel Rule/税务/隐私/安全IR/DR/映射表安全/多币种/对账/容量规划/SDK* +*更新: v2.1补充会计处理(ASC 606/递延负债/GAAP对账)/消费者保护法(FTC/UDAAP/CARD Act冲突检测/GENIUS Act)/州级合规路由(MTL/BitLicense/DFAL)/SEC证券合规(Reg D/A+/CF预留)/商业保险跟踪* diff --git a/frontend/admin-app/lib/app/i18n/app_localizations.dart b/frontend/admin-app/lib/app/i18n/app_localizations.dart new file mode 100644 index 0000000..17017a1 --- /dev/null +++ b/frontend/admin-app/lib/app/i18n/app_localizations.dart @@ -0,0 +1,459 @@ +/// Genex Admin App (Issuer) - 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建议', + + // ── Issuer-specific ── + 'issuer_dashboard': '数据概览', + 'issuer_coupons': '券管理', + 'issuer_redeem': '核销', + 'issuer_finance': '财务', + 'issuer_profile': '我的', + 'issuer_onboarding': '入驻申请', + 'issuer_credit': '信用评级', + 'batch_issue': '批量发行', + 'batch_recall': '批量召回', + 'reconciliation': '对账', + 'store_management': '门店管理', + + // Issuer Dashboard + 'issuer_total_issued': '已发行总量', + 'issuer_total_redeemed': '已核销总量', + 'issuer_active_coupons': '流通中', + 'issuer_revenue': '收入', + 'issuer_today_stats': '今日数据', + 'issuer_weekly_stats': '本周数据', + 'issuer_monthly_stats': '本月数据', + + // Coupon Management + 'issuer_create_coupon': '创建券', + 'issuer_coupon_template': '券模板', + 'issuer_coupon_status': '券状态', + 'issuer_coupon_active': '生效中', + 'issuer_coupon_paused': '已暂停', + 'issuer_coupon_recalled': '已召回', + + // Redemption + 'issuer_scan_qr': '扫码核销', + 'issuer_manual_redeem': '手动核销', + 'issuer_redeem_history': '核销记录', + 'issuer_redeem_success': '核销成功', + 'issuer_redeem_failed': '核销失败', + + // Store Management + 'store_add': '添加门店', + 'store_edit': '编辑门店', + 'store_name': '门店名称', + 'store_address': '门店地址', + 'store_staff': '员工管理', + 'store_device': '设备管理', + + // Finance + 'finance_settlement': '结算', + 'finance_settlement_pending': '待结算', + 'finance_settlement_completed': '已结算', + 'finance_invoice': '发票', + 'finance_report': '财务报表', + }; + + 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', + + // ── Issuer-specific ── + 'issuer_dashboard': 'Dashboard', + 'issuer_coupons': 'Coupon Management', + 'issuer_redeem': 'Redemption', + 'issuer_finance': 'Finance', + 'issuer_profile': 'My Profile', + 'issuer_onboarding': 'Onboarding', + 'issuer_credit': 'Credit Rating', + 'batch_issue': 'Batch Issue', + 'batch_recall': 'Batch Recall', + 'reconciliation': 'Reconciliation', + 'store_management': 'Store Management', + + // Issuer Dashboard + 'issuer_total_issued': 'Total Issued', + 'issuer_total_redeemed': 'Total Redeemed', + 'issuer_active_coupons': 'In Circulation', + 'issuer_revenue': 'Revenue', + 'issuer_today_stats': 'Today', + 'issuer_weekly_stats': 'This Week', + 'issuer_monthly_stats': 'This Month', + + // Coupon Management + 'issuer_create_coupon': 'Create Coupon', + 'issuer_coupon_template': 'Coupon Template', + 'issuer_coupon_status': 'Coupon Status', + 'issuer_coupon_active': 'Active', + 'issuer_coupon_paused': 'Paused', + 'issuer_coupon_recalled': 'Recalled', + + // Redemption + 'issuer_scan_qr': 'Scan QR Code', + 'issuer_manual_redeem': 'Manual Redemption', + 'issuer_redeem_history': 'Redemption History', + 'issuer_redeem_success': 'Redemption Successful', + 'issuer_redeem_failed': 'Redemption Failed', + + // Store Management + 'store_add': 'Add Store', + 'store_edit': 'Edit Store', + 'store_name': 'Store Name', + 'store_address': 'Store Address', + 'store_staff': 'Staff Management', + 'store_device': 'Device Management', + + // Finance + 'finance_settlement': 'Settlement', + 'finance_settlement_pending': 'Pending Settlement', + 'finance_settlement_completed': 'Settled', + 'finance_invoice': 'Invoice', + 'finance_report': 'Financial Report', + }; + + 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の提案', + + // ── Issuer-specific ── + 'issuer_dashboard': 'ダッシュボード', + 'issuer_coupons': 'クーポン管理', + 'issuer_redeem': '検証', + 'issuer_finance': '財務', + 'issuer_profile': 'マイページ', + 'issuer_onboarding': '申請', + 'issuer_credit': '信用格付け', + 'batch_issue': '一括発行', + 'batch_recall': '一括リコール', + 'reconciliation': '照合', + 'store_management': '店舗管理', + + // Issuer Dashboard + 'issuer_total_issued': '発行総数', + 'issuer_total_redeemed': '検証総数', + 'issuer_active_coupons': '流通中', + 'issuer_revenue': '収益', + 'issuer_today_stats': '本日', + 'issuer_weekly_stats': '今週', + 'issuer_monthly_stats': '今月', + + // Coupon Management + 'issuer_create_coupon': 'クーポン作成', + 'issuer_coupon_template': 'クーポンテンプレート', + 'issuer_coupon_status': 'クーポンステータス', + 'issuer_coupon_active': '有効', + 'issuer_coupon_paused': '一時停止', + 'issuer_coupon_recalled': 'リコール済み', + + // Redemption + 'issuer_scan_qr': 'QRコードスキャン', + 'issuer_manual_redeem': '手動検証', + 'issuer_redeem_history': '検証履歴', + 'issuer_redeem_success': '検証成功', + 'issuer_redeem_failed': '検証失敗', + + // Store Management + 'store_add': '店舗追加', + 'store_edit': '店舗編集', + 'store_name': '店舗名', + 'store_address': '店舗住所', + 'store_staff': 'スタッフ管理', + 'store_device': 'デバイス管理', + + // Finance + 'finance_settlement': '決済', + 'finance_settlement_pending': '未決済', + 'finance_settlement_completed': '決済済み', + 'finance_invoice': '請求書', + 'finance_report': '財務レポート', + }; +} diff --git a/frontend/admin-app/lib/app/issuer_main_shell.dart b/frontend/admin-app/lib/app/issuer_main_shell.dart new file mode 100644 index 0000000..9e3e4b1 --- /dev/null +++ b/frontend/admin-app/lib/app/issuer_main_shell.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'theme/app_colors.dart'; +import '../features/dashboard/presentation/pages/issuer_dashboard_page.dart'; +import '../features/coupon_management/presentation/pages/coupon_list_page.dart'; +import '../features/redemption/presentation/pages/redemption_page.dart'; +import '../features/finance/presentation/pages/finance_page.dart'; +import '../features/settings/presentation/pages/settings_page.dart'; + +/// 发行方主导航Shell +/// +/// 底部5Tab:数据概览 / 券管理 / 核销 / 财务 / 我的 +class IssuerMainShell extends StatefulWidget { + const IssuerMainShell({super.key}); + + @override + State createState() => _IssuerMainShellState(); +} + +class _IssuerMainShellState extends State { + int _currentIndex = 0; + + final _pages = const [ + IssuerDashboardPage(), + CouponListPage(), + RedemptionPage(), + FinancePage(), + SettingsPage(), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: IndexedStack( + index: _currentIndex, + children: _pages, + ), + bottomNavigationBar: NavigationBar( + selectedIndex: _currentIndex, + onDestinationSelected: (index) { + setState(() => _currentIndex = index); + }, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.dashboard_outlined), + selectedIcon: Icon(Icons.dashboard_rounded), + label: '数据概览', + ), + NavigationDestination( + icon: Icon(Icons.confirmation_number_outlined), + selectedIcon: Icon(Icons.confirmation_number_rounded), + label: '券管理', + ), + NavigationDestination( + icon: Icon(Icons.qr_code_scanner_outlined), + selectedIcon: Icon(Icons.qr_code_scanner_rounded), + label: '核销', + ), + NavigationDestination( + icon: Icon(Icons.account_balance_wallet_outlined), + selectedIcon: Icon(Icons.account_balance_wallet_rounded), + label: '财务', + ), + NavigationDestination( + icon: Icon(Icons.settings_outlined), + selectedIcon: Icon(Icons.settings_rounded), + label: '我的', + ), + ], + ), + ); + } +} diff --git a/frontend/admin-app/lib/app/router.dart b/frontend/admin-app/lib/app/router.dart new file mode 100644 index 0000000..39c9356 --- /dev/null +++ b/frontend/admin-app/lib/app/router.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import '../features/auth/presentation/pages/issuer_login_page.dart'; +import '../features/onboarding/presentation/pages/onboarding_page.dart'; +import '../features/dashboard/presentation/pages/issuer_dashboard_page.dart'; +import '../features/coupon_management/presentation/pages/coupon_list_page.dart'; +import '../features/coupon_management/presentation/pages/create_coupon_page.dart'; +import '../features/coupon_management/presentation/pages/coupon_detail_page.dart'; +import '../features/redemption/presentation/pages/redemption_page.dart'; +import '../features/finance/presentation/pages/finance_page.dart'; +import '../features/credit/presentation/pages/credit_page.dart'; +import '../features/ai_agent/presentation/pages/ai_agent_page.dart'; +import '../features/settings/presentation/pages/settings_page.dart'; +import '../features/store_management/presentation/pages/store_management_page.dart'; +import '../app/issuer_main_shell.dart'; + +/// 发行方App路由配置 +class AppRouter { + static const String splash = '/'; + static const String login = '/login'; + static const String onboarding = '/onboarding'; + static const String main = '/main'; + static const String couponList = '/coupons'; + static const String createCoupon = '/coupons/create'; + static const String couponDetail = '/coupons/detail'; + static const String redemption = '/redemption'; + static const String finance = '/finance'; + static const String credit = '/credit'; + static const String aiAgent = '/ai-agent'; + static const String settings = '/settings'; + static const String storeManagement = '/stores'; + + static Route generateRoute(RouteSettings routeSettings) { + switch (routeSettings.name) { + case splash: + case login: + return MaterialPageRoute(builder: (_) => const IssuerLoginPage()); + case onboarding: + return MaterialPageRoute(builder: (_) => const OnboardingPage()); + case main: + return MaterialPageRoute(builder: (_) => const IssuerMainShell()); + case couponList: + return MaterialPageRoute(builder: (_) => const CouponListPage()); + case createCoupon: + return MaterialPageRoute(builder: (_) => const CreateCouponPage()); + case couponDetail: + return MaterialPageRoute(builder: (_) => const IssuerCouponDetailPage()); + case redemption: + return MaterialPageRoute(builder: (_) => const RedemptionPage()); + case finance: + return MaterialPageRoute(builder: (_) => const FinancePage()); + case credit: + return MaterialPageRoute(builder: (_) => const CreditPage()); + case aiAgent: + return MaterialPageRoute(builder: (_) => const AiAgentPage()); + case settings: + return MaterialPageRoute(builder: (_) => const SettingsPage()); + case storeManagement: + return MaterialPageRoute(builder: (_) => const StoreManagementPage()); + default: + return MaterialPageRoute( + builder: (_) => Scaffold( + body: Center(child: Text('Route not found: ${routeSettings.name}')), + ), + ); + } + } +} diff --git a/frontend/admin-app/lib/app/theme/app_colors.dart b/frontend/admin-app/lib/app/theme/app_colors.dart new file mode 100644 index 0000000..673ad00 --- /dev/null +++ b/frontend/admin-app/lib/app/theme/app_colors.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +/// Genex Issuer Console - Color Tokens +/// +/// 企业端主题:沿用紫色主色系,更沉稳的企业风格 +/// Primary: #6C5CE7 (与Consumer App共享品牌色) +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 + 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); + + // Text Colors + static const Color textPrimary = Color(0xFF141723); + static const Color textSecondary = Color(0xFF5C6478); + static const Color textTertiary = Color(0xFFA0A8BE); + static const Color textOnPrimary = Color(0xFFFFFFFF); + + // Border Colors + static const Color border = Color(0xFFE4E7F0); + static const Color borderLight = Color(0xFFF1F3F8); + + // Issuer Tier Colors (发行方层级) + static const Color tierSilver = Color(0xFFA0A8BE); + static const Color tierGold = Color(0xFFFFAB2E); + static const Color tierPlatinum = Color(0xFF3B82F6); + static const Color tierDiamond = Color(0xFF6C5CE7); + + // 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); + + // Gradients + 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)], + ); +} diff --git a/frontend/admin-app/lib/app/theme/app_spacing.dart b/frontend/admin-app/lib/app/theme/app_spacing.dart new file mode 100644 index 0000000..85a8c83 --- /dev/null +++ b/frontend/admin-app/lib/app/theme/app_spacing.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; + +/// Genex Issuer Console - 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), + ), + ]; + + // ============================================================ + // 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; +} diff --git a/frontend/admin-app/lib/app/theme/app_theme.dart b/frontend/admin-app/lib/app/theme/app_theme.dart new file mode 100644 index 0000000..1611941 --- /dev/null +++ b/frontend/admin-app/lib/app/theme/app_theme.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'app_colors.dart'; + +/// Genex Issuer Console - Material 3 Theme +/// +/// 企业端主题:更沉稳的企业管理风格 +class AppTheme { + AppTheme._(); + + static ThemeData get light => ThemeData( + useMaterial3: true, + brightness: Brightness.light, + colorScheme: const ColorScheme.light( + primary: AppColors.primary, + primaryContainer: AppColors.primaryContainer, + secondary: AppColors.success, + error: AppColors.error, + surface: AppColors.surface, + onPrimary: Colors.white, + onSurface: AppColors.textPrimary, + outline: AppColors.border, + ), + scaffoldBackgroundColor: AppColors.background, + 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, + ), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: AppColors.surface, + indicatorColor: AppColors.primaryContainer, + surfaceTintColor: Colors.transparent, + elevation: 0, + height: 80, + labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, + ), + cardTheme: CardTheme( + elevation: 0, + color: AppColors.surface, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: AppColors.borderLight, width: 1), + ), + margin: EdgeInsets.zero, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + elevation: 0, + minimumSize: const Size(double.infinity, 52), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: AppColors.gray50, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.borderLight, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary, width: 1.5), + ), + ), + ); +} diff --git a/frontend/admin-app/lib/app/theme/app_typography.dart b/frontend/admin-app/lib/app/theme/app_typography.dart new file mode 100644 index 0000000..c3375d1 --- /dev/null +++ b/frontend/admin-app/lib/app/theme/app_typography.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'app_colors.dart'; + +/// Genex Issuer Console - Typography Tokens +/// +/// 企业端字体层级:清晰、专业、易读 +/// 基于 SF Pro (iOS) / Roboto (Android) 系统字体 +class AppTypography { + AppTypography._(); + + // ============================================================ + // 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, + ); +} diff --git a/frontend/admin-app/lib/features/ai_agent/presentation/pages/ai_agent_page.dart b/frontend/admin-app/lib/features/ai_agent/presentation/pages/ai_agent_page.dart new file mode 100644 index 0000000..5842faf --- /dev/null +++ b/frontend/admin-app/lib/features/ai_agent/presentation/pages/ai_agent_page.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; + +/// 发行方AI Agent对话页面 +/// +/// 场景:发券建议、定价优化、信用提升、销售分析、额度规划、合规助手 +class AiAgentPage extends StatefulWidget { + const AiAgentPage({super.key}); + + @override + State createState() => _AiAgentPageState(); +} + +class _AiAgentPageState extends State { + final _messageController = TextEditingController(); + final _scrollController = ScrollController(); + + final List<_ChatMessage> _messages = [ + _ChatMessage( + isAi: true, + text: '您好!我是 Genex AI 助手,可以帮您分析销售数据、优化定价策略、提升信用评级。有什么可以帮您的吗?', + ), + ]; + + final _quickActions = [ + '分析本月销售数据', + '推荐最优发券时间', + '如何提升信用评级?', + '额度使用情况分析', + ]; + + @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 助手'), + ], + ), + ), + body: Column( + children: [ + // Messages + Expanded( + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(16), + itemCount: _messages.length, + itemBuilder: (context, index) => _buildMessageBubble(_messages[index]), + ), + ), + + // Quick Actions (shown when few messages) + if (_messages.length <= 2) + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: _quickActions.map((action) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: ActionChip( + label: Text(action, style: const TextStyle(fontSize: 12)), + onPressed: () => _sendMessage(action), + backgroundColor: AppColors.primarySurface, + side: BorderSide.none, + ), + ); + }).toList(), + ), + ), + + // Input Area + 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: _messageController, + decoration: InputDecoration( + hintText: '输入问题...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: const BorderSide(color: AppColors.borderLight), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + onSubmitted: _sendMessage, + ), + ), + 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: () => _sendMessage(_messageController.text), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildMessageBubble(_ChatMessage 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: TextStyle( + fontSize: 14, + color: msg.isAi ? AppColors.textPrimary : Colors.white, + height: 1.5, + ), + ), + ), + ), + ], + ), + ); + } + + void _sendMessage(String text) { + if (text.trim().isEmpty) return; + setState(() { + _messages.add(_ChatMessage(isAi: false, text: text)); + _messageController.clear(); + }); + // Simulate AI response + Future.delayed(const Duration(milliseconds: 800), () { + if (mounted) { + setState(() { + _messages.add(_ChatMessage( + isAi: true, + text: '正在分析您的数据...\n\n根据过去30天的销售数据,您的 ¥25 礼品卡表现最佳,建议在周五下午发布新券以获得最大曝光。当前定价 \$21.25 处于最优区间。', + )); + }); + } + }); + } +} + +class _ChatMessage { + final bool isAi; + final String text; + + _ChatMessage({required this.isAi, required this.text}); +} diff --git a/frontend/admin-app/lib/features/auth/presentation/pages/issuer_login_page.dart b/frontend/admin-app/lib/features/auth/presentation/pages/issuer_login_page.dart new file mode 100644 index 0000000..5568c78 --- /dev/null +++ b/frontend/admin-app/lib/features/auth/presentation/pages/issuer_login_page.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/router.dart'; + +/// 发行方登录页 +/// +/// 企业账号登录:手机号+验证码 / 账号密码 +/// 新发行方引导至入驻流程 +class IssuerLoginPage extends StatefulWidget { + const IssuerLoginPage({super.key}); + + @override + State createState() => _IssuerLoginPageState(); +} + +class _IssuerLoginPageState extends State { + final _phoneController = TextEditingController(); + final _codeController = TextEditingController(); + bool _agreedToTerms = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 60), + + // Logo + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(14), + ), + child: const Icon(Icons.storefront_rounded, color: Colors.white, size: 28), + ), + const SizedBox(height: 24), + + // Title + const Text( + 'Genex 发行方控制台', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: AppColors.textPrimary), + ), + const SizedBox(height: 8), + const Text( + '登录您的企业账号管理券发行', + style: TextStyle(fontSize: 15, color: AppColors.textSecondary), + ), + const SizedBox(height: 40), + + // Phone Input + TextField( + controller: _phoneController, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + labelText: '手机号', + prefixIcon: Icon(Icons.phone_outlined), + hintText: '请输入企业管理员手机号', + ), + ), + const SizedBox(height: 16), + + // Verification Code + Row( + children: [ + Expanded( + child: TextField( + controller: _codeController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: '验证码', + prefixIcon: Icon(Icons.lock_outline_rounded), + ), + ), + ), + const SizedBox(width: 12), + SizedBox( + height: 52, + child: OutlinedButton( + onPressed: () {}, + child: const Text('获取验证码'), + ), + ), + ], + ), + const SizedBox(height: 24), + + // Terms + Row( + children: [ + Checkbox( + value: _agreedToTerms, + onChanged: (v) => setState(() => _agreedToTerms = v ?? false), + activeColor: AppColors.primary, + ), + Expanded( + child: Text.rich( + TextSpan( + text: '我已阅读并同意', + style: const TextStyle(fontSize: 13, color: AppColors.textSecondary), + children: [ + TextSpan( + text: '《发行方服务协议》', + style: const TextStyle(color: AppColors.primary), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 24), + + // Login Button + SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: _agreedToTerms + ? () => Navigator.pushReplacementNamed(context, AppRouter.main) + : null, + child: const Text('登录', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + ), + ), + const SizedBox(height: 16), + + // Register + Center( + child: TextButton( + onPressed: () => Navigator.pushNamed(context, AppRouter.onboarding), + child: const Text('还没有账号?申请入驻'), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/admin-app/lib/features/coupon_management/presentation/pages/batch_operations_page.dart b/frontend/admin-app/lib/features/coupon_management/presentation/pages/batch_operations_page.dart new file mode 100644 index 0000000..0dd62db --- /dev/null +++ b/frontend/admin-app/lib/features/coupon_management/presentation/pages/batch_operations_page.dart @@ -0,0 +1,1055 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// 批量操作页面 +/// +/// 支持批量发行、批量召回、批量调价三大操作 +/// 每个操作支持预览、确认、进度跟踪 +/// 底部展示操作历史记录 +class BatchOperationsPage extends StatefulWidget { + const BatchOperationsPage({super.key}); + + @override + State createState() => _BatchOperationsPageState(); +} + +class _BatchOperationsPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + // Batch issue state + String? _selectedTemplate; + int _issueQuantity = 500; + bool _customQuantity = false; + final _customQuantityController = TextEditingController(); + DateTimeRange? _dateRange; + + // Batch recall state + String _recallCategory = '全部'; + String _recallStatus = '全部'; + bool _selectAll = false; + final _recallReasonController = TextEditingController(); + final Set _selectedRecallItems = {}; + + // Batch price adjustment state + double _priceAdjustment = 0.0; + + // Ongoing operation + bool _isOperationRunning = false; + double _operationProgress = 0.0; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + _customQuantityController.dispose(); + _recallReasonController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('批量操作'), + actions: [ + IconButton( + icon: const Icon(Icons.history_rounded), + onPressed: () {}, + tooltip: '操作历史', + ), + ], + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: '批量发行'), + Tab(text: '批量召回'), + Tab(text: '批量调价'), + ], + ), + ), + body: Column( + children: [ + // Progress indicator for ongoing operations + if (_isOperationRunning) _buildProgressIndicator(), + + // Tab content + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildBatchIssueTab(), + _buildBatchRecallTab(), + _buildBatchPriceAdjustTab(), + ], + ), + ), + ], + ), + ); + } + + // ============================================================ + // Progress Indicator + // ============================================================ + + Widget _buildProgressIndicator() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + color: AppColors.primarySurface, + child: Row( + children: [ + const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(AppColors.primary), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '批量操作进行中...', + style: AppTypography.labelMedium.copyWith(color: AppColors.primary), + ), + const SizedBox(height: 4), + ClipRRect( + borderRadius: AppSpacing.borderRadiusSm, + child: LinearProgressIndicator( + value: _operationProgress, + backgroundColor: AppColors.primaryContainer, + valueColor: const AlwaysStoppedAnimation(AppColors.primary), + minHeight: 6, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + Text( + '${(_operationProgress * 100).toInt()}%', + style: AppTypography.labelMedium.copyWith(color: AppColors.primary), + ), + ], + ), + ); + } + + // ============================================================ + // Tab 1: 批量发行 + // ============================================================ + + Widget _buildBatchIssueTab() { + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Template Selector + _buildSectionTitle('选择券模板'), + const SizedBox(height: 12), + _buildTemplateSelector(), + const SizedBox(height: 24), + + // Quantity Input + _buildSectionTitle('发行数量'), + const SizedBox(height: 12), + _buildQuantitySelector(), + const SizedBox(height: 24), + + // Date Range Picker + _buildSectionTitle('有效期范围'), + const SizedBox(height: 12), + _buildDateRangePicker(), + const SizedBox(height: 24), + + // Preview Summary + _buildIssueSummary(), + const SizedBox(height: 20), + + // Execute Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _selectedTemplate != null ? _executeBatchIssue : null, + child: const Text('确认批量发行'), + ), + ), + const SizedBox(height: 32), + + // Operation History + _buildOperationHistory(), + ], + ), + ); + } + + Widget _buildTemplateSelector() { + final templates = [ + ('折扣券', Icons.percent_rounded, AppColors.error), + ('代金券', Icons.money_rounded, AppColors.primary), + ('礼品卡', Icons.card_giftcard_rounded, AppColors.info), + ('储值券', Icons.account_balance_wallet_rounded, AppColors.success), + ]; + + return Wrap( + spacing: 12, + runSpacing: 12, + children: templates.map((t) { + final (name, icon, color) = t; + final isSelected = _selectedTemplate == name; + return GestureDetector( + onTap: () => setState(() => _selectedTemplate = name), + child: Container( + width: (MediaQuery.of(context).size.width - 52) / 2, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: isSelected ? color.withValues(alpha: 0.08) : AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all( + color: isSelected ? color : AppColors.borderLight, + width: isSelected ? 1.5 : 1, + ), + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + ), + child: Icon(icon, color: color, size: 18), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + name, + style: AppTypography.labelMedium.copyWith( + color: isSelected ? color : AppColors.textPrimary, + ), + ), + ), + if (isSelected) + Icon(Icons.check_circle_rounded, color: color, size: 18), + ], + ), + ), + ); + }).toList(), + ); + } + + Widget _buildQuantitySelector() { + final presets = [100, 500, 1000]; + return Column( + children: [ + Row( + children: [ + ...presets.map((q) { + final isSelected = !_customQuantity && _issueQuantity == q; + return Expanded( + child: Padding( + padding: EdgeInsets.only(right: q != 1000 ? 10 : 0), + child: GestureDetector( + onTap: () => setState(() { + _customQuantity = false; + _issueQuantity = q; + }), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: isSelected ? AppColors.primary : AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + border: Border.all( + color: isSelected ? AppColors.primary : AppColors.borderLight, + ), + ), + child: Center( + child: Text( + '$q', + style: AppTypography.labelMedium.copyWith( + color: isSelected ? Colors.white : AppColors.textPrimary, + ), + ), + ), + ), + ), + ), + ); + }), + ], + ), + const SizedBox(height: 10), + GestureDetector( + onTap: () => setState(() => _customQuantity = true), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + color: _customQuantity ? AppColors.primarySurface : AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + border: Border.all( + color: _customQuantity ? AppColors.primary : AppColors.borderLight, + ), + ), + child: _customQuantity + ? TextField( + controller: _customQuantityController, + keyboardType: TextInputType.number, + autofocus: true, + decoration: const InputDecoration( + hintText: '输入自定义数量', + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + style: AppTypography.labelMedium, + onChanged: (v) { + final parsed = int.tryParse(v); + if (parsed != null) { + setState(() => _issueQuantity = parsed); + } + }, + ) + : Center( + child: Text( + '自定义数量', + style: AppTypography.labelMedium.copyWith( + color: AppColors.textSecondary, + ), + ), + ), + ), + ), + ], + ); + } + + Widget _buildDateRangePicker() { + return GestureDetector( + onTap: () async { + final picked = await showDateRangePicker( + context: context, + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: Theme.of(context).colorScheme.copyWith( + primary: AppColors.primary, + ), + ), + child: child!, + ); + }, + ); + if (picked != null) { + setState(() => _dateRange = picked); + } + }, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + const Icon(Icons.date_range_rounded, color: AppColors.textSecondary, size: 20), + const SizedBox(width: 12), + Expanded( + child: Text( + _dateRange != null + ? '${_formatDate(_dateRange!.start)} ~ ${_formatDate(_dateRange!.end)}' + : '点击选择有效期范围', + style: AppTypography.bodyMedium.copyWith( + color: _dateRange != null ? AppColors.textPrimary : AppColors.textTertiary, + ), + ), + ), + const Icon(Icons.chevron_right_rounded, color: AppColors.textTertiary), + ], + ), + ), + ); + } + + Widget _buildIssueSummary() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.preview_rounded, color: AppColors.primary, size: 18), + const SizedBox(width: 8), + Text('发行预览', style: AppTypography.labelMedium.copyWith(color: AppColors.primary)), + ], + ), + const SizedBox(height: 12), + _buildSummaryRow('券模板', _selectedTemplate ?? '未选择'), + _buildSummaryRow('发行数量', '$_issueQuantity 张'), + _buildSummaryRow( + '有效期', + _dateRange != null + ? '${_formatDate(_dateRange!.start)} ~ ${_formatDate(_dateRange!.end)}' + : '未设置', + ), + _buildSummaryRow( + '预估总面值', + _selectedTemplate != null ? '\$${(_issueQuantity * 25).toStringAsFixed(0)}' : '--', + ), + ], + ), + ); + } + + Widget _buildSummaryRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.bodySmall), + Text( + value, + style: AppTypography.labelSmall.copyWith(color: AppColors.textPrimary), + ), + ], + ), + ); + } + + void _executeBatchIssue() { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('确认批量发行'), + content: Text('将发行 $_issueQuantity 张 $_selectedTemplate,确认执行?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(ctx); + _startOperation(); + }, + child: const Text('确认'), + ), + ], + ), + ); + } + + // ============================================================ + // Tab 2: 批量召回 + // ============================================================ + + Widget _buildBatchRecallTab() { + final categories = ['全部', '折扣券', '代金券', '礼品卡', '储值券']; + final statuses = ['全部', '在售中', '已售出', '已过期']; + + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Filter by category + _buildSectionTitle('按类别筛选'), + const SizedBox(height: 12), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: categories.map((c) { + final isSelected = _recallCategory == c; + return Padding( + padding: const EdgeInsets.only(right: 8), + child: FilterChip( + label: Text(c), + selected: isSelected, + onSelected: (_) => setState(() => _recallCategory = c), + selectedColor: AppColors.primaryContainer, + checkmarkColor: AppColors.primary, + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 16), + + // Filter by status + _buildSectionTitle('按状态筛选'), + const SizedBox(height: 12), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: statuses.map((s) { + final isSelected = _recallStatus == s; + return Padding( + padding: const EdgeInsets.only(right: 8), + child: FilterChip( + label: Text(s), + selected: isSelected, + onSelected: (_) => setState(() => _recallStatus = s), + selectedColor: AppColors.primaryContainer, + checkmarkColor: AppColors.primary, + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 20), + + // Select all / deselect + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('符合条件的券 (${_mockRecallCoupons.length})', style: AppTypography.labelMedium), + Row( + children: [ + TextButton( + onPressed: () => setState(() { + _selectAll = true; + _selectedRecallItems.addAll( + List.generate(_mockRecallCoupons.length, (i) => i), + ); + }), + child: const Text('全选'), + ), + TextButton( + onPressed: () => setState(() { + _selectAll = false; + _selectedRecallItems.clear(); + }), + child: const Text('取消全选'), + ), + ], + ), + ], + ), + const SizedBox(height: 8), + + // Coupon list for recall + ...List.generate(_mockRecallCoupons.length, (index) { + final coupon = _mockRecallCoupons[index]; + final isSelected = _selectedRecallItems.contains(index); + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: isSelected ? AppColors.errorLight : AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all( + color: isSelected ? AppColors.error.withValues(alpha: 0.3) : AppColors.borderLight, + ), + ), + child: Row( + children: [ + Checkbox( + value: isSelected, + onChanged: (v) => setState(() { + if (v == true) { + _selectedRecallItems.add(index); + } else { + _selectedRecallItems.remove(index); + } + }), + activeColor: AppColors.error, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(coupon.name, style: AppTypography.labelMedium), + const SizedBox(height: 2), + Text( + '${coupon.category} · 剩余 ${coupon.remaining} 张', + style: AppTypography.bodySmall, + ), + ], + ), + ), + _buildStatusBadge(coupon.status), + ], + ), + ); + }), + const SizedBox(height: 20), + + // Reason input + _buildSectionTitle('召回原因'), + const SizedBox(height: 12), + TextField( + controller: _recallReasonController, + maxLines: 3, + decoration: const InputDecoration( + hintText: '请输入批量召回原因(必填)', + ), + ), + const SizedBox(height: 20), + + // Recall confirmation + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _selectedRecallItems.isNotEmpty && + _recallReasonController.text.isNotEmpty + ? _executeBatchRecall + : null, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.error, + foregroundColor: Colors.white, + ), + child: Text('确认召回 (${_selectedRecallItems.length} 张)'), + ), + ), + const SizedBox(height: 32), + + // Operation History + _buildOperationHistory(), + ], + ), + ); + } + + void _executeBatchRecall() { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Row( + children: [ + Icon(Icons.warning_rounded, color: AppColors.error, size: 22), + const SizedBox(width: 8), + const Text('确认批量召回'), + ], + ), + content: Text( + '将召回 ${_selectedRecallItems.length} 张券,此操作不可撤销。\n\n原因:${_recallReasonController.text}', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(ctx); + _startOperation(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.error, + foregroundColor: Colors.white, + ), + child: const Text('确认召回'), + ), + ], + ), + ); + } + + // ============================================================ + // Tab 3: 批量调价 + // ============================================================ + + Widget _buildBatchPriceAdjustTab() { + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Percentage adjustment slider + _buildSectionTitle('价格调整比例'), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + Text( + '${_priceAdjustment > 0 ? '+' : ''}${_priceAdjustment.toStringAsFixed(0)}%', + style: AppTypography.h1.copyWith( + color: _priceAdjustment > 0 + ? AppColors.success + : _priceAdjustment < 0 + ? AppColors.error + : AppColors.textPrimary, + ), + ), + const SizedBox(height: 16), + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: _priceAdjustment >= 0 ? AppColors.success : AppColors.error, + inactiveTrackColor: AppColors.gray200, + thumbColor: _priceAdjustment >= 0 ? AppColors.success : AppColors.error, + overlayColor: (_priceAdjustment >= 0 ? AppColors.success : AppColors.error) + .withValues(alpha: 0.1), + ), + child: Slider( + value: _priceAdjustment, + min: -20, + max: 20, + divisions: 40, + label: '${_priceAdjustment > 0 ? '+' : ''}${_priceAdjustment.toStringAsFixed(0)}%', + onChanged: (v) => setState(() => _priceAdjustment = v), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('-20%', style: AppTypography.caption), + Text('0%', style: AppTypography.caption), + Text('+20%', style: AppTypography.caption), + ], + ), + ], + ), + ), + const SizedBox(height: 24), + + // Affected coupons preview + _buildSectionTitle('受影响的券'), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + _buildPriceAdjustItem('¥25 星巴克礼品卡', 21.25, _priceAdjustment), + const Divider(height: 20), + _buildPriceAdjustItem('¥100 购物代金券', 85.00, _priceAdjustment), + const Divider(height: 20), + _buildPriceAdjustItem('8折餐饮折扣券', 40.00, _priceAdjustment), + const Divider(height: 20), + _buildPriceAdjustItem('¥200 储值卡', 170.00, _priceAdjustment), + ], + ), + ), + const SizedBox(height: 16), + + // Impact summary + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: _priceAdjustment < 0 ? AppColors.warningLight : AppColors.successLight, + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + ), + child: Row( + children: [ + Icon( + _priceAdjustment < 0 + ? Icons.trending_down_rounded + : Icons.trending_up_rounded, + color: _priceAdjustment < 0 ? AppColors.warning : AppColors.success, + size: 20, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + _priceAdjustment < 0 + ? '降价 ${_priceAdjustment.abs().toStringAsFixed(0)}% 预计可提升销量 ${(_priceAdjustment.abs() * 1.5).toStringAsFixed(0)}%' + : _priceAdjustment > 0 + ? '涨价 ${_priceAdjustment.toStringAsFixed(0)}% 预计利润提升 ${(_priceAdjustment * 0.8).toStringAsFixed(0)}%' + : '当前价格不变', + style: AppTypography.bodySmall.copyWith( + color: _priceAdjustment < 0 ? AppColors.warning : AppColors.success, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // Confirmation button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _priceAdjustment != 0 ? _executeBatchPriceAdjust : null, + child: const Text('确认批量调价'), + ), + ), + const SizedBox(height: 32), + + // Operation History + _buildOperationHistory(), + ], + ), + ); + } + + Widget _buildPriceAdjustItem(String name, double currentPrice, double adjustPercent) { + final newPrice = currentPrice * (1 + adjustPercent / 100); + final diff = newPrice - currentPrice; + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: AppTypography.labelMedium), + const SizedBox(height: 2), + Text('当前价: \$${currentPrice.toStringAsFixed(2)}', style: AppTypography.bodySmall), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '\$${newPrice.toStringAsFixed(2)}', + style: AppTypography.labelMedium.copyWith( + color: diff > 0 + ? AppColors.success + : diff < 0 + ? AppColors.error + : AppColors.textPrimary, + ), + ), + Text( + '${diff >= 0 ? '+' : ''}\$${diff.toStringAsFixed(2)}', + style: AppTypography.caption.copyWith( + color: diff > 0 + ? AppColors.success + : diff < 0 + ? AppColors.error + : AppColors.textTertiary, + ), + ), + ], + ), + ], + ); + } + + void _executeBatchPriceAdjust() { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('确认批量调价'), + content: Text( + '将对 4 种券批量调价 ${_priceAdjustment > 0 ? '+' : ''}${_priceAdjustment.toStringAsFixed(0)}%,确认执行?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(ctx); + _startOperation(); + }, + child: const Text('确认'), + ), + ], + ), + ); + } + + // ============================================================ + // Operation History + // ============================================================ + + Widget _buildOperationHistory() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('操作历史', style: AppTypography.h3), + TextButton( + onPressed: () {}, + child: const Text('查看全部'), + ), + ], + ), + const SizedBox(height: 12), + ...(_mockHistory.map((h) { + final (type, count, status, date, icon, statusColor) = h; + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + ), + child: Icon(icon, color: AppColors.textSecondary, size: 18), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(type, style: AppTypography.labelMedium), + const SizedBox(height: 2), + Text('$count 张 · $date', style: AppTypography.caption), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppSpacing.radiusFull), + ), + child: Text( + status, + style: AppTypography.caption.copyWith( + color: statusColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + })), + ], + ); + } + + // ============================================================ + // Shared Helpers + // ============================================================ + + Widget _buildSectionTitle(String title) { + return Text(title, style: AppTypography.h3); + } + + Widget _buildStatusBadge(String status) { + Color color; + switch (status) { + case '在售中': + color = AppColors.success; + break; + case '已售出': + color = AppColors.info; + break; + case '已过期': + color = AppColors.textTertiary; + break; + default: + color = AppColors.warning; + } + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppSpacing.radiusFull), + ), + child: Text( + status, + style: AppTypography.caption.copyWith(color: color, fontWeight: FontWeight.w600), + ), + ); + } + + String _formatDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + } + + void _startOperation() { + setState(() { + _isOperationRunning = true; + _operationProgress = 0.0; + }); + // Simulate progress + Future.doWhile(() async { + await Future.delayed(const Duration(milliseconds: 200)); + if (!mounted) return false; + setState(() { + _operationProgress += 0.05; + }); + if (_operationProgress >= 1.0) { + setState(() { + _isOperationRunning = false; + _operationProgress = 0.0; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('批量操作完成'), + backgroundColor: AppColors.success, + ), + ); + } + return false; + } + return true; + }); + } +} + +// ============================================================ +// Mock Data +// ============================================================ + +class _RecallCoupon { + final String name; + final String category; + final String status; + final int remaining; + + const _RecallCoupon({ + required this.name, + required this.category, + required this.status, + required this.remaining, + }); +} + +const _mockRecallCoupons = [ + _RecallCoupon(name: '¥25 星巴克礼品卡', category: '礼品卡', status: '在售中', remaining: 800), + _RecallCoupon(name: '¥100 购物代金券', category: '代金券', status: '在售中', remaining: 420), + _RecallCoupon(name: '8折餐饮折扣券', category: '折扣券', status: '已售出', remaining: 150), + _RecallCoupon(name: '¥200 储值卡', category: '储值券', status: '已过期', remaining: 80), + _RecallCoupon(name: '¥50 生活券', category: '代金券', status: '在售中', remaining: 330), +]; + +const _mockHistory = [ + ('批量发行 - 代金券', 1000, '已完成', '2026-02-08', Icons.add_circle_outline_rounded, AppColors.success), + ('批量召回 - 折扣券', 200, '已完成', '2026-02-05', Icons.remove_circle_outline_rounded, AppColors.success), + ('批量调价 - 礼品卡', 500, '进行中', '2026-02-03', Icons.price_change_rounded, AppColors.warning), + ('批量发行 - 储值券', 2000, '已完成', '2026-01-28', Icons.add_circle_outline_rounded, AppColors.success), + ('批量召回 - 代金券', 150, '失败', '2026-01-25', Icons.remove_circle_outline_rounded, AppColors.error), +]; diff --git a/frontend/admin-app/lib/features/coupon_management/presentation/pages/coupon_detail_page.dart b/frontend/admin-app/lib/features/coupon_management/presentation/pages/coupon_detail_page.dart new file mode 100644 index 0000000..ba5e667 --- /dev/null +++ b/frontend/admin-app/lib/features/coupon_management/presentation/pages/coupon_detail_page.dart @@ -0,0 +1,295 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; + +/// 发行方券详情页 +/// +/// 展示单批次券的详细数据:销量、核销率、二级市场分析 +/// 操作:下架、召回、退款、增发 +class IssuerCouponDetailPage extends StatelessWidget { + const IssuerCouponDetailPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('券详情'), + actions: [ + PopupMenuButton( + onSelected: (value) { + if (value == 'recall') _showRecallDialog(context); + if (value == 'delist') _showDelistDialog(context); + }, + itemBuilder: (ctx) => [ + const PopupMenuItem(value: 'edit', child: Text('编辑信息')), + const PopupMenuItem(value: 'reissue', child: Text('增发')), + const PopupMenuItem(value: 'delist', child: Text('下架')), + const PopupMenuItem(value: 'recall', child: Text('召回未售出')), + ], + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Card + _buildHeaderCard(), + const SizedBox(height: 20), + + // Sales Data + _buildSalesDataCard(), + const SizedBox(height: 20), + + // Secondary Market Analysis + _buildSecondaryMarketCard(), + const SizedBox(height: 20), + + // Financing Effect + _buildFinancingEffectCard(), + const SizedBox(height: 20), + + // Redemption Timeline + _buildRedemptionTimeline(), + ], + ), + ), + ); + } + + Widget _buildHeaderCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Expanded( + child: Text( + '¥25 星巴克礼品卡', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Colors.white), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(999), + ), + child: const Text('在售中', style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w600)), + ), + ], + ), + const SizedBox(height: 6), + Text( + '礼品卡 · 面值 \$25 · 发行价 \$21.25', + style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.8)), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildHeaderStat('发行量', '5,000'), + _buildHeaderStat('已售', '4,200'), + _buildHeaderStat('已核销', '3,300'), + _buildHeaderStat('核销率', '78.5%'), + ], + ), + ], + ), + ); + } + + Widget _buildHeaderStat(String label, String value) { + return Column( + children: [ + Text(value, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Colors.white)), + const SizedBox(height: 2), + Text(label, style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: 0.7))), + ], + ); + } + + Widget _buildSalesDataCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('销售数据', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + _buildDataRow('销售收入', '\$89,250'), + _buildDataRow('Breakage收入(过期券)', '\$3,400'), + _buildDataRow('平台手续费', '-\$1,070'), + const Divider(height: 24), + _buildDataRow('净收入', '\$91,580', bold: true), + const SizedBox(height: 16), + // Chart placeholder + Container( + height: 120, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: BorderRadius.circular(8), + ), + child: const Center(child: Text('日销量趋势', style: TextStyle(color: AppColors.textTertiary))), + ), + ], + ), + ); + } + + Widget _buildSecondaryMarketCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('二级市场分析', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + _buildDataRow('挂单数', '128'), + _buildDataRow('平均转售价', '\$22.80'), + _buildDataRow('平均折扣率', '91.2%'), + _buildDataRow('转售成交量', '856'), + _buildDataRow('转售成交额', '\$19,517'), + const SizedBox(height: 16), + Container( + height: 100, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: BorderRadius.circular(8), + ), + child: const Center(child: Text('价格走势K线', style: TextStyle(color: AppColors.textTertiary))), + ), + ], + ), + ); + } + + Widget _buildFinancingEffectCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('融资效果', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + _buildDataRow('现金提前回笼', '\$89,250'), + _buildDataRow('平均提前回笼天数', '45 天'), + _buildDataRow('融资成本', '\$4,463'), + _buildDataRow('等效年利率', '3.6%'), + ], + ), + ); + } + + Widget _buildRedemptionTimeline() { + final events = [ + ('核销 5 张 · 门店A', '10分钟前', AppColors.success), + ('核销 2 张 · 门店B', '25分钟前', AppColors.success), + ('退款 1 张 · 自动退款', '1小时前', AppColors.warning), + ('核销 8 张 · 门店A', '2小时前', AppColors.success), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('最近核销记录', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + ...events.map((e) { + final (desc, time, color) = e; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + Container(width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), + const SizedBox(width: 12), + Expanded(child: Text(desc, style: const TextStyle(fontSize: 13))), + Text(time, style: const TextStyle(fontSize: 11, color: AppColors.textTertiary)), + ], + ), + ); + }), + ], + ), + ); + } + + Widget _buildDataRow(String label, String value, {bool bold = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontSize: 13, color: AppColors.textSecondary)), + Text(value, style: TextStyle(fontSize: 14, fontWeight: bold ? FontWeight.w700 : FontWeight.w500)), + ], + ), + ); + } + + void _showRecallDialog(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('召回未售出券'), + content: const Text('确认召回所有未售出的券?此操作不可逆。'), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')), + ElevatedButton(onPressed: () => Navigator.pop(ctx), child: const Text('确认召回')), + ], + ), + ); + } + + void _showDelistDialog(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('紧急下架'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('确认下架此券?下架后消费者将无法购买。'), + const SizedBox(height: 16), + TextField(decoration: const InputDecoration(labelText: '下架原因')), + ], + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')), + ElevatedButton( + onPressed: () => Navigator.pop(ctx), + style: ElevatedButton.styleFrom(backgroundColor: AppColors.error), + child: const Text('确认下架'), + ), + ], + ), + ); + } +} diff --git a/frontend/admin-app/lib/features/coupon_management/presentation/pages/coupon_list_page.dart b/frontend/admin-app/lib/features/coupon_management/presentation/pages/coupon_list_page.dart new file mode 100644 index 0000000..65cb531 --- /dev/null +++ b/frontend/admin-app/lib/features/coupon_management/presentation/pages/coupon_list_page.dart @@ -0,0 +1,227 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/router.dart'; + +/// 券管理 - 列表页 +/// +/// 展示发行方所有券批次,支持按状态筛选 +/// 顶部AI建议条 + FAB创建新券 +class CouponListPage extends StatelessWidget { + const CouponListPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('券管理'), + actions: [ + IconButton(icon: const Icon(Icons.search_rounded), onPressed: () {}), + IconButton(icon: const Icon(Icons.filter_list_rounded), onPressed: () {}), + ], + ), + body: Column( + children: [ + // AI Suggestion + _buildAiSuggestion(context), + + // Filter Chips + _buildFilterChips(), + + // Coupon List + Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: _mockCoupons.length, + itemBuilder: (context, index) { + final coupon = _mockCoupons[index]; + return _buildCouponItem(context, coupon); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () => Navigator.pushNamed(context, AppRouter.createCoupon), + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + icon: const Icon(Icons.add_rounded), + label: const Text('发券'), + ), + ); + } + + Widget _buildAiSuggestion(BuildContext context) { + return Container( + margin: const EdgeInsets.fromLTRB(20, 8, 20, 8), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + const Icon(Icons.auto_awesome_rounded, color: AppColors.primary, size: 18), + const SizedBox(width: 8), + const Expanded( + child: Text( + '建议:周末发行餐饮券销量通常提升30%', + style: TextStyle(fontSize: 12, color: AppColors.primary), + ), + ), + GestureDetector( + onTap: () {}, + child: const Icon(Icons.close, size: 16, color: AppColors.textTertiary), + ), + ], + ), + ); + } + + Widget _buildFilterChips() { + final filters = ['全部', '在售中', '已售罄', '待审核', '已下架']; + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: Row( + children: filters.map((f) { + final isSelected = f == '全部'; + return Padding( + padding: const EdgeInsets.only(right: 8), + child: FilterChip( + label: Text(f), + selected: isSelected, + onSelected: (_) {}, + selectedColor: AppColors.primaryContainer, + checkmarkColor: AppColors.primary, + ), + ); + }).toList(), + ), + ); + } + + Widget _buildCouponItem(BuildContext context, _MockCoupon coupon) { + return GestureDetector( + onTap: () => Navigator.pushNamed(context, AppRouter.couponDetail), + child: Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: BorderRadius.circular(10), + ), + child: const Icon(Icons.confirmation_number_rounded, color: AppColors.primary, size: 22), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + coupon.name, + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 2), + Text( + '${coupon.template} · 面值 \$${coupon.faceValue}', + style: const TextStyle(fontSize: 12, color: AppColors.textSecondary), + ), + ], + ), + ), + _buildStatusBadge(coupon.status), + ], + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildMiniStat('发行量', '${coupon.issued}'), + _buildMiniStat('已售', '${coupon.sold}'), + _buildMiniStat('已核销', '${coupon.redeemed}'), + _buildMiniStat('核销率', '${coupon.redemptionRate}%'), + ], + ), + ], + ), + ), + ); + } + + Widget _buildStatusBadge(String status) { + Color color; + switch (status) { + case '在售中': + color = AppColors.success; + break; + case '待审核': + color = AppColors.warning; + break; + case '已售罄': + color = AppColors.info; + break; + default: + color = AppColors.textTertiary; + } + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(999), + ), + child: Text(status, style: TextStyle(fontSize: 11, color: color, fontWeight: FontWeight.w600)), + ); + } + + Widget _buildMiniStat(String label, String value) { + return Column( + children: [ + Text(value, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.textPrimary)), + const SizedBox(height: 2), + Text(label, style: const TextStyle(fontSize: 11, color: AppColors.textTertiary)), + ], + ); + } +} + +class _MockCoupon { + final String name; + final String template; + final double faceValue; + final String status; + final int issued; + final int sold; + final int redeemed; + final double redemptionRate; + + const _MockCoupon({ + required this.name, + required this.template, + required this.faceValue, + required this.status, + required this.issued, + required this.sold, + required this.redeemed, + required this.redemptionRate, + }); +} + +const _mockCoupons = [ + _MockCoupon(name: '¥25 星巴克礼品卡', template: '礼品卡', faceValue: 25, status: '在售中', issued: 5000, sold: 4200, redeemed: 3300, redemptionRate: 78.5), + _MockCoupon(name: '¥100 购物代金券', template: '代金券', faceValue: 100, status: '在售中', issued: 2000, sold: 1580, redeemed: 980, redemptionRate: 62.0), + _MockCoupon(name: '8折餐饮折扣券', template: '折扣券', faceValue: 50, status: '待审核', issued: 1000, sold: 0, redeemed: 0, redemptionRate: 0), + _MockCoupon(name: '¥200 储值卡', template: '储值券', faceValue: 200, status: '已售罄', issued: 500, sold: 500, redeemed: 420, redemptionRate: 84.0), +]; diff --git a/frontend/admin-app/lib/features/coupon_management/presentation/pages/create_coupon_page.dart b/frontend/admin-app/lib/features/coupon_management/presentation/pages/create_coupon_page.dart new file mode 100644 index 0000000..c937d59 --- /dev/null +++ b/frontend/admin-app/lib/features/coupon_management/presentation/pages/create_coupon_page.dart @@ -0,0 +1,462 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; + +/// 模板化发券页面 +/// +/// 流程:选择模板 → 填写信息 → 设定规则 → 预览 → 提交审核 +/// 发行方看到的是"创建优惠活动",不是"链上铸造NFT" +/// AI推荐定价+对比分析 +class CreateCouponPage extends StatefulWidget { + const CreateCouponPage({super.key}); + + @override + State createState() => _CreateCouponPageState(); +} + +class _CreateCouponPageState extends State { + int _currentStep = 0; + String? _selectedTemplate; + final _nameController = TextEditingController(); + final _faceValueController = TextEditingController(); + final _quantityController = TextEditingController(); + final _issuePriceController = TextEditingController(); + bool _transferable = true; + int _maxResaleCount = 2; + int _refundWindowDays = 7; + bool _autoRefund = true; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('发行新券'), + actions: [ + TextButton(onPressed: () {}, child: const Text('存为草稿')), + ], + ), + body: Column( + children: [ + // Step Indicator + _buildStepBar(), + + // Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: _buildStepContent(), + ), + ), + + // Bottom Actions + _buildBottomActions(), + ], + ), + ); + } + + Widget _buildStepBar() { + final steps = ['选择模板', '基本信息', '规则设置', '预览确认']; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + children: List.generate(steps.length, (i) { + final isActive = i <= _currentStep; + return Expanded( + child: Column( + children: [ + Container( + height: 3, + color: isActive ? AppColors.primary : AppColors.gray200, + ), + const SizedBox(height: 6), + Text( + steps[i], + style: TextStyle( + fontSize: 11, + color: isActive ? AppColors.primary : AppColors.textTertiary, + fontWeight: isActive ? FontWeight.w600 : FontWeight.w400, + ), + ), + ], + ), + ); + }), + ), + ); + } + + Widget _buildStepContent() { + switch (_currentStep) { + case 0: + return _buildTemplateStep(); + case 1: + return _buildInfoStep(); + case 2: + return _buildRulesStep(); + case 3: + return _buildPreviewStep(); + default: + return const SizedBox(); + } + } + + Widget _buildTemplateStep() { + final templates = [ + ('折扣券', '按比例打折', Icons.percent_rounded, AppColors.error), + ('代金券', '抵扣固定金额', Icons.money_rounded, AppColors.primary), + ('礼品卡', '可充值消费', Icons.card_giftcard_rounded, AppColors.info), + ('储值券', '预存金额消费', Icons.account_balance_wallet_rounded, AppColors.success), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('选择券模板', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + const Text('选择适合您业务场景的券类型', style: TextStyle(color: AppColors.textSecondary)), + const SizedBox(height: 24), + ...templates.map((t) { + final (name, desc, icon, color) = t; + final isSelected = _selectedTemplate == name; + return GestureDetector( + onTap: () => setState(() => _selectedTemplate = name), + child: Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isSelected ? color.withValues(alpha: 0.05) : AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? color : AppColors.borderLight, + width: isSelected ? 1.5 : 1, + ), + ), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: color, size: 22), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + Text(desc, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + ], + ), + ), + if (isSelected) + Icon(Icons.check_circle_rounded, color: color, size: 22), + ], + ), + ), + ); + }), + ], + ); + } + + Widget _buildInfoStep() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('基本信息', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const SizedBox(height: 24), + TextField( + controller: _nameController, + decoration: const InputDecoration(labelText: '券名称', hintText: '如:¥25 星巴克礼品卡'), + ), + const SizedBox(height: 16), + TextField( + controller: _faceValueController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: '面值 (\$)', hintText: '输入面值金额'), + ), + const SizedBox(height: 16), + TextField( + controller: _issuePriceController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: '发行价 (\$)', hintText: '通常低于面值'), + ), + + // AI Price Suggestion + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: BorderRadius.circular(10), + ), + child: const Row( + children: [ + Icon(Icons.auto_awesome_rounded, color: AppColors.primary, size: 16), + SizedBox(width: 8), + Expanded( + child: Text( + 'AI建议:面值¥25的礼品卡,最优发行价为¥21.25(8.5折),可最大化销量', + style: TextStyle(fontSize: 12, color: AppColors.primary), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + TextField( + controller: _quantityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: '发行数量', hintText: '本次发行总量'), + ), + const SizedBox(height: 16), + InputDatePickerFormField( + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + fieldLabelText: '有效期截止日(最长12个月)', + ), + const SizedBox(height: 16), + TextField( + maxLines: 3, + decoration: const InputDecoration(labelText: '券描述(可选)', hintText: '详细描述使用规则'), + ), + ], + ); + } + + Widget _buildRulesStep() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('规则设置', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const SizedBox(height: 24), + + // Transfer settings + SwitchListTile( + title: const Text('允许转让'), + subtitle: const Text('消费者可在二级市场转售此券'), + value: _transferable, + onChanged: (v) => setState(() => _transferable = v), + activeColor: AppColors.primary, + contentPadding: EdgeInsets.zero, + ), + if (_transferable) ...[ + const SizedBox(height: 8), + Row( + children: [ + const Text('最大转售次数', style: TextStyle(fontSize: 14)), + const Spacer(), + IconButton( + icon: const Icon(Icons.remove_circle_outline), + onPressed: () => setState(() => _maxResaleCount = (_maxResaleCount - 1).clamp(1, 5)), + ), + Text('$_maxResaleCount', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: () => setState(() => _maxResaleCount = (_maxResaleCount + 1).clamp(1, 5)), + ), + ], + ), + ], + const Divider(height: 32), + + // Refund Policy + const Text('退款策略', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + Row( + children: [ + const Text('退款窗口(天)', style: TextStyle(fontSize: 14)), + const Spacer(), + IconButton( + icon: const Icon(Icons.remove_circle_outline), + onPressed: () => setState(() => _refundWindowDays = (_refundWindowDays - 1).clamp(0, 30)), + ), + Text('$_refundWindowDays', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: () => setState(() => _refundWindowDays = (_refundWindowDays + 1).clamp(0, 30)), + ), + ], + ), + SwitchListTile( + title: const Text('允许自动退款'), + subtitle: const Text('窗口期内用户可直接退款无需审核'), + value: _autoRefund, + onChanged: (v) => setState(() => _autoRefund = v), + activeColor: AppColors.primary, + contentPadding: EdgeInsets.zero, + ), + const Divider(height: 32), + + // Usage Rules + SwitchListTile( + title: const Text('可叠加使用'), + subtitle: const Text('同一订单可使用多张此券'), + value: false, + onChanged: (_) {}, + contentPadding: EdgeInsets.zero, + ), + TextField( + keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: '最低消费金额 (\$,可选)'), + ), + const SizedBox(height: 16), + TextField( + decoration: const InputDecoration(labelText: '限定门店(可选)', hintText: '留空则全部门店可用'), + ), + ], + ); + } + + Widget _buildPreviewStep() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('预览确认', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + const Text('请确认以下信息,提交后将进入审核流程', style: TextStyle(color: AppColors.textSecondary)), + const SizedBox(height: 24), + + // Preview Card + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppColors.cardGradient, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _nameController.text.isNotEmpty ? _nameController.text : '¥25 星巴克礼品卡', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Colors.white), + ), + const SizedBox(height: 8), + Text( + '模板:${_selectedTemplate ?? '礼品卡'}', + style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.8)), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildPreviewStat('面值', '\$${_faceValueController.text.isNotEmpty ? _faceValueController.text : '25'}'), + _buildPreviewStat('发行价', '\$${_issuePriceController.text.isNotEmpty ? _issuePriceController.text : '21.25'}'), + _buildPreviewStat('数量', _quantityController.text.isNotEmpty ? _quantityController.text : '5000'), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + + // Details List + _buildDetailRow('可转让', _transferable ? '是(最多${_maxResaleCount}次)' : '否'), + _buildDetailRow('退款窗口', '$_refundWindowDays 天'), + _buildDetailRow('自动退款', _autoRefund ? '是' : '否'), + + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.infoLight, + borderRadius: BorderRadius.circular(10), + ), + child: const Row( + children: [ + Icon(Icons.info_outline_rounded, color: AppColors.info, size: 18), + SizedBox(width: 8), + Expanded( + child: Text( + '提交后将自动进入平台审核,审核通过后券将自动上架销售', + style: TextStyle(fontSize: 12, color: AppColors.info), + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildPreviewStat(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 _buildDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(color: AppColors.textSecondary)), + Text(value, style: const TextStyle(fontWeight: FontWeight.w500)), + ], + ), + ); + } + + Widget _buildBottomActions() { + return Container( + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + color: AppColors.surface, + border: Border(top: BorderSide(color: AppColors.borderLight)), + ), + child: Row( + children: [ + if (_currentStep > 0) + Expanded( + child: OutlinedButton( + onPressed: () => setState(() => _currentStep--), + child: const Text('上一步'), + ), + ), + if (_currentStep > 0) const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + if (_currentStep < 3) { + setState(() => _currentStep++); + } else { + _submitForReview(); + } + }, + child: Text(_currentStep < 3 ? '下一步' : '提交审核'), + ), + ), + ], + ), + ); + } + + void _submitForReview() { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('提交成功'), + content: const Text('您的券已提交审核,预计1-2个工作日内完成。'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + Navigator.of(context).pop(); + }, + child: const Text('确定'), + ), + ], + ), + ); + } +} diff --git a/frontend/admin-app/lib/features/credit/presentation/pages/credit_page.dart b/frontend/admin-app/lib/features/credit/presentation/pages/credit_page.dart new file mode 100644 index 0000000..19c2dc0 --- /dev/null +++ b/frontend/admin-app/lib/features/credit/presentation/pages/credit_page.dart @@ -0,0 +1,293 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; + +/// 信用评级页面 +/// +/// 四因子信用评分:核销率35% + (1-Breakage率)25% + 市场存续20% + 用户满意度20% +/// AI建议列表:信用提升建议 +class CreditPage extends StatelessWidget { + const CreditPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('信用评级')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // Score Gauge + _buildScoreGauge(), + const SizedBox(height: 24), + + // Four Factors + _buildFactorsCard(), + const SizedBox(height: 20), + + // Tier Progress + _buildTierProgress(), + const SizedBox(height: 20), + + // AI Suggestions + _buildAiSuggestions(), + const SizedBox(height: 20), + + // Credit History + _buildCreditHistory(), + ], + ), + ), + ); + } + + Widget _buildScoreGauge() { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [AppColors.creditAA, AppColors.creditAA.withValues(alpha: 0.3)], + ), + ), + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('AA', style: TextStyle(fontSize: 32, fontWeight: FontWeight.w700, color: Colors.white)), + Text('82分', style: TextStyle(fontSize: 14, color: Colors.white70)), + ], + ), + ), + ), + const SizedBox(height: 16), + const Text('信用等级 AA', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), + const SizedBox(height: 4), + const Text('距离 AAA 等级还差 8 分', style: TextStyle(fontSize: 13, color: AppColors.textSecondary)), + ], + ), + ); + } + + Widget _buildFactorsCard() { + final factors = [ + ('核销率', 0.85, 0.35, AppColors.success), + ('沉淀控制', 0.72, 0.25, AppColors.info), + ('市场存续', 0.90, 0.20, AppColors.primary), + ('用户满意度', 0.78, 0.20, AppColors.warning), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('评分因子', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + ...factors.map((f) { + final (label, score, weight, color) = f; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontSize: 13)), + Text( + '${(score * 100).toInt()}分 (权重${(weight * 100).toInt()}%)', + style: const TextStyle(fontSize: 12, color: AppColors.textSecondary), + ), + ], + ), + const SizedBox(height: 6), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: score, + backgroundColor: AppColors.gray100, + valueColor: AlwaysStoppedAnimation(color), + minHeight: 8, + ), + ), + ], + ), + ); + }), + ], + ), + ); + } + + Widget _buildTierProgress() { + final tiers = [ + ('白银', AppColors.tierSilver, true), + ('黄金', AppColors.tierGold, true), + ('铂金', AppColors.tierPlatinum, false), + ('钻石', AppColors.tierDiamond, false), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('发行方层级', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: tiers.map((t) { + final (name, color, isReached) = t; + return Column( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: isReached ? color.withValues(alpha: 0.15) : AppColors.gray100, + shape: BoxShape.circle, + border: isReached ? Border.all(color: color, width: 2) : null, + ), + child: Icon( + Icons.star_rounded, + color: isReached ? color : AppColors.textTertiary, + size: 22, + ), + ), + const SizedBox(height: 6), + Text( + name, + style: TextStyle( + fontSize: 12, + color: isReached ? color : AppColors.textTertiary, + fontWeight: isReached ? FontWeight.w600 : FontWeight.w400, + ), + ), + ], + ); + }).toList(), + ), + const SizedBox(height: 12), + const Text( + '当前:黄金 → 铂金需月发行量达500万', + style: TextStyle(fontSize: 12, color: AppColors.textSecondary), + ), + ], + ), + ); + } + + Widget _buildAiSuggestions() { + final suggestions = [ + ('提升核销率', '建议在周末推出限时核销活动,预计可提升核销率5%', Icons.trending_up_rounded), + ('降低Breakage', '当前有12%的券过期未用,建议到期前7天推送提醒', Icons.notification_important_rounded), + ('增加用户满意度', '回复消费者评价可提升满意度评分', Icons.rate_review_rounded), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(Icons.auto_awesome_rounded, color: AppColors.primary, size: 20), + SizedBox(width: 8), + Text('AI 信用提升建议', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + ], + ), + const SizedBox(height: 12), + ...suggestions.map((s) { + final (title, desc, icon) = s; + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + Icon(icon, color: AppColors.primary, size: 20), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), + const SizedBox(height: 2), + Text(desc, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + ], + ), + ), + ], + ), + ); + }), + ], + ); + } + + Widget _buildCreditHistory() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('信用变动记录', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + _buildHistoryItem('信用分 +3', '核销率提升至85%', '2天前', AppColors.success), + _buildHistoryItem('信用分 -1', 'Breakage率微升', '1周前', AppColors.error), + _buildHistoryItem('升级至黄金', '月发行量达100万', '2周前', AppColors.tierGold), + _buildHistoryItem('信用分 +5', '完成首月营业', '1月前', AppColors.success), + ], + ), + ); + } + + Widget _buildHistoryItem(String title, String desc, String time, Color color) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container(width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: color)), + Text(desc, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + ], + ), + ), + Text(time, style: const TextStyle(fontSize: 11, color: AppColors.textTertiary)), + ], + ), + ); + } +} diff --git a/frontend/admin-app/lib/features/credit/presentation/pages/quota_management_page.dart b/frontend/admin-app/lib/features/credit/presentation/pages/quota_management_page.dart new file mode 100644 index 0000000..0d73aed --- /dev/null +++ b/frontend/admin-app/lib/features/credit/presentation/pages/quota_management_page.dart @@ -0,0 +1,454 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; + +/// 发行方配额管理页面 +/// +/// 按层级(Silver/Gold/Platinum/Diamond)管理发行配额: +/// - 当前配额使用量 & 剩余 +/// - 配额申请(提额) +/// - 配额使用历史 +/// - 层级升级进度 +class QuotaManagementPage extends StatefulWidget { + const QuotaManagementPage({super.key}); + + @override + State createState() => _QuotaManagementPageState(); +} + +class _QuotaManagementPageState extends State { + String _selectedPeriod = '本月'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('配额管理'), + actions: [ + TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add_circle_outline_rounded, size: 18), + label: const Text('申请提额'), + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Current Quota Overview + _buildQuotaOverview(), + const SizedBox(height: 20), + + // Quota Breakdown + _buildQuotaBreakdown(), + const SizedBox(height: 20), + + // Period Selector & Usage History + _buildPeriodSelector(), + const SizedBox(height: 12), + _buildUsageHistory(), + const SizedBox(height: 20), + + // Tier Upgrade Progress + _buildTierUpgrade(), + const SizedBox(height: 20), + + // Pending Requests + _buildPendingRequests(), + ], + ), + ), + ); + } + + Widget _buildQuotaOverview() { + const usedPercent = 0.62; + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppColors.cardGradient, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Row( + children: [ + const Icon(Icons.pie_chart_rounded, color: Colors.white, size: 22), + const SizedBox(width: 10), + const Text( + '当前配额', + style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Colors.white), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(999), + ), + child: const Text( + '黄金层级', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white), + ), + ), + ], + ), + const SizedBox(height: 20), + // Circular gauge + SizedBox( + width: 130, + height: 130, + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 130, + height: 130, + child: CircularProgressIndicator( + value: usedPercent, + strokeWidth: 10, + backgroundColor: Colors.white.withValues(alpha: 0.2), + valueColor: const AlwaysStoppedAnimation(Colors.white), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + '62%', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: Colors.white), + ), + Text( + '已使用', + style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.7)), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildQuotaStat('月发行限额', '\$5,000,000'), + _buildQuotaStat('已使用', '\$3,100,000'), + _buildQuotaStat('剩余', '\$1,900,000'), + ], + ), + ], + ), + ); + } + + Widget _buildQuotaStat(String label, String value) { + return Column( + children: [ + Text( + value, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white), + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: 0.7)), + ), + ], + ); + } + + Widget _buildQuotaBreakdown() { + final quotaTypes = [ + ('礼品卡', 2100000, 3000000, AppColors.primary), + ('折扣券', 650000, 1000000, AppColors.info), + ('储值卡', 250000, 500000, AppColors.success), + ('体验券', 100000, 500000, AppColors.warning), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('配额分配明细', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + ...quotaTypes.map((q) { + final (name, used, total, color) = q; + final pct = used / total; + return Padding( + padding: const EdgeInsets.only(bottom: 14), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(name, style: const TextStyle(fontSize: 13)), + Text( + '\$${(used / 10000).toStringAsFixed(0)}万 / \$${(total / 10000).toStringAsFixed(0)}万', + style: const TextStyle(fontSize: 12, color: AppColors.textSecondary), + ), + ], + ), + const SizedBox(height: 6), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: pct, + backgroundColor: AppColors.gray100, + valueColor: AlwaysStoppedAnimation(color), + minHeight: 8, + ), + ), + ], + ), + ); + }), + ], + ), + ); + } + + Widget _buildPeriodSelector() { + final periods = ['本月', '本季', '本年']; + return Row( + children: [ + const Text('使用记录', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const Spacer(), + ...periods.map((p) => Padding( + padding: const EdgeInsets.only(left: 6), + child: ChoiceChip( + label: Text(p, style: const TextStyle(fontSize: 12)), + selected: _selectedPeriod == p, + onSelected: (_) => setState(() => _selectedPeriod = p), + selectedColor: AppColors.primaryContainer, + labelStyle: TextStyle( + color: _selectedPeriod == p ? AppColors.primary : AppColors.textSecondary, + fontWeight: _selectedPeriod == p ? FontWeight.w600 : FontWeight.w400, + ), + ), + )), + ], + ); + } + + Widget _buildUsageHistory() { + final history = [ + ('批量发行: 星巴克 \$25 x 2000张', '\$50,000', '2026-02-08', AppColors.primary), + ('批量发行: Amazon \$50 x 500张', '\$25,000', '2026-02-06', AppColors.info), + ('单张发行: Nike \$100 限量版', '\$10,000', '2026-02-05', AppColors.success), + ('批量发行: 餐饮券 \$15 x 3000张', '\$45,000', '2026-02-03', AppColors.warning), + ('批量发行: 电影票 \$12 x 1000张', '\$12,000', '2026-02-01', AppColors.primary), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: history.map((h) { + final (desc, amount, date, color) = h; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container( + width: 8, height: 8, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(desc, style: const TextStyle(fontSize: 13)), + Text(date, style: const TextStyle(fontSize: 11, color: AppColors.textTertiary)), + ], + ), + ), + Text(amount, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: color)), + ], + ), + ); + }).toList(), + ), + ); + } + + Widget _buildTierUpgrade() { + final tiers = [ + ('Silver', '白银', '\$1M/月', true, AppColors.tierSilver), + ('Gold', '黄金', '\$5M/月', true, AppColors.tierGold), + ('Platinum', '铂金', '\$20M/月', false, AppColors.tierPlatinum), + ('Diamond', '钻石', '无上限', false, AppColors.tierDiamond), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('层级与配额', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + ...tiers.map((t) { + final (nameEn, nameCn, quota, reached, color) = t; + final isCurrent = nameEn == 'Gold'; + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isCurrent ? color.withValues(alpha: 0.08) : AppColors.gray50, + borderRadius: BorderRadius.circular(10), + border: isCurrent ? Border.all(color: color, width: 1.5) : Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Icon( + Icons.star_rounded, + color: reached ? color : AppColors.textTertiary, + size: 22, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '$nameCn ($nameEn)', + style: TextStyle( + fontSize: 13, + fontWeight: isCurrent ? FontWeight.w700 : FontWeight.w500, + color: reached ? color : AppColors.textTertiary, + ), + ), + if (isCurrent) ...[ + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(999), + ), + child: const Text('当前', style: TextStyle(fontSize: 9, color: Colors.white, fontWeight: FontWeight.w700)), + ), + ], + ], + ), + Text( + '月发行配额: $quota', + style: const TextStyle(fontSize: 11, color: AppColors.textSecondary), + ), + ], + ), + ), + if (reached) + const Icon(Icons.check_circle_rounded, color: AppColors.success, size: 18), + if (!reached) + const Icon(Icons.lock_outline_rounded, color: AppColors.textTertiary, size: 18), + ], + ), + ); + }), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: BorderRadius.circular(8), + ), + child: const Row( + children: [ + Icon(Icons.trending_up_rounded, color: AppColors.primary, size: 18), + SizedBox(width: 8), + Expanded( + child: Text( + '距铂金升级: 信用分达90+ 且 月发行量连续3月 ≥\$10M', + style: TextStyle(fontSize: 12, color: AppColors.primary), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPendingRequests() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('提额申请记录', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + _buildRequestItem('REQ-001', '临时提额: +\$2M', '2026-02-05', '审核中', AppColors.warning), + _buildRequestItem('REQ-002', '长期提额: Gold→Platinum', '2026-01-20', '已驳回', AppColors.error), + _buildRequestItem('REQ-003', '临时提额: +\$500K (春节活动)', '2026-01-15', '已批准', AppColors.success), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add_rounded, size: 18), + label: const Text('提交新申请'), + ), + ), + ], + ), + ); + } + + Widget _buildRequestItem(String id, String desc, String date, String status, Color color) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(id, style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: AppColors.textTertiary)), + const SizedBox(width: 8), + Expanded(child: Text(desc, style: const TextStyle(fontSize: 13))), + ], + ), + Text(date, style: const TextStyle(fontSize: 11, color: AppColors.textTertiary)), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(999), + ), + child: Text(status, style: TextStyle(fontSize: 11, color: color, fontWeight: FontWeight.w600)), + ), + ], + ), + ); + } +} diff --git a/frontend/admin-app/lib/features/dashboard/presentation/pages/issuer_dashboard_page.dart b/frontend/admin-app/lib/features/dashboard/presentation/pages/issuer_dashboard_page.dart new file mode 100644 index 0000000..583fa5a --- /dev/null +++ b/frontend/admin-app/lib/features/dashboard/presentation/pages/issuer_dashboard_page.dart @@ -0,0 +1,356 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; + +/// 发行方数据仪表盘 +/// +/// 展示:总发行量、核销率、销售收入、信用等级、额度使用 +/// AI洞察卡片:智能解读销售数据 +class IssuerDashboardPage extends StatelessWidget { + const IssuerDashboardPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('数据概览'), + actions: [ + IconButton(icon: const Icon(Icons.notifications_outlined), onPressed: () {}), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Issuer Info Card + _buildIssuerInfoCard(), + const SizedBox(height: 20), + + // Stats Grid (2x2) + _buildStatsGrid(), + const SizedBox(height: 20), + + // AI Insight Card + _buildAiInsightCard(), + const SizedBox(height: 20), + + // Credit & Quota + _buildCreditQuotaCard(), + const SizedBox(height: 20), + + // Sales Trend Chart Placeholder + _buildSalesTrendCard(), + const SizedBox(height: 20), + + // Recent Activity + _buildRecentActivity(), + ], + ), + ), + ); + } + + Widget _buildIssuerInfoCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(Icons.storefront_rounded, color: Colors.white, size: 24), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Starbucks China', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Colors.white), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: AppColors.tierGold.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(999), + ), + child: const Text( + '黄金发行方', + style: TextStyle(fontSize: 11, color: Colors.white, fontWeight: FontWeight.w600), + ), + ), + ], + ), + ), + const Icon(Icons.chevron_right_rounded, color: Colors.white), + ], + ), + ); + } + + Widget _buildStatsGrid() { + final stats = [ + ('总发行量', '12,580', Icons.confirmation_number_rounded, AppColors.primary), + ('核销率', '78.5%', Icons.check_circle_rounded, AppColors.success), + ('销售收入', '\$125,800', Icons.attach_money_rounded, AppColors.info), + ('可提现', '\$42,300', Icons.account_balance_wallet_rounded, AppColors.warning), + ]; + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.6, + ), + itemCount: stats.length, + itemBuilder: (context, index) { + final (label, value, icon, color) = stats[index]; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(icon, size: 18, color: color), + const SizedBox(width: 6), + Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + ], + ), + Text( + value, + style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: color), + ), + ], + ), + ); + }, + ); + } + + Widget _buildAiInsightCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.primary.withValues(alpha: 0.15)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(Icons.auto_awesome_rounded, color: AppColors.primary, size: 20), + SizedBox(width: 8), + Text('AI 洞察', style: TextStyle(fontSize: 14, color: AppColors.primary, fontWeight: FontWeight.w600)), + ], + ), + const SizedBox(height: 10), + const Text( + '您的 ¥25 礼品卡核销率达到 92%,远高于同类平均。建议增发 500 张以满足市场需求。', + style: TextStyle(fontSize: 13, color: AppColors.textSecondary, height: 1.5), + ), + const SizedBox(height: 12), + Row( + children: [ + OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + minimumSize: Size.zero, + ), + child: const Text('忽略', style: TextStyle(fontSize: 13)), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + minimumSize: Size.zero, + ), + child: const Text('采纳建议', style: TextStyle(fontSize: 13)), + ), + ], + ), + ], + ), + ); + } + + Widget _buildCreditQuotaCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + // Credit Rating + Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.creditAA.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Text('AA', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.creditAA)), + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('信用等级 AA', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + Text('距离 AAA 还差 12 分', style: TextStyle(fontSize: 12, color: AppColors.textTertiary)), + ], + ), + ), + TextButton( + onPressed: () {}, + child: const Text('提升建议'), + ), + ], + ), + const SizedBox(height: 16), + const Divider(height: 1), + const SizedBox(height: 16), + + // Quota Progress + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('发行额度', style: TextStyle(fontSize: 13, color: AppColors.textSecondary)), + Text('\$380,000 / \$500,000', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), + ], + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: 0.76, + backgroundColor: AppColors.gray100, + valueColor: const AlwaysStoppedAnimation(AppColors.primary), + minHeight: 8, + ), + ), + const SizedBox(height: 4), + const Align( + alignment: Alignment.centerRight, + child: Text('已用 76%', style: TextStyle(fontSize: 11, color: AppColors.textTertiary)), + ), + ], + ), + ); + } + + Widget _buildSalesTrendCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('销售趋势', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + Text('近7天', style: TextStyle(fontSize: 12, color: AppColors.primary)), + ], + ), + const SizedBox(height: 16), + // Chart placeholder + Container( + height: 160, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: Text('销售趋势图表', style: TextStyle(color: AppColors.textTertiary)), + ), + ), + ], + ), + ); + } + + Widget _buildRecentActivity() { + final activities = [ + ('¥25 礼品卡', '售出 15 张', '2分钟前', AppColors.success), + ('¥100 购物券', '核销 8 张', '15分钟前', AppColors.info), + ('¥50 生活券', '新增挂单 3 张', '1小时前', AppColors.warning), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('最近动态', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + ...activities.map((a) { + final (title, desc, time, color) = a; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + Text(desc, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + ], + ), + ), + Text(time, style: const TextStyle(fontSize: 11, color: AppColors.textTertiary)), + ], + ), + ); + }), + ], + ), + ); + } +} diff --git a/frontend/admin-app/lib/features/dashboard/presentation/pages/user_portrait_page.dart b/frontend/admin-app/lib/features/dashboard/presentation/pages/user_portrait_page.dart new file mode 100644 index 0000000..bc0318a --- /dev/null +++ b/frontend/admin-app/lib/features/dashboard/presentation/pages/user_portrait_page.dart @@ -0,0 +1,452 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; + +/// 用户画像分析页面(发行方管理后台) +/// +/// 展示购买该发行方券的用户分布数据: +/// - 用户总量 & 活跃度 +/// - 年龄分布 +/// - 地域分布 +/// - 消费偏好 +/// - 复购分析 +/// - AI 用户洞察 +class UserPortraitPage extends StatelessWidget { + const UserPortraitPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('用户画像'), + actions: [ + TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.file_download_outlined, size: 18), + label: const Text('导出'), + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // User Stats Summary + _buildUserStatsSummary(), + const SizedBox(height: 20), + + // Age Distribution + _buildAgeDistribution(), + const SizedBox(height: 20), + + // Geographic Distribution + _buildGeoDistribution(), + const SizedBox(height: 20), + + // Purchase Preference + _buildPurchasePreference(), + const SizedBox(height: 20), + + // Repurchase Analysis + _buildRepurchaseAnalysis(), + const SizedBox(height: 20), + + // AI Insight + _buildAiInsight(), + ], + ), + ), + ); + } + + Widget _buildUserStatsSummary() { + final stats = [ + ('总购买用户', '12,456', Icons.people_alt_rounded, AppColors.primary), + ('月活用户', '3,281', Icons.trending_up_rounded, AppColors.success), + ('平均客单价', '\$23.5', Icons.attach_money_rounded, AppColors.info), + ('复购率', '34.2%', Icons.replay_rounded, AppColors.warning), + ]; + + return Row( + children: stats.map((s) { + final (label, value, icon, color) = s; + return Expanded( + child: Container( + margin: EdgeInsets.only( + right: s != stats.last ? 10 : 0), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Icon(icon, color: color, size: 22), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: color), + ), + const SizedBox(height: 2), + Text( + label, + style: const TextStyle( + fontSize: 10, color: AppColors.textSecondary), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + }).toList(), + ); + } + + Widget _buildAgeDistribution() { + final ages = [ + ('18-24', 0.15, '15%'), + ('25-34', 0.38, '38%'), + ('35-44', 0.28, '28%'), + ('45-54', 0.12, '12%'), + ('55+', 0.07, '7%'), + ]; + + return _card( + icon: Icons.cake_rounded, + title: '年龄分布', + child: Column( + children: ages.map((a) { + final (range, pct, label) = a; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + SizedBox( + width: 44, + child: Text(range, + style: const TextStyle( + fontSize: 12, color: AppColors.textSecondary)), + ), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: pct, + backgroundColor: AppColors.gray100, + valueColor: AlwaysStoppedAnimation( + pct > 0.3 + ? AppColors.primary + : AppColors.primaryLight, + ), + minHeight: 20, + ), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 32, + child: Text(label, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.primary)), + ), + ], + ), + ); + }).toList(), + ), + ); + } + + Widget _buildGeoDistribution() { + final regions = [ + ('加利福尼亚', 2845, AppColors.primary), + ('纽约', 2134, AppColors.info), + ('德克萨斯', 1567, AppColors.success), + ('佛罗里达', 1203, AppColors.warning), + ('华盛顿', 890, AppColors.couponDining), + ('其他', 3817, AppColors.gray400), + ]; + final total = regions.fold(0, (sum, r) => sum + r.$2); + + return _card( + icon: Icons.location_on_rounded, + title: '地域分布 (Top 5)', + child: Column( + children: regions.map((r) { + final (name, count, color) = r; + final pct = count / total; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 8), + SizedBox( + width: 80, + child: Text(name, + style: const TextStyle(fontSize: 13)), + ), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(3), + child: LinearProgressIndicator( + value: pct, + backgroundColor: AppColors.gray100, + valueColor: AlwaysStoppedAnimation(color), + minHeight: 10, + ), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 55, + child: Text( + '${(pct * 100).toStringAsFixed(1)}%', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: color), + textAlign: TextAlign.right, + ), + ), + ], + ), + ); + }).toList(), + ), + ); + } + + Widget _buildPurchasePreference() { + final categories = [ + ('餐饮', 42, AppColors.couponDining, Icons.restaurant_rounded), + ('购物', 28, AppColors.couponShopping, Icons.shopping_bag_rounded), + ('娱乐', 15, AppColors.couponEntertainment, Icons.movie_rounded), + ('旅行', 10, AppColors.couponTravel, Icons.flight_rounded), + ('其他', 5, AppColors.couponOther, Icons.more_horiz_rounded), + ]; + + return _card( + icon: Icons.favorite_rounded, + title: '消费偏好', + child: Wrap( + spacing: 10, + runSpacing: 10, + children: categories.map((c) { + final (name, pct, color, icon) = c; + return Container( + width: (MediaQuery.of( + // ignore: use_build_context_synchronously + WidgetsBinding.instance.rootElement! + .findRenderObject()! + .paintBounds + .width > + 0 + ? 140 + : 140)), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: color.withValues(alpha: 0.2)), + ), + child: Row( + children: [ + Icon(icon, color: color, size: 20), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: color)), + Text('$pct%', + style: const TextStyle( + fontSize: 11, + color: AppColors.textSecondary)), + ], + ), + ), + ], + ), + ); + }).toList(), + ), + ); + } + + Widget _buildRepurchaseAnalysis() { + final cohorts = [ + ('首次购买', 12456, '100%', AppColors.gray400), + ('2次购买', 4260, '34.2%', AppColors.primaryLight), + ('3-5次', 2134, '17.1%', AppColors.primary), + ('6-10次', 856, '6.9%', AppColors.primaryDark), + ('10次以上', 312, '2.5%', AppColors.primaryDark), + ]; + + return _card( + icon: Icons.replay_circle_filled_rounded, + title: '复购漏斗', + child: Column( + children: cohorts.asMap().entries.map((entry) { + final i = entry.key; + final (label, count, pct, color) = entry.value; + final widthFactor = 1.0 - (i * 0.18); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: const TextStyle( + fontSize: 12, + color: AppColors.textSecondary)), + Text('$count人 ($pct)', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: color)), + ], + ), + const SizedBox(height: 4), + Align( + alignment: Alignment.centerLeft, + child: FractionallySizedBox( + widthFactor: widthFactor.clamp(0.2, 1.0), + child: Container( + height: 24, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ); + } + + Widget _buildAiInsight() { + final insights = [ + ('核心用户群体', '25-34岁加利福尼亚用户,偏好餐饮类券,客单价\$28.5,高于整体均值21%'), + ('复购提升建议', '针对首次购买用户,7天内推送同品牌关联券可将复购率提升至42%'), + ('地域扩展机会', '德州、佛州用户增长率最快(+35% MoM),建议增加当地品牌合作'), + ('流失预警', '30天未活跃用户占比18%,建议发放专属回归优惠券'), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: + Text('✨', style: TextStyle(fontSize: 14))), + ), + const SizedBox(width: 10), + const Text('AI 用户洞察', + style: TextStyle( + fontSize: 15, fontWeight: FontWeight.w600)), + ], + ), + const SizedBox(height: 14), + ...insights.map((ins) { + final (title, desc) = ins; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.auto_awesome_rounded, + color: AppColors.primary, size: 16), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.primary)), + const SizedBox(height: 2), + Text(desc, + style: const TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + height: 1.4)), + ], + ), + ), + ], + ), + ); + }), + ], + ), + ); + } + + Widget _card( + {required IconData icon, + required String title, + required Widget child}) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, color: AppColors.primary, size: 20), + const SizedBox(width: 8), + Text(title, + style: const TextStyle( + fontSize: 15, fontWeight: FontWeight.w600)), + ], + ), + const SizedBox(height: 16), + child, + ], + ), + ); + } +} diff --git a/frontend/admin-app/lib/features/finance/presentation/pages/finance_page.dart b/frontend/admin-app/lib/features/finance/presentation/pages/finance_page.dart new file mode 100644 index 0000000..518c45f --- /dev/null +++ b/frontend/admin-app/lib/features/finance/presentation/pages/finance_page.dart @@ -0,0 +1,336 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; + +/// 财务管理页面 +/// +/// 法币展示,不暴露链上稳定币细节 +/// 包含:销售收入、Breakage收入、手续费、保证金、冻结款、提现、对账报表 +class FinancePage extends StatelessWidget { + const FinancePage({super.key}); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 3, + child: Scaffold( + appBar: AppBar( + title: const Text('财务管理'), + actions: [ + IconButton( + icon: const Icon(Icons.download_rounded), + onPressed: () => _showExportDialog(context), + ), + ], + bottom: const TabBar( + tabs: [ + Tab(text: '概览'), + Tab(text: '交易明细'), + Tab(text: '对账报表'), + ], + ), + ), + body: const TabBarView( + children: [ + _OverviewTab(), + _TransactionDetailTab(), + _ReconciliationTab(), + ], + ), + ), + ); + } + + void _showExportDialog(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => SimpleDialog( + title: const Text('导出数据'), + children: [ + SimpleDialogOption(onPressed: () => Navigator.pop(ctx), child: const Text('导出 CSV')), + SimpleDialogOption(onPressed: () => Navigator.pop(ctx), child: const Text('导出 Excel')), + SimpleDialogOption(onPressed: () => Navigator.pop(ctx), child: const Text('导出 PDF')), + ], + ), + ); + } +} + +class _OverviewTab extends StatelessWidget { + const _OverviewTab(); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // Balance Card + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('可提现余额', style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.7))), + const SizedBox(height: 4), + const Text('\$42,300.00', style: TextStyle(fontSize: 32, fontWeight: FontWeight.w700, color: Colors.white)), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: AppColors.primary, + ), + child: const Text('提现'), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + // Financial Stats + _buildFinanceStatsGrid(), + const SizedBox(height: 20), + + // Guarantee Fund + _buildGuaranteeFundCard(), + const SizedBox(height: 20), + + // Revenue Trend + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('收入趋势', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + Container( + height: 160, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: BorderRadius.circular(8), + ), + child: const Center(child: Text('月度收入趋势图', style: TextStyle(color: AppColors.textTertiary))), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildFinanceStatsGrid() { + final stats = [ + ('销售收入', '\$125,800', AppColors.success), + ('Breakage收入', '\$8,200', AppColors.info), + ('平台手续费', '-\$1,510', AppColors.error), + ('待结算', '\$15,400', AppColors.warning), + ('已提现', '\$66,790', AppColors.textSecondary), + ('总收入', '\$132,490', AppColors.primary), + ]; + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 2, + ), + itemCount: stats.length, + itemBuilder: (context, index) { + final (label, value, color) = stats[index]; + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + Text(value, style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: color)), + ], + ), + ); + }, + ); + } + + Widget _buildGuaranteeFundCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(Icons.shield_rounded, color: AppColors.info, size: 20), + SizedBox(width: 8), + Text('保证金与冻结款', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + ], + ), + const SizedBox(height: 16), + _buildRow('已缴纳保证金', '\$10,000'), + _buildRow('冻结销售款', '\$5,200'), + _buildRow('冻结比例', '20%'), + const SizedBox(height: 12), + SwitchListTile( + title: const Text('自动冻结销售款', style: TextStyle(fontSize: 14)), + subtitle: const Text('开启后自动冻结20%销售额以提升信用'), + value: true, + onChanged: (_) {}, + activeColor: AppColors.primary, + contentPadding: EdgeInsets.zero, + ), + ], + ), + ); + } + + Widget _buildRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontSize: 13, color: AppColors.textSecondary)), + Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + ], + ), + ); + } +} + +class _TransactionDetailTab extends StatelessWidget { + const _TransactionDetailTab(); + + @override + Widget build(BuildContext context) { + final transactions = [ + ('售出 ¥25 礼品卡 x5', '+\$106.25', '今天 14:32', AppColors.success), + ('核销结算 ¥100 购物券 x2', '+\$200.00', '今天 12:15', AppColors.success), + ('平台手续费', '-\$3.19', '今天 14:32', AppColors.error), + ('退款 ¥25 礼品卡', '-\$21.25', '今天 10:08', AppColors.warning), + ('售出 ¥50 生活券 x3', '+\$127.50', '昨天 18:45', AppColors.success), + ('提现至银行账户', '-\$5,000.00', '昨天 16:00', AppColors.info), + ]; + + return ListView.separated( + padding: const EdgeInsets.all(20), + itemCount: transactions.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final (desc, amount, time, color) = transactions[index]; + return ListTile( + contentPadding: const EdgeInsets.symmetric(vertical: 6), + title: Text(desc, style: const TextStyle(fontSize: 14)), + subtitle: Text(time, style: const TextStyle(fontSize: 12, color: AppColors.textTertiary)), + trailing: Text( + amount, + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: color), + ), + ); + }, + ); + } +} + +class _ReconciliationTab extends StatelessWidget { + const _ReconciliationTab(); + + @override + Widget build(BuildContext context) { + final reports = [ + ('2026年1月对账单', '总收入: \$28,450 | 总支出: \$3,210', '已生成'), + ('2025年12月对账单', '总收入: \$32,100 | 总支出: \$4,080', '已生成'), + ('2025年11月对账单', '总收入: \$25,800 | 总支出: \$2,900', '已生成'), + ]; + + return ListView( + padding: const EdgeInsets.all(20), + children: [ + // Generate New + OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add_rounded), + label: const Text('生成新对账单'), + style: OutlinedButton.styleFrom( + minimumSize: const Size(double.infinity, 48), + ), + ), + const SizedBox(height: 20), + + ...reports.map((r) { + final (title, summary, status) = r; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: AppColors.successLight, + borderRadius: BorderRadius.circular(999), + ), + child: Text(status, style: const TextStyle(fontSize: 11, color: AppColors.success)), + ), + ], + ), + const SizedBox(height: 6), + Text(summary, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + const SizedBox(height: 12), + Row( + children: [ + TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.visibility_rounded, size: 16), + label: const Text('查看'), + ), + TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.download_rounded, size: 16), + label: const Text('导出'), + ), + ], + ), + ], + ), + ); + }), + ], + ); + } +} diff --git a/frontend/admin-app/lib/features/finance/presentation/pages/financing_analysis_page.dart b/frontend/admin-app/lib/features/finance/presentation/pages/financing_analysis_page.dart new file mode 100644 index 0000000..c586969 --- /dev/null +++ b/frontend/admin-app/lib/features/finance/presentation/pages/financing_analysis_page.dart @@ -0,0 +1,783 @@ +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 FinancingAnalysisPage extends StatelessWidget { + const FinancingAnalysisPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('融资效果分析'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh_rounded), + onPressed: () {}, + tooltip: '刷新数据', + ), + IconButton( + icon: const Icon(Icons.download_rounded), + onPressed: () {}, + tooltip: '导出报告', + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Stats Cards + _buildStatsCards(), + const SizedBox(height: 24), + + // Financing Timeline + _buildFinancingTimeline(), + const SizedBox(height: 24), + + // Cost-Benefit Analysis + _buildCostBenefitCard(), + const SizedBox(height: 24), + + // Liquidity Metrics + _buildLiquidityMetrics(), + const SizedBox(height: 24), + + // Risk Indicators + _buildRiskIndicators(), + const SizedBox(height: 24), + + // AI Recommendation + _buildAiRecommendation(), + ], + ), + ), + ); + } + + // ============================================================ + // Stats Cards + // ============================================================ + + Widget _buildStatsCards() { + final stats = [ + ('融资总额', '\$2,850,000', AppColors.primary, Icons.account_balance_rounded, '+12.5%'), + ('平均利率', '4.2%', AppColors.info, Icons.percent_rounded, '-0.3%'), + ('融资笔数', '18', AppColors.success, Icons.receipt_long_rounded, '+3'), + ('资金利用率', '87.6%', AppColors.warning, Icons.pie_chart_rounded, '+5.2%'), + ]; + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.35, + ), + itemCount: stats.length, + itemBuilder: (context, index) { + final (label, value, color, icon, trend) = stats[index]; + final isPositiveTrend = trend.startsWith('+'); + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + ), + child: Icon(icon, color: color, size: 16), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: isPositiveTrend ? AppColors.successLight : AppColors.errorLight, + borderRadius: BorderRadius.circular(AppSpacing.radiusFull), + ), + child: Text( + trend, + style: AppTypography.caption.copyWith( + color: isPositiveTrend ? AppColors.success : AppColors.error, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(value, style: AppTypography.h3.copyWith(color: color)), + const SizedBox(height: 2), + Text(label, style: AppTypography.caption), + ], + ), + ], + ), + ); + }, + ); + } + + // ============================================================ + // Financing Timeline + // ============================================================ + + Widget _buildFinancingTimeline() { + final stages = [ + ('申请提交', '2026-01-15', true, AppColors.success), + ('审批通过', '2026-01-17', true, AppColors.success), + ('资金到账', '2026-01-18', true, AppColors.success), + ('使用中', '2026-01-18 ~', true, AppColors.primary), + ('还款期', '2026-07-18', false, AppColors.textTertiary), + ('结清', '待定', false, AppColors.textTertiary), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.timeline_rounded, color: AppColors.primary, size: 20), + const SizedBox(width: 8), + Text('融资生命周期', style: AppTypography.h3), + ], + ), + const SizedBox(height: 20), + ...List.generate(stages.length, (index) { + final (name, date, isCompleted, color) = stages[index]; + final isLast = index == stages.length - 1; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Timeline indicator + Column( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: isCompleted ? color.withValues(alpha: 0.15) : AppColors.gray100, + shape: BoxShape.circle, + border: Border.all(color: color, width: 2), + ), + child: isCompleted + ? Icon(Icons.check_rounded, color: color, size: 14) + : null, + ), + if (!isLast) + Container( + width: 2, + height: 36, + color: isCompleted ? color.withValues(alpha: 0.3) : AppColors.gray200, + ), + ], + ), + const SizedBox(width: 14), + // Content + Expanded( + child: Padding( + padding: EdgeInsets.only(bottom: isLast ? 0 : 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: AppTypography.labelMedium.copyWith( + color: isCompleted ? AppColors.textPrimary : AppColors.textTertiary, + ), + ), + const SizedBox(height: 2), + Text( + date, + style: AppTypography.caption.copyWith( + color: isCompleted ? AppColors.textSecondary : AppColors.textTertiary, + ), + ), + ], + ), + ), + ), + ], + ); + }), + ], + ), + ); + } + + // ============================================================ + // Cost-Benefit Analysis + // ============================================================ + + Widget _buildCostBenefitCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.analytics_rounded, color: AppColors.primary, size: 20), + const SizedBox(width: 8), + Text('成本效益分析', style: AppTypography.h3), + ], + ), + const SizedBox(height: 20), + + // Interest cost vs revenue + Row( + children: [ + Expanded( + child: _buildCostBenefitItem( + '利息成本', + '\$119,700', + '年化 4.2%', + AppColors.error, + Icons.trending_down_rounded, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildCostBenefitItem( + '产生收入', + '\$458,200', + '利用融资收入', + AppColors.success, + Icons.trending_up_rounded, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Net benefit + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '净收益', + style: AppTypography.labelMedium.copyWith(color: Colors.white), + ), + Text( + '\$338,500', + style: AppTypography.h2.copyWith(color: Colors.white), + ), + ], + ), + ), + const SizedBox(height: 16), + + // ROI + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('投资回报率 (ROI)', style: AppTypography.bodySmall), + Text( + '282.7%', + style: AppTypography.labelLarge.copyWith(color: AppColors.success), + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('收益/成本比', style: AppTypography.bodySmall), + Text( + '3.83x', + style: AppTypography.labelLarge.copyWith(color: AppColors.primary), + ), + ], + ), + ], + ), + ); + } + + Widget _buildCostBenefitItem( + String label, + String value, + String subtitle, + Color color, + IconData icon, + ) { + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + border: Border.all(color: color.withValues(alpha: 0.15)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, color: color, size: 16), + const SizedBox(width: 6), + Text(label, style: AppTypography.bodySmall.copyWith(color: color)), + ], + ), + const SizedBox(height: 8), + Text(value, style: AppTypography.h3.copyWith(color: color)), + const SizedBox(height: 2), + Text(subtitle, style: AppTypography.caption), + ], + ), + ); + } + + // ============================================================ + // Liquidity Metrics + // ============================================================ + + Widget _buildLiquidityMetrics() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.water_drop_rounded, color: AppColors.info, size: 20), + const SizedBox(width: 8), + Text('流动性指标', style: AppTypography.h3), + ], + ), + const SizedBox(height: 20), + + // Quick Ratio + _buildMetricRow( + '速动比率 (Quick Ratio)', + '1.85', + '健康', + AppColors.success, + 0.85, + ), + const SizedBox(height: 16), + + // Current Ratio + _buildMetricRow( + '流动比率 (Current Ratio)', + '2.34', + '良好', + AppColors.success, + 0.78, + ), + const SizedBox(height: 16), + + // Cash Flow Forecast + _buildSectionTitle('现金流预测'), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + ), + child: Column( + children: [ + _buildForecastRow('本月预计流入', '+\$85,200', AppColors.success), + const SizedBox(height: 8), + _buildForecastRow('本月预计流出', '-\$52,800', AppColors.error), + const Divider(height: 16), + _buildForecastRow('净现金流', '+\$32,400', AppColors.primary), + const SizedBox(height: 12), + _buildForecastRow('下月预计流入', '+\$92,500', AppColors.success), + const SizedBox(height: 8), + _buildForecastRow('下月预计流出', '-\$68,300', AppColors.error), + const Divider(height: 16), + _buildForecastRow('下月净现金流', '+\$24,200', AppColors.primary), + ], + ), + ), + ], + ), + ); + } + + Widget _buildMetricRow( + String label, + String value, + String status, + Color statusColor, + double progress, + ) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: Text(label, style: AppTypography.bodySmall.copyWith(color: AppColors.textPrimary))), + Row( + children: [ + Text(value, style: AppTypography.labelLarge.copyWith(color: AppColors.textPrimary)), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppSpacing.radiusFull), + ), + child: Text( + status, + style: AppTypography.caption.copyWith( + color: statusColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: progress, + backgroundColor: AppColors.gray100, + valueColor: AlwaysStoppedAnimation(statusColor), + minHeight: 6, + ), + ), + ], + ); + } + + Widget _buildForecastRow(String label, String value, Color color) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.bodySmall), + Text(value, style: AppTypography.labelMedium.copyWith(color: color)), + ], + ); + } + + // ============================================================ + // Risk Indicators + // ============================================================ + + Widget _buildRiskIndicators() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.shield_rounded, color: AppColors.warning, size: 20), + const SizedBox(width: 8), + Text('风险指标', style: AppTypography.h3), + ], + ), + const SizedBox(height: 20), + + // Risk gauge cards + Row( + children: [ + Expanded(child: _buildRiskGauge('违约率', '0.8%', AppColors.success, 0.08)), + const SizedBox(width: 12), + Expanded(child: _buildRiskGauge('逾期率', '2.3%', AppColors.warning, 0.23)), + const SizedBox(width: 12), + Expanded(child: _buildRiskGauge('集中度', '35%', AppColors.info, 0.35)), + ], + ), + const SizedBox(height: 20), + + // Risk detail rows + _buildRiskDetailRow( + '违约率 (Default Rate)', + '0.8%', + '低于行业平均 1.5%', + AppColors.success, + Icons.check_circle_rounded, + ), + const SizedBox(height: 12), + _buildRiskDetailRow( + '逾期率 (Overdue Rate)', + '2.3%', + '接近预警线 3.0%,需关注', + AppColors.warning, + Icons.warning_rounded, + ), + const SizedBox(height: 12), + _buildRiskDetailRow( + '集中度风险 (Concentration)', + '35%', + '最大单笔占比 35%,建议分散', + AppColors.info, + Icons.info_rounded, + ), + ], + ), + ); + } + + Widget _buildRiskGauge(String label, String value, Color color, double ratio) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + ), + child: Column( + children: [ + SizedBox( + width: 48, + height: 48, + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + value: ratio, + strokeWidth: 4, + backgroundColor: AppColors.gray200, + valueColor: AlwaysStoppedAnimation(color), + ), + Text( + value, + style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: color), + ), + ], + ), + ), + const SizedBox(height: 8), + Text( + label, + style: AppTypography.caption.copyWith(color: AppColors.textPrimary), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + Widget _buildRiskDetailRow( + String label, + String value, + String description, + Color color, + IconData icon, + ) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.04), + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + border: Border.all(color: color.withValues(alpha: 0.12)), + ), + child: Row( + children: [ + Icon(icon, color: color, size: 20), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.labelSmall.copyWith(color: AppColors.textPrimary)), + Text( + value, + style: AppTypography.labelMedium.copyWith(color: color), + ), + ], + ), + const SizedBox(height: 2), + Text(description, style: AppTypography.caption), + ], + ), + ), + ], + ), + ); + } + + // ============================================================ + // AI Recommendation + // ============================================================ + + Widget _buildAiRecommendation() { + final recommendations = [ + ( + '优化融资结构', + '建议将短期融资占比从当前 60% 调整至 45%,增加中期融资比例,降低再融资风险。预计可节省利息成本 \$12,000/年。', + Icons.account_tree_rounded, + '高', + AppColors.error, + ), + ( + '把握低利率窗口', + '当前市场利率处于近12个月低位,建议在未来2周内锁定长期融资利率。预测下季度利率可能上行 0.3-0.5%。', + Icons.access_time_rounded, + '高', + AppColors.error, + ), + ( + '提升资金利用率', + '当前有 \$354,000 闲置资金,建议投入短期理财或增加券发行量,预计可增加收益 \$8,500/月。', + Icons.rocket_launch_rounded, + '中', + AppColors.warning, + ), + ( + '分散集中度风险', + '最大单笔融资占总额 35%,建议拆分为2-3笔,从不同渠道融资以降低单一来源依赖风险。', + Icons.scatter_plot_rounded, + '中', + AppColors.warning, + ), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + ), + 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.h3), + Text('基于您的经营数据智能分析', style: AppTypography.caption), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + + ...recommendations.map((r) { + final (title, desc, icon, priority, priorityColor) = r; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, color: AppColors.primary, size: 18), + const SizedBox(width: 8), + Expanded( + child: Text(title, style: AppTypography.labelMedium.copyWith(color: AppColors.primary)), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: priorityColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppSpacing.radiusFull), + ), + child: Text( + '$priority优先', + style: AppTypography.caption.copyWith( + color: priorityColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text(desc, style: AppTypography.bodySmall.copyWith(height: 1.6)), + ], + ), + ); + }), + + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.auto_awesome_rounded, size: 18), + label: const Text('获取详细融资方案'), + ), + ), + ], + ), + ); + } + + // ============================================================ + // Shared Helpers + // ============================================================ + + Widget _buildSectionTitle(String title) { + return Text(title, style: AppTypography.labelMedium); + } +} diff --git a/frontend/admin-app/lib/features/finance/presentation/pages/reconciliation_page.dart b/frontend/admin-app/lib/features/finance/presentation/pages/reconciliation_page.dart new file mode 100644 index 0000000..9a52307 --- /dev/null +++ b/frontend/admin-app/lib/features/finance/presentation/pages/reconciliation_page.dart @@ -0,0 +1,586 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// 对账与结算报表页面 +/// +/// 支持按日/周/月/季度查看对账数据 +/// 包含:汇总卡片、对账明细表、自动对账状态、差异调查、导出功能 +class ReconciliationPage extends StatefulWidget { + const ReconciliationPage({super.key}); + + @override + State createState() => _ReconciliationPageState(); +} + +class _ReconciliationPageState extends State { + String _selectedPeriod = '月'; + final _periods = ['日', '周', '月', '季度']; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('对账与结算'), + actions: [ + IconButton( + icon: const Icon(Icons.download_rounded), + onPressed: () => _showExportDialog(context), + tooltip: '导出报表', + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Period Selector + _buildPeriodSelector(), + const SizedBox(height: 20), + + // Summary Cards + _buildSummaryCards(), + const SizedBox(height: 24), + + // Auto-reconciliation Status + _buildAutoReconciliationStatus(), + const SizedBox(height: 24), + + // Reconciliation Table + _buildReconciliationTable(), + const SizedBox(height: 24), + + // Discrepancy Details + _buildDiscrepancySection(), + const SizedBox(height: 24), + + // Export Buttons + _buildExportButtons(), + ], + ), + ), + ); + } + + // ============================================================ + // Period Selector + // ============================================================ + + Widget _buildPeriodSelector() { + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + ), + child: Row( + children: _periods.map((p) { + final isSelected = _selectedPeriod == p; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _selectedPeriod = p), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: isSelected ? AppColors.surface : Colors.transparent, + borderRadius: BorderRadius.circular(AppSpacing.radiusSm - 2), + boxShadow: isSelected ? AppSpacing.shadowSm : null, + ), + child: Center( + child: Text( + p, + style: AppTypography.labelMedium.copyWith( + color: isSelected ? AppColors.primary : AppColors.textSecondary, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + ), + ), + ), + ); + }).toList(), + ), + ); + } + + // ============================================================ + // Summary Cards + // ============================================================ + + Widget _buildSummaryCards() { + final summaries = [ + ('应结金额', '\$132,490', AppColors.primary, Icons.account_balance_rounded), + ('已结金额', '\$118,200', AppColors.success, Icons.check_circle_rounded), + ('待结金额', '\$12,180', AppColors.warning, Icons.schedule_rounded), + ('差异金额', '\$2,110', AppColors.error, Icons.error_outline_rounded), + ]; + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.6, + ), + itemCount: summaries.length, + itemBuilder: (context, index) { + final (label, value, color, icon) = summaries[index]; + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(icon, color: color, size: 16), + const SizedBox(width: 6), + Text(label, style: AppTypography.bodySmall), + ], + ), + Text( + value, + style: AppTypography.h2.copyWith(color: color), + ), + ], + ), + ); + }, + ); + } + + // ============================================================ + // Auto-reconciliation Status + // ============================================================ + + Widget _buildAutoReconciliationStatus() { + const matchRate = 96.8; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.successLight, + borderRadius: BorderRadius.circular(AppSpacing.radiusSm), + ), + child: const Icon(Icons.auto_fix_high_rounded, color: AppColors.success, size: 18), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('自动对账', style: AppTypography.labelMedium), + Text('上次运行: 今天 06:00', style: AppTypography.caption), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppColors.successLight, + borderRadius: BorderRadius.circular(AppSpacing.radiusFull), + ), + child: Text( + '运行中', + style: AppTypography.caption.copyWith( + color: AppColors.success, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('匹配率', style: AppTypography.bodySmall), + Text( + '$matchRate%', + style: AppTypography.labelMedium.copyWith(color: AppColors.success), + ), + ], + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: matchRate / 100, + backgroundColor: AppColors.gray100, + valueColor: const AlwaysStoppedAnimation(AppColors.success), + minHeight: 8, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildMiniStat('已匹配', '4,832', AppColors.success), + _buildMiniStat('待核查', '158', AppColors.warning), + _buildMiniStat('有差异', '12', AppColors.error), + ], + ), + ], + ), + ); + } + + Widget _buildMiniStat(String label, String value, Color color) { + return Column( + children: [ + Text( + value, + style: AppTypography.labelLarge.copyWith(color: color), + ), + const SizedBox(height: 2), + Text(label, style: AppTypography.caption), + ], + ); + } + + // ============================================================ + // Reconciliation Table + // ============================================================ + + Widget _buildReconciliationTable() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('对账明细', style: AppTypography.h3), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + // Table Header + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: const BoxDecoration( + color: AppColors.gray50, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: Row( + children: [ + Expanded(flex: 2, child: Text('期间', style: AppTypography.labelSmall)), + Expanded(flex: 2, child: Text('应结', style: AppTypography.labelSmall)), + Expanded(flex: 2, child: Text('实结', style: AppTypography.labelSmall)), + Expanded(flex: 2, child: Text('差异', style: AppTypography.labelSmall)), + Expanded(flex: 2, child: Text('状态', style: AppTypography.labelSmall)), + ], + ), + ), + // Table Rows + ..._mockReconciliationData.map((row) { + final (period, expected, actual, diff, status) = row; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: AppColors.borderLight, width: 0.5), + ), + ), + child: Row( + children: [ + Expanded( + flex: 2, + child: Text(period, style: AppTypography.bodySmall.copyWith(color: AppColors.textPrimary)), + ), + Expanded( + flex: 2, + child: Text(expected, style: AppTypography.bodySmall.copyWith(color: AppColors.textPrimary)), + ), + Expanded( + flex: 2, + child: Text(actual, style: AppTypography.bodySmall.copyWith(color: AppColors.textPrimary)), + ), + Expanded( + flex: 2, + child: Text( + diff, + style: AppTypography.bodySmall.copyWith( + color: diff == '\$0' ? AppColors.textTertiary : AppColors.error, + ), + ), + ), + Expanded( + flex: 2, + child: _buildReconciliationStatus(status), + ), + ], + ), + ); + }), + ], + ), + ), + ], + ); + } + + Widget _buildReconciliationStatus(String status) { + Color color; + Color bgColor; + switch (status) { + case '已对账': + color = AppColors.success; + bgColor = AppColors.successLight; + break; + case '有差异': + color = AppColors.error; + bgColor = AppColors.errorLight; + break; + default: + color = AppColors.warning; + bgColor = AppColors.warningLight; + } + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(AppSpacing.radiusFull), + ), + child: Text( + status, + style: TextStyle(fontSize: 10, color: color, fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + ), + ); + } + + // ============================================================ + // Discrepancy Details + // ============================================================ + + Widget _buildDiscrepancySection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('差异调查', style: AppTypography.h3), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: AppColors.errorLight, + borderRadius: BorderRadius.circular(AppSpacing.radiusFull), + ), + child: Text( + '3 项待处理', + style: AppTypography.caption.copyWith( + color: AppColors.error, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + ..._mockDiscrepancies.map((d) { + final (desc, amount, investigationStatus, date) = d; + Color statusColor; + switch (investigationStatus) { + case '调查中': + statusColor = AppColors.warning; + break; + case '已解决': + statusColor = AppColors.success; + break; + default: + statusColor = AppColors.error; + } + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text(desc, style: AppTypography.labelMedium), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppSpacing.radiusFull), + ), + child: Text( + investigationStatus, + style: AppTypography.caption.copyWith( + color: statusColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '差异金额: $amount', + style: AppTypography.bodySmall.copyWith(color: AppColors.error), + ), + Text(date, style: AppTypography.caption), + ], + ), + ], + ), + ); + }), + ], + ); + } + + // ============================================================ + // Export Buttons + // ============================================================ + + Widget _buildExportButtons() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppSpacing.radiusMd), + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('导出报表', style: AppTypography.h3), + const SizedBox(height: 14), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.picture_as_pdf_rounded, size: 18), + label: const Text('导出 PDF'), + style: OutlinedButton.styleFrom( + minimumSize: const Size(0, 48), + foregroundColor: AppColors.error, + side: const BorderSide(color: AppColors.error, width: 1), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.table_chart_rounded, size: 18), + label: const Text('导出 Excel'), + style: OutlinedButton.styleFrom( + minimumSize: const Size(0, 48), + foregroundColor: AppColors.success, + side: const BorderSide(color: AppColors.success, width: 1), + ), + ), + ), + ], + ), + ], + ), + ); + } + + // ============================================================ + // Export Dialog + // ============================================================ + + void _showExportDialog(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => SimpleDialog( + title: const Text('导出对账报表'), + children: [ + SimpleDialogOption( + onPressed: () => Navigator.pop(ctx), + child: const Row( + children: [ + Icon(Icons.picture_as_pdf_rounded, color: AppColors.error, size: 20), + SizedBox(width: 12), + Text('导出 PDF'), + ], + ), + ), + SimpleDialogOption( + onPressed: () => Navigator.pop(ctx), + child: const Row( + children: [ + Icon(Icons.table_chart_rounded, color: AppColors.success, size: 20), + SizedBox(width: 12), + Text('导出 Excel'), + ], + ), + ), + SimpleDialogOption( + onPressed: () => Navigator.pop(ctx), + child: const Row( + children: [ + Icon(Icons.description_rounded, color: AppColors.info, size: 20), + SizedBox(width: 12), + Text('导出 CSV'), + ], + ), + ), + ], + ), + ); + } +} + +// ============================================================ +// Mock Data +// ============================================================ + +const _mockReconciliationData = [ + ('2026年1月', '\$32,100', '\$32,100', '\$0', '已对账'), + ('2025年12月', '\$28,450', '\$28,450', '\$0', '已对账'), + ('2025年11月', '\$25,800', '\$24,690', '\$1,110', '有差异'), + ('2025年10月', '\$30,200', '\$30,200', '\$0', '已对账'), + ('2025年9月', '\$22,940', '\$22,940', '\$0', '已对账'), + ('2025年8月', '\$18,600', '\$17,600', '\$1,000', '有差异'), + ('2025年7月', '\$15,400', '\$15,400', '\$0', '待对账'), +]; + +const _mockDiscrepancies = [ + ('11月退款差异 - 部分退款未入账', '\$780', '调查中', '2025-12-15'), + ('11月手续费差异 - 费率计算偏差', '\$330', '调查中', '2025-12-12'), + ('8月核销结算延迟', '\$1,000', '已解决', '2025-09-20'), +]; diff --git a/frontend/admin-app/lib/features/onboarding/presentation/pages/onboarding_page.dart b/frontend/admin-app/lib/features/onboarding/presentation/pages/onboarding_page.dart new file mode 100644 index 0000000..7f70de8 --- /dev/null +++ b/frontend/admin-app/lib/features/onboarding/presentation/pages/onboarding_page.dart @@ -0,0 +1,379 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/router.dart'; + +/// 发行方入驻审核流程 +/// +/// 步骤:企业信息 → 资质上传 → 联系人 → 提交审核 → 审核结果 +/// 零保证金入驻:审核通过后给予初始低额度 +enum OnboardingStep { companyInfo, documents, contactPerson, review, approved, rejected } + +class OnboardingPage extends StatefulWidget { + const OnboardingPage({super.key}); + + @override + State createState() => _OnboardingPageState(); +} + +class _OnboardingPageState extends State { + OnboardingStep _currentStep = OnboardingStep.companyInfo; + final _companyNameController = TextEditingController(); + final _licenseController = TextEditingController(); + final _contactController = TextEditingController(); + final _contactPhoneController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('企业入驻')), + body: Column( + children: [ + // Step Indicator + _buildStepIndicator(), + const Divider(height: 1), + + // Step Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: _buildStepContent(), + ), + ), + + // Bottom Actions + Padding( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + if (_currentStep.index > 0 && _currentStep.index < 3) + Expanded( + child: OutlinedButton( + onPressed: _goBack, + child: const Text('上一步'), + ), + ), + if (_currentStep.index > 0 && _currentStep.index < 3) const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: _goNext, + child: Text(_currentStep.index < 2 ? '下一步' : '提交审核'), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildStepIndicator() { + final steps = ['企业信息', '资质上传', '联系人', '审核中']; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Row( + children: List.generate(steps.length, (i) { + final isActive = i <= _currentStep.index && _currentStep.index < 4; + final isComplete = i < _currentStep.index; + return Expanded( + child: Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: isComplete + ? AppColors.success + : isActive + ? AppColors.primary + : AppColors.gray200, + shape: BoxShape.circle, + ), + child: Center( + child: isComplete + ? const Icon(Icons.check, color: Colors.white, size: 16) + : Text( + '${i + 1}', + style: TextStyle( + color: isActive ? Colors.white : AppColors.textTertiary, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(width: 6), + Expanded( + child: Text( + steps[i], + style: TextStyle( + fontSize: 12, + color: isActive ? AppColors.textPrimary : AppColors.textTertiary, + fontWeight: isActive ? FontWeight.w600 : FontWeight.w400, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + }), + ), + ); + } + + Widget _buildStepContent() { + switch (_currentStep) { + case OnboardingStep.companyInfo: + return _buildCompanyInfoStep(); + case OnboardingStep.documents: + return _buildDocumentsStep(); + case OnboardingStep.contactPerson: + return _buildContactStep(); + case OnboardingStep.review: + return _buildReviewStep(); + case OnboardingStep.approved: + return _buildApprovedStep(); + case OnboardingStep.rejected: + return _buildRejectedStep(); + } + } + + Widget _buildCompanyInfoStep() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('企业基本信息', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + const Text('请填写真实的企业信息,用于入驻审核', style: TextStyle(color: AppColors.textSecondary)), + const SizedBox(height: 24), + TextField( + controller: _companyNameController, + decoration: const InputDecoration(labelText: '企业名称', hintText: '请输入企业全称'), + ), + const SizedBox(height: 16), + TextField( + controller: _licenseController, + decoration: const InputDecoration(labelText: '统一社会信用代码', hintText: '请输入18位信用代码'), + ), + const SizedBox(height: 16), + DropdownButtonFormField( + decoration: const InputDecoration(labelText: '企业类型'), + items: const [ + DropdownMenuItem(value: 'restaurant', child: Text('餐饮企业')), + DropdownMenuItem(value: 'retail', child: Text('零售企业')), + DropdownMenuItem(value: 'entertainment', child: Text('娱乐/文旅')), + DropdownMenuItem(value: 'other', child: Text('其他')), + ], + onChanged: (_) {}, + ), + const SizedBox(height: 16), + TextField( + decoration: const InputDecoration(labelText: '企业地址', hintText: '请输入企业注册地址'), + ), + + // AI合规助手 + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.primary.withValues(alpha: 0.15)), + ), + child: const Row( + children: [ + Icon(Icons.auto_awesome_rounded, color: AppColors.primary, size: 20), + SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('AI 合规助手', style: TextStyle(fontSize: 13, color: AppColors.primary, fontWeight: FontWeight.w600)), + SizedBox(height: 2), + Text('填写信息后,AI将自动检查合规要求', style: TextStyle(fontSize: 12, color: AppColors.textSecondary)), + ], + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildDocumentsStep() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('资质文件上传', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + const Text('请上传清晰的企业资质文件', style: TextStyle(color: AppColors.textSecondary)), + const SizedBox(height: 24), + _buildUploadArea('营业执照', Icons.business_rounded, true), + const SizedBox(height: 16), + _buildUploadArea('法人身份证(正反面)', Icons.badge_rounded, true), + const SizedBox(height: 16), + _buildUploadArea('行业资质证书(可选)', Icons.verified_rounded, false), + ], + ); + } + + Widget _buildUploadArea(String title, IconData icon, bool required) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + border: Border.all(color: AppColors.border, style: BorderStyle.solid), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Icon(icon, size: 40, color: AppColors.textTertiary), + const SizedBox(height: 8), + Text( + title, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + ), + if (required) + const Text('*必填', style: TextStyle(fontSize: 11, color: AppColors.error)), + const SizedBox(height: 8), + OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.upload_rounded, size: 18), + label: const Text('点击上传'), + ), + ], + ), + ); + } + + Widget _buildContactStep() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('联系人信息', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const SizedBox(height: 24), + TextField( + controller: _contactController, + decoration: const InputDecoration(labelText: '联系人姓名'), + ), + const SizedBox(height: 16), + TextField( + controller: _contactPhoneController, + keyboardType: TextInputType.phone, + decoration: const InputDecoration(labelText: '联系人手机号'), + ), + const SizedBox(height: 16), + TextField( + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration(labelText: '企业邮箱'), + ), + const SizedBox(height: 16), + TextField( + decoration: const InputDecoration(labelText: '职位/角色'), + ), + ], + ); + } + + Widget _buildReviewStep() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 40), + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: AppColors.warningLight, + shape: BoxShape.circle, + ), + child: const Icon(Icons.hourglass_bottom_rounded, color: AppColors.warning, size: 40), + ), + const SizedBox(height: 24), + const Text('审核中', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700)), + const SizedBox(height: 8), + const Text('您的入驻申请已提交,预计1-3个工作日内完成审核', style: TextStyle(color: AppColors.textSecondary)), + const SizedBox(height: 32), + OutlinedButton( + onPressed: () => Navigator.pop(context), + child: const Text('返回登录'), + ), + ], + ), + ); + } + + Widget _buildApprovedStep() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 40), + Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + color: AppColors.successLight, + shape: BoxShape.circle, + ), + child: const Icon(Icons.check_circle_rounded, color: AppColors.success, size: 40), + ), + const SizedBox(height: 24), + const Text('审核通过', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700)), + const SizedBox(height: 8), + const Text('恭喜!您已获得白银级初始额度', style: TextStyle(color: AppColors.textSecondary)), + const SizedBox(height: 32), + ElevatedButton( + onPressed: () => Navigator.pushReplacementNamed(context, AppRouter.main), + child: const Text('进入控制台'), + ), + ], + ), + ); + } + + Widget _buildRejectedStep() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 40), + Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + color: AppColors.errorLight, + shape: BoxShape.circle, + ), + child: const Icon(Icons.cancel_rounded, color: AppColors.error, size: 40), + ), + const SizedBox(height: 24), + const Text('审核未通过', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700)), + const SizedBox(height: 8), + const Text('原因:资质文件不清晰,请重新上传', style: TextStyle(color: AppColors.textSecondary)), + const SizedBox(height: 32), + ElevatedButton( + onPressed: () => setState(() => _currentStep = OnboardingStep.documents), + child: const Text('重新提交'), + ), + ], + ), + ); + } + + void _goBack() { + if (_currentStep.index > 0) { + setState(() => _currentStep = OnboardingStep.values[_currentStep.index - 1]); + } + } + + void _goNext() { + if (_currentStep.index < 2) { + setState(() => _currentStep = OnboardingStep.values[_currentStep.index + 1]); + } else if (_currentStep == OnboardingStep.contactPerson) { + setState(() => _currentStep = OnboardingStep.review); + } + } +} diff --git a/frontend/admin-app/lib/features/redemption/presentation/pages/redemption_page.dart b/frontend/admin-app/lib/features/redemption/presentation/pages/redemption_page.dart new file mode 100644 index 0000000..569587b --- /dev/null +++ b/frontend/admin-app/lib/features/redemption/presentation/pages/redemption_page.dart @@ -0,0 +1,263 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; + +/// 核销管理页面 +/// +/// 扫码核销 + 手动输入券码 + 批量核销 + 核销记录 +class RedemptionPage extends StatelessWidget { + const RedemptionPage({super.key}); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: const Text('核销管理'), + bottom: const TabBar( + tabs: [ + Tab(text: '扫码核销'), + Tab(text: '核销记录'), + ], + ), + ), + body: const TabBarView( + children: [ + _ScanRedeemTab(), + _RedeemHistoryTab(), + ], + ), + ), + ); + } +} + +class _ScanRedeemTab extends StatelessWidget { + const _ScanRedeemTab(); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // Scan Area + Container( + height: 260, + decoration: BoxDecoration( + color: AppColors.gray900, + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 160, + height: 160, + decoration: BoxDecoration( + border: Border.all(color: AppColors.primary, width: 2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(Icons.qr_code_scanner_rounded, color: AppColors.primary, size: 64), + ), + const SizedBox(height: 16), + const Text('将券码对准扫描框', style: TextStyle(color: Colors.white70, fontSize: 14)), + ], + ), + ), + ), + const SizedBox(height: 20), + + // Manual Input + Row( + children: [ + Expanded( + child: TextField( + decoration: const InputDecoration( + hintText: '手动输入券码', + prefixIcon: Icon(Icons.keyboard_rounded), + ), + ), + ), + const SizedBox(width: 12), + SizedBox( + height: 52, + child: ElevatedButton( + onPressed: () => _showRedeemConfirm(context), + child: const Text('核销'), + ), + ), + ], + ), + const SizedBox(height: 20), + + // Batch Redeem + OutlinedButton.icon( + onPressed: () => _showBatchRedeem(context), + icon: const Icon(Icons.list_alt_rounded), + label: const Text('批量核销'), + style: OutlinedButton.styleFrom( + minimumSize: const Size(double.infinity, 48), + ), + ), + const SizedBox(height: 24), + + // Today Stats + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('今日核销', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _StatItem(label: '核销次数', value: '45'), + _StatItem(label: '核销金额', value: '\$1,125'), + _StatItem(label: '门店数', value: '3'), + ], + ), + ], + ), + ), + ], + ), + ); + } + + void _showRedeemConfirm(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (ctx) => Container( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.check_circle_rounded, color: AppColors.success, size: 56), + const SizedBox(height: 16), + const Text('核销确认', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + const Text('¥25 星巴克礼品卡', style: TextStyle(color: AppColors.textSecondary)), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('确认核销'), + ), + ), + const SizedBox(height: 12), + ], + ), + ), + ); + } + + void _showBatchRedeem(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (ctx) => DraggableScrollableSheet( + expand: false, + initialChildSize: 0.6, + builder: (_, controller) => Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('批量核销', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + const Text('输入多个券码,每行一个', style: TextStyle(color: AppColors.textSecondary)), + const SizedBox(height: 16), + Expanded( + child: TextField( + maxLines: null, + expands: true, + textAlignVertical: TextAlignVertical.top, + decoration: const InputDecoration( + hintText: '粘贴券码列表...', + border: OutlineInputBorder(), + ), + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('批量核销'), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _RedeemHistoryTab extends StatelessWidget { + const _RedeemHistoryTab(); + + @override + Widget build(BuildContext context) { + final records = [ + ('¥25 礼品卡', '门店A · 收银员张三', '10分钟前', true), + ('¥100 购物券', '门店B · 收银员李四', '25分钟前', true), + ('¥50 生活券', '手动输入', '1小时前', true), + ('¥25 礼品卡', '门店A · 扫码', '2小时前', false), + ]; + + return ListView.separated( + padding: const EdgeInsets.all(20), + itemCount: records.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final (name, source, time, success) = records[index]; + return ListTile( + contentPadding: const EdgeInsets.symmetric(vertical: 8), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: success ? AppColors.successLight : AppColors.errorLight, + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + success ? Icons.check_rounded : Icons.close_rounded, + color: success ? AppColors.success : AppColors.error, + size: 20, + ), + ), + title: Text(name, style: const TextStyle(fontWeight: FontWeight.w500)), + subtitle: Text(source, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + trailing: Text(time, style: const TextStyle(fontSize: 11, color: AppColors.textTertiary)), + ); + }, + ); + } +} + +class _StatItem extends StatelessWidget { + final String label; + final String value; + const _StatItem({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.primary)), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textTertiary)), + ], + ); + } +} diff --git a/frontend/admin-app/lib/features/settings/presentation/pages/settings_page.dart b/frontend/admin-app/lib/features/settings/presentation/pages/settings_page.dart new file mode 100644 index 0000000..a8270c7 --- /dev/null +++ b/frontend/admin-app/lib/features/settings/presentation/pages/settings_page.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; + +/// 发行方设置页面(我的) +/// +/// 企业信息、门店管理、员工管理、专属客服、安全设置 +class SettingsPage extends StatelessWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('我的')), + body: SingleChildScrollView( + child: Column( + children: [ + // Profile Card + _buildProfileCard(context), + + // Tier & Benefits + _buildTierCard(), + + const SizedBox(height: 8), + + // Menu Groups + _buildMenuGroup('企业管理', [ + _MenuItem('企业信息', Icons.business_rounded, () {}), + _MenuItem('门店管理', Icons.store_rounded, () { + Navigator.pushNamed(context, '/stores'); + }), + _MenuItem('员工管理', Icons.people_rounded, () {}), + _MenuItem('权限设置', Icons.admin_panel_settings_rounded, () {}), + ]), + + _buildMenuGroup('服务支持', [ + _MenuItem('AI 助手', Icons.auto_awesome_rounded, () { + Navigator.pushNamed(context, '/ai-agent'); + }), + _MenuItem('专属客服', Icons.headset_mic_rounded, () {}), + _MenuItem('帮助中心', Icons.help_outline_rounded, () {}), + _MenuItem('意见反馈', Icons.feedback_rounded, () {}), + ]), + + _buildMenuGroup('安全与账号', [ + _MenuItem('修改密码', Icons.lock_outline_rounded, () {}), + _MenuItem('操作日志', Icons.history_rounded, () {}), + _MenuItem('关于 Genex', Icons.info_outline_rounded, () {}), + ]), + + // Logout + Padding( + padding: const EdgeInsets.all(20), + child: SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () { + Navigator.pushNamedAndRemoveUntil(context, '/', (_) => false); + }, + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.error, + side: const BorderSide(color: AppColors.error), + ), + child: const Text('退出登录'), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildProfileCard(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), + color: AppColors.surface, + child: Row( + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(14), + ), + child: const Icon(Icons.storefront_rounded, color: Colors.white, size: 28), + ), + const SizedBox(width: 14), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Starbucks China', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), + SizedBox(height: 4), + Text('管理员:张经理', style: TextStyle(fontSize: 13, color: AppColors.textSecondary)), + ], + ), + ), + const Icon(Icons.chevron_right_rounded, color: AppColors.textTertiary), + ], + ), + ); + } + + Widget _buildTierCard() { + return Container( + margin: const EdgeInsets.fromLTRB(20, 12, 20, 0), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFFFF7E6), Color(0xFFFFECC7)], + ), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.star_rounded, color: AppColors.tierGold, size: 28), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('黄金发行方', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.tierGold)), + Text('手续费率 1.2% · 高级数据分析', style: TextStyle(fontSize: 12, color: AppColors.textSecondary)), + ], + ), + ), + TextButton( + onPressed: () {}, + child: const Text('升级', style: TextStyle(color: AppColors.tierGold)), + ), + ], + ), + ); + } + + Widget _buildMenuGroup(String title, List<_MenuItem> items) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 8), + child: Text(title, style: const TextStyle(fontSize: 13, color: AppColors.textTertiary, fontWeight: FontWeight.w500)), + ), + Container( + color: AppColors.surface, + child: Column( + children: items.map((item) { + return ListTile( + leading: Icon(item.icon, color: AppColors.textSecondary, size: 22), + title: Text(item.title, style: const TextStyle(fontSize: 15)), + trailing: const Icon(Icons.chevron_right_rounded, size: 20, color: AppColors.textTertiary), + onTap: item.onTap, + ); + }).toList(), + ), + ), + ], + ); + } +} + +class _MenuItem { + final String title; + final IconData icon; + final VoidCallback onTap; + + const _MenuItem(this.title, this.icon, this.onTap); +} diff --git a/frontend/admin-app/lib/features/store_management/presentation/pages/store_management_page.dart b/frontend/admin-app/lib/features/store_management/presentation/pages/store_management_page.dart new file mode 100644 index 0000000..bc0375e --- /dev/null +++ b/frontend/admin-app/lib/features/store_management/presentation/pages/store_management_page.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; + +/// 多门店管理页面 +/// +/// 门店层级:总部 → 区域 → 门店 +/// 门店员工管理:收银员/店长/管理员 +class StoreManagementPage extends StatelessWidget { + const StoreManagementPage({super.key}); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: const Text('门店管理'), + bottom: const TabBar( + tabs: [ + Tab(text: '门店列表'), + Tab(text: '员工管理'), + ], + ), + ), + body: const TabBarView( + children: [ + _StoreListTab(), + _EmployeeListTab(), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () {}, + backgroundColor: AppColors.primary, + child: const Icon(Icons.add_rounded, color: Colors.white), + ), + ), + ); + } +} + +class _StoreListTab extends StatelessWidget { + const _StoreListTab(); + + @override + Widget build(BuildContext context) { + final stores = [ + _Store('总部', 'headquarters', '上海市黄浦区', 15, true), + _Store('华东区', 'regional', '上海/杭州/南京', 8, true), + _Store('徐汇门店', 'store', '上海市徐汇区xxx路', 3, true), + _Store('静安门店', 'store', '上海市静安区xxx路', 2, true), + _Store('杭州西湖店', 'store', '杭州市西湖区xxx路', 2, false), + ]; + + return ListView.builder( + padding: const EdgeInsets.all(20), + itemCount: stores.length, + itemBuilder: (context, index) { + final store = stores[index]; + return Container( + margin: EdgeInsets.only( + left: store.level == 'regional' ? 20 : store.level == 'store' ? 40 : 0, + bottom: 10, + ), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: _levelColor(store.level).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(_levelIcon(store.level), color: _levelColor(store.level), size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(store.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + color: store.isActive ? AppColors.successLight : AppColors.gray100, + borderRadius: BorderRadius.circular(999), + ), + child: Text( + store.isActive ? '营业中' : '休息中', + style: TextStyle(fontSize: 10, color: store.isActive ? AppColors.success : AppColors.textTertiary), + ), + ), + ], + ), + const SizedBox(height: 2), + Text(store.address, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + ], + ), + ), + Text('${store.staffCount}人', style: const TextStyle(fontSize: 12, color: AppColors.textTertiary)), + const SizedBox(width: 4), + const Icon(Icons.chevron_right_rounded, size: 18, color: AppColors.textTertiary), + ], + ), + ); + }, + ); + } + + Color _levelColor(String level) { + switch (level) { + case 'headquarters': return AppColors.primary; + case 'regional': return AppColors.info; + default: return AppColors.success; + } + } + + IconData _levelIcon(String level) { + switch (level) { + case 'headquarters': return Icons.business_rounded; + case 'regional': return Icons.location_city_rounded; + default: return Icons.store_rounded; + } + } +} + +class _EmployeeListTab extends StatelessWidget { + const _EmployeeListTab(); + + @override + Widget build(BuildContext context) { + final employees = [ + ('张经理', '管理员', '总部', Icons.admin_panel_settings_rounded, AppColors.primary), + ('李店长', '店长', '徐汇门店', Icons.manage_accounts_rounded, AppColors.info), + ('王店长', '店长', '静安门店', Icons.manage_accounts_rounded, AppColors.info), + ('赵收银', '收银员', '徐汇门店', Icons.point_of_sale_rounded, AppColors.success), + ('钱收银', '收银员', '徐汇门店', Icons.point_of_sale_rounded, AppColors.success), + ('孙收银', '收银员', '静安门店', Icons.point_of_sale_rounded, AppColors.success), + ]; + + return ListView.separated( + padding: const EdgeInsets.all(20), + itemCount: employees.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final (name, role, store, icon, color) = employees[index]; + return ListTile( + contentPadding: const EdgeInsets.symmetric(vertical: 4), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: color, size: 20), + ), + title: Text(name, style: const TextStyle(fontWeight: FontWeight.w500)), + subtitle: Text('$role · $store', style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + trailing: PopupMenuButton( + itemBuilder: (ctx) => [ + const PopupMenuItem(value: 'edit', child: Text('编辑')), + const PopupMenuItem(value: 'delete', child: Text('移除')), + ], + ), + ); + }, + ); + } +} + +class _Store { + final String name; + final String level; + final String address; + final int staffCount; + final bool isActive; + + _Store(this.name, this.level, this.address, this.staffCount, this.isActive); +} diff --git a/frontend/admin-app/lib/main.dart b/frontend/admin-app/lib/main.dart new file mode 100644 index 0000000..9d74853 --- /dev/null +++ b/frontend/admin-app/lib/main.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'app/theme/app_theme.dart'; +import 'app/router.dart'; + +void main() { + runApp(const GenexIssuerApp()); +} + +/// Genex 发行方管理控制台 (Issuer Console) +/// +/// 企业/政府/机构使用的券发行与管理工具 +/// Web2体验:发券 = 创建优惠活动,链上铸造在后台自动完成 +class GenexIssuerApp extends StatelessWidget { + const GenexIssuerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Genex Issuer Console', + theme: AppTheme.light, + debugShowCheckedModeBanner: false, + initialRoute: AppRouter.splash, + onGenerateRoute: AppRouter.generateRoute, + ); + } +} diff --git a/frontend/admin-app/lib/shared/widgets/ai_suggestion_card.dart b/frontend/admin-app/lib/shared/widgets/ai_suggestion_card.dart new file mode 100644 index 0000000..69261fe --- /dev/null +++ b/frontend/admin-app/lib/shared/widgets/ai_suggestion_card.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; + +/// 通用AI建议卡片(发行方专用) +/// +/// 场景:发券建议、定价优化、信用提升、销售分析、额度规划 +class AiSuggestionCard extends StatelessWidget { + final String content; + final bool actionable; + final VoidCallback? onAccept; + final VoidCallback? onDismiss; + + const AiSuggestionCard({ + super.key, + required this.content, + this.actionable = true, + this.onAccept, + this.onDismiss, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.primary.withValues(alpha: 0.15)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(7), + ), + child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 14), + ), + const SizedBox(width: 8), + const Text('AI 建议', style: TextStyle(fontSize: 13, color: AppColors.primary, fontWeight: FontWeight.w600)), + ], + ), + const SizedBox(height: 10), + Text( + content, + style: const TextStyle(fontSize: 13, color: AppColors.textSecondary, height: 1.5), + ), + if (actionable) ...[ + const SizedBox(height: 12), + Row( + children: [ + TextButton( + onPressed: onDismiss, + child: const Text('忽略', style: TextStyle(fontSize: 13)), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: onAccept, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + minimumSize: Size.zero, + ), + child: const Text('采纳', style: TextStyle(fontSize: 13)), + ), + ], + ), + ], + ], + ), + ); + } +} diff --git a/frontend/admin-app/pubspec.yaml b/frontend/admin-app/pubspec.yaml new file mode 100644 index 0000000..11d8cf2 --- /dev/null +++ b/frontend/admin-app/pubspec.yaml @@ -0,0 +1,30 @@ +name: genex_issuer +description: Genex Issuer Console - 发行方管理控制台 +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_riverpod: ^2.4.0 + go_router: ^12.0.0 + dio: ^5.3.0 + freezed_annotation: ^2.4.0 + json_annotation: ^4.8.0 + fl_chart: ^0.65.0 + qr_code_scanner: ^1.0.1 + intl: ^0.18.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + build_runner: ^2.4.0 + freezed: ^2.4.0 + json_serializable: ^6.7.0 + +flutter: + uses-material-design: true diff --git a/frontend/admin-web/src/i18n/locales.ts b/frontend/admin-web/src/i18n/locales.ts new file mode 100644 index 0000000..3775812 --- /dev/null +++ b/frontend/admin-web/src/i18n/locales.ts @@ -0,0 +1,622 @@ +/** + * Genex Admin Web - i18n 多语言支持 + * + * 支持: zh-CN (默认), en-US, ja-JP + * 使用方式: import { t } from '@/i18n/locales'; 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': '完成', + 'create': '创建', + 'export': '导出', + 'import': '导入', + 'filter': '筛选', + 'reset': '重置', + 'submit': '提交', + 'approve': '批准', + 'reject': '拒绝', + 'enable': '启用', + 'disable': '禁用', + 'status': '状态', + 'actions': '操作', + 'details': '详情', + 'total': '合计', + 'yes': '是', + 'no': '否', + + // ── Sidebar / Navigation ── + 'nav_dashboard': '仪表盘', + 'nav_overview': '总览', + 'nav_users': '用户管理', + 'nav_coupons': '券审核', + 'nav_issuers': '发行方管理', + 'nav_finance': '财务', + 'nav_chain': '链监控', + 'nav_reports': '报表', + 'nav_compliance': '合规', + 'nav_analytics': '数据分析', + 'nav_ai_agent': 'AI 代理', + 'nav_system': '系统设置', + 'nav_market_maker': '做市商', + 'nav_insurance': '保险', + + // ── Dashboard ── + 'dashboard_title': '仪表盘', + 'dashboard_total_users': '总用户数', + 'dashboard_active_users': '活跃用户', + 'dashboard_total_coupons': '券总量', + 'dashboard_trading_volume': '交易量', + 'dashboard_revenue': '平台收入', + 'dashboard_today': '今日', + 'dashboard_weekly': '本周', + 'dashboard_monthly': '本月', + 'dashboard_yearly': '本年', + 'dashboard_real_time': '实时数据', + 'dashboard_system_health': '系统健康', + 'dashboard_alerts': '告警', + + // ── User Management ── + 'user_list': '用户列表', + 'user_detail': '用户详情', + 'user_id': '用户ID', + 'user_name': '用户名', + 'user_email': '邮箱', + 'user_phone': '手机号', + 'user_role': '角色', + 'user_status': '状态', + 'user_kyc_level': 'KYC等级', + 'user_created_at': '注册时间', + 'user_last_login': '最后登录', + 'user_freeze': '冻结', + 'user_unfreeze': '解冻', + 'user_ban': '封禁', + 'user_consumer': '消费者', + 'user_issuer': '发行方', + 'user_admin': '管理员', + + // ── Coupon Review ── + 'coupon_review': '券审核', + 'coupon_pending_review': '待审核', + 'coupon_approved': '已通过', + 'coupon_rejected': '已拒绝', + 'coupon_issuer': '发行方', + 'coupon_face_value': '面值', + 'coupon_quantity': '发行量', + 'coupon_valid_period': '有效期', + 'coupon_category': '类别', + 'coupon_risk_score': '风险评分', + 'coupon_chain_status': '上链状态', + 'coupon_approve': '通过审核', + 'coupon_reject': '拒绝审核', + 'coupon_review_comment': '审核意见', + + // ── Issuer Management ── + 'issuer_list': '发行方列表', + 'issuer_detail': '发行方详情', + 'issuer_name': '名称', + 'issuer_credit_rating': '信用评级', + 'issuer_onboarding_status': '入驻状态', + 'issuer_onboarding_pending': '待审核', + 'issuer_onboarding_approved': '已入驻', + 'issuer_onboarding_rejected': '已拒绝', + 'issuer_total_issued': '已发行总量', + 'issuer_total_redeemed': '已核销总量', + 'issuer_collateral': '质押品', + 'issuer_deposit': '保证金', + + // ── Finance ── + 'finance_overview': '财务概览', + 'finance_settlement': '结算管理', + 'finance_fee': '手续费', + 'finance_revenue': '收入', + 'finance_payout': '支出', + 'finance_reconciliation': '对账', + 'finance_invoice': '发票', + 'finance_tax': '税务', + 'finance_report': '财务报表', + + // ── Chain Monitor ── + 'chain_monitor': '链监控', + 'chain_block_height': '区块高度', + 'chain_tps': 'TPS', + 'chain_node_status': '节点状态', + 'chain_contracts': '智能合约', + 'chain_transactions': '链上交易', + 'chain_gas_usage': 'Gas 消耗', + 'chain_explorer': '区块浏览器', + + // ── Reports ── + 'report_list': '报表列表', + 'report_generate': '生成报表', + 'report_daily': '日报', + 'report_weekly': '周报', + 'report_monthly': '月报', + 'report_custom': '自定义报表', + 'report_download': '下载报表', + + // ── Compliance / Regulatory ── + 'compliance_overview': '合规概览', + 'compliance_sec_filing': 'SEC 申报', + 'compliance_license': '牌照管理', + 'compliance_sox': 'SOX 审计', + 'compliance_tax_report': '税务报表', + 'compliance_aml': '反洗钱', + 'compliance_kyc_review': 'KYC 审核', + 'compliance_consumer_protection': '消费者保护', + 'compliance_risk_assessment': '风险评估', + 'compliance_audit_log': '审计日志', + 'compliance_policy': '合规政策', + + // ── Analytics ── + 'analytics_overview': '分析概览', + 'analytics_user_growth': '用户增长', + 'analytics_trading_volume': '交易量分析', + 'analytics_coupon_lifecycle': '券生命周期', + 'analytics_conversion': '转化率', + 'analytics_retention': '留存率', + 'analytics_heatmap': '热力图', + 'analytics_funnel': '漏斗分析', + 'analytics_cohort': '群组分析', + + // ── Market Maker ── + 'market_maker_list': '做市商列表', + 'market_maker_config': '做市商配置', + 'market_maker_spread': '价差', + 'market_maker_liquidity': '流动性', + 'market_maker_algorithm': '算法策略', + 'market_maker_pnl': '盈亏', + + // ── Insurance ── + 'insurance_pool': '保险池', + 'insurance_premium': '保费', + 'insurance_claim': '理赔', + 'insurance_coverage': '覆盖范围', + 'insurance_policy': '保险策略', + + // ── AI Agent ── + 'ai_agent_list': 'AI 代理列表', + 'ai_agent_config': 'AI 代理配置', + 'ai_agent_logs': 'AI 运行日志', + 'ai_agent_pricing': 'AI 定价引擎', + 'ai_agent_recommendation': 'AI 推荐引擎', + 'ai_agent_risk': 'AI 风控', + 'ai_agent_status': '运行状态', + + // ── System Settings ── + 'system_general': '通用设置', + 'system_roles': '角色权限', + 'system_api_keys': 'API 密钥', + 'system_webhooks': 'Webhooks', + 'system_notifications': '通知设置', + 'system_maintenance': '系统维护', + 'system_backup': '备份', + 'system_logs': '系统日志', + 'system_feature_flags': '功能开关', + 'system_rate_limit': '限流配置', + }, + + 'en-US': { + // ── Common ── + 'app_name': 'Genex Admin', + 'confirm': 'Confirm', + 'cancel': 'Cancel', + 'save': 'Save', + 'delete': 'Delete', + 'edit': 'Edit', + 'search': 'Search', + 'loading': 'Loading...', + 'retry': 'Retry', + 'done': 'Done', + 'create': 'Create', + 'export': 'Export', + 'import': 'Import', + 'filter': 'Filter', + 'reset': 'Reset', + 'submit': 'Submit', + 'approve': 'Approve', + 'reject': 'Reject', + 'enable': 'Enable', + 'disable': 'Disable', + 'status': 'Status', + 'actions': 'Actions', + 'details': 'Details', + 'total': 'Total', + 'yes': 'Yes', + 'no': 'No', + + // ── Sidebar / Navigation ── + 'nav_dashboard': 'Dashboard', + 'nav_overview': 'Overview', + 'nav_users': 'User Management', + 'nav_coupons': 'Coupon Review', + 'nav_issuers': 'Issuer Management', + 'nav_finance': 'Finance', + 'nav_chain': 'Chain Monitor', + 'nav_reports': 'Reports', + 'nav_compliance': 'Compliance', + 'nav_analytics': 'Analytics', + 'nav_ai_agent': 'AI Agent', + 'nav_system': 'System Settings', + 'nav_market_maker': 'Market Maker', + 'nav_insurance': 'Insurance', + + // ── Dashboard ── + 'dashboard_title': 'Dashboard', + 'dashboard_total_users': 'Total Users', + 'dashboard_active_users': 'Active Users', + 'dashboard_total_coupons': 'Total Coupons', + 'dashboard_trading_volume': 'Trading Volume', + 'dashboard_revenue': 'Platform Revenue', + 'dashboard_today': 'Today', + 'dashboard_weekly': 'This Week', + 'dashboard_monthly': 'This Month', + 'dashboard_yearly': 'This Year', + 'dashboard_real_time': 'Real-time Data', + 'dashboard_system_health': 'System Health', + 'dashboard_alerts': 'Alerts', + + // ── User Management ── + 'user_list': 'User List', + 'user_detail': 'User Details', + 'user_id': 'User ID', + 'user_name': 'Username', + 'user_email': 'Email', + 'user_phone': 'Phone', + 'user_role': 'Role', + 'user_status': 'Status', + 'user_kyc_level': 'KYC Level', + 'user_created_at': 'Registered', + 'user_last_login': 'Last Login', + 'user_freeze': 'Freeze', + 'user_unfreeze': 'Unfreeze', + 'user_ban': 'Ban', + 'user_consumer': 'Consumer', + 'user_issuer': 'Issuer', + 'user_admin': 'Admin', + + // ── Coupon Review ── + 'coupon_review': 'Coupon Review', + 'coupon_pending_review': 'Pending Review', + 'coupon_approved': 'Approved', + 'coupon_rejected': 'Rejected', + 'coupon_issuer': 'Issuer', + 'coupon_face_value': 'Face Value', + 'coupon_quantity': 'Quantity', + 'coupon_valid_period': 'Valid Period', + 'coupon_category': 'Category', + 'coupon_risk_score': 'Risk Score', + 'coupon_chain_status': 'On-chain Status', + 'coupon_approve': 'Approve', + 'coupon_reject': 'Reject', + 'coupon_review_comment': 'Review Comment', + + // ── Issuer Management ── + 'issuer_list': 'Issuer List', + 'issuer_detail': 'Issuer Details', + 'issuer_name': 'Name', + 'issuer_credit_rating': 'Credit Rating', + 'issuer_onboarding_status': 'Onboarding Status', + 'issuer_onboarding_pending': 'Pending', + 'issuer_onboarding_approved': 'Approved', + 'issuer_onboarding_rejected': 'Rejected', + 'issuer_total_issued': 'Total Issued', + 'issuer_total_redeemed': 'Total Redeemed', + 'issuer_collateral': 'Collateral', + 'issuer_deposit': 'Deposit', + + // ── Finance ── + 'finance_overview': 'Finance Overview', + 'finance_settlement': 'Settlement', + 'finance_fee': 'Fees', + 'finance_revenue': 'Revenue', + 'finance_payout': 'Payouts', + 'finance_reconciliation': 'Reconciliation', + 'finance_invoice': 'Invoices', + 'finance_tax': 'Tax', + 'finance_report': 'Financial Report', + + // ── Chain Monitor ── + 'chain_monitor': 'Chain Monitor', + 'chain_block_height': 'Block Height', + 'chain_tps': 'TPS', + 'chain_node_status': 'Node Status', + 'chain_contracts': 'Smart Contracts', + 'chain_transactions': 'On-chain Transactions', + 'chain_gas_usage': 'Gas Usage', + 'chain_explorer': 'Block Explorer', + + // ── Reports ── + 'report_list': 'Report List', + 'report_generate': 'Generate Report', + 'report_daily': 'Daily Report', + 'report_weekly': 'Weekly Report', + 'report_monthly': 'Monthly Report', + 'report_custom': 'Custom Report', + 'report_download': 'Download Report', + + // ── Compliance / Regulatory ── + 'compliance_overview': 'Compliance Overview', + 'compliance_sec_filing': 'SEC Filing', + 'compliance_license': 'License Management', + 'compliance_sox': 'SOX Audit', + 'compliance_tax_report': 'Tax Report', + 'compliance_aml': 'Anti-Money Laundering', + 'compliance_kyc_review': 'KYC Review', + 'compliance_consumer_protection': 'Consumer Protection', + 'compliance_risk_assessment': 'Risk Assessment', + 'compliance_audit_log': 'Audit Log', + 'compliance_policy': 'Compliance Policy', + + // ── Analytics ── + 'analytics_overview': 'Analytics Overview', + 'analytics_user_growth': 'User Growth', + 'analytics_trading_volume': 'Trading Volume Analysis', + 'analytics_coupon_lifecycle': 'Coupon Lifecycle', + 'analytics_conversion': 'Conversion Rate', + 'analytics_retention': 'Retention Rate', + 'analytics_heatmap': 'Heatmap', + 'analytics_funnel': 'Funnel Analysis', + 'analytics_cohort': 'Cohort Analysis', + + // ── Market Maker ── + 'market_maker_list': 'Market Makers', + 'market_maker_config': 'Market Maker Config', + 'market_maker_spread': 'Spread', + 'market_maker_liquidity': 'Liquidity', + 'market_maker_algorithm': 'Algorithm Strategy', + 'market_maker_pnl': 'P&L', + + // ── Insurance ── + 'insurance_pool': 'Insurance Pool', + 'insurance_premium': 'Premium', + 'insurance_claim': 'Claims', + 'insurance_coverage': 'Coverage', + 'insurance_policy': 'Insurance Policy', + + // ── AI Agent ── + 'ai_agent_list': 'AI Agent List', + 'ai_agent_config': 'AI Agent Config', + 'ai_agent_logs': 'AI Execution Logs', + 'ai_agent_pricing': 'AI Pricing Engine', + 'ai_agent_recommendation': 'AI Recommendation Engine', + 'ai_agent_risk': 'AI Risk Control', + 'ai_agent_status': 'Running Status', + + // ── System Settings ── + 'system_general': 'General Settings', + 'system_roles': 'Roles & Permissions', + 'system_api_keys': 'API Keys', + 'system_webhooks': 'Webhooks', + 'system_notifications': 'Notification Settings', + 'system_maintenance': 'Maintenance', + 'system_backup': 'Backup', + 'system_logs': 'System Logs', + 'system_feature_flags': 'Feature Flags', + 'system_rate_limit': 'Rate Limiting', + }, + + 'ja-JP': { + // ── Common ── + 'app_name': 'Genex 管理画面', + 'confirm': '確認', + 'cancel': 'キャンセル', + 'save': '保存', + 'delete': '削除', + 'edit': '編集', + 'search': '検索', + 'loading': '読み込み中...', + 'retry': 'リトライ', + 'done': '完了', + 'create': '作成', + 'export': 'エクスポート', + 'import': 'インポート', + 'filter': 'フィルター', + 'reset': 'リセット', + 'submit': '送信', + 'approve': '承認', + 'reject': '却下', + 'enable': '有効', + 'disable': '無効', + 'status': 'ステータス', + 'actions': '操作', + 'details': '詳細', + 'total': '合計', + 'yes': 'はい', + 'no': 'いいえ', + + // ── Sidebar / Navigation ── + 'nav_dashboard': 'ダッシュボード', + 'nav_overview': '概要', + 'nav_users': 'ユーザー管理', + 'nav_coupons': 'クーポン審査', + 'nav_issuers': '発行者管理', + 'nav_finance': '財務', + 'nav_chain': 'チェーン監視', + 'nav_reports': 'レポート', + 'nav_compliance': 'コンプライアンス', + 'nav_analytics': 'データ分析', + 'nav_ai_agent': 'AIエージェント', + 'nav_system': 'システム設定', + 'nav_market_maker': 'マーケットメーカー', + 'nav_insurance': '保険', + + // ── Dashboard ── + 'dashboard_title': 'ダッシュボード', + 'dashboard_total_users': '総ユーザー数', + 'dashboard_active_users': 'アクティブユーザー', + 'dashboard_total_coupons': 'クーポン総数', + 'dashboard_trading_volume': '取引量', + 'dashboard_revenue': 'プラットフォーム収益', + 'dashboard_today': '本日', + 'dashboard_weekly': '今週', + 'dashboard_monthly': '今月', + 'dashboard_yearly': '今年', + 'dashboard_real_time': 'リアルタイムデータ', + 'dashboard_system_health': 'システムヘルス', + 'dashboard_alerts': 'アラート', + + // ── User Management ── + 'user_list': 'ユーザー一覧', + 'user_detail': 'ユーザー詳細', + 'user_id': 'ユーザーID', + 'user_name': 'ユーザー名', + 'user_email': 'メール', + 'user_phone': '電話番号', + 'user_role': '役割', + 'user_status': 'ステータス', + 'user_kyc_level': 'KYCレベル', + 'user_created_at': '登録日', + 'user_last_login': '最終ログイン', + 'user_freeze': '凍結', + 'user_unfreeze': '凍結解除', + 'user_ban': 'アカウント停止', + 'user_consumer': '消費者', + 'user_issuer': '発行者', + 'user_admin': '管理者', + + // ── Coupon Review ── + 'coupon_review': 'クーポン審査', + 'coupon_pending_review': '審査待ち', + 'coupon_approved': '承認済み', + 'coupon_rejected': '却下済み', + 'coupon_issuer': '発行者', + 'coupon_face_value': '額面', + 'coupon_quantity': '発行数', + 'coupon_valid_period': '有効期間', + 'coupon_category': 'カテゴリー', + 'coupon_risk_score': 'リスクスコア', + 'coupon_chain_status': 'オンチェーン状態', + 'coupon_approve': '承認', + 'coupon_reject': '却下', + 'coupon_review_comment': '審査コメント', + + // ── Issuer Management ── + 'issuer_list': '発行者一覧', + 'issuer_detail': '発行者詳細', + 'issuer_name': '名称', + 'issuer_credit_rating': '信用格付け', + 'issuer_onboarding_status': '申請状態', + 'issuer_onboarding_pending': '審査中', + 'issuer_onboarding_approved': '承認済み', + 'issuer_onboarding_rejected': '却下済み', + 'issuer_total_issued': '発行総数', + 'issuer_total_redeemed': '検証総数', + 'issuer_collateral': '担保', + 'issuer_deposit': '保証金', + + // ── Finance ── + 'finance_overview': '財務概要', + 'finance_settlement': '決済管理', + 'finance_fee': '手数料', + 'finance_revenue': '収益', + 'finance_payout': '支払い', + 'finance_reconciliation': '照合', + 'finance_invoice': '請求書', + 'finance_tax': '税務', + 'finance_report': '財務レポート', + + // ── Chain Monitor ── + 'chain_monitor': 'チェーン監視', + 'chain_block_height': 'ブロック高さ', + 'chain_tps': 'TPS', + 'chain_node_status': 'ノードステータス', + 'chain_contracts': 'スマートコントラクト', + 'chain_transactions': 'オンチェーン取引', + 'chain_gas_usage': 'Gas消費', + 'chain_explorer': 'ブロックエクスプローラー', + + // ── Reports ── + 'report_list': 'レポート一覧', + 'report_generate': 'レポート生成', + 'report_daily': '日次レポート', + 'report_weekly': '週次レポート', + 'report_monthly': '月次レポート', + 'report_custom': 'カスタムレポート', + 'report_download': 'レポートダウンロード', + + // ── Compliance / Regulatory ── + 'compliance_overview': 'コンプライアンス概要', + 'compliance_sec_filing': 'SEC申告', + 'compliance_license': 'ライセンス管理', + 'compliance_sox': 'SOX監査', + 'compliance_tax_report': '税務報告', + 'compliance_aml': 'マネーロンダリング対策', + 'compliance_kyc_review': 'KYC審査', + 'compliance_consumer_protection': '消費者保護', + 'compliance_risk_assessment': 'リスク評価', + 'compliance_audit_log': '監査ログ', + 'compliance_policy': 'コンプライアンスポリシー', + + // ── Analytics ── + 'analytics_overview': '分析概要', + 'analytics_user_growth': 'ユーザー成長', + 'analytics_trading_volume': '取引量分析', + 'analytics_coupon_lifecycle': 'クーポンライフサイクル', + 'analytics_conversion': 'コンバージョン率', + 'analytics_retention': 'リテンション率', + 'analytics_heatmap': 'ヒートマップ', + 'analytics_funnel': 'ファネル分析', + 'analytics_cohort': 'コホート分析', + + // ── Market Maker ── + 'market_maker_list': 'マーケットメーカー一覧', + 'market_maker_config': 'マーケットメーカー設定', + 'market_maker_spread': 'スプレッド', + 'market_maker_liquidity': '流動性', + 'market_maker_algorithm': 'アルゴリズム戦略', + 'market_maker_pnl': '損益', + + // ── Insurance ── + 'insurance_pool': '保険プール', + 'insurance_premium': '保険料', + 'insurance_claim': '保険請求', + 'insurance_coverage': '補償範囲', + 'insurance_policy': '保険ポリシー', + + // ── AI Agent ── + 'ai_agent_list': 'AIエージェント一覧', + 'ai_agent_config': 'AIエージェント設定', + 'ai_agent_logs': 'AI実行ログ', + 'ai_agent_pricing': 'AI価格エンジン', + 'ai_agent_recommendation': 'AIレコメンドエンジン', + 'ai_agent_risk': 'AIリスク管理', + 'ai_agent_status': '実行ステータス', + + // ── System Settings ── + 'system_general': '一般設定', + 'system_roles': '役割と権限', + 'system_api_keys': 'APIキー', + 'system_webhooks': 'Webhooks', + 'system_notifications': '通知設定', + 'system_maintenance': 'メンテナンス', + 'system_backup': 'バックアップ', + 'system_logs': 'システムログ', + 'system_feature_flags': '機能フラグ', + 'system_rate_limit': 'レート制限', + }, +}; diff --git a/frontend/admin-web/src/layouts/AdminLayout.tsx b/frontend/admin-web/src/layouts/AdminLayout.tsx new file mode 100644 index 0000000..ee5e132 --- /dev/null +++ b/frontend/admin-web/src/layouts/AdminLayout.tsx @@ -0,0 +1,316 @@ +import React, { useState } from 'react'; + +/** + * D. Web管理前端 - 主布局 + * + * 左侧Sidebar导航 + 顶部Header + 主内容区 + * 覆盖 D1-D8 所有管理模块的路由结构 + */ + +interface NavItem { + key: string; + icon: string; + label: string; + children?: NavItem[]; + badge?: number; +} + +const navItems: NavItem[] = [ + { key: 'dashboard', icon: '📊', label: '运营总览' }, + { + key: 'issuers', icon: '🏢', label: '发行方管理', + children: [ + { key: 'issuers/review', icon: '', label: '入驻审核' }, + { key: 'issuers/list', icon: '', label: '发行方列表' }, + { key: 'issuers/coupons', icon: '', label: '券审核' }, + ], + }, + { + key: 'users', icon: '👤', label: '用户管理', + children: [ + { key: 'users/list', icon: '', label: '用户列表' }, + { key: 'users/kyc', icon: '', label: 'KYC审核', badge: 5 }, + ], + }, + { + key: 'trading', icon: '💱', label: '交易监控', + children: [ + { key: 'trading/realtime', icon: '', label: '实时交易流' }, + { key: 'trading/stats', icon: '', label: '交易统计' }, + { key: 'trading/orders', icon: '', label: '订单管理' }, + ], + }, + { + key: 'risk', icon: '🛡️', label: '风控中心', + children: [ + { key: 'risk/dashboard', icon: '', label: '风险仪表盘', badge: 3 }, + { key: 'risk/suspicious', icon: '', label: '可疑交易' }, + { key: 'risk/blacklist', icon: '', label: '黑名单管理' }, + { key: 'risk/ofac', icon: '', label: 'OFAC筛查日志' }, + ], + }, + { + key: 'compliance', icon: '📋', label: '合规报表', + children: [ + { key: 'compliance/sar', icon: '', label: 'SAR管理' }, + { key: 'compliance/ctr', icon: '', label: 'CTR管理' }, + { key: 'compliance/audit', icon: '', label: '审计日志' }, + { key: 'compliance/reports', icon: '', label: '监管报表' }, + ], + }, + { + key: 'system', icon: '⚙️', label: '系统管理', + children: [ + { key: 'system/admins', icon: '', label: '管理员账号' }, + { key: 'system/config', icon: '', label: '系统配置' }, + { key: 'system/contracts', icon: '', label: '合约管理' }, + { key: 'system/monitor', icon: '', label: '系统监控' }, + ], + }, + { key: 'disputes', icon: '⚖️', label: '争议处理', badge: 8 }, +]; + +export const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [collapsed, setCollapsed] = useState(false); + const [activeKey, setActiveKey] = useState('dashboard'); + const [expandedKeys, setExpandedKeys] = useState(['issuers', 'risk']); + + const toggleExpand = (key: string) => { + setExpandedKeys(prev => + prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key] + ); + }; + + return ( +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ {/* Header */} +
+
+ +
+
+ {/* AI Agent Button */} + + {/* Notifications */} + + {/* Admin avatar */} +
+ A +
+
+
+ + {/* Page Content */} +
+ {children} +
+
+
+ ); +}; diff --git a/frontend/admin-web/src/pages/agent/AgentPanelPage.tsx b/frontend/admin-web/src/pages/agent/AgentPanelPage.tsx new file mode 100644 index 0000000..d74a1cf --- /dev/null +++ b/frontend/admin-web/src/pages/agent/AgentPanelPage.tsx @@ -0,0 +1,101 @@ +import React from 'react'; + +/** + * D7. AI Agent管理面板 - 平台管理员的AI Agent运营监控 + * + * Agent会话统计、常见问题Top10、响应质量监控、模型配置 + */ + +const agentStats = [ + { label: '今日会话数', value: '3,456', change: '+18%', color: 'var(--color-primary)' }, + { label: '平均响应时间', value: '1.2s', change: '-0.3s', color: 'var(--color-success)' }, + { label: '用户满意度', value: '94.5%', change: '+2.1%', color: 'var(--color-info)' }, + { label: '人工接管率', value: '3.2%', change: '-0.5%', color: 'var(--color-warning)' }, +]; + +const topQuestions = [ + { question: '如何购买券?', count: 234, category: '使用指引' }, + { question: '推荐高折扣券', count: 189, category: '智能推券' }, + { question: '我的券快过期了', count: 156, category: '到期管理' }, + { question: '如何出售我的券?', count: 134, category: '交易指引' }, + { question: '退款怎么操作?', count: 98, category: '售后服务' }, +]; + +const agentModules = [ + { name: '智能推券', status: 'active', accuracy: '92%', desc: '根据用户画像推荐券' }, + { name: '比价分析', status: 'active', accuracy: '96%', desc: '三因子定价模型分析' }, + { name: '投资教育', status: 'active', accuracy: '89%', desc: '券投资知识科普' }, + { name: '客服对话', status: 'active', accuracy: '91%', desc: '常见问题自动应答' }, + { name: '发行方助手', status: 'active', accuracy: '94%', desc: '发券建议/定价优化' }, + { name: '风险预警', status: 'beta', accuracy: '87%', desc: '异常交易智能预警' }, +]; + +export const AgentPanelPage: React.FC = () => { + return ( +
+

AI Agent 管理

+ + {/* Stats */} +
+ {agentStats.map(s => ( +
+
{s.label}
+
{s.value}
+
{s.change}
+
+ ))} +
+ +
+ {/* Top Questions */} +
+

热门问题 Top 5

+ {topQuestions.map((q, i) => ( +
+ {i + 1} +
+
{q.question}
+ {q.category} +
+ {q.count}次 +
+ ))} +
+ + {/* Agent Modules */} +
+

Agent 模块

+ {agentModules.map((m, i) => ( +
+
+
+ {m.name} + {m.status === 'active' ? '运行中' : 'Beta'} +
+
{m.desc}
+
+
+
{m.accuracy}
+
准确率
+
+
+ ))} +
+
+
+ ); +}; diff --git a/frontend/admin-web/src/pages/analytics/ConsumerProtectionPage.tsx b/frontend/admin-web/src/pages/analytics/ConsumerProtectionPage.tsx new file mode 100644 index 0000000..825240a --- /dev/null +++ b/frontend/admin-web/src/pages/analytics/ConsumerProtectionPage.tsx @@ -0,0 +1,349 @@ +import React from 'react'; + +/** + * 消费者保护分析仪表盘 + * + * 投诉统计、分类分析、满意度趋势、保障基金、退款合规 + */ + +const stats = [ + { label: '投诉总数', value: '234', change: '-5.2%', trend: 'down' as const, color: 'var(--color-error)' }, + { label: '已解决', value: '198', change: '+12.3%', trend: 'up' as const, color: 'var(--color-success)' }, + { label: '处理中', value: '28', change: '-8.1%', trend: 'down' as const, color: 'var(--color-warning)' }, + { label: '平均解决时间', value: '2.3天', change: '-0.4天', trend: 'down' as const, color: 'var(--color-info)' }, +]; + +const complaintCategories = [ + { name: '核销失败', count: 82, percent: 35, color: 'var(--color-error)' }, + { name: '退款纠纷', count: 68, percent: 29, color: 'var(--color-warning)' }, + { name: '虚假券', count: 49, percent: 21, color: 'var(--color-primary)' }, + { name: '其他', count: 35, percent: 15, color: 'var(--color-gray-400)' }, +]; + +const csatTrend = [ + { month: '9月', score: 4.1 }, + { month: '10月', score: 4.2 }, + { month: '11月', score: 4.0 }, + { month: '12月', score: 4.3 }, + { month: '1月', score: 4.4 }, + { month: '2月', score: 4.5 }, +]; + +const recentComplaints = [ + { id: 'CMP-0234', severity: '高' as const, category: '虚假券', title: '品牌方否认发行该优惠券', status: '处理中' as const, assignee: '张明', created: '2026-02-10' }, + { id: 'CMP-0233', severity: '高' as const, category: '退款纠纷', title: '核销后商家拒绝提供服务', status: '处理中' as const, assignee: '李华', created: '2026-02-10' }, + { id: 'CMP-0232', severity: '中' as const, category: '核销失败', title: '二维码扫描无反应,门店系统故障', status: '已解决' as const, assignee: '王芳', created: '2026-02-09' }, + { id: 'CMP-0231', severity: '低' as const, category: '其他', title: '券面信息与实际服务不符', status: '已解决' as const, assignee: '赵丽', created: '2026-02-09' }, + { id: 'CMP-0230', severity: '高' as const, category: '退款纠纷', title: '过期券退款被拒,用户称未收到提醒', status: '处理中' as const, assignee: '张明', created: '2026-02-09' }, + { id: 'CMP-0229', severity: '中' as const, category: '核销失败', title: '跨区域核销失败,券面未标注限制', status: '已解决' as const, assignee: '李华', created: '2026-02-08' }, + { id: 'CMP-0228', severity: '低' as const, category: '其他', title: '赠送的券对方未收到', status: '已解决' as const, assignee: '王芳', created: '2026-02-08' }, + { id: 'CMP-0227', severity: '中' as const, category: '虚假券', title: '折扣力度与宣传不符', status: '处理中' as const, assignee: '赵丽', created: '2026-02-07' }, +]; + +const severityConfig: 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)' }, +}; + +const statusConfig: Record = { + '处理中': { bg: 'var(--color-warning-light)', color: 'var(--color-warning)' }, + '已解决': { bg: 'var(--color-success-light)', color: 'var(--color-success)' }, + '已关闭': { bg: 'var(--color-gray-100)', color: 'var(--color-text-tertiary)' }, +}; + +const nonCompliantIssuers = [ + { rank: 1, issuer: '快乐生活电商', violations: 12, refundRate: '45%', avgDelay: '5.2天', riskLevel: '高' as const }, + { rank: 2, issuer: '优选旅游服务', violations: 9, refundRate: '52%', avgDelay: '4.8天', riskLevel: '高' as const }, + { rank: 3, issuer: '星辰数码官方', violations: 7, refundRate: '61%', avgDelay: '3.5天', riskLevel: '中' as const }, + { rank: 4, issuer: '美味餐饮集团', violations: 5, refundRate: '68%', avgDelay: '2.9天', riskLevel: '中' as const }, + { rank: 5, issuer: '悦享娱乐传媒', violations: 4, refundRate: '72%', avgDelay: '2.1天', riskLevel: '低' as const }, +]; + +export const ConsumerProtectionPage: React.FC = () => { + return ( +
+
+

消费者保护

+ +
+ + {/* 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 => ( + + ))} + + + + {recentComplaints.map(row => { + const sev = severityConfig[row.severity]; + const st = statusConfig[row.status]; + return ( + + + + + + + + + + ); + })} + +
{h}
+ {row.id} + + {row.severity} + {row.category}{row.title} + {row.status} + {row.assignee}{row.created}
+
+ + {/* Refund Policy Compliance */} +
+
+ 退款合规 - 不合规发行方 Top 5 +
+ + + + {['排名', '发行方', '违规次数', '退款通过率', '平均处理延迟', '风险等级', '操作'].map(h => ( + + ))} + + + + {nonCompliantIssuers.map(row => { + const risk = severityConfig[row.riskLevel]; + return ( + + + + + + + + + + ); + })} + +
{h}
{row.rank}{row.issuer}{row.violations} +
+
+
+
+ {row.refundRate} +
+
{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 => ( + + ))} + + + + {topCoupons.map(row => ( + + + + + + + + + ))} + +
{h}
{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 => ( + + ))} + + + + {marketMakers.map(mm => { + const s = statusConfig[mm.status]; + return ( + + + + + + + + + + ); + })} + +
{h}
{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 => ( + + ))} + + + + {geoDistribution.map(row => ( + + + + + + + ))} + +
{h}
{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 => ( + + ))} + + + + {cohortRetention.map(row => ( + + + {[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 ( + + ); + })} + + ))} + +
{h}
{row.cohort} + {val} +
+
+ + {/* Active User Segments */} +
+
活跃用户分群
+ {userSegments.map(seg => ( +
+ +
+
{seg.name}
+
{seg.count} 人
+
+
+
{seg.percent}%
+
+
+ ))} +
+ 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}
+
+
+
{e.hash}
+
{e.time}
+
+
+ ))} +
+ + {/* 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 => ( + + ))} + + + + {[ + { 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 => ( + + + + + + + + + + + ))} + +
{h}
{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 => ( +
+
+ {l.label} +
+ ))} +
+
+ +
+ {/* 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 => ( + + ))} + + + + {items.map(item => { + const st = statusConfig[item.status]; + return ( + + + + + + + + ); + })} + +
{h}
{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 && ( +
+ )} +
+
+
{m.name}
+
{m.date}
+
+
+ ))} +
+ + {/* 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 => ( +
+
+
{c.role}
+
{c.name}
+
+ {c.status} +
+ ))} +
+
+
+
+ ); +}; 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 => ( + + ))} + + + + {licenses.map(l => ( + + + + + + + + + + + ))} + +
{h}
{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 => ( + + ))} + + + + {secFilings.map(f => ( + + + + + + + + + + + ))} + +
{h}
{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 */} +
+
+ 披露文件完成度 + 57% +
+
+
+
+
+
+
+
+ ); +}; 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 => ( + + ))} + + + + {cat.controls.map((ctrl, ctrlIdx) => ( + + + + + + + ))} + +
{h}
{ctrl.name} + {ctrl.result} + {ctrl.lastTest}{ctrl.nextTest}
+
+ ))} +
+ +
+ {/* Deficiency Tracking */} +
+
+

缺陷追踪

+
+ + + + {['编号', '控制点', '严重程度', '描述', '整改期限', '状态', '负责方'].map(h => ( + + ))} + + + + {deficiencies.map(d => ( + + + + + + + + + + ))} + +
{h}
{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 */} +
+
+ 审计进度 + 33% +
+
+
+
+
+
+
+
+ ); +}; 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 => ( + + ))} + + + + {taxObligations.map((t, i) => ( + + + + + + + + + + ))} + +
{h}
+ {t.jurisdiction} + {t.taxType}{t.period}{t.amount}{t.paid}{t.dueDate} + {t.status} +
+
+ + {/* Tax Type Breakdown + IRS Filings */} +
+ {/* Tax Type Breakdown */} +
+
+

税种分类汇总

+
+ + + + {['税种', '联邦', '州级', '合计', '占比'].map(h => ( + + ))} + + + + {taxTypeBreakdown.map((row, i) => ( + + + + + + + + ))} + +
{h}
{row.type}{row.federal}{row.state}{row.total} +
+
+
+
+ {row.percentage}% +
+
+
+ + {/* Tax Calendar / Deadlines */} +
+

税务日历

+ {taxDeadlines.map((evt, i) => ( +
+
+ {evt.done ? '✓' : '!'} +
+
+
{evt.event}
+
{evt.date}
+
+ + {evt.done ? '已完成' : '待处理'} + +
+ ))} +
+
+ + {/* IRS Filing Tracker */} +
+
+

IRS表格提交追踪

+
+ + + + {['表格', '说明', '税务年度', '截止日期', '提交日期', '状态'].map(h => ( + + ))} + + + + {irsFilings.map((f, i) => ( + + + + + + + + + ))} + +
{h}
+ {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 => ( + + ))} + + + + {filtered.map(coupon => ( + + + + + + + + + + + + + ))} + +
{h}
{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 => ( + + ))} + + + + {[ + { 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) => ( + + + + + + + + ))} + +
{h}
{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 => ( + + ))} + + + + {mockDisputes.map(d => { + const sc = statusConfig[d.status]; + const tc = typeConfig[d.type]; + return ( + + + + + + + + + + + + + ); + })} + +
{h}
{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 => ( + + ))} + + + {recentSettlements.map((s, i) => ( + + + + + + + ))} + +
{h}
{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 => ( + + ))} + + + {recentClaims.map(c => ( + + + + + + + + ))} + +
{h}
{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 */} +
+
+ 总体进度 + 72% +
+
+
+
+
+
+
+
+ ); +}; 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 => ( + + ))} + + + + {filtered.map(issuer => { + const ss = statusStyle(issuer.status); + return ( + + + + + + + + + + + ); + })} + +
{h}
+ {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.name}
+
{r.desc}
+
+
+ {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 => ( + + ))} + + + + {[ + { 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 => ( + + + + + + + + + + ))} + +
{h}
{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 => ( + + ))} + + + + {[ + { 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 => ( + + + + + + + + + ))} + +
{h}
{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 => ( + + ))} + + + + {Array.from({ length: 8 }, (_, i) => ( + + + + + + + + + + + ))} + +
{h}
+ 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 => ( + + ))} + + + + {filtered.map(user => ( + + + + + + + + + + + + ))} + +
{h}
{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 */} +
+
+ 💎 + 来自 Genex 的分享 +
+
+ + {/* Coupon Card */} +
+ {/* Image */} +
+ 🎫 +
+ + {/* Info */} +
+
+
S
+ Starbucks + AAA +
+ +

+ 星巴克 $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) => ( +
+
{f.icon}
+
+
{f.title}
+
{f.desc}
+
+
+ ))} + +
+ + +
+
+ ); +}; + +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); +}