feat: 小程序功能移植,新增12页面+160 i18n keys,覆盖率提升至~70%
新增页面:search, purchase(重写), payment-success, my-coupon-detail, transfer, ai-chat, orders(重写), messages, wallet(只读), settings, kyc 增强:detail页新增附近门店和相似好券模块 导航:首页搜索→search, 券卡→detail→purchase, 我的券→my-coupon-detail, 个人中心菜单→wallet/messages/ai-chat/settings/kyc Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3cdb6a5eb9
commit
c58b2df728
|
|
@ -312,6 +312,162 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||||
|
|
||||||
// ── Coupon Detail (stores) ──
|
// ── Coupon Detail (stores) ──
|
||||||
'coupon_stores_count': '全国 12,800+ 门店',
|
'coupon_stores_count': '全国 12,800+ 门店',
|
||||||
|
|
||||||
|
// ── Purchase / Order Confirm ──
|
||||||
|
'purchase_title': '确认订单',
|
||||||
|
'purchase_quantity': '购买数量',
|
||||||
|
'purchase_wechat_pay': '微信支付',
|
||||||
|
'purchase_payment_method': '支付方式',
|
||||||
|
'purchase_unit_price': '单价',
|
||||||
|
'purchase_count': '数量',
|
||||||
|
'purchase_confirm_pay': '确认支付',
|
||||||
|
'purchase_buying_note': '您正在购买平台担保的消费券',
|
||||||
|
'purchase_save_badge': '比面值节省',
|
||||||
|
'purchase_price_detail': '价格明细',
|
||||||
|
|
||||||
|
// ── Payment Success ──
|
||||||
|
'payment_success_title': '支付成功',
|
||||||
|
'payment_success_hint': '券已到账,可在「我的券」中查看',
|
||||||
|
'payment_success_coupon_name': '券名称',
|
||||||
|
'payment_success_pay_amount': '支付金额',
|
||||||
|
'payment_success_order_no': '订单号',
|
||||||
|
'payment_success_pay_time': '支付时间',
|
||||||
|
'payment_success_view_coupon': '查看我的券',
|
||||||
|
'payment_success_continue': '继续逛逛',
|
||||||
|
|
||||||
|
// ── My Coupon Detail ──
|
||||||
|
'my_coupon_detail_title': '券详情',
|
||||||
|
'my_coupon_active': '可使用',
|
||||||
|
'my_coupon_show_qr_hint': '出示此二维码给商户扫描核销',
|
||||||
|
'my_coupon_switch_barcode': '切换条形码',
|
||||||
|
'my_coupon_purchase_price': '购买价格',
|
||||||
|
'my_coupon_order_no': '订单号',
|
||||||
|
'my_coupon_resell_count': '剩余可转售次数',
|
||||||
|
'my_coupon_usage_note': '使用说明',
|
||||||
|
'my_coupon_use_in_store': '全国门店通用',
|
||||||
|
'my_coupon_use_in_time': '请在有效期内使用',
|
||||||
|
'my_coupon_one_per_visit': '每次消费仅可使用一张',
|
||||||
|
'my_coupon_no_cash': '不可兑换现金',
|
||||||
|
'my_coupon_transfer': '转赠好友',
|
||||||
|
'my_coupon_sell': '挂单出售',
|
||||||
|
'my_coupon_extract_wallet': '提取到外部钱包',
|
||||||
|
'my_coupon_require_kyc': '需KYC L2+认证',
|
||||||
|
'my_coupon_view_trades': '查看交易记录',
|
||||||
|
'my_coupon_help': '使用帮助',
|
||||||
|
'my_coupon_sell_in_app': '出售功能请使用App',
|
||||||
|
|
||||||
|
// ── Transfer ──
|
||||||
|
'transfer_title': '转赠',
|
||||||
|
'transfer_share_card': '分享小程序卡片',
|
||||||
|
'transfer_share_desc': '直接分享给微信好友',
|
||||||
|
'transfer_scan': '扫码转赠',
|
||||||
|
'transfer_scan_desc': '扫描对方接收码',
|
||||||
|
'transfer_input': '输入ID',
|
||||||
|
'transfer_input_desc': '手动输入对方信息',
|
||||||
|
'transfer_recent': '最近转赠',
|
||||||
|
'transfer_manage': '管理',
|
||||||
|
'transfer_no_recent': '暂无最近转赠记录',
|
||||||
|
'transfer_expired': '已过期',
|
||||||
|
'transfer_refresh': '刷新',
|
||||||
|
'transfer_history': '转赠记录',
|
||||||
|
'transfer_last_transfer': '最近转赠',
|
||||||
|
'transfer_input_recipient': '输入收件人',
|
||||||
|
'transfer_recipient_hint': 'ID / 邮箱 / 手机号',
|
||||||
|
'transfer_paste': '粘贴',
|
||||||
|
'transfer_select_coupon': '选择转赠的券',
|
||||||
|
'transfer_confirm': '确认转赠',
|
||||||
|
'transfer_to': '转赠给',
|
||||||
|
'transfer_confirm_btn': '确认转赠',
|
||||||
|
'transfer_contact_email': '邮箱',
|
||||||
|
'transfer_contact_phone': '手机',
|
||||||
|
'transfer_warning': '转赠后将无法撤回,请确认信息无误',
|
||||||
|
'transfer_success': '转赠成功',
|
||||||
|
'transfer_outgoing': '转出',
|
||||||
|
'transfer_incoming': '收到',
|
||||||
|
|
||||||
|
// ── AI Chat ──
|
||||||
|
'ai_chat_title': 'AI 助手',
|
||||||
|
'ai_chat_greeting': '你好!我是 Genex AI 助手,可以帮你发现高性价比好券。试试问我:',
|
||||||
|
'ai_chat_suggest1': '推荐适合我的券',
|
||||||
|
'ai_chat_suggest2': '星巴克券值不值得买?',
|
||||||
|
'ai_chat_suggest3': '帮我做比价分析',
|
||||||
|
'ai_chat_suggest4': '我的券快到期了怎么办?',
|
||||||
|
'ai_chat_input_hint': '问我任何关于券的问题...',
|
||||||
|
'ai_chat_send': '发送',
|
||||||
|
|
||||||
|
// ── Orders (additional) ──
|
||||||
|
'order_view_detail': '查看详情',
|
||||||
|
'order_empty': '暂无订单',
|
||||||
|
'order_status_paid': '已支付',
|
||||||
|
|
||||||
|
// ── Messages ──
|
||||||
|
'messages_title': '消息',
|
||||||
|
'messages_mark_all_read': '全部已读',
|
||||||
|
'messages_tab_trade': '交易',
|
||||||
|
'messages_tab_expiry': '到期',
|
||||||
|
'messages_tab_announcement': '公告',
|
||||||
|
'messages_type_price': '价格提醒',
|
||||||
|
'messages_empty': '暂无消息',
|
||||||
|
|
||||||
|
// ── Wallet ──
|
||||||
|
'wallet_balance': '我的余额',
|
||||||
|
'wallet_total_balance': '总余额',
|
||||||
|
'wallet_available': '可用',
|
||||||
|
'wallet_frozen': '冻结中',
|
||||||
|
'wallet_records': '交易记录',
|
||||||
|
'wallet_buy_in': '买入',
|
||||||
|
'wallet_sell_out': '卖出',
|
||||||
|
'wallet_gift_transfer': '转赠',
|
||||||
|
'wallet_redeem_use': '核销',
|
||||||
|
'wallet_read_only_hint': '完整钱包功能请使用App',
|
||||||
|
'wallet_filter': '筛选',
|
||||||
|
'wallet_deposit_record': '充值',
|
||||||
|
'wallet_withdraw_record': '提现',
|
||||||
|
|
||||||
|
// ── Settings ──
|
||||||
|
'settings_title': '设置',
|
||||||
|
'settings_notifications': '通知设置',
|
||||||
|
'settings_trade_notify': '交易通知',
|
||||||
|
'settings_expiry_remind': '到期提醒',
|
||||||
|
'settings_marketing_push': '营销推送',
|
||||||
|
'settings_general': '通用',
|
||||||
|
'settings_clear_cache': '清除缓存',
|
||||||
|
'settings_about': '关于',
|
||||||
|
'settings_version': '版本',
|
||||||
|
'settings_help_center': '帮助中心',
|
||||||
|
'settings_logout': '退出登录',
|
||||||
|
'settings_select_language': '选择语言',
|
||||||
|
'settings_select_currency': '选择计价货币',
|
||||||
|
'settings_currency_note': '此设置影响价格显示的计价货币',
|
||||||
|
'settings_cache_cleared': '缓存已清除',
|
||||||
|
|
||||||
|
// ── KYC ──
|
||||||
|
'kyc_title': '身份认证',
|
||||||
|
'kyc_current_level': '当前认证等级',
|
||||||
|
'kyc_daily_limit': '每日购买限额',
|
||||||
|
'kyc_l1_title': 'L1 基础认证',
|
||||||
|
'kyc_l1_desc': '手机号 + 邮箱验证',
|
||||||
|
'kyc_l1_limit': '¥500',
|
||||||
|
'kyc_l1_feature': '可购买券、出示核销',
|
||||||
|
'kyc_l2_title': 'L2 身份认证',
|
||||||
|
'kyc_l2_desc': '身份证 / 护照验证',
|
||||||
|
'kyc_l2_limit': '¥5,000',
|
||||||
|
'kyc_l2_feature': '解锁二级市场交易、P2P转赠',
|
||||||
|
'kyc_l3_title': 'L3 高级认证',
|
||||||
|
'kyc_l3_desc': '视频面审 + 地址证明',
|
||||||
|
'kyc_l3_limit': '无限额',
|
||||||
|
'kyc_l3_feature': '解锁大额交易、提现无限制',
|
||||||
|
'kyc_completed': '已完成',
|
||||||
|
'kyc_go_verify': '去认证',
|
||||||
|
'kyc_verify_in_app': '请使用App完成认证',
|
||||||
|
|
||||||
|
// ── Detail (enhancement) ──
|
||||||
|
'detail_nearby_stores': '附近门店',
|
||||||
|
'detail_view_all': '查看全部',
|
||||||
|
'detail_similar_coupons': '相似好券',
|
||||||
|
|
||||||
|
// ── AI Assistant (profile) ──
|
||||||
|
'profile_ai_assistant': 'AI 助手',
|
||||||
},
|
},
|
||||||
|
|
||||||
'en-US': {
|
'en-US': {
|
||||||
|
|
@ -606,6 +762,162 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||||
|
|
||||||
// ── Coupon Detail (stores) ──
|
// ── Coupon Detail (stores) ──
|
||||||
'coupon_stores_count': '12,800+ stores nationwide',
|
'coupon_stores_count': '12,800+ stores nationwide',
|
||||||
|
|
||||||
|
// ── Purchase / Order Confirm ──
|
||||||
|
'purchase_title': 'Confirm Order',
|
||||||
|
'purchase_quantity': 'Quantity',
|
||||||
|
'purchase_wechat_pay': 'WeChat Pay',
|
||||||
|
'purchase_payment_method': 'Payment Method',
|
||||||
|
'purchase_unit_price': 'Unit Price',
|
||||||
|
'purchase_count': 'Qty',
|
||||||
|
'purchase_confirm_pay': 'Confirm Payment',
|
||||||
|
'purchase_buying_note': 'You are purchasing a platform-guaranteed consumer coupon',
|
||||||
|
'purchase_save_badge': 'Save vs. face value',
|
||||||
|
'purchase_price_detail': 'Price Details',
|
||||||
|
|
||||||
|
// ── Payment Success ──
|
||||||
|
'payment_success_title': 'Payment Successful',
|
||||||
|
'payment_success_hint': 'Coupon is in your account. View in "My Coupons".',
|
||||||
|
'payment_success_coupon_name': 'Coupon Name',
|
||||||
|
'payment_success_pay_amount': 'Amount Paid',
|
||||||
|
'payment_success_order_no': 'Order Number',
|
||||||
|
'payment_success_pay_time': 'Payment Time',
|
||||||
|
'payment_success_view_coupon': 'View My Coupons',
|
||||||
|
'payment_success_continue': 'Continue Browsing',
|
||||||
|
|
||||||
|
// ── My Coupon Detail ──
|
||||||
|
'my_coupon_detail_title': 'Coupon Details',
|
||||||
|
'my_coupon_active': 'Active',
|
||||||
|
'my_coupon_show_qr_hint': 'Show this QR code to the merchant for scanning',
|
||||||
|
'my_coupon_switch_barcode': 'Switch to Barcode',
|
||||||
|
'my_coupon_purchase_price': 'Purchase Price',
|
||||||
|
'my_coupon_order_no': 'Order Number',
|
||||||
|
'my_coupon_resell_count': 'Resale Remaining',
|
||||||
|
'my_coupon_usage_note': 'Usage Instructions',
|
||||||
|
'my_coupon_use_in_store': 'Valid at all stores nationwide',
|
||||||
|
'my_coupon_use_in_time': 'Use before expiry date',
|
||||||
|
'my_coupon_one_per_visit': 'One coupon per visit',
|
||||||
|
'my_coupon_no_cash': 'Not redeemable for cash',
|
||||||
|
'my_coupon_transfer': 'Gift to Friend',
|
||||||
|
'my_coupon_sell': 'List for Sale',
|
||||||
|
'my_coupon_extract_wallet': 'Extract to External Wallet',
|
||||||
|
'my_coupon_require_kyc': 'Requires KYC L2+ Verification',
|
||||||
|
'my_coupon_view_trades': 'View Trade History',
|
||||||
|
'my_coupon_help': 'Help',
|
||||||
|
'my_coupon_sell_in_app': 'Use the App to sell coupons',
|
||||||
|
|
||||||
|
// ── Transfer ──
|
||||||
|
'transfer_title': 'Gift',
|
||||||
|
'transfer_share_card': 'Share Mini Program Card',
|
||||||
|
'transfer_share_desc': 'Share directly with WeChat friends',
|
||||||
|
'transfer_scan': 'Scan to Gift',
|
||||||
|
'transfer_scan_desc': 'Scan recipient\'s receive code',
|
||||||
|
'transfer_input': 'Enter ID',
|
||||||
|
'transfer_input_desc': 'Enter recipient info manually',
|
||||||
|
'transfer_recent': 'Recent Gifts',
|
||||||
|
'transfer_manage': 'Manage',
|
||||||
|
'transfer_no_recent': 'No recent gift records',
|
||||||
|
'transfer_expired': 'Expired',
|
||||||
|
'transfer_refresh': 'Refresh',
|
||||||
|
'transfer_history': 'Gift History',
|
||||||
|
'transfer_last_transfer': 'Last Gift',
|
||||||
|
'transfer_input_recipient': 'Enter Recipient',
|
||||||
|
'transfer_recipient_hint': 'ID / Email / Phone',
|
||||||
|
'transfer_paste': 'Paste',
|
||||||
|
'transfer_select_coupon': 'Select Coupon to Gift',
|
||||||
|
'transfer_confirm': 'Confirm Gift',
|
||||||
|
'transfer_to': 'Gift to',
|
||||||
|
'transfer_confirm_btn': 'Confirm Gift',
|
||||||
|
'transfer_contact_email': 'Email',
|
||||||
|
'transfer_contact_phone': 'Phone',
|
||||||
|
'transfer_warning': 'Gifts cannot be revoked. Please verify the details.',
|
||||||
|
'transfer_success': 'Gift Sent Successfully',
|
||||||
|
'transfer_outgoing': 'Sent',
|
||||||
|
'transfer_incoming': 'Received',
|
||||||
|
|
||||||
|
// ── AI Chat ──
|
||||||
|
'ai_chat_title': 'AI Assistant',
|
||||||
|
'ai_chat_greeting': 'Hi! I\'m the Genex AI Assistant. I can help you find the best coupon deals. Try asking me:',
|
||||||
|
'ai_chat_suggest1': 'Recommend coupons for me',
|
||||||
|
'ai_chat_suggest2': 'Is the Starbucks coupon worth it?',
|
||||||
|
'ai_chat_suggest3': 'Compare prices for me',
|
||||||
|
'ai_chat_suggest4': 'My coupons are expiring soon, what should I do?',
|
||||||
|
'ai_chat_input_hint': 'Ask me anything about coupons...',
|
||||||
|
'ai_chat_send': 'Send',
|
||||||
|
|
||||||
|
// ── Orders (additional) ──
|
||||||
|
'order_view_detail': 'View Details',
|
||||||
|
'order_empty': 'No orders yet',
|
||||||
|
'order_status_paid': 'Paid',
|
||||||
|
|
||||||
|
// ── Messages ──
|
||||||
|
'messages_title': 'Messages',
|
||||||
|
'messages_mark_all_read': 'Mark All Read',
|
||||||
|
'messages_tab_trade': 'Trades',
|
||||||
|
'messages_tab_expiry': 'Expiring',
|
||||||
|
'messages_tab_announcement': 'Announcements',
|
||||||
|
'messages_type_price': 'Price Alert',
|
||||||
|
'messages_empty': 'No messages',
|
||||||
|
|
||||||
|
// ── Wallet ──
|
||||||
|
'wallet_balance': 'My Balance',
|
||||||
|
'wallet_total_balance': 'Total Balance',
|
||||||
|
'wallet_available': 'Available',
|
||||||
|
'wallet_frozen': 'Frozen',
|
||||||
|
'wallet_records': 'Transaction Records',
|
||||||
|
'wallet_buy_in': 'Buy',
|
||||||
|
'wallet_sell_out': 'Sell',
|
||||||
|
'wallet_gift_transfer': 'Gift',
|
||||||
|
'wallet_redeem_use': 'Redeem',
|
||||||
|
'wallet_read_only_hint': 'Use the App for full wallet features',
|
||||||
|
'wallet_filter': 'Filter',
|
||||||
|
'wallet_deposit_record': 'Deposit',
|
||||||
|
'wallet_withdraw_record': 'Withdraw',
|
||||||
|
|
||||||
|
// ── Settings ──
|
||||||
|
'settings_title': 'Settings',
|
||||||
|
'settings_notifications': 'Notifications',
|
||||||
|
'settings_trade_notify': 'Trade Notifications',
|
||||||
|
'settings_expiry_remind': 'Expiry Reminders',
|
||||||
|
'settings_marketing_push': 'Promotional Notifications',
|
||||||
|
'settings_general': 'General',
|
||||||
|
'settings_clear_cache': 'Clear Cache',
|
||||||
|
'settings_about': 'About',
|
||||||
|
'settings_version': 'Version',
|
||||||
|
'settings_help_center': 'Help Center',
|
||||||
|
'settings_logout': 'Log Out',
|
||||||
|
'settings_select_language': 'Select Language',
|
||||||
|
'settings_select_currency': 'Select Currency',
|
||||||
|
'settings_currency_note': 'This affects the pricing currency display',
|
||||||
|
'settings_cache_cleared': 'Cache cleared',
|
||||||
|
|
||||||
|
// ── KYC ──
|
||||||
|
'kyc_title': 'Identity Verification',
|
||||||
|
'kyc_current_level': 'Current Level',
|
||||||
|
'kyc_daily_limit': 'Daily Purchase Limit',
|
||||||
|
'kyc_l1_title': 'L1 Basic Verification',
|
||||||
|
'kyc_l1_desc': 'Phone + Email Verification',
|
||||||
|
'kyc_l1_limit': '¥500',
|
||||||
|
'kyc_l1_feature': 'Buy coupons, redeem at stores',
|
||||||
|
'kyc_l2_title': 'L2 Identity Verification',
|
||||||
|
'kyc_l2_desc': 'ID Card / Passport Verification',
|
||||||
|
'kyc_l2_limit': '¥5,000',
|
||||||
|
'kyc_l2_feature': 'Unlock secondary market, P2P gifting',
|
||||||
|
'kyc_l3_title': 'L3 Advanced Verification',
|
||||||
|
'kyc_l3_desc': 'Video verification + Address proof',
|
||||||
|
'kyc_l3_limit': 'Unlimited',
|
||||||
|
'kyc_l3_feature': 'Unlock large transactions, unlimited withdrawals',
|
||||||
|
'kyc_completed': 'Completed',
|
||||||
|
'kyc_go_verify': 'Verify Now',
|
||||||
|
'kyc_verify_in_app': 'Please use the App to complete verification',
|
||||||
|
|
||||||
|
// ── Detail (enhancement) ──
|
||||||
|
'detail_nearby_stores': 'Nearby Stores',
|
||||||
|
'detail_view_all': 'View All',
|
||||||
|
'detail_similar_coupons': 'Similar Coupons',
|
||||||
|
|
||||||
|
// ── AI Assistant (profile) ──
|
||||||
|
'profile_ai_assistant': 'AI Assistant',
|
||||||
},
|
},
|
||||||
|
|
||||||
'ja-JP': {
|
'ja-JP': {
|
||||||
|
|
@ -900,5 +1212,161 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||||
|
|
||||||
// ── Coupon Detail (stores) ──
|
// ── Coupon Detail (stores) ──
|
||||||
'coupon_stores_count': '全国12,800+店舗',
|
'coupon_stores_count': '全国12,800+店舗',
|
||||||
|
|
||||||
|
// ── Purchase / Order Confirm ──
|
||||||
|
'purchase_title': '注文確認',
|
||||||
|
'purchase_quantity': '購入数量',
|
||||||
|
'purchase_wechat_pay': 'WeChat Pay',
|
||||||
|
'purchase_payment_method': '支払い方法',
|
||||||
|
'purchase_unit_price': '単価',
|
||||||
|
'purchase_count': '数量',
|
||||||
|
'purchase_confirm_pay': '支払いを確認',
|
||||||
|
'purchase_buying_note': 'プラットフォーム保証付きの消費クーポンを購入します',
|
||||||
|
'purchase_save_badge': '額面よりお得',
|
||||||
|
'purchase_price_detail': '価格詳細',
|
||||||
|
|
||||||
|
// ── Payment Success ──
|
||||||
|
'payment_success_title': '支払い完了',
|
||||||
|
'payment_success_hint': 'クーポンがアカウントに届きました。「マイクーポン」でご確認ください。',
|
||||||
|
'payment_success_coupon_name': 'クーポン名',
|
||||||
|
'payment_success_pay_amount': '支払い金額',
|
||||||
|
'payment_success_order_no': '注文番号',
|
||||||
|
'payment_success_pay_time': '支払い日時',
|
||||||
|
'payment_success_view_coupon': 'マイクーポンを見る',
|
||||||
|
'payment_success_continue': '買い物を続ける',
|
||||||
|
|
||||||
|
// ── My Coupon Detail ──
|
||||||
|
'my_coupon_detail_title': 'クーポン詳細',
|
||||||
|
'my_coupon_active': '利用可能',
|
||||||
|
'my_coupon_show_qr_hint': 'このQRコードを店舗スタッフに提示してください',
|
||||||
|
'my_coupon_switch_barcode': 'バーコードに切替',
|
||||||
|
'my_coupon_purchase_price': '購入価格',
|
||||||
|
'my_coupon_order_no': '注文番号',
|
||||||
|
'my_coupon_resell_count': '残り転売回数',
|
||||||
|
'my_coupon_usage_note': '利用案内',
|
||||||
|
'my_coupon_use_in_store': '全国の店舗で利用可能',
|
||||||
|
'my_coupon_use_in_time': '有効期限内にご利用ください',
|
||||||
|
'my_coupon_one_per_visit': '1回の来店につき1枚まで',
|
||||||
|
'my_coupon_no_cash': '現金との交換不可',
|
||||||
|
'my_coupon_transfer': '友達にプレゼント',
|
||||||
|
'my_coupon_sell': '出品する',
|
||||||
|
'my_coupon_extract_wallet': '外部ウォレットに引出',
|
||||||
|
'my_coupon_require_kyc': 'KYC L2+認証が必要',
|
||||||
|
'my_coupon_view_trades': '取引履歴を見る',
|
||||||
|
'my_coupon_help': 'ヘルプ',
|
||||||
|
'my_coupon_sell_in_app': 'アプリで出品してください',
|
||||||
|
|
||||||
|
// ── Transfer ──
|
||||||
|
'transfer_title': 'ギフト',
|
||||||
|
'transfer_share_card': 'ミニプログラムカードをシェア',
|
||||||
|
'transfer_share_desc': 'WeChat友達に直接シェア',
|
||||||
|
'transfer_scan': 'スキャンしてギフト',
|
||||||
|
'transfer_scan_desc': '相手の受取コードをスキャン',
|
||||||
|
'transfer_input': 'IDを入力',
|
||||||
|
'transfer_input_desc': '相手の情報を手動入力',
|
||||||
|
'transfer_recent': '最近のギフト',
|
||||||
|
'transfer_manage': '管理',
|
||||||
|
'transfer_no_recent': '最近のギフト記録はありません',
|
||||||
|
'transfer_expired': '期限切れ',
|
||||||
|
'transfer_refresh': '更新',
|
||||||
|
'transfer_history': 'ギフト履歴',
|
||||||
|
'transfer_last_transfer': '最後のギフト',
|
||||||
|
'transfer_input_recipient': '受取人を入力',
|
||||||
|
'transfer_recipient_hint': 'ID / メール / 電話番号',
|
||||||
|
'transfer_paste': '貼り付け',
|
||||||
|
'transfer_select_coupon': 'ギフトするクーポンを選択',
|
||||||
|
'transfer_confirm': 'ギフトを確認',
|
||||||
|
'transfer_to': 'ギフト先',
|
||||||
|
'transfer_confirm_btn': 'ギフトを確認',
|
||||||
|
'transfer_contact_email': 'メール',
|
||||||
|
'transfer_contact_phone': '電話',
|
||||||
|
'transfer_warning': 'ギフトは取り消しできません。情報をご確認ください。',
|
||||||
|
'transfer_success': 'ギフト完了',
|
||||||
|
'transfer_outgoing': '送信',
|
||||||
|
'transfer_incoming': '受取',
|
||||||
|
|
||||||
|
// ── AI Chat ──
|
||||||
|
'ai_chat_title': 'AIアシスタント',
|
||||||
|
'ai_chat_greeting': 'こんにちは!Genex AIアシスタントです。お得なクーポンを見つけるお手伝いをします。こんな質問をどうぞ:',
|
||||||
|
'ai_chat_suggest1': 'おすすめのクーポンは?',
|
||||||
|
'ai_chat_suggest2': 'スタバのクーポンは買い?',
|
||||||
|
'ai_chat_suggest3': '価格を比較して',
|
||||||
|
'ai_chat_suggest4': 'クーポンの期限が近い、どうする?',
|
||||||
|
'ai_chat_input_hint': 'クーポンについて何でも聞いてください...',
|
||||||
|
'ai_chat_send': '送信',
|
||||||
|
|
||||||
|
// ── Orders (additional) ──
|
||||||
|
'order_view_detail': '詳細を見る',
|
||||||
|
'order_empty': '注文はありません',
|
||||||
|
'order_status_paid': '支払い済み',
|
||||||
|
|
||||||
|
// ── Messages ──
|
||||||
|
'messages_title': 'メッセージ',
|
||||||
|
'messages_mark_all_read': 'すべて既読',
|
||||||
|
'messages_tab_trade': '取引',
|
||||||
|
'messages_tab_expiry': '期限切れ',
|
||||||
|
'messages_tab_announcement': 'お知らせ',
|
||||||
|
'messages_type_price': '価格アラート',
|
||||||
|
'messages_empty': 'メッセージはありません',
|
||||||
|
|
||||||
|
// ── Wallet ──
|
||||||
|
'wallet_balance': '残高',
|
||||||
|
'wallet_total_balance': '総残高',
|
||||||
|
'wallet_available': '利用可能',
|
||||||
|
'wallet_frozen': '凍結中',
|
||||||
|
'wallet_records': '取引履歴',
|
||||||
|
'wallet_buy_in': '購入',
|
||||||
|
'wallet_sell_out': '売却',
|
||||||
|
'wallet_gift_transfer': 'ギフト',
|
||||||
|
'wallet_redeem_use': '使用',
|
||||||
|
'wallet_read_only_hint': 'フル機能はアプリをご利用ください',
|
||||||
|
'wallet_filter': 'フィルター',
|
||||||
|
'wallet_deposit_record': '入金',
|
||||||
|
'wallet_withdraw_record': '出金',
|
||||||
|
|
||||||
|
// ── Settings ──
|
||||||
|
'settings_title': '設定',
|
||||||
|
'settings_notifications': '通知設定',
|
||||||
|
'settings_trade_notify': '取引通知',
|
||||||
|
'settings_expiry_remind': '期限リマインダー',
|
||||||
|
'settings_marketing_push': 'プロモーション通知',
|
||||||
|
'settings_general': '一般',
|
||||||
|
'settings_clear_cache': 'キャッシュを消去',
|
||||||
|
'settings_about': 'アプリについて',
|
||||||
|
'settings_version': 'バージョン',
|
||||||
|
'settings_help_center': 'ヘルプセンター',
|
||||||
|
'settings_logout': 'ログアウト',
|
||||||
|
'settings_select_language': '言語を選択',
|
||||||
|
'settings_select_currency': '通貨を選択',
|
||||||
|
'settings_currency_note': 'この設定は価格表示の通貨に影響します',
|
||||||
|
'settings_cache_cleared': 'キャッシュを消去しました',
|
||||||
|
|
||||||
|
// ── KYC ──
|
||||||
|
'kyc_title': '本人確認',
|
||||||
|
'kyc_current_level': '現在の認証レベル',
|
||||||
|
'kyc_daily_limit': '1日の購入限度額',
|
||||||
|
'kyc_l1_title': 'L1 基本認証',
|
||||||
|
'kyc_l1_desc': '電話番号 + メール認証',
|
||||||
|
'kyc_l1_limit': '¥500',
|
||||||
|
'kyc_l1_feature': 'クーポン購入・使用が可能',
|
||||||
|
'kyc_l2_title': 'L2 本人確認',
|
||||||
|
'kyc_l2_desc': '身分証明書 / パスポート認証',
|
||||||
|
'kyc_l2_limit': '¥5,000',
|
||||||
|
'kyc_l2_feature': '二次市場取引・P2Pギフトを解放',
|
||||||
|
'kyc_l3_title': 'L3 上級認証',
|
||||||
|
'kyc_l3_desc': 'ビデオ認証 + 住所証明',
|
||||||
|
'kyc_l3_limit': '無制限',
|
||||||
|
'kyc_l3_feature': '大口取引・無制限出金を解放',
|
||||||
|
'kyc_completed': '完了',
|
||||||
|
'kyc_go_verify': '認証する',
|
||||||
|
'kyc_verify_in_app': 'アプリで認証を完了してください',
|
||||||
|
|
||||||
|
// ── Detail (enhancement) ──
|
||||||
|
'detail_nearby_stores': '近くの店舗',
|
||||||
|
'detail_view_all': 'すべて見る',
|
||||||
|
'detail_similar_coupons': '似ているクーポン',
|
||||||
|
|
||||||
|
// ── AI Assistant (profile) ──
|
||||||
|
'profile_ai_assistant': 'AIアシスタント',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
// Taro mini-program - AI Chat Assistant
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P1. AI Chat - Full-screen AI chat interface
|
||||||
|
*
|
||||||
|
* Suggestion chips, message list, fixed input bar
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
isAi: boolean;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MOCK_AI_RESPONSE =
|
||||||
|
'根据您的偏好和消费习惯,推荐以下高性价比券:\n\n1. 星巴克 ¥25 礼品卡 - 当前售价 ¥21.25(8.5折),信用AAA\n2. Amazon ¥100 购物券 - 当前售价 ¥85(8.5折),信用AA\n\n这两张券的折扣率在同类中最优,且发行方信用等级高。';
|
||||||
|
|
||||||
|
const AiChatPage: React.FC = () => {
|
||||||
|
const [messages, setMessages] = React.useState<ChatMessage[]>([
|
||||||
|
{ isAi: true, text: t('ai_chat_greeting') },
|
||||||
|
]);
|
||||||
|
const [inputText, setInputText] = React.useState('');
|
||||||
|
const scrollId = React.useRef('msg-0');
|
||||||
|
const [scrollToId, setScrollToId] = React.useState('msg-0');
|
||||||
|
|
||||||
|
const suggestions = [
|
||||||
|
t('ai_chat_suggest1'),
|
||||||
|
t('ai_chat_suggest2'),
|
||||||
|
t('ai_chat_suggest3'),
|
||||||
|
t('ai_chat_suggest4'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const addMessages = (userText: string) => {
|
||||||
|
const userMsg: ChatMessage = { isAi: false, text: userText };
|
||||||
|
setMessages((prev) => {
|
||||||
|
const next = [...prev, userMsg];
|
||||||
|
const userIdx = next.length - 1;
|
||||||
|
setScrollToId(`msg-${userIdx}`);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setInputText('');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setMessages((prev) => {
|
||||||
|
const next = [...prev, { isAi: true, text: MOCK_AI_RESPONSE }];
|
||||||
|
const aiIdx = next.length - 1;
|
||||||
|
setScrollToId(`msg-${aiIdx}`);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, 800);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
const text = inputText.trim();
|
||||||
|
if (!text) return;
|
||||||
|
addMessages(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChipTap = (chip: string) => {
|
||||||
|
addMessages(chip);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<view className="chat-page">
|
||||||
|
{/* Header */}
|
||||||
|
<view className="chat-header">
|
||||||
|
<text className="chat-header-title">{t('ai_chat_title')}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Message List */}
|
||||||
|
<scroll-view
|
||||||
|
scrollY
|
||||||
|
className="chat-messages"
|
||||||
|
scrollIntoView={scrollToId}
|
||||||
|
scrollWithAnimation
|
||||||
|
>
|
||||||
|
{messages.map((msg, i) => (
|
||||||
|
<view
|
||||||
|
key={i}
|
||||||
|
id={`msg-${i}`}
|
||||||
|
className={`msg-row ${msg.isAi ? 'msg-ai' : 'msg-user'}`}
|
||||||
|
>
|
||||||
|
{msg.isAi && (
|
||||||
|
<view className="ai-avatar">
|
||||||
|
<text className="ai-avatar-icon">✨</text>
|
||||||
|
</view>
|
||||||
|
)}
|
||||||
|
<view
|
||||||
|
className={`msg-bubble ${msg.isAi ? 'bubble-ai' : 'bubble-user'}`}
|
||||||
|
>
|
||||||
|
<text className={`msg-text ${msg.isAi ? 'text-ai' : 'text-user'}`}>
|
||||||
|
{msg.text}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Suggestion Chips */}
|
||||||
|
{messages.length <= 2 && (
|
||||||
|
<view className="chips-container">
|
||||||
|
<scroll-view scrollX className="chips-scroll">
|
||||||
|
{suggestions.map((chip, i) => (
|
||||||
|
<view
|
||||||
|
key={i}
|
||||||
|
className="chip"
|
||||||
|
onClick={() => handleChipTap(chip)}
|
||||||
|
>
|
||||||
|
<text className="chip-text">{chip}</text>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bottom spacer for scroll */}
|
||||||
|
<view className="msg-spacer" />
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
{/* Input Bar */}
|
||||||
|
<view className="input-bar">
|
||||||
|
<view className="input-wrap">
|
||||||
|
<input
|
||||||
|
className="chat-input"
|
||||||
|
placeholder={t('ai_chat_input_hint')}
|
||||||
|
value={inputText}
|
||||||
|
onInput={(e: any) => setInputText(e.detail.value)}
|
||||||
|
onConfirm={handleSend}
|
||||||
|
confirmType="send"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view className="send-btn" onClick={handleSend}>
|
||||||
|
<text className="send-icon">↑</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AiChatPage;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CSS (index.scss):
|
||||||
|
|
||||||
|
.chat-page {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
height: 100vh; background: #F8F9FC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
height: 88rpx; background: white;
|
||||||
|
border-bottom: 1rpx solid #F1F3F8;
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
.chat-header-title { font-size: 32rpx; font-weight: 600; color: #141723; }
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1; padding: 24rpx 32rpx; overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-row { display: flex; margin-bottom: 24rpx; }
|
||||||
|
.msg-ai { justify-content: flex-start; }
|
||||||
|
.msg-user { justify-content: flex-end; }
|
||||||
|
|
||||||
|
.ai-avatar {
|
||||||
|
width: 64rpx; height: 64rpx; border-radius: 16rpx;
|
||||||
|
background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
margin-right: 16rpx; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.ai-avatar-icon { font-size: 28rpx; }
|
||||||
|
|
||||||
|
.msg-bubble { max-width: 75%; border-radius: 20rpx; padding: 20rpx 24rpx; }
|
||||||
|
.bubble-ai { background: #F1F3F8; border-top-left-radius: 4rpx; }
|
||||||
|
.bubble-user { background: #6C5CE7; border-top-right-radius: 4rpx; }
|
||||||
|
|
||||||
|
.msg-text { font-size: 28rpx; line-height: 1.6; white-space: pre-wrap; }
|
||||||
|
.text-ai { color: #141723; }
|
||||||
|
.text-user { color: white; }
|
||||||
|
|
||||||
|
.chips-container { padding: 8rpx 0 16rpx; }
|
||||||
|
.chips-scroll { white-space: nowrap; }
|
||||||
|
.chip {
|
||||||
|
display: inline-block; margin-right: 16rpx;
|
||||||
|
padding: 14rpx 24rpx; background: #F3F1FF;
|
||||||
|
border: 1rpx solid rgba(108,92,231,0.2);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
.chip-text { font-size: 24rpx; color: #6C5CE7; white-space: nowrap; }
|
||||||
|
|
||||||
|
.msg-spacer { height: 32rpx; }
|
||||||
|
|
||||||
|
.input-bar {
|
||||||
|
display: flex; align-items: center;
|
||||||
|
padding: 16rpx 24rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||||
|
background: white; border-top: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.input-wrap {
|
||||||
|
flex: 1; height: 72rpx; background: #F1F3F8; border-radius: 999rpx;
|
||||||
|
display: flex; align-items: center; padding: 0 24rpx;
|
||||||
|
}
|
||||||
|
.chat-input { flex: 1; font-size: 28rpx; color: #141723; background: transparent; }
|
||||||
|
.send-btn {
|
||||||
|
width: 72rpx; height: 72rpx; margin-left: 16rpx;
|
||||||
|
background: #6C5CE7; border-radius: 50%;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.send-icon { color: white; font-size: 36rpx; font-weight: 700; }
|
||||||
|
*/
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
// Taro mini-program - Coupon Detail + Purchase
|
// Taro mini-program - Coupon Detail + Purchase
|
||||||
|
|
||||||
|
|
@ -80,13 +81,60 @@ const DetailPage: React.FC = () => {
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
{/* Nearby Stores */}
|
||||||
|
<view className="section-card">
|
||||||
|
<view className="section-header">
|
||||||
|
<text className="section-title">{t('detail_nearby_stores')}</text>
|
||||||
|
<text className="section-link">{t('detail_view_all')} ></text>
|
||||||
|
</view>
|
||||||
|
{[
|
||||||
|
{ name: '星巴克 国贸店', distance: '0.8km' },
|
||||||
|
{ name: '星巴克 三里屯店', distance: '1.2km' },
|
||||||
|
{ name: '星巴克 望京店', distance: '2.5km' },
|
||||||
|
].map((store, i) => (
|
||||||
|
<view key={i} className="store-row">
|
||||||
|
<text className="store-name">{store.name}</text>
|
||||||
|
<view className="store-distance-badge">
|
||||||
|
<text className="store-distance">{store.distance}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Similar Coupons */}
|
||||||
|
<view className="section-card">
|
||||||
|
<view className="section-header">
|
||||||
|
<text className="section-title">{t('detail_similar_coupons')}</text>
|
||||||
|
<text className="section-link">{t('more')} ></text>
|
||||||
|
</view>
|
||||||
|
<scroll-view scrollX className="similar-scroll">
|
||||||
|
{[
|
||||||
|
{ name: 'Costa ¥20 咖啡券', price: '¥16.00', discount: '8折' },
|
||||||
|
{ name: 'Luckin ¥15 咖啡券', price: '¥12.75', discount: '8.5折' },
|
||||||
|
{ name: 'Tim Hortons ¥18 饮品券', price: '¥14.40', discount: '8折' },
|
||||||
|
{ name: 'Pacific Coffee ¥22', price: '¥17.60', discount: '8折' },
|
||||||
|
].map((item, i) => (
|
||||||
|
<view key={i} className="similar-card">
|
||||||
|
<view className="similar-icon-wrap">
|
||||||
|
<text className="similar-icon">🎫</text>
|
||||||
|
</view>
|
||||||
|
<text className="similar-name">{item.name}</text>
|
||||||
|
<view className="similar-price-row">
|
||||||
|
<text className="similar-price">{item.price}</text>
|
||||||
|
<text className="similar-discount">{item.discount}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
{/* Bottom Buy Bar */}
|
{/* Bottom Buy Bar */}
|
||||||
<view className="buy-bar">
|
<view className="buy-bar">
|
||||||
<view className="buy-bar-price">
|
<view className="buy-bar-price">
|
||||||
<text className="buy-label">{t('order_total')}</text>
|
<text className="buy-label">{t('order_total')}</text>
|
||||||
<text className="buy-price">¥21.25</text>
|
<text className="buy-price">¥21.25</text>
|
||||||
</view>
|
</view>
|
||||||
<view className="buy-button">
|
<view className="buy-button" onClick={() => Taro.navigateTo({ url: '/pages/purchase/index' })}>
|
||||||
<text className="buy-button-text">{t('coupon_buy_now')}</text>
|
<text className="buy-button-text">{t('coupon_buy_now')}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
@ -175,6 +223,52 @@ CSS (index.scss):
|
||||||
.utility-icon { color: #00C48C; margin-right: 12rpx; font-weight: 700; }
|
.utility-icon { color: #00C48C; margin-right: 12rpx; font-weight: 700; }
|
||||||
.utility-text { font-size: 24rpx; color: #3D4459; }
|
.utility-text { font-size: 24rpx; color: #3D4459; }
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
background: white; border-radius: 16rpx; padding: 24rpx;
|
||||||
|
margin: 0 32rpx 16rpx; border: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
.section-title { font-size: 28rpx; font-weight: 600; color: #141723; }
|
||||||
|
.section-link { font-size: 24rpx; color: #6C5CE7; }
|
||||||
|
|
||||||
|
.store-row {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
padding: 16rpx 0; border-bottom: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.store-row:last-child { border-bottom: none; }
|
||||||
|
.store-name { font-size: 26rpx; color: #141723; }
|
||||||
|
.store-distance-badge {
|
||||||
|
padding: 4rpx 16rpx; background: #F3F1FF; border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
.store-distance { font-size: 22rpx; color: #6C5CE7; font-weight: 500; }
|
||||||
|
|
||||||
|
.similar-scroll { white-space: nowrap; }
|
||||||
|
.similar-card {
|
||||||
|
display: inline-block; width: 200rpx; margin-right: 16rpx;
|
||||||
|
background: #F8F9FC; border-radius: 12rpx; padding: 16rpx;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.similar-icon-wrap {
|
||||||
|
width: 100%; height: 100rpx; background: #F3F1FF; border-radius: 8rpx;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
.similar-icon { font-size: 36rpx; }
|
||||||
|
.similar-name {
|
||||||
|
font-size: 22rpx; color: #141723; font-weight: 500;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
display: block; margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
.similar-price-row { display: flex; align-items: center; }
|
||||||
|
.similar-price { font-size: 24rpx; color: #6C5CE7; font-weight: 700; }
|
||||||
|
.similar-discount {
|
||||||
|
font-size: 18rpx; color: #9B8FFF; margin-left: 8rpx;
|
||||||
|
padding: 2rpx 8rpx; background: #F3F1FF; border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.buy-bar {
|
.buy-bar {
|
||||||
position: fixed; bottom: 0; left: 0; right: 0;
|
position: fixed; bottom: 0; left: 0; right: 0;
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
// Taro mini-program component (WeChat / Alipay)
|
// Taro mini-program component (WeChat / Alipay)
|
||||||
|
|
||||||
|
|
@ -13,7 +14,7 @@ const HomePage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<view className="home-page">
|
<view className="home-page">
|
||||||
{/* Search Bar */}
|
{/* Search Bar */}
|
||||||
<view className="search-bar">
|
<view className="search-bar" onClick={() => Taro.navigateTo({ url: '/pages/search/index' })}>
|
||||||
<view className="search-input">
|
<view className="search-input">
|
||||||
<text className="search-icon">🔍</text>
|
<text className="search-icon">🔍</text>
|
||||||
<text className="search-placeholder">{t('home_search_hint')}</text>
|
<text className="search-placeholder">{t('home_search_hint')}</text>
|
||||||
|
|
@ -59,7 +60,7 @@ const HomePage: React.FC = () => {
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
{/* AI Suggestion (轻量版) */}
|
{/* AI Suggestion (轻量版) */}
|
||||||
<view className="ai-suggestion">
|
<view className="ai-suggestion" onClick={() => Taro.navigateTo({ url: '/pages/ai-chat/index' })}>
|
||||||
<view className="ai-icon">✨</view>
|
<view className="ai-icon">✨</view>
|
||||||
<view className="ai-content">
|
<view className="ai-content">
|
||||||
<text className="ai-title">{t('home_recommended')}</text>
|
<text className="ai-title">{t('home_recommended')}</text>
|
||||||
|
|
@ -81,7 +82,7 @@ const HomePage: React.FC = () => {
|
||||||
{ brand: 'Nike', name: 'Nike ¥80 运动券', price: '¥68.00', face: '¥80', 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折' },
|
{ brand: 'Target', name: 'Target ¥30 折扣券', price: '¥24.00', face: '¥30', discount: '8折' },
|
||||||
].map((coupon, i) => (
|
].map((coupon, i) => (
|
||||||
<view key={i} className="coupon-card">
|
<view key={i} className="coupon-card" onClick={() => Taro.navigateTo({ url: '/pages/detail/index' })}>
|
||||||
<view className="coupon-image">
|
<view className="coupon-image">
|
||||||
<text className="coupon-image-icon">🎫</text>
|
<text className="coupon-image-icon">🎫</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
// Taro mini-program component
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P2. KYC 身份认证
|
||||||
|
*
|
||||||
|
* 当前认证等级 + 3级认证卡片
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface KycTier {
|
||||||
|
level: string;
|
||||||
|
titleKey: string;
|
||||||
|
descKey: string;
|
||||||
|
featureKey: string;
|
||||||
|
limitKey: string;
|
||||||
|
status: 'completed' | 'locked';
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tiers: KycTier[] = [
|
||||||
|
{
|
||||||
|
level: 'L1',
|
||||||
|
titleKey: 'kyc_l1_title',
|
||||||
|
descKey: 'kyc_l1_desc',
|
||||||
|
featureKey: 'kyc_l1_feature',
|
||||||
|
limitKey: 'kyc_l1_limit',
|
||||||
|
status: 'completed',
|
||||||
|
icon: '\u2705',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'L2',
|
||||||
|
titleKey: 'kyc_l2_title',
|
||||||
|
descKey: 'kyc_l2_desc',
|
||||||
|
featureKey: 'kyc_l2_feature',
|
||||||
|
limitKey: 'kyc_l2_limit',
|
||||||
|
status: 'locked',
|
||||||
|
icon: '\uD83D\uDD12',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'L3',
|
||||||
|
titleKey: 'kyc_l3_title',
|
||||||
|
descKey: 'kyc_l3_desc',
|
||||||
|
featureKey: 'kyc_l3_feature',
|
||||||
|
limitKey: 'kyc_l3_limit',
|
||||||
|
status: 'locked',
|
||||||
|
icon: '\uD83D\uDD12',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const KycPage: React.FC = () => {
|
||||||
|
const handleVerify = () => {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '',
|
||||||
|
content: t('kyc_verify_in_app'),
|
||||||
|
showCancel: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<view className="kyc-page">
|
||||||
|
{/* Current Level Card */}
|
||||||
|
<view className="kyc-current-card">
|
||||||
|
<view className="kyc-current-top">
|
||||||
|
<text className="kyc-shield-icon">{'\uD83D\uDEE1\uFE0F'}</text>
|
||||||
|
<text className="kyc-current-label">{t('kyc_current_level')}</text>
|
||||||
|
</view>
|
||||||
|
<text className="kyc-current-title">{t('kyc_l1_title')}</text>
|
||||||
|
<view className="kyc-current-limit">
|
||||||
|
<text className="kyc-limit-label">{t('kyc_daily_limit')}</text>
|
||||||
|
<text className="kyc-limit-value">{t('kyc_l1_limit')}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Tier Cards */}
|
||||||
|
<view className="kyc-tiers">
|
||||||
|
{tiers.map((tier) => (
|
||||||
|
<view
|
||||||
|
key={tier.level}
|
||||||
|
className={`kyc-tier-card ${tier.status === 'completed' ? 'kyc-tier-completed' : 'kyc-tier-locked'}`}
|
||||||
|
>
|
||||||
|
<view className="kyc-tier-header">
|
||||||
|
<view className="kyc-tier-icon-row">
|
||||||
|
<text className="kyc-tier-icon">{tier.icon}</text>
|
||||||
|
<text className="kyc-tier-title">{t(tier.titleKey)}</text>
|
||||||
|
</view>
|
||||||
|
{tier.status === 'completed' && (
|
||||||
|
<view className="kyc-completed-badge">
|
||||||
|
<text className="kyc-completed-text">{t('kyc_completed')}</text>
|
||||||
|
</view>
|
||||||
|
)}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text className="kyc-tier-desc">{t(tier.descKey)}</text>
|
||||||
|
|
||||||
|
<view className="kyc-tier-features">
|
||||||
|
<view className="kyc-feature-row">
|
||||||
|
<text className="kyc-feature-dot">•</text>
|
||||||
|
<text className="kyc-feature-text">{t(tier.featureKey)}</text>
|
||||||
|
</view>
|
||||||
|
<view className="kyc-feature-row">
|
||||||
|
<text className="kyc-feature-dot">•</text>
|
||||||
|
<text className="kyc-feature-text">{t('kyc_daily_limit')}: {t(tier.limitKey)}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{tier.status === 'locked' && (
|
||||||
|
<view className="kyc-verify-btn" onClick={handleVerify}>
|
||||||
|
<text className="kyc-verify-text">{t('kyc_go_verify')}</text>
|
||||||
|
</view>
|
||||||
|
)}
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KycPage;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CSS:
|
||||||
|
|
||||||
|
.kyc-page { background: #F8F9FC; min-height: 100vh; padding-bottom: 60rpx; }
|
||||||
|
|
||||||
|
.kyc-current-card {
|
||||||
|
margin: 32rpx; padding: 36rpx 32rpx;
|
||||||
|
background: linear-gradient(135deg, #6C5CE7, #4834D4);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
}
|
||||||
|
.kyc-current-top {
|
||||||
|
display: flex; align-items: center; margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
.kyc-shield-icon { font-size: 36rpx; margin-right: 10rpx; }
|
||||||
|
.kyc-current-label { font-size: 24rpx; color: rgba(255,255,255,0.7); }
|
||||||
|
.kyc-current-title {
|
||||||
|
display: block; font-size: 40rpx; font-weight: 700; color: white;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.kyc-current-limit {
|
||||||
|
display: flex; align-items: center;
|
||||||
|
padding-top: 20rpx; border-top: 1rpx solid rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
.kyc-limit-label { font-size: 24rpx; color: rgba(255,255,255,0.6); margin-right: 12rpx; }
|
||||||
|
.kyc-limit-value { font-size: 30rpx; font-weight: 600; color: white; }
|
||||||
|
|
||||||
|
.kyc-tiers { padding: 0 32rpx; }
|
||||||
|
|
||||||
|
.kyc-tier-card {
|
||||||
|
background: white; border-radius: 20rpx; padding: 28rpx;
|
||||||
|
margin-bottom: 20rpx; border: 2rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.kyc-tier-completed { border-color: #00C48C; }
|
||||||
|
.kyc-tier-locked { border-color: #F1F3F8; }
|
||||||
|
|
||||||
|
.kyc-tier-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
.kyc-tier-icon-row { display: flex; align-items: center; }
|
||||||
|
.kyc-tier-icon { font-size: 32rpx; margin-right: 12rpx; }
|
||||||
|
.kyc-tier-title { font-size: 30rpx; font-weight: 600; color: #141723; }
|
||||||
|
|
||||||
|
.kyc-completed-badge {
|
||||||
|
padding: 6rpx 16rpx; background: #E6FAF3; border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
.kyc-completed-text { font-size: 22rpx; color: #00C48C; font-weight: 500; }
|
||||||
|
|
||||||
|
.kyc-tier-desc { font-size: 24rpx; color: #5C6478; margin-bottom: 16rpx; }
|
||||||
|
|
||||||
|
.kyc-tier-features { margin-bottom: 16rpx; }
|
||||||
|
.kyc-feature-row { display: flex; align-items: flex-start; margin-bottom: 8rpx; }
|
||||||
|
.kyc-feature-dot { color: #A0A8BE; margin-right: 10rpx; font-size: 24rpx; }
|
||||||
|
.kyc-feature-text { font-size: 24rpx; color: #5C6478; }
|
||||||
|
|
||||||
|
.kyc-verify-btn {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
padding: 18rpx 0; border: 2rpx solid #6C5CE7;
|
||||||
|
border-radius: 12rpx; margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
.kyc-verify-text { font-size: 28rpx; color: #6C5CE7; font-weight: 500; }
|
||||||
|
*/
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
// Taro mini-program component
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P2. 消息中心
|
||||||
|
*
|
||||||
|
* 交易通知、到期提醒、价格提醒、公告
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
time: string;
|
||||||
|
type: 'transaction' | 'expiry' | 'price' | 'announcement';
|
||||||
|
isRead: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockMessages: Message[] = [
|
||||||
|
{ id: 1, title: '购买成功', body: '您已成功购买 星巴克 ¥25 礼品卡,花费 ¥21.25', time: '14:32', type: 'transaction', isRead: false },
|
||||||
|
{ id: 2, title: '券即将到期', body: 'Target ¥30 折扣券 将于3天后到期', time: '10:15', type: 'expiry', isRead: false },
|
||||||
|
{ id: 3, title: '价格提醒', body: 'Amazon ¥100 购物券 价格降至 ¥82', time: '昨天', type: 'price', isRead: true },
|
||||||
|
{ id: 4, title: '出售成交', body: 'Nike ¥80 运动券 已售出,收入 ¥68', time: '02/07', type: 'transaction', isRead: true },
|
||||||
|
{ id: 5, title: '核销成功', body: 'Walmart ¥50 生活券 已核销', time: '02/06', type: 'transaction', isRead: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const typeIconMap: Record<string, string> = {
|
||||||
|
transaction: '\uD83D\uDD04',
|
||||||
|
expiry: '\u23F0',
|
||||||
|
price: '\uD83D\uDCC8',
|
||||||
|
announcement: '\uD83D\uDCE2',
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeColorMap: Record<string, string> = {
|
||||||
|
transaction: '#6C5CE7',
|
||||||
|
expiry: '#FF9500',
|
||||||
|
price: '#00C48C',
|
||||||
|
announcement: '#3498DB',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MessagesPage: React.FC = () => {
|
||||||
|
const [activeTab, setActiveTab] = React.useState(0);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ label: t('all'), filter: null },
|
||||||
|
{ label: t('messages_tab_trade'), filter: 'transaction' },
|
||||||
|
{ label: t('messages_tab_expiry'), filter: 'expiry' },
|
||||||
|
{ label: t('messages_tab_announcement'), filter: 'announcement' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const filteredMessages = activeTab === 0
|
||||||
|
? mockMessages
|
||||||
|
: mockMessages.filter((m) => m.type === tabs[activeTab].filter);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<view className="messages-page">
|
||||||
|
{/* Header */}
|
||||||
|
<view className="msg-header">
|
||||||
|
<text className="msg-header-title">{t('messages_title')}</text>
|
||||||
|
<text className="msg-mark-read">{t('messages_mark_all_read')}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Tab Row */}
|
||||||
|
<view className="msg-tabs">
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<view
|
||||||
|
key={i}
|
||||||
|
className={`msg-tab ${activeTab === i ? 'msg-tab-active' : ''}`}
|
||||||
|
onClick={() => setActiveTab(i)}
|
||||||
|
>
|
||||||
|
<text className={`msg-tab-text ${activeTab === i ? 'msg-tab-text-active' : ''}`}>
|
||||||
|
{tab.label}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Message List */}
|
||||||
|
<scroll-view scrollY className="msg-list">
|
||||||
|
{filteredMessages.length === 0 ? (
|
||||||
|
<view className="msg-empty">
|
||||||
|
<text className="msg-empty-icon">{typeIconMap.announcement}</text>
|
||||||
|
<text className="msg-empty-text">{t('messages_empty')}</text>
|
||||||
|
</view>
|
||||||
|
) : (
|
||||||
|
filteredMessages.map((msg) => (
|
||||||
|
<view key={msg.id} className="msg-item">
|
||||||
|
<view className="msg-icon-circle" style={{ background: typeColorMap[msg.type] }}>
|
||||||
|
<text className="msg-icon-emoji">{typeIconMap[msg.type]}</text>
|
||||||
|
</view>
|
||||||
|
<view className="msg-content">
|
||||||
|
<view className="msg-title-row">
|
||||||
|
<text className={`msg-title ${!msg.isRead ? 'msg-title-unread' : ''}`}>
|
||||||
|
{msg.title}
|
||||||
|
</text>
|
||||||
|
<text className="msg-time">{msg.time}</text>
|
||||||
|
</view>
|
||||||
|
<text className="msg-body">{msg.body}</text>
|
||||||
|
</view>
|
||||||
|
{!msg.isRead && <view className="msg-unread-dot" />}
|
||||||
|
</view>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessagesPage;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CSS:
|
||||||
|
|
||||||
|
.messages-page { background: #F8F9FC; min-height: 100vh; }
|
||||||
|
|
||||||
|
.msg-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 48rpx 32rpx 24rpx; background: white;
|
||||||
|
}
|
||||||
|
.msg-header-title { font-size: 36rpx; font-weight: 700; color: #141723; }
|
||||||
|
.msg-mark-read { font-size: 24rpx; color: #6C5CE7; }
|
||||||
|
|
||||||
|
.msg-tabs {
|
||||||
|
display: flex; padding: 0 32rpx 20rpx; background: white;
|
||||||
|
border-bottom: 1rpx solid #F1F3F8; gap: 12rpx;
|
||||||
|
}
|
||||||
|
.msg-tab {
|
||||||
|
padding: 10rpx 24rpx; border-radius: 999rpx;
|
||||||
|
background: #F1F3F8;
|
||||||
|
}
|
||||||
|
.msg-tab-active { background: #6C5CE7; }
|
||||||
|
.msg-tab-text { font-size: 24rpx; color: #5C6478; }
|
||||||
|
.msg-tab-text-active { color: white; font-weight: 500; }
|
||||||
|
|
||||||
|
.msg-list { padding: 16rpx 0; }
|
||||||
|
|
||||||
|
.msg-item {
|
||||||
|
display: flex; align-items: flex-start;
|
||||||
|
padding: 24rpx 32rpx; background: white;
|
||||||
|
margin-bottom: 2rpx; position: relative;
|
||||||
|
}
|
||||||
|
.msg-icon-circle {
|
||||||
|
width: 72rpx; height: 72rpx; border-radius: 50%;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.msg-icon-emoji { font-size: 32rpx; }
|
||||||
|
.msg-content { flex: 1; margin-left: 20rpx; margin-right: 16rpx; }
|
||||||
|
.msg-title-row {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
}
|
||||||
|
.msg-title { font-size: 28rpx; color: #141723; }
|
||||||
|
.msg-title-unread { font-weight: 600; }
|
||||||
|
.msg-time { font-size: 22rpx; color: #A0A8BE; flex-shrink: 0; }
|
||||||
|
.msg-body {
|
||||||
|
font-size: 24rpx; color: #5C6478; margin-top: 8rpx;
|
||||||
|
display: -webkit-box; -webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical; overflow: hidden;
|
||||||
|
}
|
||||||
|
.msg-unread-dot {
|
||||||
|
width: 16rpx; height: 16rpx; border-radius: 50%;
|
||||||
|
background: #6C5CE7; flex-shrink: 0; margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-empty {
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
}
|
||||||
|
.msg-empty-icon { font-size: 80rpx; margin-bottom: 20rpx; }
|
||||||
|
.msg-empty-text { font-size: 28rpx; color: #A0A8BE; }
|
||||||
|
*/
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
// Taro mini-program component (WeChat / Alipay)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2. 小程序核心页面 - 持有券详情
|
||||||
|
*
|
||||||
|
* QR code card (gradient bg) + coupon info + action buttons + usage rules
|
||||||
|
* 支持转赠 / 出售操作
|
||||||
|
*/
|
||||||
|
|
||||||
|
const infoRows = [
|
||||||
|
{ label: t('coupon_face_value'), value: '¥25.00' },
|
||||||
|
{ label: t('my_coupon_purchase_price'), value: '¥21.25' },
|
||||||
|
{ label: t('coupon_valid_until'), value: '2026/12/31' },
|
||||||
|
{ label: t('my_coupon_order_no'), value: 'GNX-20260209-001234' },
|
||||||
|
{ label: t('my_coupon_resell_count'), value: '3次' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const usageRules = [
|
||||||
|
t('my_coupon_use_in_store'),
|
||||||
|
t('my_coupon_use_in_time'),
|
||||||
|
t('my_coupon_one_per_visit'),
|
||||||
|
t('my_coupon_no_cash'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const MyCouponDetailPage: React.FC = () => {
|
||||||
|
const handleTransfer = () => {
|
||||||
|
Taro.navigateTo({ url: '/pages/transfer/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSell = () => {
|
||||||
|
Taro.showModal({
|
||||||
|
title: t('my_coupon_sell'),
|
||||||
|
content: t('my_coupon_sell_in_app'),
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: t('confirm'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<view className="coupon-detail-page">
|
||||||
|
{/* QR Code Card */}
|
||||||
|
<view className="qr-card">
|
||||||
|
<view className="qr-header">
|
||||||
|
<view className="qr-brand-row">
|
||||||
|
<text className="qr-brand">Starbucks</text>
|
||||||
|
<view className="qr-status">
|
||||||
|
<text className="qr-status-text">{t('my_coupon_active')}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text className="qr-name">星巴克 ¥25 礼品卡</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view className="qr-code-area">
|
||||||
|
<view className="qr-placeholder">
|
||||||
|
<text className="qr-placeholder-text">QR CODE</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text className="qr-code-text">GNX-STB-A1B2C3D4</text>
|
||||||
|
<text className="qr-hint">{t('my_coupon_show_qr_hint')}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Info Card */}
|
||||||
|
<view className="info-card">
|
||||||
|
{infoRows.map((item, i) => (
|
||||||
|
<view key={i} className="info-row">
|
||||||
|
<text className="info-label">{item.label}</text>
|
||||||
|
<text className="info-value">{item.value}</text>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<view className="action-row">
|
||||||
|
<view className="action-btn action-btn-secondary" onClick={handleTransfer}>
|
||||||
|
<text className="action-btn-secondary-text">{t('my_coupon_transfer')}</text>
|
||||||
|
</view>
|
||||||
|
<view className="action-btn action-btn-outline" onClick={handleSell}>
|
||||||
|
<text className="action-btn-outline-text">{t('my_coupon_sell')}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Usage Rules */}
|
||||||
|
<view className="rules-card">
|
||||||
|
<text className="rules-title">{t('my_coupon_usage_note')}</text>
|
||||||
|
{usageRules.map((rule, i) => (
|
||||||
|
<view key={i} className="rule-item">
|
||||||
|
<text className="rule-dot">•</text>
|
||||||
|
<text className="rule-text">{rule}</text>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MyCouponDetailPage;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CSS (index.scss):
|
||||||
|
|
||||||
|
.coupon-detail-page { background: #F8F9FC; min-height: 100vh; padding: 0 32rpx 40rpx; }
|
||||||
|
|
||||||
|
.qr-card {
|
||||||
|
margin-top: 24rpx; padding: 32rpx;
|
||||||
|
background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
}
|
||||||
|
.qr-header { width: 100%; margin-bottom: 24rpx; }
|
||||||
|
.qr-brand-row {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
}
|
||||||
|
.qr-brand { font-size: 26rpx; color: rgba(255,255,255,0.8); }
|
||||||
|
.qr-status {
|
||||||
|
padding: 4rpx 16rpx; background: rgba(0,196,140,0.2);
|
||||||
|
border: 1rpx solid rgba(0,196,140,0.5); border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
.qr-status-text { font-size: 22rpx; color: #00E6A0; font-weight: 600; }
|
||||||
|
.qr-name {
|
||||||
|
font-size: 32rpx; font-weight: 600; color: white; margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-area {
|
||||||
|
width: 360rpx; height: 360rpx;
|
||||||
|
background: white; border-radius: 16rpx;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.qr-placeholder {
|
||||||
|
width: 280rpx; height: 280rpx;
|
||||||
|
border: 4rpx dashed #E4E7F0; border-radius: 8rpx;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.qr-placeholder-text { font-size: 28rpx; color: #A0A8BE; font-weight: 600; }
|
||||||
|
|
||||||
|
.qr-code-text {
|
||||||
|
font-size: 28rpx; color: white; font-weight: 600;
|
||||||
|
font-family: 'Menlo', 'Courier New', monospace;
|
||||||
|
letter-spacing: 2rpx; margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
.qr-hint { font-size: 22rpx; color: rgba(255,255,255,0.7); }
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: white; border-radius: 16rpx; padding: 24rpx;
|
||||||
|
border: 1rpx solid #F1F3F8; margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
display: flex; gap: 20rpx; margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
.action-btn { flex: 1; height: 88rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.action-btn-secondary { background: #F3F1FF; }
|
||||||
|
.action-btn-secondary-text { color: #6C5CE7; font-size: 28rpx; font-weight: 600; }
|
||||||
|
.action-btn-outline { background: white; border: 2rpx solid #F1F3F8; }
|
||||||
|
.action-btn-outline-text { color: #5C6478; font-size: 28rpx; font-weight: 600; }
|
||||||
|
|
||||||
|
.rules-card {
|
||||||
|
background: white; border-radius: 16rpx; padding: 24rpx;
|
||||||
|
border: 1rpx solid #F1F3F8; margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
.rules-title { font-size: 28rpx; font-weight: 600; color: #141723; margin-bottom: 16rpx; }
|
||||||
|
.rule-item { display: flex; align-items: flex-start; margin-bottom: 12rpx; }
|
||||||
|
.rule-dot { color: #A0A8BE; margin-right: 12rpx; font-size: 24rpx; }
|
||||||
|
.rule-text { font-size: 24rpx; color: #5C6478; line-height: 1.5; }
|
||||||
|
*/
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
// Taro mini-program component
|
// Taro mini-program component
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ const MyCouponsPage: React.FC = () => {
|
||||||
{ brand: 'Amazon', name: 'Amazon ¥100 购物券', expiry: '2026-03-20', status: 'active' },
|
{ brand: 'Amazon', name: 'Amazon ¥100 购物券', expiry: '2026-03-20', status: 'active' },
|
||||||
{ brand: 'Nike', name: 'Nike ¥80 运动券', expiry: '2026-05-01', status: 'active' },
|
{ brand: 'Nike', name: 'Nike ¥80 运动券', expiry: '2026-05-01', status: 'active' },
|
||||||
].map((coupon, i) => (
|
].map((coupon, i) => (
|
||||||
<view key={i} className="my-coupon-card">
|
<view key={i} className="my-coupon-card" onClick={() => Taro.navigateTo({ url: '/pages/my-coupon-detail/index' })}>
|
||||||
<view className="coupon-left">
|
<view className="coupon-left">
|
||||||
<view className="coupon-icon-wrap">
|
<view className="coupon-icon-wrap">
|
||||||
<text className="coupon-icon-text">🎫</text>
|
<text className="coupon-icon-text">🎫</text>
|
||||||
|
|
|
||||||
|
|
@ -1 +1,208 @@
|
||||||
test
|
import React from 'react';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
// Taro mini-program - Order List
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P1. Order List - Tab-filtered order management
|
||||||
|
*
|
||||||
|
* Tabs: all / pending payment / pending delivery / completed / cancelled
|
||||||
|
* Mock order data, status badges, filter logic
|
||||||
|
*/
|
||||||
|
|
||||||
|
type OrderStatus = 'pending_payment' | 'pending_delivery' | 'completed' | 'cancelled';
|
||||||
|
|
||||||
|
interface Order {
|
||||||
|
id: string;
|
||||||
|
brand: string;
|
||||||
|
name: string;
|
||||||
|
price: string;
|
||||||
|
status: OrderStatus;
|
||||||
|
time: string;
|
||||||
|
orderNo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_COLORS: Record<OrderStatus, string> = {
|
||||||
|
pending_payment: '#FF9500',
|
||||||
|
pending_delivery: '#6C5CE7',
|
||||||
|
completed: '#00C48C',
|
||||||
|
cancelled: '#A0A8BE',
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<OrderStatus, string> = {
|
||||||
|
pending_payment: 'order_pending_payment',
|
||||||
|
pending_delivery: 'order_pending_delivery',
|
||||||
|
completed: 'order_completed',
|
||||||
|
cancelled: 'order_cancelled',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MOCK_ORDERS: Order[] = [
|
||||||
|
{ id: '1', brand: 'Starbucks', name: '星巴克 ¥25 礼品卡', price: '¥21.25', status: 'completed', time: '2026-02-09 14:32', orderNo: 'GNX-20260209-001' },
|
||||||
|
{ id: '2', brand: 'Amazon', name: 'Amazon ¥100 购物券', price: '¥85.00', status: 'pending_payment', time: '2026-02-10 10:15', orderNo: 'GNX-20260210-002' },
|
||||||
|
{ id: '3', brand: 'Target', name: 'Target ¥30 折扣券', price: '¥24.00', status: 'completed', time: '2026-02-08 16:20', orderNo: 'GNX-20260208-003' },
|
||||||
|
{ id: '4', brand: 'Nike', name: 'Nike ¥80 运动券', price: '¥68.00', status: 'pending_delivery', time: '2026-02-07 09:30', orderNo: 'GNX-20260207-004' },
|
||||||
|
{ id: '5', brand: 'Walmart', name: 'Walmart ¥50 生活券', price: '¥42.50', status: 'cancelled', time: '2026-02-06 11:45', orderNo: 'GNX-20260206-005' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TAB_STATUS_MAP: (OrderStatus | null)[] = [
|
||||||
|
null,
|
||||||
|
'pending_payment',
|
||||||
|
'pending_delivery',
|
||||||
|
'completed',
|
||||||
|
'cancelled',
|
||||||
|
];
|
||||||
|
|
||||||
|
const OrdersPage: React.FC = () => {
|
||||||
|
const [activeTab, setActiveTab] = React.useState(0);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
t('order_all'),
|
||||||
|
t('order_pending_payment'),
|
||||||
|
t('order_pending_delivery'),
|
||||||
|
t('order_completed'),
|
||||||
|
t('order_cancelled'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const filteredOrders = React.useMemo(() => {
|
||||||
|
const statusFilter = TAB_STATUS_MAP[activeTab];
|
||||||
|
if (statusFilter === null) return MOCK_ORDERS;
|
||||||
|
return MOCK_ORDERS.filter((o) => o.status === statusFilter);
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<view className="orders-page">
|
||||||
|
{/* Header */}
|
||||||
|
<view className="orders-header">
|
||||||
|
<text className="orders-header-title">{t('order_list')}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Tab Row */}
|
||||||
|
<scroll-view scrollX className="tab-scroll">
|
||||||
|
<view className="tab-row">
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<view
|
||||||
|
key={i}
|
||||||
|
className={`tab-item ${activeTab === i ? 'tab-active' : ''}`}
|
||||||
|
onClick={() => setActiveTab(i)}
|
||||||
|
>
|
||||||
|
<text className={`tab-text ${activeTab === i ? 'tab-text-active' : ''}`}>
|
||||||
|
{tab}
|
||||||
|
</text>
|
||||||
|
{activeTab === i && <view className="tab-indicator" />}
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
{/* Order List */}
|
||||||
|
<scroll-view scrollY className="order-list">
|
||||||
|
{filteredOrders.length === 0 ? (
|
||||||
|
<view className="empty-state">
|
||||||
|
<text className="empty-icon">📦</text>
|
||||||
|
<text className="empty-text">{t('order_empty')}</text>
|
||||||
|
</view>
|
||||||
|
) : (
|
||||||
|
filteredOrders.map((order) => (
|
||||||
|
<view key={order.id} className="order-card">
|
||||||
|
{/* Top row: icon + brand + name */}
|
||||||
|
<view className="order-top">
|
||||||
|
<text className="order-coupon-icon">🎫</text>
|
||||||
|
<view className="order-name-col">
|
||||||
|
<text className="order-brand">{order.brand}</text>
|
||||||
|
<text className="order-name">{order.name}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
{/* Middle row: price + status */}
|
||||||
|
<view className="order-mid">
|
||||||
|
<text className="order-price">{order.price}</text>
|
||||||
|
<view
|
||||||
|
className="status-badge"
|
||||||
|
style={{ background: STATUS_COLORS[order.status] + '18' }}
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
className="status-text"
|
||||||
|
style={{ color: STATUS_COLORS[order.status] }}
|
||||||
|
>
|
||||||
|
{t(STATUS_LABELS[order.status])}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
{/* Bottom row: order no + time */}
|
||||||
|
<view className="order-bottom">
|
||||||
|
<text className="order-no">{order.orderNo}</text>
|
||||||
|
<text className="order-time">{order.time}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<view className="list-spacer" />
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrdersPage;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CSS (index.scss):
|
||||||
|
|
||||||
|
.orders-page {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
min-height: 100vh; background: #F8F9FC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-header {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
height: 88rpx; background: white;
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
.orders-header-title { font-size: 32rpx; font-weight: 600; color: #141723; }
|
||||||
|
|
||||||
|
.tab-scroll { background: white; border-bottom: 1rpx solid #F1F3F8; }
|
||||||
|
.tab-row {
|
||||||
|
display: flex; white-space: nowrap; padding: 0 16rpx;
|
||||||
|
}
|
||||||
|
.tab-item {
|
||||||
|
display: inline-flex; flex-direction: column; align-items: center;
|
||||||
|
padding: 20rpx 24rpx; position: relative;
|
||||||
|
}
|
||||||
|
.tab-text { font-size: 26rpx; color: #5C6478; white-space: nowrap; }
|
||||||
|
.tab-text-active { color: #6C5CE7; font-weight: 600; }
|
||||||
|
.tab-indicator {
|
||||||
|
position: absolute; bottom: 0; left: 50%; transform: translateX(-50%);
|
||||||
|
width: 40rpx; height: 4rpx; background: #6C5CE7; border-radius: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list { flex: 1; padding: 16rpx 32rpx; }
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
background: white; border-radius: 16rpx; padding: 24rpx;
|
||||||
|
margin-bottom: 16rpx; border: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-top { display: flex; align-items: center; margin-bottom: 16rpx; }
|
||||||
|
.order-coupon-icon { font-size: 40rpx; margin-right: 16rpx; }
|
||||||
|
.order-name-col { display: flex; flex-direction: column; }
|
||||||
|
.order-brand { font-size: 22rpx; color: #A0A8BE; }
|
||||||
|
.order-name { font-size: 28rpx; font-weight: 500; color: #141723; margin-top: 4rpx; }
|
||||||
|
|
||||||
|
.order-mid {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
margin-bottom: 16rpx; padding-bottom: 16rpx; border-bottom: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.order-price { font-size: 32rpx; font-weight: 700; color: #6C5CE7; }
|
||||||
|
.status-badge { padding: 4rpx 16rpx; border-radius: 999rpx; }
|
||||||
|
.status-text { font-size: 22rpx; font-weight: 600; }
|
||||||
|
|
||||||
|
.order-bottom { display: flex; justify-content: space-between; }
|
||||||
|
.order-no { font-size: 22rpx; color: #A0A8BE; }
|
||||||
|
.order-time { font-size: 22rpx; color: #A0A8BE; }
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
}
|
||||||
|
.empty-icon { font-size: 80rpx; opacity: 0.3; margin-bottom: 16rpx; }
|
||||||
|
.empty-text { font-size: 28rpx; color: #A0A8BE; }
|
||||||
|
|
||||||
|
.list-spacer { height: 120rpx; }
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
// Taro mini-program component (WeChat / Alipay)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E1. 小程序核心页面 - 支付成功页
|
||||||
|
*
|
||||||
|
* 支付成功庆祝动画 + 订单信息卡片 + 跳转按钮
|
||||||
|
*/
|
||||||
|
|
||||||
|
const orderInfo = [
|
||||||
|
{ label: t('payment_success_coupon_name'), value: '星巴克 ¥25 礼品卡' },
|
||||||
|
{ label: t('payment_success_pay_amount'), value: '¥21.25' },
|
||||||
|
{ label: t('payment_success_order_no'), value: 'GNX-20260209-001234' },
|
||||||
|
{ label: t('payment_success_pay_time'), value: '2026-02-09 14:32' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PaymentSuccessPage: React.FC = () => {
|
||||||
|
const handleViewCoupon = () => {
|
||||||
|
Taro.reLaunch({ url: '/pages/my-coupons/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContinue = () => {
|
||||||
|
Taro.reLaunch({ url: '/pages/home/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<view className="success-page">
|
||||||
|
{/* Success Icon */}
|
||||||
|
<view className="success-hero">
|
||||||
|
<view className="success-circle">
|
||||||
|
<text className="success-check">✓</text>
|
||||||
|
</view>
|
||||||
|
<text className="success-title">{t('payment_success_title')}</text>
|
||||||
|
<text className="success-hint">{t('payment_success_hint')}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Order Info Card */}
|
||||||
|
<view className="order-card">
|
||||||
|
{orderInfo.map((item, i) => (
|
||||||
|
<view key={i} className="order-row">
|
||||||
|
<text className="order-label">{item.label}</text>
|
||||||
|
<text className="order-value">{item.value}</text>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<view className="action-buttons">
|
||||||
|
<view className="btn-primary" onClick={handleViewCoupon}>
|
||||||
|
<text className="btn-primary-text">{t('payment_success_view_coupon')}</text>
|
||||||
|
</view>
|
||||||
|
<view className="btn-outline" onClick={handleContinue}>
|
||||||
|
<text className="btn-outline-text">{t('payment_success_continue')}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaymentSuccessPage;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CSS (index.scss):
|
||||||
|
|
||||||
|
.success-page { background: #F8F9FC; min-height: 100vh; padding: 0 32rpx; }
|
||||||
|
|
||||||
|
.success-hero {
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
padding-top: 120rpx; padding-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
.success-circle {
|
||||||
|
width: 160rpx; height: 160rpx; border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #00C48C, #00E6A0);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
box-shadow: 0 16rpx 40rpx rgba(0,196,140,0.3);
|
||||||
|
}
|
||||||
|
.success-check { color: white; font-size: 72rpx; font-weight: 700; }
|
||||||
|
.success-title {
|
||||||
|
font-size: 40rpx; font-weight: 700; color: #141723;
|
||||||
|
margin-top: 32rpx;
|
||||||
|
}
|
||||||
|
.success-hint {
|
||||||
|
font-size: 26rpx; color: #5C6478; margin-top: 12rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
background: white; border-radius: 16rpx; padding: 28rpx;
|
||||||
|
border: 1rpx solid #F1F3F8; margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
.order-row {
|
||||||
|
display: flex; justify-content: space-between;
|
||||||
|
padding: 16rpx 0; border-bottom: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.order-row:last-child { border-bottom: none; }
|
||||||
|
.order-label { font-size: 26rpx; color: #5C6478; }
|
||||||
|
.order-value { font-size: 26rpx; color: #141723; font-weight: 500; }
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex; flex-direction: column; gap: 20rpx;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
height: 96rpx; border-radius: 16rpx;
|
||||||
|
background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.btn-primary-text { color: white; font-size: 30rpx; font-weight: 600; }
|
||||||
|
.btn-outline {
|
||||||
|
height: 96rpx; border-radius: 16rpx;
|
||||||
|
background: white; border: 2rpx solid #6C5CE7;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.btn-outline-text { color: #6C5CE7; font-size: 30rpx; font-weight: 600; }
|
||||||
|
*/
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
// Taro mini-program component
|
// Taro mini-program component
|
||||||
|
|
||||||
|
|
@ -43,10 +44,11 @@ const ProfilePage: React.FC = () => {
|
||||||
{[
|
{[
|
||||||
{ icon: '🎫', label: t('my_coupons'), path: '/pages/my-coupons/index' },
|
{ icon: '🎫', label: t('my_coupons'), path: '/pages/my-coupons/index' },
|
||||||
{ icon: '📋', label: t('profile_orders'), path: '/pages/orders/index' },
|
{ icon: '📋', label: t('profile_orders'), path: '/pages/orders/index' },
|
||||||
{ icon: '💳', label: t('profile_payment'), path: '' },
|
{ icon: '💳', label: t('profile_payment'), path: '/pages/wallet/index' },
|
||||||
{ icon: '🔔', label: t('profile_notifications'), path: '' },
|
{ icon: '🔔', label: t('profile_notifications'), path: '/pages/messages/index' },
|
||||||
|
{ icon: '✨', label: t('profile_ai_assistant'), path: '/pages/ai-chat/index' },
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<view key={i} className="menu-item">
|
<view key={i} className="menu-item" onClick={() => Taro.navigateTo({ url: item.path })}>
|
||||||
<text className="menu-icon">{item.icon}</text>
|
<text className="menu-icon">{item.icon}</text>
|
||||||
<text className="menu-label">{item.label}</text>
|
<text className="menu-label">{item.label}</text>
|
||||||
<text className="menu-arrow">›</text>
|
<text className="menu-arrow">›</text>
|
||||||
|
|
@ -56,12 +58,13 @@ const ProfilePage: React.FC = () => {
|
||||||
|
|
||||||
<view className="menu-section">
|
<view className="menu-section">
|
||||||
{[
|
{[
|
||||||
{ icon: '🌐', label: t('profile_language'), value: '简体中文' },
|
{ icon: '🛡️', label: t('profile_kyc'), path: '/pages/kyc/index', value: '' },
|
||||||
{ icon: '💰', label: t('profile_currency'), value: 'USD' },
|
{ icon: '🌐', label: t('profile_language'), path: '/pages/settings/index', value: '简体中文' },
|
||||||
{ icon: '❓', label: t('profile_help'), value: '' },
|
{ icon: '💰', label: t('profile_currency'), path: '/pages/settings/index', value: 'USD' },
|
||||||
{ icon: '⚙️', label: t('profile_settings'), value: '' },
|
{ icon: '❓', label: t('profile_help'), path: '', value: '' },
|
||||||
|
{ icon: '⚙️', label: t('profile_settings'), path: '/pages/settings/index', value: '' },
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<view key={i} className="menu-item">
|
<view key={i} className="menu-item" onClick={() => item.path && Taro.navigateTo({ url: item.path })}>
|
||||||
<text className="menu-icon">{item.icon}</text>
|
<text className="menu-icon">{item.icon}</text>
|
||||||
<text className="menu-label">{item.label}</text>
|
<text className="menu-label">{item.label}</text>
|
||||||
{item.value ? <text className="menu-value">{item.value}</text> : null}
|
{item.value ? <text className="menu-value">{item.value}</text> : null}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,37 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
// Taro mini-program component (WeChat / Alipay)
|
// Taro mini-program component (WeChat / Alipay)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* E1. 小程序核心页面 - 购买页
|
* E1. 小程序核心页面 - 购买/确认订单页
|
||||||
*
|
*
|
||||||
* Coupon summary card, quantity selector, price calculation,
|
* Coupon summary card, quantity selector, payment method,
|
||||||
* payment button (微信支付/支付宝/H5支付), order confirmation
|
* price breakdown, fixed bottom bar with confirm button
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const UNIT_PRICE = 21.25;
|
||||||
|
const FACE_VALUE = 25;
|
||||||
|
|
||||||
const PurchasePage: React.FC = () => {
|
const PurchasePage: React.FC = () => {
|
||||||
|
const [quantity, setQuantity] = React.useState(1);
|
||||||
|
|
||||||
|
const totalPrice = (UNIT_PRICE * quantity).toFixed(2);
|
||||||
|
const totalFace = (FACE_VALUE * quantity).toFixed(2);
|
||||||
|
const savings = ((FACE_VALUE - UNIT_PRICE) * quantity).toFixed(2);
|
||||||
|
|
||||||
|
const handleMinus = () => {
|
||||||
|
if (quantity > 1) setQuantity(quantity - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlus = () => {
|
||||||
|
if (quantity < 10) setQuantity(quantity + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
Taro.navigateTo({ url: '/pages/payment-success/index' });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<view className="purchase-page">
|
<view className="purchase-page">
|
||||||
{/* Coupon Summary Card */}
|
{/* Coupon Summary Card */}
|
||||||
|
|
@ -22,12 +45,199 @@ const PurchasePage: React.FC = () => {
|
||||||
<view className="summary-price-row">
|
<view className="summary-price-row">
|
||||||
<text className="summary-price">¥21.25</text>
|
<text className="summary-price">¥21.25</text>
|
||||||
<text className="summary-face">¥25</text>
|
<text className="summary-face">¥25</text>
|
||||||
<view className="summary-discount">8.1折</view>
|
<view className="summary-discount">
|
||||||
|
<text className="summary-discount-text">8.5折</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
{/* Quantity Selector */}
|
||||||
|
<view className="section-card">
|
||||||
|
<view className="section-row">
|
||||||
|
<text className="section-label">{t('purchase_quantity')}</text>
|
||||||
|
<view className="qty-selector">
|
||||||
|
<view className={`qty-btn ${quantity <= 1 ? 'qty-btn-disabled' : ''}`} onClick={handleMinus}>
|
||||||
|
<text className="qty-btn-text">-</text>
|
||||||
|
</view>
|
||||||
|
<view className="qty-value">
|
||||||
|
<text className="qty-value-text">{quantity}</text>
|
||||||
|
</view>
|
||||||
|
<view className={`qty-btn ${quantity >= 10 ? 'qty-btn-disabled' : ''}`} onClick={handlePlus}>
|
||||||
|
<text className="qty-btn-text">+</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Payment Method */}
|
||||||
|
<view className="section-card">
|
||||||
|
<text className="card-title">{t('purchase_payment_method')}</text>
|
||||||
|
<view className="payment-row">
|
||||||
|
<view className="payment-icon-wrap">
|
||||||
|
<text className="payment-icon">💬</text>
|
||||||
|
</view>
|
||||||
|
<text className="payment-name">{t('purchase_wechat_pay')}</text>
|
||||||
|
<view className="payment-check">
|
||||||
|
<text className="payment-check-text">✓</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Price Breakdown */}
|
||||||
|
<view className="section-card">
|
||||||
|
<text className="card-title">{t('purchase_price_detail')}</text>
|
||||||
|
<view className="price-row">
|
||||||
|
<text className="price-label">{t('purchase_unit_price')}</text>
|
||||||
|
<text className="price-value">¥{UNIT_PRICE.toFixed(2)}</text>
|
||||||
|
</view>
|
||||||
|
<view className="price-row">
|
||||||
|
<text className="price-label">{t('purchase_count')}</text>
|
||||||
|
<text className="price-value">x{quantity}</text>
|
||||||
|
</view>
|
||||||
|
<view className="price-divider" />
|
||||||
|
<view className="price-row price-row-total">
|
||||||
|
<text className="price-label-total">{t('order_total')}</text>
|
||||||
|
<text className="price-value-total">¥{totalPrice}</text>
|
||||||
|
</view>
|
||||||
|
<view className="savings-badge">
|
||||||
|
<text className="savings-icon">🏷️</text>
|
||||||
|
<text className="savings-text">{t('purchase_save_badge')} ¥{savings}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Info Notice */}
|
||||||
|
<view className="info-notice">
|
||||||
|
<text className="notice-icon">🛡️</text>
|
||||||
|
<text className="notice-text">{t('purchase_buying_note')}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Fixed Bottom Bar */}
|
||||||
|
<view className="bottom-bar">
|
||||||
|
<view className="bottom-left">
|
||||||
|
<text className="bottom-label">{t('order_total')}</text>
|
||||||
|
<text className="bottom-price">¥{totalPrice}</text>
|
||||||
|
</view>
|
||||||
|
<view className="bottom-btn" onClick={handleConfirm}>
|
||||||
|
<text className="bottom-btn-text">{t('purchase_confirm_pay')}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PurchasePage;
|
export default PurchasePage;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CSS (index.scss):
|
||||||
|
|
||||||
|
.purchase-page { padding: 24rpx 32rpx; padding-bottom: 180rpx; background: #F8F9FC; min-height: 100vh; }
|
||||||
|
|
||||||
|
.coupon-summary {
|
||||||
|
display: flex; padding: 24rpx;
|
||||||
|
background: white; border-radius: 16rpx;
|
||||||
|
border: 1rpx solid #F1F3F8; margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.summary-image {
|
||||||
|
width: 140rpx; height: 140rpx; background: #F3F1FF;
|
||||||
|
border-radius: 12rpx; display: flex;
|
||||||
|
align-items: center; justify-content: center; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.summary-icon { font-size: 48rpx; }
|
||||||
|
.summary-info {
|
||||||
|
flex: 1; padding-left: 20rpx;
|
||||||
|
display: flex; flex-direction: column; justify-content: space-between;
|
||||||
|
}
|
||||||
|
.summary-brand { font-size: 22rpx; color: #A0A8BE; }
|
||||||
|
.summary-name { font-size: 28rpx; font-weight: 500; color: #141723; margin-top: 4rpx; }
|
||||||
|
.summary-price-row { display: flex; align-items: flex-end; margin-top: 8rpx; }
|
||||||
|
.summary-price { font-size: 32rpx; font-weight: 700; color: #6C5CE7; }
|
||||||
|
.summary-face { font-size: 22rpx; color: #A0A8BE; text-decoration: line-through; margin-left: 8rpx; }
|
||||||
|
.summary-discount {
|
||||||
|
margin-left: 8rpx; padding: 2rpx 10rpx;
|
||||||
|
background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
.summary-discount-text { color: white; font-size: 20rpx; font-weight: 700; }
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
background: white; border-radius: 16rpx; padding: 24rpx;
|
||||||
|
border: 1rpx solid #F1F3F8; margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.card-title { font-size: 28rpx; font-weight: 600; color: #141723; margin-bottom: 20rpx; }
|
||||||
|
|
||||||
|
.section-row {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
}
|
||||||
|
.section-label { font-size: 28rpx; font-weight: 500; color: #141723; }
|
||||||
|
|
||||||
|
.qty-selector { display: flex; align-items: center; }
|
||||||
|
.qty-btn {
|
||||||
|
width: 56rpx; height: 56rpx; border-radius: 12rpx;
|
||||||
|
background: #F3F1FF; display: flex;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.qty-btn-disabled { background: #F1F3F8; opacity: 0.5; }
|
||||||
|
.qty-btn-text { font-size: 32rpx; font-weight: 600; color: #6C5CE7; }
|
||||||
|
.qty-value {
|
||||||
|
width: 80rpx; display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.qty-value-text { font-size: 32rpx; font-weight: 600; color: #141723; }
|
||||||
|
|
||||||
|
.payment-row {
|
||||||
|
display: flex; align-items: center; padding: 12rpx 0;
|
||||||
|
}
|
||||||
|
.payment-icon-wrap {
|
||||||
|
width: 48rpx; height: 48rpx; border-radius: 12rpx;
|
||||||
|
background: #E6FAF3; display: flex;
|
||||||
|
align-items: center; justify-content: center; margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
.payment-icon { font-size: 28rpx; }
|
||||||
|
.payment-name { flex: 1; font-size: 28rpx; color: #141723; }
|
||||||
|
.payment-check {
|
||||||
|
width: 40rpx; height: 40rpx; border-radius: 50%;
|
||||||
|
background: #00C48C; display: flex;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.payment-check-text { color: white; font-size: 24rpx; font-weight: 700; }
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
display: flex; justify-content: space-between; padding: 12rpx 0;
|
||||||
|
}
|
||||||
|
.price-label { font-size: 26rpx; color: #5C6478; }
|
||||||
|
.price-value { font-size: 26rpx; color: #141723; }
|
||||||
|
.price-divider { height: 1rpx; background: #F1F3F8; margin: 12rpx 0; }
|
||||||
|
.price-row-total { padding-top: 16rpx; }
|
||||||
|
.price-label-total { font-size: 28rpx; font-weight: 600; color: #141723; }
|
||||||
|
.price-value-total { font-size: 36rpx; font-weight: 700; color: #6C5CE7; }
|
||||||
|
|
||||||
|
.savings-badge {
|
||||||
|
display: flex; align-items: center; margin-top: 12rpx;
|
||||||
|
padding: 10rpx 16rpx; background: #E6FAF3; border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
.savings-icon { font-size: 24rpx; margin-right: 8rpx; }
|
||||||
|
.savings-text { font-size: 24rpx; color: #00C48C; font-weight: 500; }
|
||||||
|
|
||||||
|
.info-notice {
|
||||||
|
display: flex; align-items: center;
|
||||||
|
padding: 20rpx 24rpx; background: #E6FAF3;
|
||||||
|
border-radius: 12rpx; margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.notice-icon { font-size: 28rpx; margin-right: 12rpx; }
|
||||||
|
.notice-text { font-size: 24rpx; color: #3D4459; }
|
||||||
|
|
||||||
|
.bottom-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;
|
||||||
|
}
|
||||||
|
.bottom-left { display: flex; flex-direction: column; }
|
||||||
|
.bottom-label { font-size: 22rpx; color: #A0A8BE; }
|
||||||
|
.bottom-price { font-size: 40rpx; font-weight: 700; color: #6C5CE7; }
|
||||||
|
.bottom-btn {
|
||||||
|
padding: 20rpx 56rpx; border-radius: 16rpx;
|
||||||
|
background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
|
||||||
|
}
|
||||||
|
.bottom-btn-text { color: white; font-size: 30rpx; font-weight: 600; }
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import CouponCard from '@/components/coupon-card';
|
||||||
|
// Taro mini-program component (WeChat / Alipay)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E1. 小程序核心页面 - 搜索页
|
||||||
|
*
|
||||||
|
* 热门搜索标签 + 搜索历史 + 搜索结果
|
||||||
|
* 输入内容后展示券列表,点击跳转详情
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mockResults = [
|
||||||
|
{ brand: 'Starbucks', name: '星巴克 ¥25 礼品卡', price: '¥21.25', faceValue: '¥25', discount: '8.5折' },
|
||||||
|
{ brand: 'Amazon', name: 'Amazon ¥100 购物券', price: '¥85.00', faceValue: '¥100', discount: '8.5折' },
|
||||||
|
{ brand: 'Walmart', name: 'Walmart ¥50 购物券', price: '¥42.50', faceValue: '¥50', discount: '8.5折' },
|
||||||
|
{ brand: 'Target', name: 'Target ¥30 折扣券', price: '¥24.00', faceValue: '¥30', discount: '8折' },
|
||||||
|
{ brand: 'Nike', name: 'Nike ¥80 运动券', price: '¥68.00', faceValue: '¥80', discount: '8.5折' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const hotTags = ['Starbucks', 'Amazon', t('category_food'), t('coupon_sort_discount'), t('category_travel'), 'Nike'];
|
||||||
|
|
||||||
|
const historyItems = ['星巴克 礼品卡', 'Nike 运动券', '餐饮 折扣'];
|
||||||
|
|
||||||
|
const SearchPage: React.FC = () => {
|
||||||
|
const [searchText, setSearchText] = React.useState('');
|
||||||
|
const showResults = searchText.length > 0;
|
||||||
|
|
||||||
|
const handleResultTap = () => {
|
||||||
|
Taro.navigateTo({ url: '/pages/detail/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<view className="search-page">
|
||||||
|
{/* Header: Search Input + Cancel */}
|
||||||
|
<view className="search-header">
|
||||||
|
<view className="search-input-wrap">
|
||||||
|
<text className="search-icon">🔍</text>
|
||||||
|
<input
|
||||||
|
className="search-input"
|
||||||
|
placeholder={t('search_placeholder')}
|
||||||
|
value={searchText}
|
||||||
|
onInput={(e: any) => setSearchText(e.detail.value)}
|
||||||
|
focus
|
||||||
|
/>
|
||||||
|
{searchText.length > 0 && (
|
||||||
|
<text className="search-clear" onClick={() => setSearchText('')}>✕</text>
|
||||||
|
)}
|
||||||
|
</view>
|
||||||
|
<text className="search-cancel" onClick={() => Taro.navigateBack()}>{t('cancel')}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{!showResults ? (
|
||||||
|
<scroll-view scrollY className="search-body">
|
||||||
|
{/* Hot Tags */}
|
||||||
|
<view className="section">
|
||||||
|
<text className="section-title">{t('search_hot_keywords')}</text>
|
||||||
|
<view className="tag-wrap">
|
||||||
|
{hotTags.map((tag, i) => (
|
||||||
|
<view key={i} className="hot-tag" onClick={() => setSearchText(tag)}>
|
||||||
|
<text className="hot-tag-text">{tag}</text>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Search History */}
|
||||||
|
<view className="section">
|
||||||
|
<view className="section-header">
|
||||||
|
<text className="section-title">{t('search_history')}</text>
|
||||||
|
<text className="section-clear">{t('search_clear_history')}</text>
|
||||||
|
</view>
|
||||||
|
{historyItems.map((item, i) => (
|
||||||
|
<view key={i} className="history-item" onClick={() => setSearchText(item)}>
|
||||||
|
<text className="history-icon">🕐</text>
|
||||||
|
<text className="history-text">{item}</text>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
) : (
|
||||||
|
<scroll-view scrollY className="search-results">
|
||||||
|
<view className="result-count">
|
||||||
|
<text className="result-count-text">{t('search_result_count').replace('{count}', String(mockResults.length))}</text>
|
||||||
|
</view>
|
||||||
|
<view className="result-list">
|
||||||
|
{mockResults.map((coupon, i) => (
|
||||||
|
<CouponCard
|
||||||
|
key={i}
|
||||||
|
brand={coupon.brand}
|
||||||
|
name={coupon.name}
|
||||||
|
price={coupon.price}
|
||||||
|
faceValue={coupon.faceValue}
|
||||||
|
discount={coupon.discount}
|
||||||
|
onClick={handleResultTap}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
)}
|
||||||
|
</view>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchPage;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CSS (index.scss):
|
||||||
|
|
||||||
|
.search-page { background: #F8F9FC; min-height: 100vh; }
|
||||||
|
|
||||||
|
.search-header {
|
||||||
|
display: flex; align-items: center;
|
||||||
|
padding: 20rpx 32rpx; background: white;
|
||||||
|
border-bottom: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.search-input-wrap {
|
||||||
|
flex: 1; display: flex; align-items: center;
|
||||||
|
height: 72rpx; padding: 0 24rpx;
|
||||||
|
background: #F1F3F8; border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
.search-icon { font-size: 28rpx; margin-right: 12rpx; flex-shrink: 0; }
|
||||||
|
.search-input { flex: 1; font-size: 28rpx; color: #141723; background: transparent; }
|
||||||
|
.search-clear {
|
||||||
|
font-size: 24rpx; color: #A0A8BE; margin-left: 12rpx;
|
||||||
|
width: 40rpx; height: 40rpx; display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.search-cancel { font-size: 28rpx; color: #6C5CE7; margin-left: 24rpx; flex-shrink: 0; }
|
||||||
|
|
||||||
|
.search-body { padding: 0 32rpx; height: calc(100vh - 112rpx); }
|
||||||
|
.search-results { padding: 0 32rpx; height: calc(100vh - 112rpx); }
|
||||||
|
|
||||||
|
.section { margin-top: 32rpx; }
|
||||||
|
.section-header {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.section-title { font-size: 30rpx; font-weight: 600; color: #141723; }
|
||||||
|
.section-clear { font-size: 24rpx; color: #A0A8BE; }
|
||||||
|
|
||||||
|
.tag-wrap { display: flex; flex-wrap: wrap; gap: 16rpx; margin-top: 20rpx; }
|
||||||
|
.hot-tag {
|
||||||
|
padding: 12rpx 28rpx; background: #F3F1FF;
|
||||||
|
border-radius: 999rpx; border: 1rpx solid rgba(108,92,231,0.12);
|
||||||
|
}
|
||||||
|
.hot-tag-text { font-size: 26rpx; color: #6C5CE7; }
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
display: flex; align-items: center; padding: 20rpx 0;
|
||||||
|
border-bottom: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.history-icon { font-size: 24rpx; margin-right: 16rpx; color: #A0A8BE; }
|
||||||
|
.history-text { font-size: 28rpx; color: #5C6478; }
|
||||||
|
|
||||||
|
.result-count { padding: 20rpx 0; }
|
||||||
|
.result-count-text { font-size: 24rpx; color: #A0A8BE; }
|
||||||
|
.result-list { padding-bottom: 40rpx; }
|
||||||
|
*/
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
// Taro mini-program component
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P2. 设置页面
|
||||||
|
*
|
||||||
|
* 通知开关、语言/货币选择、关于、退出登录
|
||||||
|
*/
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{ value: 'zh-CN', label: '简体中文' },
|
||||||
|
{ value: 'en-US', label: 'English' },
|
||||||
|
{ value: 'ja-JP', label: '日本語' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const currencies = [
|
||||||
|
{ code: 'CNY', symbol: '¥', label: 'CNY (¥)' },
|
||||||
|
{ code: 'USD', symbol: '$', label: 'USD ($)' },
|
||||||
|
{ code: 'EUR', symbol: '€', label: 'EUR (€)' },
|
||||||
|
{ code: 'GBP', symbol: '£', label: 'GBP (£)' },
|
||||||
|
{ code: 'JPY', symbol: '¥', label: 'JPY (¥)' },
|
||||||
|
{ code: 'HKD', symbol: 'HK$', label: 'HKD (HK$)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SettingsPage: React.FC = () => {
|
||||||
|
const [notifyTrade, setNotifyTrade] = React.useState(true);
|
||||||
|
const [notifyExpiry, setNotifyExpiry] = React.useState(true);
|
||||||
|
const [notifyMarketing, setNotifyMarketing] = React.useState(false);
|
||||||
|
const [showLangPicker, setShowLangPicker] = React.useState(false);
|
||||||
|
const [showCurrencyPicker, setShowCurrencyPicker] = React.useState(false);
|
||||||
|
const [selectedLang, setSelectedLang] = React.useState('zh-CN');
|
||||||
|
const [selectedCurrency, setSelectedCurrency] = React.useState('USD');
|
||||||
|
|
||||||
|
const currentLangLabel = languages.find((l) => l.value === selectedLang)?.label || '简体中文';
|
||||||
|
const currentCurrencyLabel = currencies.find((c) => c.code === selectedCurrency)?.label || 'USD ($)';
|
||||||
|
|
||||||
|
const handleClearCache = () => {
|
||||||
|
Taro.showToast({ title: t('settings_cache_cleared'), icon: 'success' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleItems = [
|
||||||
|
{ label: t('settings_trade_notify'), value: notifyTrade, setter: setNotifyTrade },
|
||||||
|
{ label: t('settings_expiry_remind'), value: notifyExpiry, setter: setNotifyExpiry },
|
||||||
|
{ label: t('settings_marketing_push'), value: notifyMarketing, setter: setNotifyMarketing },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<view className="settings-page">
|
||||||
|
{/* Section: Notifications */}
|
||||||
|
<view className="settings-section">
|
||||||
|
<text className="settings-section-title">{t('settings_notifications')}</text>
|
||||||
|
{toggleItems.map((item, i) => (
|
||||||
|
<view key={i} className="settings-row">
|
||||||
|
<text className="settings-row-label">{item.label}</text>
|
||||||
|
<view
|
||||||
|
className={`toggle-track ${item.value ? 'toggle-track-on' : ''}`}
|
||||||
|
onClick={() => item.setter(!item.value)}
|
||||||
|
>
|
||||||
|
<view className={`toggle-thumb ${item.value ? 'toggle-thumb-on' : ''}`} />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Section: General */}
|
||||||
|
<view className="settings-section">
|
||||||
|
<text className="settings-section-title">{t('settings_general')}</text>
|
||||||
|
|
||||||
|
<view className="settings-row" onClick={() => setShowLangPicker(true)}>
|
||||||
|
<text className="settings-row-label">{t('profile_language')}</text>
|
||||||
|
<view className="settings-row-right">
|
||||||
|
<text className="settings-row-value">{currentLangLabel}</text>
|
||||||
|
<text className="settings-row-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view className="settings-row" onClick={() => setShowCurrencyPicker(true)}>
|
||||||
|
<text className="settings-row-label">{t('profile_currency')}</text>
|
||||||
|
<view className="settings-row-right">
|
||||||
|
<text className="settings-row-value">{currentCurrencyLabel}</text>
|
||||||
|
<text className="settings-row-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view className="settings-row" onClick={handleClearCache}>
|
||||||
|
<text className="settings-row-label">{t('settings_clear_cache')}</text>
|
||||||
|
<text className="settings-row-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Section: About */}
|
||||||
|
<view className="settings-section">
|
||||||
|
<text className="settings-section-title">{t('settings_about')}</text>
|
||||||
|
|
||||||
|
<view className="settings-row">
|
||||||
|
<text className="settings-row-label">{t('settings_version')}</text>
|
||||||
|
<text className="settings-row-value">v1.0.0</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view className="settings-row">
|
||||||
|
<text className="settings-row-label">{t('login_user_agreement')}</text>
|
||||||
|
<text className="settings-row-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view className="settings-row">
|
||||||
|
<text className="settings-row-label">{t('login_privacy_policy')}</text>
|
||||||
|
<text className="settings-row-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view className="settings-row">
|
||||||
|
<text className="settings-row-label">{t('settings_help_center')}</text>
|
||||||
|
<text className="settings-row-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Logout Button */}
|
||||||
|
<view className="settings-logout-btn">
|
||||||
|
<text className="settings-logout-text">{t('settings_logout')}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Language Picker Modal */}
|
||||||
|
{showLangPicker && (
|
||||||
|
<view className="picker-overlay" onClick={() => setShowLangPicker(false)}>
|
||||||
|
<view className="picker-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<view className="picker-header">
|
||||||
|
<text className="picker-title">{t('settings_select_language')}</text>
|
||||||
|
<text className="picker-close" onClick={() => setShowLangPicker(false)}>✕</text>
|
||||||
|
</view>
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<view
|
||||||
|
key={lang.value}
|
||||||
|
className="picker-option"
|
||||||
|
onClick={() => { setSelectedLang(lang.value); setShowLangPicker(false); }}
|
||||||
|
>
|
||||||
|
<text className="picker-option-label">{lang.label}</text>
|
||||||
|
{selectedLang === lang.value && <text className="picker-check">✓</text>}
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Currency Picker Modal */}
|
||||||
|
{showCurrencyPicker && (
|
||||||
|
<view className="picker-overlay" onClick={() => setShowCurrencyPicker(false)}>
|
||||||
|
<view className="picker-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<view className="picker-header">
|
||||||
|
<text className="picker-title">{t('settings_select_currency')}</text>
|
||||||
|
<text className="picker-close" onClick={() => setShowCurrencyPicker(false)}>✕</text>
|
||||||
|
</view>
|
||||||
|
<text className="picker-note">{t('settings_currency_note')}</text>
|
||||||
|
{currencies.map((cur) => (
|
||||||
|
<view
|
||||||
|
key={cur.code}
|
||||||
|
className="picker-option"
|
||||||
|
onClick={() => { setSelectedCurrency(cur.code); setShowCurrencyPicker(false); }}
|
||||||
|
>
|
||||||
|
<text className="picker-option-label">{cur.label}</text>
|
||||||
|
{selectedCurrency === cur.code && <text className="picker-check">✓</text>}
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
)}
|
||||||
|
</view>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsPage;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CSS:
|
||||||
|
|
||||||
|
.settings-page { background: #F8F9FC; min-height: 100vh; padding-bottom: 60rpx; }
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
background: white; margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
.settings-section-title {
|
||||||
|
display: block; font-size: 24rpx; color: #A0A8BE;
|
||||||
|
padding: 24rpx 32rpx 8rpx; font-weight: 500; text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 28rpx 32rpx; border-bottom: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.settings-row-label { font-size: 28rpx; color: #141723; }
|
||||||
|
.settings-row-right { display: flex; align-items: center; }
|
||||||
|
.settings-row-value { font-size: 26rpx; color: #A0A8BE; margin-right: 8rpx; }
|
||||||
|
.settings-row-arrow { font-size: 32rpx; color: #CDD2DE; }
|
||||||
|
|
||||||
|
.toggle-track {
|
||||||
|
width: 88rpx; height: 48rpx; border-radius: 999rpx;
|
||||||
|
background: #E4E7F0; position: relative; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-track-on { background: #6C5CE7; }
|
||||||
|
.toggle-thumb {
|
||||||
|
width: 40rpx; height: 40rpx; border-radius: 50%;
|
||||||
|
background: white; position: absolute; top: 4rpx; left: 4rpx;
|
||||||
|
box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.15); transition: left 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-thumb-on { left: 44rpx; }
|
||||||
|
|
||||||
|
.settings-logout-btn {
|
||||||
|
margin: 48rpx 32rpx 0; padding: 24rpx 0;
|
||||||
|
border: 2rpx solid #E74C3C; border-radius: 16rpx;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.settings-logout-text { font-size: 30rpx; color: #E74C3C; font-weight: 500; }
|
||||||
|
|
||||||
|
.picker-overlay {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.5); z-index: 999;
|
||||||
|
display: flex; align-items: flex-end; justify-content: center;
|
||||||
|
}
|
||||||
|
.picker-modal {
|
||||||
|
width: 100%; background: white; border-radius: 24rpx 24rpx 0 0;
|
||||||
|
padding: 32rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
.picker-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
.picker-title { font-size: 32rpx; font-weight: 600; color: #141723; }
|
||||||
|
.picker-close { font-size: 32rpx; color: #A0A8BE; padding: 8rpx; }
|
||||||
|
.picker-note { font-size: 22rpx; color: #A0A8BE; margin-bottom: 16rpx; }
|
||||||
|
|
||||||
|
.picker-option {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 28rpx 16rpx; border-bottom: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.picker-option-label { font-size: 28rpx; color: #141723; }
|
||||||
|
.picker-check { font-size: 32rpx; color: #6C5CE7; font-weight: 700; }
|
||||||
|
*/
|
||||||
|
|
@ -0,0 +1,407 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
// Taro mini-program component (WeChat / Alipay)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2. 小程序核心页面 - 转赠页
|
||||||
|
*
|
||||||
|
* 两种转赠方式入口 + 最近联系人 + 转赠记录
|
||||||
|
* 底部弹窗:输入ID / 选择券 / 确认转赠
|
||||||
|
*/
|
||||||
|
|
||||||
|
const recentRecipients = [
|
||||||
|
{ name: 'Alice', contact: 'alice@g***l.com', type: t('transfer_contact_email'), time: '3天前', expired: false },
|
||||||
|
{ name: 'Bob', contact: '138****1234', type: t('transfer_contact_phone'), time: '12天前', expired: false },
|
||||||
|
{ name: 'Charlie', contact: 'GNX-USR-7B2E', type: 'ID', time: '30天前', expired: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const transferHistory = [
|
||||||
|
{ name: '星巴克 ¥25', to: 'Alice', direction: 'out', time: '3天前' },
|
||||||
|
{ name: 'Nike ¥80', to: 'Bob', direction: 'in', time: '7天前' },
|
||||||
|
{ name: 'Walmart ¥50', to: 'Diana', direction: 'out', time: '15天前' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectableCoupons = [
|
||||||
|
{ brand: 'Starbucks', name: '星巴克 ¥25 礼品卡', price: '¥21.25' },
|
||||||
|
{ brand: 'Amazon', name: 'Amazon ¥100 购物券', price: '¥85.00' },
|
||||||
|
{ brand: 'Target', name: 'Target ¥30 折扣券', price: '¥24.00' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TransferPage: React.FC = () => {
|
||||||
|
const [showInputModal, setShowInputModal] = React.useState(false);
|
||||||
|
const [showSelectModal, setShowSelectModal] = React.useState(false);
|
||||||
|
const [showConfirmModal, setShowConfirmModal] = React.useState(false);
|
||||||
|
const [recipientInput, setRecipientInput] = React.useState('');
|
||||||
|
const [selectedCouponIndex, setSelectedCouponIndex] = React.useState(0);
|
||||||
|
|
||||||
|
const handleShareCard = () => {
|
||||||
|
// WeChat native share via Taro
|
||||||
|
Taro.showShareMenu({ withShareTicket: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputEntry = () => {
|
||||||
|
setShowInputModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaste = () => {
|
||||||
|
Taro.getClipboardData({
|
||||||
|
success: (res) => {
|
||||||
|
if (res.data) setRecipientInput(res.data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputConfirm = () => {
|
||||||
|
if (!recipientInput.trim()) return;
|
||||||
|
setShowInputModal(false);
|
||||||
|
setShowSelectModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCouponSelect = (index: number) => {
|
||||||
|
setSelectedCouponIndex(index);
|
||||||
|
setShowSelectModal(false);
|
||||||
|
setShowConfirmModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmTransfer = () => {
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
Taro.showToast({ title: t('transfer_success'), icon: 'success' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRecipientTap = (recipient: typeof recentRecipients[0]) => {
|
||||||
|
if (recipient.expired) return;
|
||||||
|
setRecipientInput(recipient.contact);
|
||||||
|
setShowSelectModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<view className="transfer-page">
|
||||||
|
{/* Entry Cards */}
|
||||||
|
<view className="entry-row">
|
||||||
|
<view className="entry-card entry-card-gradient" onClick={handleShareCard}>
|
||||||
|
<text className="entry-icon">🔗</text>
|
||||||
|
<text className="entry-title">{t('transfer_share_card')}</text>
|
||||||
|
<text className="entry-desc">{t('transfer_share_desc')}</text>
|
||||||
|
</view>
|
||||||
|
<view className="entry-card entry-card-white" onClick={handleInputEntry}>
|
||||||
|
<text className="entry-icon">✏️</text>
|
||||||
|
<text className="entry-title">{t('transfer_input')}</text>
|
||||||
|
<text className="entry-desc">{t('transfer_input_desc')}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Recent Recipients */}
|
||||||
|
<view className="section">
|
||||||
|
<view className="section-header">
|
||||||
|
<text className="section-title">{t('transfer_recent')}</text>
|
||||||
|
<text className="section-action">{t('transfer_manage')}</text>
|
||||||
|
</view>
|
||||||
|
{recentRecipients.map((r, i) => (
|
||||||
|
<view
|
||||||
|
key={i}
|
||||||
|
className={`recipient-item ${r.expired ? 'recipient-expired' : ''}`}
|
||||||
|
onClick={() => handleRecipientTap(r)}
|
||||||
|
>
|
||||||
|
<view className="recipient-avatar">
|
||||||
|
<text className="recipient-avatar-text">{r.name[0]}</text>
|
||||||
|
</view>
|
||||||
|
<view className="recipient-info">
|
||||||
|
<view className="recipient-name-row">
|
||||||
|
<text className="recipient-name">{r.name}</text>
|
||||||
|
<view className="recipient-badge">
|
||||||
|
<text className="recipient-badge-text">{r.type}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text className="recipient-contact">{r.contact}</text>
|
||||||
|
</view>
|
||||||
|
<view className="recipient-right">
|
||||||
|
{r.expired ? (
|
||||||
|
<text className="recipient-expired-text">{t('transfer_expired')}</text>
|
||||||
|
) : (
|
||||||
|
<view className="recipient-time-row">
|
||||||
|
<text className="recipient-time">{r.time}</text>
|
||||||
|
<text className="recipient-chevron">›</text>
|
||||||
|
</view>
|
||||||
|
)}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Transfer History */}
|
||||||
|
<view className="section">
|
||||||
|
<view className="section-header">
|
||||||
|
<text className="section-title">{t('transfer_history')}</text>
|
||||||
|
</view>
|
||||||
|
{transferHistory.map((item, i) => (
|
||||||
|
<view key={i} className="history-item">
|
||||||
|
<view className={`history-arrow ${item.direction === 'out' ? 'arrow-out' : 'arrow-in'}`}>
|
||||||
|
<text className="history-arrow-text">{item.direction === 'out' ? '↑' : '↓'}</text>
|
||||||
|
</view>
|
||||||
|
<view className="history-info">
|
||||||
|
<text className="history-name">{item.name}</text>
|
||||||
|
<text className="history-detail">
|
||||||
|
{item.direction === 'out' ? `→ ${item.to}` : `← ${item.to}`}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<text className="history-time">{item.time}</text>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Input Modal (Bottom Popup) */}
|
||||||
|
{showInputModal && (
|
||||||
|
<view className="modal-overlay" onClick={() => setShowInputModal(false)}>
|
||||||
|
<view className="modal-sheet" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<view className="modal-header">
|
||||||
|
<text className="modal-title">{t('transfer_input_recipient')}</text>
|
||||||
|
<text className="modal-close" onClick={() => setShowInputModal(false)}>✕</text>
|
||||||
|
</view>
|
||||||
|
<view className="modal-input-row">
|
||||||
|
<input
|
||||||
|
className="modal-input"
|
||||||
|
placeholder={t('transfer_recipient_hint')}
|
||||||
|
value={recipientInput}
|
||||||
|
onInput={(e: any) => setRecipientInput(e.detail.value)}
|
||||||
|
/>
|
||||||
|
<view className="modal-paste-btn" onClick={handlePaste}>
|
||||||
|
<text className="modal-paste-text">{t('transfer_paste')}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view className="modal-confirm-btn" onClick={handleInputConfirm}>
|
||||||
|
<text className="modal-confirm-text">{t('confirm')}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Select Coupon Modal */}
|
||||||
|
{showSelectModal && (
|
||||||
|
<view className="modal-overlay" onClick={() => setShowSelectModal(false)}>
|
||||||
|
<view className="modal-sheet modal-sheet-tall" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<view className="modal-header">
|
||||||
|
<text className="modal-title">{t('transfer_select_coupon')}</text>
|
||||||
|
<text className="modal-close" onClick={() => setShowSelectModal(false)}>✕</text>
|
||||||
|
</view>
|
||||||
|
{selectableCoupons.map((coupon, i) => (
|
||||||
|
<view key={i} className="select-coupon-item" onClick={() => handleCouponSelect(i)}>
|
||||||
|
<view className="select-coupon-icon">
|
||||||
|
<text className="select-coupon-icon-text">🎫</text>
|
||||||
|
</view>
|
||||||
|
<view className="select-coupon-info">
|
||||||
|
<text className="select-coupon-brand">{coupon.brand}</text>
|
||||||
|
<text className="select-coupon-name">{coupon.name}</text>
|
||||||
|
</view>
|
||||||
|
<text className="select-coupon-price">{coupon.price}</text>
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Confirm Modal */}
|
||||||
|
{showConfirmModal && (
|
||||||
|
<view className="modal-overlay" onClick={() => setShowConfirmModal(false)}>
|
||||||
|
<view className="modal-sheet" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<view className="modal-header">
|
||||||
|
<text className="modal-title">{t('transfer_confirm')}</text>
|
||||||
|
<text className="modal-close" onClick={() => setShowConfirmModal(false)}>✕</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view className="confirm-info">
|
||||||
|
<view className="confirm-row">
|
||||||
|
<text className="confirm-label">{t('transfer_select_coupon')}</text>
|
||||||
|
<text className="confirm-value">{selectableCoupons[selectedCouponIndex].name}</text>
|
||||||
|
</view>
|
||||||
|
<view className="confirm-row">
|
||||||
|
<text className="confirm-label">{t('transfer_to')}</text>
|
||||||
|
<text className="confirm-value">{recipientInput}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view className="confirm-warning">
|
||||||
|
<text className="confirm-warning-icon">⚠️</text>
|
||||||
|
<text className="confirm-warning-text">{t('transfer_warning')}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view className="confirm-buttons">
|
||||||
|
<view className="confirm-btn-cancel" onClick={() => setShowConfirmModal(false)}>
|
||||||
|
<text className="confirm-btn-cancel-text">{t('cancel')}</text>
|
||||||
|
</view>
|
||||||
|
<view className="confirm-btn-ok" onClick={handleConfirmTransfer}>
|
||||||
|
<text className="confirm-btn-ok-text">{t('transfer_confirm_btn')}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
)}
|
||||||
|
</view>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransferPage;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CSS (index.scss):
|
||||||
|
|
||||||
|
.transfer-page { background: #F8F9FC; min-height: 100vh; padding: 24rpx 32rpx 40rpx; }
|
||||||
|
|
||||||
|
.entry-row { display: flex; gap: 20rpx; margin-bottom: 24rpx; }
|
||||||
|
.entry-card {
|
||||||
|
flex: 1; padding: 28rpx 20rpx; border-radius: 16rpx;
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
}
|
||||||
|
.entry-card-gradient {
|
||||||
|
background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
|
||||||
|
}
|
||||||
|
.entry-card-gradient .entry-title { color: white; }
|
||||||
|
.entry-card-gradient .entry-desc { color: rgba(255,255,255,0.7); }
|
||||||
|
.entry-card-white {
|
||||||
|
background: white; border: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.entry-card-white .entry-title { color: #141723; }
|
||||||
|
.entry-card-white .entry-desc { color: #A0A8BE; }
|
||||||
|
.entry-icon { font-size: 40rpx; margin-bottom: 12rpx; }
|
||||||
|
.entry-title { font-size: 28rpx; font-weight: 600; margin-bottom: 4rpx; }
|
||||||
|
.entry-desc { font-size: 22rpx; text-align: center; }
|
||||||
|
|
||||||
|
.section { margin-bottom: 24rpx; }
|
||||||
|
.section-header {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
.section-title { font-size: 30rpx; font-weight: 600; color: #141723; }
|
||||||
|
.section-action { font-size: 24rpx; color: #6C5CE7; }
|
||||||
|
|
||||||
|
.recipient-item {
|
||||||
|
display: flex; align-items: center;
|
||||||
|
background: white; border-radius: 16rpx; padding: 20rpx;
|
||||||
|
border: 1rpx solid #F1F3F8; margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
.recipient-expired { opacity: 0.5; }
|
||||||
|
.recipient-avatar {
|
||||||
|
width: 72rpx; height: 72rpx; border-radius: 50%;
|
||||||
|
background: #F3F1FF; display: flex;
|
||||||
|
align-items: center; justify-content: center; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.recipient-avatar-text { font-size: 28rpx; font-weight: 700; color: #6C5CE7; }
|
||||||
|
.recipient-info { flex: 1; margin-left: 16rpx; }
|
||||||
|
.recipient-name-row { display: flex; align-items: center; }
|
||||||
|
.recipient-name { font-size: 28rpx; font-weight: 500; color: #141723; }
|
||||||
|
.recipient-badge {
|
||||||
|
margin-left: 8rpx; padding: 2rpx 10rpx;
|
||||||
|
background: #F3F1FF; border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
.recipient-badge-text { font-size: 20rpx; color: #6C5CE7; }
|
||||||
|
.recipient-contact { font-size: 24rpx; color: #A0A8BE; margin-top: 4rpx; }
|
||||||
|
.recipient-right { margin-left: 12rpx; flex-shrink: 0; }
|
||||||
|
.recipient-expired-text { font-size: 22rpx; color: #A0A8BE; }
|
||||||
|
.recipient-time-row { display: flex; align-items: center; }
|
||||||
|
.recipient-time { font-size: 22rpx; color: #A0A8BE; }
|
||||||
|
.recipient-chevron { font-size: 28rpx; color: #A0A8BE; margin-left: 4rpx; }
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
display: flex; align-items: center;
|
||||||
|
background: white; border-radius: 16rpx; padding: 20rpx;
|
||||||
|
border: 1rpx solid #F1F3F8; margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
.history-arrow {
|
||||||
|
width: 56rpx; height: 56rpx; border-radius: 12rpx;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
flex-shrink: 0; margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
.arrow-out { background: #FFF0E6; }
|
||||||
|
.arrow-in { background: #E6FAF3; }
|
||||||
|
.history-arrow-text { font-size: 28rpx; font-weight: 700; }
|
||||||
|
.arrow-out .history-arrow-text { color: #FF8C42; }
|
||||||
|
.arrow-in .history-arrow-text { color: #00C48C; }
|
||||||
|
.history-info { flex: 1; }
|
||||||
|
.history-name { font-size: 28rpx; font-weight: 500; color: #141723; }
|
||||||
|
.history-detail { font-size: 24rpx; color: #A0A8BE; margin-top: 4rpx; }
|
||||||
|
.history-time { font-size: 22rpx; color: #A0A8BE; flex-shrink: 0; }
|
||||||
|
|
||||||
|
/* Modal Overlay & Sheet */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.5); z-index: 1000;
|
||||||
|
display: flex; align-items: flex-end; justify-content: center;
|
||||||
|
}
|
||||||
|
.modal-sheet {
|
||||||
|
width: 100%; background: white; border-radius: 32rpx 32rpx 0 0;
|
||||||
|
padding: 32rpx; padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
.modal-sheet-tall { max-height: 70vh; }
|
||||||
|
.modal-header {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
margin-bottom: 28rpx;
|
||||||
|
}
|
||||||
|
.modal-title { font-size: 32rpx; font-weight: 600; color: #141723; }
|
||||||
|
.modal-close { font-size: 28rpx; color: #A0A8BE; padding: 8rpx; }
|
||||||
|
|
||||||
|
.modal-input-row {
|
||||||
|
display: flex; align-items: center; gap: 16rpx; margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
.modal-input {
|
||||||
|
flex: 1; height: 80rpx; padding: 0 24rpx;
|
||||||
|
background: #F1F3F8; border-radius: 12rpx;
|
||||||
|
font-size: 28rpx; color: #141723;
|
||||||
|
}
|
||||||
|
.modal-paste-btn {
|
||||||
|
padding: 0 24rpx; height: 80rpx; border-radius: 12rpx;
|
||||||
|
background: #F3F1FF; display: flex; align-items: center;
|
||||||
|
}
|
||||||
|
.modal-paste-text { font-size: 26rpx; color: #6C5CE7; font-weight: 500; }
|
||||||
|
|
||||||
|
.modal-confirm-btn {
|
||||||
|
height: 88rpx; border-radius: 16rpx;
|
||||||
|
background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.modal-confirm-text { color: white; font-size: 30rpx; font-weight: 600; }
|
||||||
|
|
||||||
|
.select-coupon-item {
|
||||||
|
display: flex; align-items: center; padding: 20rpx;
|
||||||
|
border-bottom: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.select-coupon-icon {
|
||||||
|
width: 80rpx; height: 80rpx; background: #F3F1FF;
|
||||||
|
border-radius: 12rpx; display: flex;
|
||||||
|
align-items: center; justify-content: center; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.select-coupon-icon-text { font-size: 32rpx; }
|
||||||
|
.select-coupon-info { flex: 1; margin-left: 16rpx; }
|
||||||
|
.select-coupon-brand { font-size: 22rpx; color: #A0A8BE; }
|
||||||
|
.select-coupon-name { font-size: 28rpx; font-weight: 500; color: #141723; margin-top: 4rpx; }
|
||||||
|
.select-coupon-price { font-size: 28rpx; font-weight: 700; color: #6C5CE7; flex-shrink: 0; }
|
||||||
|
|
||||||
|
.confirm-info {
|
||||||
|
background: #F8F9FC; border-radius: 12rpx; padding: 20rpx; margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.confirm-row {
|
||||||
|
display: flex; justify-content: space-between; padding: 12rpx 0;
|
||||||
|
}
|
||||||
|
.confirm-label { font-size: 26rpx; color: #5C6478; }
|
||||||
|
.confirm-value { font-size: 26rpx; color: #141723; font-weight: 500; }
|
||||||
|
|
||||||
|
.confirm-warning {
|
||||||
|
display: flex; align-items: center; padding: 16rpx 20rpx;
|
||||||
|
background: #FFF8E6; border-radius: 12rpx; margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
.confirm-warning-icon { font-size: 28rpx; margin-right: 12rpx; }
|
||||||
|
.confirm-warning-text { font-size: 24rpx; color: #B8860B; }
|
||||||
|
|
||||||
|
.confirm-buttons { display: flex; gap: 20rpx; }
|
||||||
|
.confirm-btn-cancel {
|
||||||
|
flex: 1; height: 88rpx; border-radius: 16rpx;
|
||||||
|
background: #F1F3F8; display: flex;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.confirm-btn-cancel-text { font-size: 28rpx; color: #5C6478; font-weight: 600; }
|
||||||
|
.confirm-btn-ok {
|
||||||
|
flex: 1; height: 88rpx; border-radius: 16rpx;
|
||||||
|
background: linear-gradient(135deg, #6C5CE7, #9B8FFF);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.confirm-btn-ok-text { color: white; font-size: 28rpx; font-weight: 600; }
|
||||||
|
*/
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
// Taro mini-program component
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P2. 钱包页面 (只读)
|
||||||
|
*
|
||||||
|
* 余额展示 + 交易记录,无充值/提现操作
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Transaction {
|
||||||
|
id: number;
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
amount: string;
|
||||||
|
time: string;
|
||||||
|
isPositive: boolean;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockTransactions: Transaction[] = [
|
||||||
|
{ id: 1, icon: '\uD83D\uDED2', title: '买入 Starbucks ¥25', amount: '-¥21.25', time: '14:32 today', isPositive: false, color: '#6C5CE7' },
|
||||||
|
{ id: 2, icon: '\uD83D\uDCB0', title: '卖出 Amazon ¥50', amount: '+¥42.50', time: '10:15 today', isPositive: true, color: '#00C48C' },
|
||||||
|
{ id: 3, icon: '\u2795', title: '充值', amount: '+¥500.00', time: '09:20 today', isPositive: true, color: '#3498DB' },
|
||||||
|
{ id: 4, icon: '\uD83C\uDF81', title: '转赠 Target', amount: '-¥30.00', time: '02/07', isPositive: false, color: '#FF9500' },
|
||||||
|
{ id: 5, icon: '\u2705', title: '核销 Nike', amount: '¥0', time: '02/06', isPositive: false, color: '#A0A8BE' },
|
||||||
|
{ id: 6, icon: '\uD83C\uDFE6', title: '提现', amount: '-¥200.00', time: '02/05', isPositive: false, color: '#E74C3C' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const WalletPage: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<view className="wallet-page">
|
||||||
|
{/* Balance Card */}
|
||||||
|
<view className="wallet-balance-card">
|
||||||
|
<text className="wallet-balance-label">{t('wallet_total_balance')}</text>
|
||||||
|
<text className="wallet-balance-amount">$1,234.56</text>
|
||||||
|
<view className="wallet-sub-row">
|
||||||
|
<view className="wallet-sub-item">
|
||||||
|
<text className="wallet-sub-label">{t('wallet_available')}</text>
|
||||||
|
<text className="wallet-sub-value">$1,034.56</text>
|
||||||
|
</view>
|
||||||
|
<view className="wallet-sub-divider" />
|
||||||
|
<view className="wallet-sub-item">
|
||||||
|
<text className="wallet-sub-label">{t('wallet_frozen')}</text>
|
||||||
|
<text className="wallet-sub-value">$200.00</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Notice Bar */}
|
||||||
|
<view className="wallet-notice">
|
||||||
|
<text className="wallet-notice-icon">ℹ️</text>
|
||||||
|
<text className="wallet-notice-text">{t('wallet_read_only_hint')}</text>
|
||||||
|
<text className="wallet-notice-link">{t('download_btn')}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
{/* Transaction Records */}
|
||||||
|
<view className="wallet-records-section">
|
||||||
|
<view className="wallet-records-header">
|
||||||
|
<text className="wallet-records-title">{t('wallet_records')}</text>
|
||||||
|
<text className="wallet-records-filter">{t('wallet_filter')}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view className="wallet-records-list">
|
||||||
|
{mockTransactions.map((tx, i) => (
|
||||||
|
<view key={tx.id}>
|
||||||
|
<view className="wallet-tx-item">
|
||||||
|
<view className="wallet-tx-icon" style={{ background: tx.color }}>
|
||||||
|
<text className="wallet-tx-emoji">{tx.icon}</text>
|
||||||
|
</view>
|
||||||
|
<view className="wallet-tx-info">
|
||||||
|
<text className="wallet-tx-title">{tx.title}</text>
|
||||||
|
<text className="wallet-tx-time">{tx.time}</text>
|
||||||
|
</view>
|
||||||
|
<text
|
||||||
|
className="wallet-tx-amount"
|
||||||
|
style={{ color: tx.isPositive ? '#00C48C' : '#141723' }}
|
||||||
|
>
|
||||||
|
{tx.amount}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
{i < mockTransactions.length - 1 && <view className="wallet-tx-divider" />}
|
||||||
|
</view>
|
||||||
|
))}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WalletPage;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CSS:
|
||||||
|
|
||||||
|
.wallet-page { background: #F8F9FC; min-height: 100vh; padding-bottom: 60rpx; }
|
||||||
|
|
||||||
|
.wallet-balance-card {
|
||||||
|
margin: 32rpx; padding: 40rpx 32rpx;
|
||||||
|
background: linear-gradient(135deg, #6C5CE7, #4834D4);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
}
|
||||||
|
.wallet-balance-label { font-size: 26rpx; color: rgba(255,255,255,0.7); }
|
||||||
|
.wallet-balance-amount {
|
||||||
|
display: block; font-size: 56rpx; font-weight: 700; color: white;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
.wallet-sub-row {
|
||||||
|
display: flex; align-items: center; margin-top: 28rpx;
|
||||||
|
padding-top: 24rpx; border-top: 1rpx solid rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
.wallet-sub-item { flex: 1; }
|
||||||
|
.wallet-sub-label { font-size: 22rpx; color: rgba(255,255,255,0.6); }
|
||||||
|
.wallet-sub-value { display: block; font-size: 30rpx; font-weight: 600; color: white; margin-top: 4rpx; }
|
||||||
|
.wallet-sub-divider {
|
||||||
|
width: 1rpx; height: 48rpx; background: rgba(255,255,255,0.2);
|
||||||
|
margin: 0 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-notice {
|
||||||
|
display: flex; align-items: center; margin: 0 32rpx 24rpx;
|
||||||
|
padding: 16rpx 20rpx; background: #FFF9E6;
|
||||||
|
border-radius: 12rpx; border: 1rpx solid #FFE8A3;
|
||||||
|
}
|
||||||
|
.wallet-notice-icon { font-size: 28rpx; margin-right: 10rpx; }
|
||||||
|
.wallet-notice-text { flex: 1; font-size: 24rpx; color: #8B6914; }
|
||||||
|
.wallet-notice-link { font-size: 24rpx; color: #6C5CE7; font-weight: 500; }
|
||||||
|
|
||||||
|
.wallet-records-section {
|
||||||
|
margin: 0 32rpx; background: white; border-radius: 20rpx;
|
||||||
|
overflow: hidden; border: 1rpx solid #F1F3F8;
|
||||||
|
}
|
||||||
|
.wallet-records-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 24rpx 24rpx 16rpx;
|
||||||
|
}
|
||||||
|
.wallet-records-title { font-size: 30rpx; font-weight: 600; color: #141723; }
|
||||||
|
.wallet-records-filter { font-size: 24rpx; color: #6C5CE7; }
|
||||||
|
|
||||||
|
.wallet-records-list { padding: 0 24rpx; }
|
||||||
|
.wallet-tx-item {
|
||||||
|
display: flex; align-items: center; padding: 20rpx 0;
|
||||||
|
}
|
||||||
|
.wallet-tx-icon {
|
||||||
|
width: 64rpx; height: 64rpx; border-radius: 50%;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.wallet-tx-emoji { font-size: 28rpx; }
|
||||||
|
.wallet-tx-info { flex: 1; margin-left: 18rpx; }
|
||||||
|
.wallet-tx-title { font-size: 28rpx; color: #141723; font-weight: 500; }
|
||||||
|
.wallet-tx-time { display: block; font-size: 22rpx; color: #A0A8BE; margin-top: 4rpx; }
|
||||||
|
.wallet-tx-amount { font-size: 30rpx; font-weight: 600; flex-shrink: 0; }
|
||||||
|
.wallet-tx-divider { height: 1rpx; background: #F1F3F8; margin-left: 82rpx; }
|
||||||
|
*/
|
||||||
Loading…
Reference in New Issue