zxz před 3 měsíci
rodič
revize
6187a63f64

+ 6 - 1
.gitignore

@@ -52,4 +52,9 @@ yarn-error.log*
 
 # --- 编辑器配置 ---
 .vscode/
-.idea/
+.idea/
+# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
+# The following patterns were generated by expo-cli
+
+expo-env.d.ts
+# @end expo-cli

+ 365 - 0
.kiro/specs/miniapp-to-rn-migration/design.md

@@ -0,0 +1,365 @@
+# Design Document
+
+## Overview
+
+本设计文档描述将 supermart-mini 微信小程序迁移到 React Native/Expo iOS 应用的技术方案。迁移遵循以下原则:
+1. 保持与小程序一致的视觉样式和交互体验
+2. 复用小程序的 CDN 图片资源(通过 Images 常量配置)
+3. 对接相同的后端 API 接口
+4. 使用 React Native 的 ImageBackground 组件实现背景图效果
+
+## Architecture
+
+```mermaid
+graph TB
+    subgraph "React Native App"
+        subgraph "Pages (app/)"
+            Tabs["Tab Pages<br/>(tabs)"]
+            Login["Login Page"]
+            Address["Address Pages"]
+            Orders["Order Pages"]
+            Product["Product Pages"]
+        end
+        
+        subgraph "Components"
+            Home["Home Components"]
+            ProductComp["Product Components"]
+            Common["Common Components"]
+        end
+        
+        subgraph "Services"
+            UserService["user.ts"]
+            MallService["mall.ts"]
+            AwardService["award.ts"]
+            AddressService["address.ts"]
+            BaseService["base.ts"]
+        end
+        
+        subgraph "Constants"
+            Images["images.ts"]
+            Theme["theme.ts"]
+        end
+        
+        subgraph "Contexts"
+            AuthContext["AuthContext"]
+        end
+    end
+    
+    subgraph "Backend API"
+        API["REST API Server"]
+    end
+    
+    Tabs --> Services
+    Login --> UserService
+    Address --> AddressService
+    Orders --> MallService
+    Product --> MallService
+    Services --> API
+    Pages --> Images
+    Pages --> Components
+```
+
+## Components and Interfaces
+
+### 1. 页面组件结构
+
+#### Login Page (`app/login.tsx`)
+```typescript
+interface LoginScreenProps {}
+
+// 状态
+- phone: string           // 手机号
+- verifyCode: string      // 验证码
+- agreeFlag: boolean      // 协议同意状态
+- loading: boolean        // 登录加载状态
+- countdown: number       // 验证码倒计时
+
+// 方法
+- handleGetVerifyCode()   // 获取验证码
+- handleLogin()           // 登录
+- goBack()               // 返回
+```
+
+#### Mine Page (`app/(tabs)/mine.tsx`)
+```typescript
+interface MineScreenProps {}
+
+interface IndexData {
+  couponCount?: number;
+  inventoryCount?: number;
+  magicBalance?: number;
+  treasureBoxCount?: number;
+}
+
+// 状态
+- userInfo: UserInfo | null
+- indexData: IndexData | null
+
+// 方法
+- loadData()              // 加载用户数据
+- handleLogin()           // 跳转登录
+- handleCopy(text)        // 复制文本
+- handleMenuPress(route)  // 菜单点击
+- showNumber(key)         // 格式化数字显示
+```
+
+#### Address List Page (`app/address/index.tsx`)
+```typescript
+interface Address {
+  id: string;
+  contactName: string;
+  contactNo: string;
+  province: string;
+  city: string;
+  district: string;
+  address: string;
+  defaultFlag: number;
+}
+
+// 方法
+- loadData()              // 加载地址列表
+- selectAddress(item)     // 选择地址
+- setDefault(item)        // 设为默认
+- handleDelete(item)      // 删除地址
+- editAddress(item)       // 编辑地址
+- addNewAddress()         // 新增地址
+```
+
+#### Orders Page (`app/orders/index.tsx`)
+```typescript
+interface OrderItem {
+  tradeNo: string;
+  status: number;
+  statusText: string;
+  goodsName: string;
+  goodsCover: string;
+  quantity: number;
+  paymentAmount: number;
+  createTime: string;
+}
+
+// Tab 配置
+const tabs = [
+  { label: '全部', value: undefined },
+  { label: '待付款', value: 1 },
+  { label: '待发货', value: 2 },
+  { label: '待收货', value: 3 },
+  { label: '已完成', value: 4 },
+];
+```
+
+### 2. 服务层接口
+
+#### Address Service (`services/address.ts`)
+```typescript
+// API 端点
+const apis = {
+  LIST: '/api/addressBook',
+  ADD: '/api/addressBook/add',
+  DELETE: '/api/addressBook/delete/{id}',
+  DEFAULT: '/api/addressBook/getDefault',
+  UPDATE: '/api/addressBook/update',
+  GET_AREA: '/api/area',
+};
+
+// 方法
+export const getAddressList: () => Promise<Address[]>
+export const getDefaultAddress: () => Promise<Address | null>
+export const addAddress: (params) => Promise<boolean>
+export const updateAddress: (params) => Promise<boolean>
+export const deleteAddress: (id) => Promise<boolean>
+export const getArea: (pid) => Promise<AreaItem[]>
+```
+
+#### Award Service (`services/award.ts`)
+```typescript
+// 新增方法
+export const getMagicIndex: () => Promise<IndexData>
+export const getPoolList: (params) => Promise<PoolListResponse>
+export const getPoolDetail: (poolId) => Promise<PoolDetail>
+export const getIPList: () => Promise<IPItem[]>
+```
+
+## Data Models
+
+### User Info
+```typescript
+interface UserInfo {
+  id: string;
+  nickname: string;
+  avatar: string;
+  phone?: string;
+  realName?: string;
+  idNum?: string;
+  balance?: number;
+}
+```
+
+### Address
+```typescript
+interface Address {
+  id: string;
+  contactName: string;
+  contactNo: string;
+  location?: string;
+  province: string;
+  city: string;
+  district: string;
+  address: string;
+  defaultFlag: number;  // 0: 非默认, 1: 默认
+}
+```
+
+### Order
+```typescript
+interface OrderItem {
+  tradeNo: string;
+  status: number;
+  statusText: string;
+  goodsName: string;
+  goodsCover: string;
+  quantity: number;
+  paymentAmount: number;
+  createTime: string;
+}
+
+interface OrderDetail extends OrderItem {
+  address?: {
+    contactName: string;
+    contactNo: string;
+    province: string;
+    city: string;
+    district: string;
+    address: string;
+  };
+  expressInfo?: ExpressInfo[];
+}
+```
+
+### Pool (奖池)
+```typescript
+interface PoolItem {
+  id: string;
+  name: string;
+  cover: string;
+  price: number;
+  mode: string;
+}
+
+interface PoolDetail extends PoolItem {
+  description?: string;
+  prizes?: PrizeItem[];
+  boxCount?: number;
+}
+```
+
+## Correctness Properties
+
+*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
+
+Property 1: Login API call with correct parameters
+*For any* valid phone number and verify code combination, when the login function is called, it SHALL send a request with loginWay, mobile, and verifycode parameters to the login API endpoint.
+**Validates: Requirements 1.3**
+
+Property 2: Navigation after login success
+*For any* successful login response, the system SHALL navigate back if needInfo is false, or navigate to user-info page if needInfo is true.
+**Validates: Requirements 1.4**
+
+Property 3: User profile data display
+*For any* user info object with avatar, nickname, id, and phone fields, the mine page SHALL render all these fields in the appropriate UI elements.
+**Validates: Requirements 2.2**
+
+Property 4: Statistics data display
+*For any* index data object, the mine page SHALL display couponCount, inventoryCount, magicBalance, and treasureBoxCount values.
+**Validates: Requirements 2.3**
+
+Property 5: Function entry navigation
+*For any* function entry tap with a valid route, the system SHALL trigger navigation to that route.
+**Validates: Requirements 2.4, 2.5**
+
+Property 6: Address list rendering
+*For any* list of addresses, the address list page SHALL render each address with contactName, contactNo, and full address (province + city + district + address).
+**Validates: Requirements 3.2**
+
+Property 7: Address save API call
+*For any* valid address form submission, the system SHALL call the add or update API and navigate back on success.
+**Validates: Requirements 3.5**
+
+Property 8: Order item rendering
+*For any* order item, the orders page SHALL display tradeNo, statusText, goodsCover, goodsName, paymentAmount, quantity, and createTime.
+**Validates: Requirements 4.2**
+
+Property 9: Order navigation
+*For any* order item tap, the system SHALL navigate to the order detail page with the correct tradeNo.
+**Validates: Requirements 4.3**
+
+Property 10: Order action handling
+*For any* order with status 1 (待付款), the pay action SHALL be available; for status 3 (待收货), the confirm receipt action SHALL be available.
+**Validates: Requirements 4.4**
+
+Property 11: Welfare room navigation
+*For any* room entry tap with type 0, 1, or 2, the system SHALL navigate to the corresponding welfare page (room, catchDoll, or wish).
+**Validates: Requirements 6.3**
+
+Property 12: Image constants consistency
+*For any* image key used in the app, the Images constant SHALL provide a valid CDN URL matching the ossurl configuration.
+**Validates: Requirements 8.1, 8.2**
+
+Property 13: API service function mapping
+*For any* service function call, the request SHALL be sent to the correct API endpoint matching the mini-program service configuration.
+**Validates: Requirements 9.1**
+
+Property 14: API error handling
+*For any* failed API call, the system SHALL catch the error and not crash, optionally displaying an error message.
+**Validates: Requirements 9.2**
+
+Property 15: Authentication token inclusion
+*For any* authenticated API request, the HTTP headers SHALL include the authorization token.
+**Validates: Requirements 9.3**
+
+## Error Handling
+
+### API 错误处理
+```typescript
+// HTTP 请求封装中的错误处理
+try {
+  const response = await fetch(url, options);
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  }
+  const data = await response.json();
+  if (data.code === '401') {
+    // Token 过期,跳转登录
+    router.push('/login');
+    return { success: false, code: '401' };
+  }
+  return data;
+} catch (error) {
+  console.error('API Error:', error);
+  return { success: false, error };
+}
+```
+
+### 页面错误处理
+- 加载失败时显示重试按钮
+- 网络错误时显示提示信息
+- 表单验证失败时显示具体错误
+
+## Testing Strategy
+
+### 单元测试
+- 使用 Jest 进行单元测试
+- 测试服务层函数的参数处理和返回值
+- 测试工具函数(如数字格式化)
+
+### 属性测试
+- 使用 fast-check 进行属性测试
+- 每个属性测试运行至少 100 次迭代
+- 测试标注格式:`**Feature: miniapp-to-rn-migration, Property {number}: {property_text}**`
+
+### 测试覆盖范围
+1. 服务层 API 调用测试
+2. 页面组件渲染测试
+3. 导航逻辑测试
+4. 表单验证测试
+5. 错误处理测试

+ 112 - 0
.kiro/specs/miniapp-to-rn-migration/requirements.md

@@ -0,0 +1,112 @@
+# Requirements Document
+
+## Introduction
+
+本项目旨在将 supermart-mini 微信小程序(基于 UniApp/Vue)完整迁移到当前的 React Native/Expo iOS 应用。迁移需保持原有的页面样式比例、业务逻辑和接口调用,同时适配 React Native 的组件和样式系统。已完成的首页可作为参考模板。
+
+## Glossary
+
+- **supermart-mini**: 原微信小程序项目,基于 UniApp + Vue 开发
+- **React Native App**: 目标 iOS 应用,基于 Expo + React Native + TypeScript
+- **ossurl**: 小程序中的 CDN 图片资源配置文件,对应 React Native 中的 `constants/images.ts`
+- **ImageBackground**: React Native 中用于设置背景图的组件,对应小程序中的 `:style="{ backgroundImage: 'url(...)' }"`
+- **Service**: API 服务层,负责与后端接口通信
+
+## Requirements
+
+### Requirement 1: 登录页面完善
+
+**User Story:** As a user, I want to use the login page with the same visual style as the mini-program, so that I can have a consistent user experience.
+
+#### Acceptance Criteria
+
+1. WHEN the login page loads THEN the System SHALL display the background image using ImageBackground component with the loginBg image from Images constants
+2. WHEN the user views the login button THEN the System SHALL display the button with loginBug background image matching the mini-program style
+3. WHEN the user taps the login button THEN the System SHALL call the login API with mobile and verifycode parameters
+4. WHEN login succeeds THEN the System SHALL navigate back to the previous screen or to user-info page if needInfo is true
+
+### Requirement 2: 个人中心页面完善
+
+**User Story:** As a user, I want to access my personal center with all features from the mini-program, so that I can manage my account and view my data.
+
+#### Acceptance Criteria
+
+1. WHEN the mine page loads THEN the System SHALL display the page with kaixinMineBg background image and kaixinMineHeadBg header background
+2. WHEN the user views their profile THEN the System SHALL display avatar, nickname, user ID with copy functionality, and phone number
+3. WHEN the user views the data statistics section THEN the System SHALL display couponCount, inventoryCount, magicBalance, and treasureBoxCount with kaixinUserDataBg background
+4. WHEN the user taps a function entry THEN the System SHALL navigate to the corresponding page (invite, integral, message, orders)
+5. WHEN the user taps order entries THEN the System SHALL navigate to order list with the appropriate tab filter
+
+### Requirement 3: 地址管理页面完善
+
+**User Story:** As a user, I want to manage my shipping addresses with the same interface as the mini-program, so that I can easily add, edit, and delete addresses.
+
+#### Acceptance Criteria
+
+1. WHEN the address list page loads THEN the System SHALL display the page with kaixinMineBg background image
+2. WHEN the user views the address list THEN the System SHALL display all addresses with contact name, phone, and full address details
+3. WHEN the user taps the add button THEN the System SHALL display the button with loginBug background image and navigate to address edit page
+4. WHEN the user edits an address THEN the System SHALL display the form with itemBg background for input fields
+5. WHEN the user saves an address THEN the System SHALL call the address API and return to the address list
+
+### Requirement 4: 订单列表页面完善
+
+**User Story:** As a user, I want to view my orders with the same layout as the mini-program, so that I can track my purchases.
+
+#### Acceptance Criteria
+
+1. WHEN the orders page loads THEN the System SHALL display tabs for different order statuses (全部, 待付款, 待发货, 待收货, 已完成)
+2. WHEN the user views an order item THEN the System SHALL display order number, status, goods image, name, price, quantity, and create time
+3. WHEN the user taps an order THEN the System SHALL navigate to the order detail page
+4. WHEN the user taps action buttons THEN the System SHALL perform the corresponding action (pay, confirm receipt)
+
+### Requirement 5: 订单详情页面完善
+
+**User Story:** As a user, I want to view order details with complete information, so that I can track my order status and shipping.
+
+#### Acceptance Criteria
+
+1. WHEN the order detail page loads THEN the System SHALL display order status, goods information, and address details
+2. WHEN the order has shipping info THEN the System SHALL display logistics tracking information
+3. WHEN the user taps action buttons THEN the System SHALL perform the corresponding action based on order status
+
+### Requirement 6: 福利页面完善
+
+**User Story:** As a user, I want to access welfare features with the same visual style, so that I can participate in welfare activities.
+
+#### Acceptance Criteria
+
+1. WHEN the welfare page loads THEN the System SHALL display the page with kaixinWelfareBg background image
+2. WHEN the user views room entries THEN the System SHALL display three room options (房间, 扭蛋机, 祈愿) with kaixinRoomBg background
+3. WHEN the user taps a room entry THEN the System SHALL navigate to the corresponding welfare activity page
+
+### Requirement 7: 商品详情页面完善
+
+**User Story:** As a user, I want to view product details with the same layout as the mini-program, so that I can make informed purchase decisions.
+
+#### Acceptance Criteria
+
+1. WHEN the product detail page loads THEN the System SHALL display product images, name, price, and description
+2. WHEN the user views recommended products THEN the System SHALL display related products list
+3. WHEN the user taps the purchase button THEN the System SHALL show the checkout modal with address selection and payment options
+
+### Requirement 8: 图片资源配置完善
+
+**User Story:** As a developer, I want all CDN image resources properly configured, so that all pages can display images correctly.
+
+#### Acceptance Criteria
+
+1. WHEN any page requires an image THEN the System SHALL load the image from the Images constants matching the ossurl configuration
+2. WHEN a background image is needed THEN the System SHALL use ImageBackground component with the correct image URI
+3. WHEN new images are required THEN the System SHALL add them to constants/images.ts following the existing pattern
+
+### Requirement 9: API 服务层完善
+
+**User Story:** As a developer, I want all API services properly implemented, so that all features can communicate with the backend.
+
+#### Acceptance Criteria
+
+1. WHEN a page needs data THEN the System SHALL call the corresponding service function matching the mini-program API
+2. WHEN an API call fails THEN the System SHALL handle the error gracefully and display appropriate feedback
+3. WHEN authentication is required THEN the System SHALL include the token in request headers
+

+ 128 - 0
.kiro/specs/miniapp-to-rn-migration/tasks.md

@@ -0,0 +1,128 @@
+# Implementation Plan
+
+- [x] 1. 完善图片资源配置
+  - [x] 1.1 扩展 constants/images.ts 添加缺失的图片资源
+    - 添加 common 分类下的 itemBg, windowBg, closeBut 等图片
+    - 添加 mine 分类下的缺失图片
+    - 添加 address, order 相关图片
+    - _Requirements: 8.1, 8.2_
+
+- [ ] 2. 完善地址服务层
+  - [x] 2.1 创建/更新 services/address.ts
+    - 实现 getAddressList, getDefaultAddress, addAddress, updateAddress, deleteAddress, getArea 方法
+    - 对接小程序的 API 端点
+    - _Requirements: 9.1, 9.3_
+  - [ ]* 2.2 Write property test for address service API calls
+    - **Property 13: API service function mapping**
+    - **Validates: Requirements 9.1**
+
+- [x] 3. 完善登录页面样式
+  - [x] 3.1 更新 app/login.tsx 使用小程序样式
+    - 使用 ImageBackground 设置 loginBg 背景
+    - 登录按钮使用 loginBug 背景图
+    - 调整表单样式匹配小程序
+    - _Requirements: 1.1, 1.2, 1.3, 1.4_
+  - [ ]* 3.2 Write property test for login navigation
+    - **Property 2: Navigation after login success**
+    - **Validates: Requirements 1.4**
+
+- [x] 4. 完善个人中心页面
+  - [x] 4.1 更新 app/(tabs)/mine.tsx 完善样式和功能
+    - 使用 kaixinMineBg 和 kaixinMineHeadBg 背景图
+    - 完善用户信息展示区域
+    - 完善数据统计区域使用 kaixinUserDataBg 背景
+    - 完善功能入口区域使用 userSection1Bg 背景
+    - 完善订单入口区域使用 userSection2Bg 背景
+    - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5_
+  - [ ]* 4.2 Write property test for user profile display
+    - **Property 3: User profile data display**
+    - **Validates: Requirements 2.2**
+  - [ ]* 4.3 Write property test for statistics display
+    - **Property 4: Statistics data display**
+    - **Validates: Requirements 2.3**
+
+- [x] 5. 完善地址列表页面
+  - [x] 5.1 更新 app/address/index.tsx 使用小程序样式
+    - 使用 ImageBackground 设置 kaixinMineBg 背景
+    - 添加新地址按钮使用 loginBug 背景图
+    - 调整地址卡片样式
+    - _Requirements: 3.1, 3.2, 3.3_
+  - [ ]* 5.2 Write property test for address list rendering
+    - **Property 6: Address list rendering**
+    - **Validates: Requirements 3.2**
+
+- [x] 6. 完善地址编辑页面
+  - [x] 6.1 更新 app/address/edit.tsx 使用小程序样式
+    - 使用 ImageBackground 设置 kaixinMineBg 背景
+    - 表单区域使用 itemBg 背景
+    - 保存按钮使用 loginBug 背景图
+    - 添加省市区三级联动选择器
+    - _Requirements: 3.4, 3.5_
+  - [ ]* 6.2 Write property test for address save
+    - **Property 7: Address save API call**
+    - **Validates: Requirements 3.5**
+
+- [ ] 7. Checkpoint - 确保所有测试通过
+  - Ensure all tests pass, ask the user if questions arise.
+
+- [x] 8. 完善订单列表页面
+  - [x] 8.1 更新 app/orders/index.tsx 完善样式和功能
+    - 完善 Tab 栏样式
+    - 完善订单卡片样式
+    - 实现订单操作按钮(支付、确认收货)
+    - _Requirements: 4.1, 4.2, 4.3, 4.4_
+  - [ ]* 8.2 Write property test for order item rendering
+    - **Property 8: Order item rendering**
+    - **Validates: Requirements 4.2**
+  - [ ]* 8.3 Write property test for order action handling
+    - **Property 10: Order action handling**
+    - **Validates: Requirements 4.4**
+
+- [x] 9. 完善订单详情页面
+  - [x] 9.1 更新 app/orders/[tradeNo].tsx 完善功能
+    - 显示订单状态和商品信息
+    - 显示收货地址信息
+    - 显示物流信息(如有)
+    - 实现订单操作按钮
+    - _Requirements: 5.1, 5.2, 5.3_
+
+- [x] 10. 完善福利页面
+  - [x] 10.1 更新 app/(tabs)/welfare.tsx 完善样式
+    - 确保使用 kaixinWelfareBg 背景
+    - 确保房间列表使用 kaixinRoomBg 背景
+    - 实现房间点击跳转逻辑
+    - _Requirements: 6.1, 6.2, 6.3_
+  - [ ]* 10.2 Write property test for welfare room navigation
+    - **Property 11: Welfare room navigation**
+    - **Validates: Requirements 6.3**
+
+- [x] 11. 完善商品详情页面
+  - [x] 11.1 更新 app/product/[id].tsx 完善功能
+    - 完善商品图片轮播
+    - 完善商品信息展示
+    - 完善推荐商品列表
+    - 完善购买按钮和结算弹窗
+    - _Requirements: 7.1, 7.2, 7.3_
+
+- [x] 12. 完善 Award 服务层
+  - [x] 12.1 更新 services/award.ts 添加缺失方法
+    - 确保 getMagicIndex 方法正确实现
+    - 确保 getPoolList 方法正确实现
+    - 确保 getIPList 方法正确实现
+    - _Requirements: 9.1_
+
+- [x] 13. 完善 HTTP 请求层
+  - [x] 13.1 更新 services/http.ts 完善错误处理
+    - 实现 401 状态码处理(跳转登录)
+    - 实现通用错误处理
+    - 确保 token 正确添加到请求头
+    - _Requirements: 9.2, 9.3_
+  - [ ]* 13.2 Write property test for authentication token
+    - **Property 15: Authentication token inclusion**
+    - **Validates: Requirements 9.3**
+  - [ ]* 13.3 Write property test for error handling
+    - **Property 14: API error handling**
+    - **Validates: Requirements 9.2**
+
+- [ ] 14. Final Checkpoint - 确保所有测试通过
+  - Ensure all tests pass, ask the user if questions arise.

+ 22 - 26
app/(tabs)/_layout.tsx

@@ -1,35 +1,31 @@
 import { Tabs } from 'expo-router';
 import React from 'react';
+import { StyleSheet } from 'react-native';
 
-import { HapticTab } from '@/components/haptic-tab';
-import { IconSymbol } from '@/components/ui/icon-symbol';
-import { Colors } from '@/constants/theme';
-import { useColorScheme } from '@/hooks/use-color-scheme';
+import { CustomTabBar } from '@/components/CustomTabBar';
 
 export default function TabLayout() {
-  const colorScheme = useColorScheme();
-
   return (
-    <Tabs
-      screenOptions={{
-        tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
-        headerShown: false,
-        tabBarButton: HapticTab,
-      }}>
-      <Tabs.Screen
-        name="index"
-        options={{
-          title: 'Home',
-          tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
-        }}
-      />
-      <Tabs.Screen
-        name="explore"
-        options={{
-          title: 'Explore',
-          tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
+    <>
+      <Tabs
+        screenOptions={{
+          headerShown: false,
+          tabBarStyle: styles.hiddenTabBar,
         }}
-      />
-    </Tabs>
+        tabBar={() => null}
+      >
+        <Tabs.Screen name="index" />
+        <Tabs.Screen name="box" />
+        <Tabs.Screen name="welfare" />
+        <Tabs.Screen name="mine" />
+      </Tabs>
+      <CustomTabBar />
+    </>
   );
 }
+
+const styles = StyleSheet.create({
+  hiddenTabBar: {
+    display: 'none',
+  },
+});

+ 446 - 0
app/(tabs)/box.tsx

@@ -0,0 +1,446 @@
+import { Image } from 'expo-image';
+import { useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    FlatList,
+    ImageBackground,
+    RefreshControl,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TextInput,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { getFeedbackList, getPoolList, PoolItem } from '@/services/award';
+
+const typeList = [
+  { label: '全部', value: '', type: 0, img: Images.box.type1, imgOn: Images.box.type1On },
+  { label: '高保赏', value: 'UNLIMITED', type: 2, img: Images.box.type3, imgOn: Images.box.type3On },
+  { label: '高爆赏', value: 'UNLIMITED', type: 1, img: Images.box.type2, imgOn: Images.box.type2On },
+  { label: '擂台赏', value: 'YFS_PRO', type: 7, img: Images.box.type5, imgOn: Images.box.type5On },
+  { label: '一番赏', value: 'YFS_PRO', type: 0, img: Images.box.type4, imgOn: Images.box.type4On },
+];
+
+interface BarrageItem {
+  id: string;
+  content: string;
+  nickname?: string;
+}
+
+export default function BoxScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+
+  const [keyword, setKeyword] = useState('');
+  const [typeIndex, setTypeIndex] = useState(0);
+  const [priceSort, setPriceSort] = useState(0);
+  const [list, setList] = useState<PoolItem[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [refreshing, setRefreshing] = useState(false);
+  const [current, setCurrent] = useState(1);
+  const [total, setTotal] = useState(0);
+  const [hasMore, setHasMore] = useState(true);
+  const [barrageList, setBarrageList] = useState<BarrageItem[]>([]);
+
+  // 加载弹幕
+  const loadBarrage = useCallback(async () => {
+    try {
+      const res = await getFeedbackList();
+      if (res.data) {
+        setBarrageList(res.data);
+      }
+    } catch (error) {
+      console.error('加载弹幕失败:', error);
+    }
+  }, []);
+
+  const loadData = useCallback(async (isRefresh = false) => {
+    if (loading) return;
+    
+    const page = isRefresh ? 1 : current;
+    if (!isRefresh && !hasMore) return;
+
+    setLoading(true);
+    try {
+      const selectedType = typeList[typeIndex];
+      const res = await getPoolList({
+        current: page,
+        size: 10,
+        mode: selectedType.value || undefined,
+        type: selectedType.type,
+        keyword: keyword || undefined,
+        priceSort: priceSort || undefined,
+      });
+
+      if (res.success && res.data) {
+        const newList = isRefresh ? res.data : [...list, ...res.data];
+        setList(newList);
+        setTotal(res.count || 0);
+        setCurrent(page + 1);
+        setHasMore(newList.length < (res.count || 0));
+      }
+    } catch (error) {
+      console.error('加载奖池列表失败:', error);
+    }
+    setLoading(false);
+    setRefreshing(false);
+  }, [current, hasMore, loading, typeIndex, list, keyword, priceSort]);
+
+  useEffect(() => {
+    loadData(true);
+    loadBarrage();
+  }, [typeIndex, priceSort]);
+
+  const handleRefresh = () => {
+    setRefreshing(true);
+    loadData(true);
+  };
+
+  const handleLoadMore = () => {
+    if (!loading && hasMore) {
+      loadData(false);
+    }
+  };
+
+  const handleTypeChange = (index: number) => {
+    setTypeIndex(index);
+    setList([]);
+    setCurrent(1);
+    setHasMore(true);
+  };
+
+  const handlePriceSort = () => {
+    setPriceSort((prev) => (prev + 1) % 3);
+  };
+
+  const handleItemPress = (item: PoolItem) => {
+    // 根据类型跳转到不同页面
+    if (item.type === 7) {
+      // 擂台赏跳转到 boxInBox 页面
+      router.push(`/boxInBox?poolId=${item.id}` as any);
+    } else if (item.mode === 'UNLIMITED') {
+      router.push(`/award-detail?poolId=${item.id}` as any);
+    } else if (item.mode === 'YFS_PRO') {
+      router.push(`/award-detail-yfs?poolId=${item.id}` as any);
+    } else {
+      router.push(`/product/${item.id}` as any);
+    }
+  };
+
+  const renderItem = ({ item }: { item: PoolItem }) => (
+    <TouchableOpacity
+      style={styles.itemContainer}
+      onPress={() => handleItemPress(item)}
+      activeOpacity={0.8}
+    >
+      <ImageBackground
+        source={{ uri: Images.box.goodsItemBg }}
+        style={styles.itemBg}
+        resizeMode="stretch"
+      >
+        <Image
+          source={{ uri: item.cover }}
+          style={styles.itemImage}
+          contentFit="cover"
+        />
+        <View style={styles.itemInfo}>
+          <Text style={styles.itemName} numberOfLines={1}>{item.name}</Text>
+          <Text style={styles.itemPrice}>
+            <Text style={styles.priceUnit}>¥</Text>
+            {item.price}起
+          </Text>
+        </View>
+      </ImageBackground>
+    </TouchableOpacity>
+  );
+
+  const renderHeader = () => (
+    <View>
+      {/* 顶部主图 */}
+      <Image
+        source={{ uri: Images.box.awardMainImg }}
+        style={styles.mainImage}
+        contentFit="contain"
+      />
+
+      {/* 弹幕区域 */}
+      {barrageList.length > 0 && (
+        <View style={styles.barrageSection}>
+          <View style={styles.barrageRow}>
+            {barrageList.slice(0, 3).map((item, index) => (
+              <View key={item.id || index} style={styles.barrageItem}>
+                <Text style={styles.barrageText} numberOfLines={1}>{item.content}</Text>
+              </View>
+            ))}
+          </View>
+          <View style={[styles.barrageRow, { marginLeft: 25 }]}>
+            {barrageList.slice(3, 6).map((item, index) => (
+              <View key={item.id || index} style={styles.barrageItem}>
+                <Text style={styles.barrageText} numberOfLines={1}>{item.content}</Text>
+              </View>
+            ))}
+          </View>
+        </View>
+      )}
+
+      {/* 分类筛选 */}
+      <View style={styles.typeSection}>
+        <View style={styles.typeList}>
+          {typeList.map((item, index) => (
+            <TouchableOpacity
+              key={index}
+              style={styles.typeItem}
+              onPress={() => handleTypeChange(index)}
+            >
+              <Image
+                source={{ uri: typeIndex === index ? item.imgOn : item.img }}
+                style={styles.typeImage}
+                contentFit="contain"
+              />
+            </TouchableOpacity>
+          ))}
+        </View>
+        <TouchableOpacity style={styles.sortBtn} onPress={handlePriceSort}>
+          <Image
+            source={{
+              uri: priceSort === 0
+                ? Images.box.sortAmount
+                : priceSort === 1
+                ? Images.box.sortAmountOnT
+                : Images.box.sortAmountOnB,
+            }}
+            style={styles.sortIcon}
+            contentFit="contain"
+          />
+        </TouchableOpacity>
+      </View>
+    </View>
+  );
+
+  const renderFooter = () => {
+    if (!loading) return null;
+    return (
+      <View style={styles.footer}>
+        <ActivityIndicator size="small" color="#fff" />
+        <Text style={styles.footerText}>加载中...</Text>
+      </View>
+    );
+  };
+
+  const renderEmpty = () => {
+    if (loading) return null;
+    return (
+      <View style={styles.empty}>
+        <Text style={styles.emptyText}>暂无数据</Text>
+      </View>
+    );
+  };
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground
+        source={{ uri: Images.common.awardBg }}
+        style={styles.background}
+        resizeMode="cover"
+      >
+        {/* 顶部搜索栏 */}
+        <View style={[styles.header, { paddingTop: insets.top + 10 }]}>
+          <Image
+            source={{ uri: Images.home.portrait }}
+            style={styles.logo}
+            contentFit="contain"
+          />
+          <View style={styles.searchBar}>
+            <Image
+              source={{ uri: Images.home.search }}
+              style={styles.searchIcon}
+              contentFit="contain"
+            />
+            <TextInput
+              style={styles.searchInput}
+              value={keyword}
+              onChangeText={setKeyword}
+              placeholder="搜索"
+              placeholderTextColor="rgba(255,255,255,0.5)"
+              returnKeyType="search"
+              onSubmitEditing={() => loadData(true)}
+            />
+          </View>
+        </View>
+
+        {/* 列表 */}
+        <FlatList
+          data={list}
+          renderItem={renderItem}
+          keyExtractor={(item) => item.id}
+          ListHeaderComponent={renderHeader}
+          ListFooterComponent={renderFooter}
+          ListEmptyComponent={renderEmpty}
+          contentContainerStyle={styles.listContent}
+          showsVerticalScrollIndicator={false}
+          refreshControl={
+            <RefreshControl
+              refreshing={refreshing}
+              onRefresh={handleRefresh}
+              tintColor="#fff"
+            />
+          }
+          onEndReached={handleLoadMore}
+          onEndReachedThreshold={0.3}
+        />
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+  },
+  background: {
+    flex: 1,
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 15,
+    paddingBottom: 10,
+  },
+  logo: {
+    width: 67,
+    height: 25,
+    marginRight: 20,
+  },
+  searchBar: {
+    flex: 1,
+    flexDirection: 'row',
+    alignItems: 'center',
+    backgroundColor: 'rgba(255,255,255,0.38)',
+    borderRadius: 180,
+    paddingHorizontal: 15,
+    height: 28,
+  },
+  searchIcon: {
+    width: 15,
+    height: 15,
+    marginRight: 5,
+  },
+  searchInput: {
+    flex: 1,
+    color: '#fff',
+    fontSize: 12,
+    padding: 0,
+  },
+  mainImage: {
+    width: '100%',
+    height: 395,
+  },
+  typeSection: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 10,
+    paddingVertical: 10,
+  },
+  typeList: {
+    flex: 1,
+    flexDirection: 'row',
+    justifyContent: 'space-around',
+  },
+  typeItem: {
+    width: 75,
+    height: 30,
+  },
+  typeImage: {
+    width: '100%',
+    height: '100%',
+  },
+  sortBtn: {
+    width: '10%',
+    alignItems: 'center',
+  },
+  sortIcon: {
+    width: 20,
+    height: 20,
+  },
+  listContent: {
+    paddingHorizontal: 10,
+    paddingBottom: 100,
+  },
+  itemContainer: {
+    marginBottom: 8,
+  },
+  itemBg: {
+    width: '100%',
+    height: 210,
+    padding: 8,
+  },
+  itemImage: {
+    width: '100%',
+    height: 142,
+    borderRadius: 8,
+  },
+  itemInfo: {
+    paddingHorizontal: 15,
+    paddingTop: 15,
+  },
+  itemName: {
+    color: '#fff',
+    fontSize: 14,
+  },
+  itemPrice: {
+    color: '#ff0000',
+    fontSize: 12,
+    fontWeight: 'bold',
+    marginTop: 5,
+  },
+  priceUnit: {
+    fontSize: 12,
+    marginRight: 2,
+  },
+  footer: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    alignItems: 'center',
+    paddingVertical: 15,
+  },
+  footerText: {
+    color: 'rgba(255,255,255,0.6)',
+    fontSize: 12,
+    marginLeft: 8,
+  },
+  empty: {
+    alignItems: 'center',
+    paddingVertical: 50,
+  },
+  emptyText: {
+    color: 'rgba(255,255,255,0.6)',
+    fontSize: 14,
+  },
+  barrageSection: {
+    marginVertical: 10,
+    paddingHorizontal: 10,
+  },
+  barrageRow: {
+    flexDirection: 'row',
+    marginBottom: 5,
+  },
+  barrageItem: {
+    backgroundColor: 'rgba(0,0,0,0.5)',
+    borderRadius: 15,
+    paddingHorizontal: 12,
+    paddingVertical: 6,
+    marginRight: 8,
+    maxWidth: 150,
+  },
+  barrageText: {
+    color: '#fff',
+    fontSize: 12,
+  },
+});

+ 0 - 112
app/(tabs)/explore.tsx

@@ -1,112 +0,0 @@
-import { Image } from 'expo-image';
-import { Platform, StyleSheet } from 'react-native';
-
-import { Collapsible } from '@/components/ui/collapsible';
-import { ExternalLink } from '@/components/external-link';
-import ParallaxScrollView from '@/components/parallax-scroll-view';
-import { ThemedText } from '@/components/themed-text';
-import { ThemedView } from '@/components/themed-view';
-import { IconSymbol } from '@/components/ui/icon-symbol';
-import { Fonts } from '@/constants/theme';
-
-export default function TabTwoScreen() {
-  return (
-    <ParallaxScrollView
-      headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
-      headerImage={
-        <IconSymbol
-          size={310}
-          color="#808080"
-          name="chevron.left.forwardslash.chevron.right"
-          style={styles.headerImage}
-        />
-      }>
-      <ThemedView style={styles.titleContainer}>
-        <ThemedText
-          type="title"
-          style={{
-            fontFamily: Fonts.rounded,
-          }}>
-          Explore
-        </ThemedText>
-      </ThemedView>
-      <ThemedText>This app includes example code to help you get started.</ThemedText>
-      <Collapsible title="File-based routing">
-        <ThemedText>
-          This app has two screens:{' '}
-          <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
-          <ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
-        </ThemedText>
-        <ThemedText>
-          The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
-          sets up the tab navigator.
-        </ThemedText>
-        <ExternalLink href="https://docs.expo.dev/router/introduction">
-          <ThemedText type="link">Learn more</ThemedText>
-        </ExternalLink>
-      </Collapsible>
-      <Collapsible title="Android, iOS, and web support">
-        <ThemedText>
-          You can open this project on Android, iOS, and the web. To open the web version, press{' '}
-          <ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
-        </ThemedText>
-      </Collapsible>
-      <Collapsible title="Images">
-        <ThemedText>
-          For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
-          <ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
-          different screen densities
-        </ThemedText>
-        <Image
-          source={require('@/assets/images/react-logo.png')}
-          style={{ width: 100, height: 100, alignSelf: 'center' }}
-        />
-        <ExternalLink href="https://reactnative.dev/docs/images">
-          <ThemedText type="link">Learn more</ThemedText>
-        </ExternalLink>
-      </Collapsible>
-      <Collapsible title="Light and dark mode components">
-        <ThemedText>
-          This template has light and dark mode support. The{' '}
-          <ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
-          what the user&apos;s current color scheme is, and so you can adjust UI colors accordingly.
-        </ThemedText>
-        <ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
-          <ThemedText type="link">Learn more</ThemedText>
-        </ExternalLink>
-      </Collapsible>
-      <Collapsible title="Animations">
-        <ThemedText>
-          This template includes an example of an animated component. The{' '}
-          <ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
-          the powerful{' '}
-          <ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}>
-            react-native-reanimated
-          </ThemedText>{' '}
-          library to create a waving hand animation.
-        </ThemedText>
-        {Platform.select({
-          ios: (
-            <ThemedText>
-              The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
-              component provides a parallax effect for the header image.
-            </ThemedText>
-          ),
-        })}
-      </Collapsible>
-    </ParallaxScrollView>
-  );
-}
-
-const styles = StyleSheet.create({
-  headerImage: {
-    color: '#808080',
-    bottom: -90,
-    left: -35,
-    position: 'absolute',
-  },
-  titleContainer: {
-    flexDirection: 'row',
-    gap: 8,
-  },
-});

+ 202 - 86
app/(tabs)/index.tsx

@@ -1,98 +1,214 @@
-import { Image } from 'expo-image';
-import { Platform, StyleSheet } from 'react-native';
+import { useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+  ActivityIndicator,
+  ImageBackground,
+  RefreshControl,
+  ScrollView,
+  StatusBar,
+  StyleSheet,
+  Text,
+  View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
 
-import { HelloWave } from '@/components/hello-wave';
-import ParallaxScrollView from '@/components/parallax-scroll-view';
-import { ThemedText } from '@/components/themed-text';
-import { ThemedView } from '@/components/themed-view';
-import { Link } from 'expo-router';
+import { Banner } from '@/components/home/Banner';
+import { GoodsList } from '@/components/home/GoodsList';
+import { IPFilter } from '@/components/home/IPFilter';
+import { QuickEntry } from '@/components/home/QuickEntry';
+import { SearchBar } from '@/components/home/SearchBar';
+import { Images } from '@/constants/images';
+
+// API 服务
+import { getIPList, IPItem } from '@/services/award';
+import { BannerItem, getPageConfig, TabItem } from '@/services/base';
+import { getGoodsList, GoodsItem, GoodsListParams } from '@/services/mall';
 
 export default function HomeScreen() {
+  const insets = useSafeAreaInsets();
+  const router = useRouter();
+  const [refreshing, setRefreshing] = useState(false);
+  const [loading, setLoading] = useState(true);
+
+  // 数据状态
+  const [goods, setGoods] = useState<GoodsItem[]>([]);
+  const [banners, setBanners] = useState<BannerItem[]>([]);
+  const [tabs, setTabs] = useState<TabItem[]>([]);
+  const [ipList, setIpList] = useState<IPItem[]>([]);
+  const [ipIndex, setIpIndex] = useState(0);
+
+  // 搜索参数
+  const [searchParams, setSearchParams] = useState<GoodsListParams>({
+    current: 1,
+    size: 15,
+    keyword: '',
+    worksId: '',
+  });
+
+  // 加载商品列表
+  const loadGoods = useCallback(async (params: GoodsListParams, append = false) => {
+    try {
+      const data = await getGoodsList(params);
+      if (append) {
+        setGoods((prev) => [...prev, ...data]);
+      } else {
+        setGoods(data);
+      }
+    } catch (error) {
+      console.error('加载商品失败:', error);
+    }
+  }, []);
+
+  // 加载 IP 列表
+  const loadIPList = useCallback(async () => {
+    try {
+      const data = await getIPList();
+      const allIP: IPItem = { id: '', name: '所有IP' };
+      setIpList([allIP, ...data.filter((item) => item !== null)]);
+    } catch (error) {
+      console.error('加载IP列表失败:', error);
+    }
+  }, []);
+
+  // 加载页面配置
+  const loadPageConfig = useCallback(async () => {
+    try {
+      const bannerConfig = await getPageConfig('index_banner');
+      if (bannerConfig?.components?.[0]?.elements) {
+        setBanners(bannerConfig.components[0].elements);
+      }
+
+      const iconConfig = await getPageConfig('index_icon');
+      if (iconConfig?.components?.[0]?.elements) {
+        setTabs(iconConfig.components[0].elements);
+      }
+    } catch (error) {
+      console.error('加载页面配置失败:', error);
+    }
+  }, []);
+
+  // 初始化数据
+  const initData = useCallback(async () => {
+    setLoading(true);
+    await Promise.all([loadGoods(searchParams), loadIPList(), loadPageConfig()]);
+    setLoading(false);
+  }, []);
+
+  useEffect(() => {
+    initData();
+  }, []);
+
+  // 下拉刷新
+  const onRefresh = async () => {
+    setRefreshing(true);
+    const newParams = { ...searchParams, current: 1 };
+    setSearchParams(newParams);
+    await Promise.all([loadGoods(newParams), loadIPList(), loadPageConfig()]);
+    setRefreshing(false);
+  };
+
+  // 搜索
+  const handleSearch = async (keyword: string) => {
+    const newParams = { ...searchParams, keyword, current: 1 };
+    setSearchParams(newParams);
+    await loadGoods(newParams);
+  };
+
+  // Banner 点击
+  const handleBannerPress = (item: BannerItem) => {
+    console.log('Banner pressed:', item);
+  };
+
+  // 功能入口点击
+  const handleQuickEntryPress = (item: TabItem) => {
+    console.log('Quick entry pressed:', item);
+  };
+
+  // IP 筛选
+  const handleIPSelect = async (item: IPItem, index: number) => {
+    setIpIndex(index);
+    const newParams = { ...searchParams, worksId: item.id, current: 1 };
+    setSearchParams(newParams);
+    await loadGoods(newParams);
+  };
+
+  // 商品点击
+  const handleGoodsPress = (item: GoodsItem) => {
+    router.push(`/product/${item.id}` as any);
+  };
+
+  if (loading) {
+    return (
+      <View style={styles.loadingContainer}>
+        <ActivityIndicator size="large" color="#fff" />
+        <Text style={styles.loadingText}>加载中...</Text>
+      </View>
+    );
+  }
+
   return (
-    <ParallaxScrollView
-      headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
-      headerImage={
-        <Image
-          source={require('@/assets/images/partial-react-logo.png')}
-          style={styles.reactLogo}
-        />
-      }>
-      <ThemedView style={styles.titleContainer}>
-        <ThemedText type="title">Welcome!</ThemedText>
-        <HelloWave />
-      </ThemedView>
-      <ThemedView style={styles.stepContainer}>
-        <ThemedText type="subtitle">Step 1: Try it</ThemedText>
-        <ThemedText>
-          Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
-          Press{' '}
-          <ThemedText type="defaultSemiBold">
-            {Platform.select({
-              ios: 'cmd + d',
-              android: 'cmd + m',
-              web: 'F12',
-            })}
-          </ThemedText>{' '}
-          to open developer tools.
-        </ThemedText>
-      </ThemedView>
-      <ThemedView style={styles.stepContainer}>
-        <Link href="/modal">
-          <Link.Trigger>
-            <ThemedText type="subtitle">Step 2: Explore</ThemedText>
-          </Link.Trigger>
-          <Link.Preview />
-          <Link.Menu>
-            <Link.MenuAction title="Action" icon="cube" onPress={() => alert('Action pressed')} />
-            <Link.MenuAction
-              title="Share"
-              icon="square.and.arrow.up"
-              onPress={() => alert('Share pressed')}
-            />
-            <Link.Menu title="More" icon="ellipsis">
-              <Link.MenuAction
-                title="Delete"
-                icon="trash"
-                destructive
-                onPress={() => alert('Delete pressed')}
-              />
-            </Link.Menu>
-          </Link.Menu>
-        </Link>
-
-        <ThemedText>
-          {`Tap the Explore tab to learn more about what's included in this starter app.`}
-        </ThemedText>
-      </ThemedView>
-      <ThemedView style={styles.stepContainer}>
-        <ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
-        <ThemedText>
-          {`When you're ready, run `}
-          <ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
-          <ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
-          <ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
-          <ThemedText type="defaultSemiBold">app-example</ThemedText>.
-        </ThemedText>
-      </ThemedView>
-    </ParallaxScrollView>
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground
+        source={{ uri: Images.common.indexBg }}
+        style={styles.background}
+        resizeMode="cover"
+      >
+        <ScrollView
+          style={styles.scrollView}
+          contentContainerStyle={{ paddingTop: insets.top + 10 }}
+          showsVerticalScrollIndicator={false}
+          refreshControl={
+            <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#fff" />
+          }
+        >
+          {/* 搜索栏 */}
+          <SearchBar onSearch={handleSearch} />
+
+          {/* Banner 轮播 */}
+          {banners.length > 0 && <Banner data={banners} onPress={handleBannerPress} />}
+
+          {/* 功能入口 */}
+          <View style={styles.section}>
+            {tabs.length > 0 && <QuickEntry data={tabs} onPress={handleQuickEntryPress} />}
+
+            {/* IP 分类筛选 */}
+            {ipList.length > 0 && (
+              <IPFilter data={ipList} activeIndex={ipIndex} onSelect={handleIPSelect} />
+            )}
+          </View>
+
+          {/* 商品列表 */}
+          <GoodsList data={goods} onItemPress={handleGoodsPress} />
+        </ScrollView>
+      </ImageBackground>
+    </View>
   );
 }
 
 const styles = StyleSheet.create({
-  titleContainer: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 8,
+  container: {
+    flex: 1,
+    backgroundColor: '#000',
+  },
+  background: {
+    flex: 1,
   },
-  stepContainer: {
-    gap: 8,
-    marginBottom: 8,
+  scrollView: {
+    flex: 1,
+  },
+  section: {
+    paddingHorizontal: 10,
+  },
+  loadingContainer: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+    justifyContent: 'center',
+    alignItems: 'center',
   },
-  reactLogo: {
-    height: 178,
-    width: 290,
-    bottom: 0,
-    left: 0,
-    position: 'absolute',
+  loadingText: {
+    color: '#fff',
+    marginTop: 10,
+    fontSize: 14,
   },
 });

+ 431 - 0
app/(tabs)/mine.tsx

@@ -0,0 +1,431 @@
+import * as Clipboard from 'expo-clipboard';
+import { Image } from 'expo-image';
+import { useFocusEffect, useRouter } from 'expo-router';
+import React, { useCallback, useState } from 'react';
+import {
+    Alert,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { getMagicIndex } from '@/services/award';
+import { getParamConfig, getUserInfo, UserInfo } from '@/services/user';
+
+interface IndexData {
+  couponCount?: number;
+  inventoryCount?: number;
+  magicBalance?: number;
+  treasureBoxCount?: number;
+}
+
+export default function MineScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
+  const [indexData, setIndexData] = useState<IndexData | null>(null);
+  const [inviteShow, setInviteShow] = useState(false);
+  const [showCredit, setShowCredit] = useState(false);
+  const [filingInfo, setFilingInfo] = useState<{ state: number; data: string } | null>(null);
+
+  const loadData = useCallback(async () => {
+    try {
+      const [info, magicData] = await Promise.all([
+        getUserInfo(),
+        getMagicIndex(),
+      ]);
+      setUserInfo(info);
+      setIndexData(magicData);
+
+      // 获取邀请配置
+      const inviteConfig = await getParamConfig('invite_config');
+      setInviteShow(inviteConfig?.state !== 0);
+
+      // 获取积分显示配置
+      const creditConfig = await getParamConfig('credit_show');
+      setShowCredit(creditConfig?.state !== 0);
+
+      // 获取备案信息
+      const filingConfig = await getParamConfig('beian_icp');
+      if (filingConfig) {
+        setFilingInfo({ state: filingConfig.state, data: filingConfig.data });
+      }
+    } catch (error) {
+      console.error('获取数据失败:', error);
+    }
+  }, []);
+
+  useFocusEffect(
+    useCallback(() => {
+      loadData();
+    }, [loadData])
+  );
+
+  const handleLogin = () => {
+    if (!userInfo) {
+      router.push('/login' as any);
+    }
+  };
+
+  const handleCopy = async (text: string) => {
+    await Clipboard.setStringAsync(text);
+    Alert.alert('提示', '复制成功');
+  };
+
+  const handleMenuPress = (route: string) => {
+    if (!userInfo) {
+      router.push('/login' as any);
+      return;
+    }
+    if (route) {
+      router.push(route as any);
+    }
+  };
+
+  const showNumber = (key: keyof IndexData) => {
+    if (!indexData) return '-';
+    const val = indexData[key];
+    if (typeof val === 'undefined') return '-';
+    return String(val);
+  };
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground
+        source={{ uri: Images.mine.kaixinMineBg }}
+        style={styles.background}
+        resizeMode="cover"
+      >
+        {/* 顶部背景 */}
+        <Image
+          source={{ uri: Images.mine.kaixinMineHeadBg }}
+          style={[styles.headerBg, { top: 0 }]}
+          contentFit="cover"
+        />
+
+        <ScrollView
+          style={styles.scrollView}
+          contentContainerStyle={{ paddingTop: insets.top }}
+          showsVerticalScrollIndicator={false}
+        >
+          {/* 顶部标题 */}
+          <View style={styles.header}>
+            <Text style={styles.title}>个人中心</Text>
+          </View>
+
+          {/* 用户信息 */}
+          <TouchableOpacity
+            style={styles.userBox}
+            onPress={handleLogin}
+            activeOpacity={0.8}
+          >
+            <ImageBackground
+              source={{ uri: Images.common.defaultAvatar }}
+              style={styles.avatarBox}
+              resizeMode="cover"
+            >
+              {userInfo?.avatar && (
+                <Image
+                  source={{ uri: userInfo.avatar }}
+                  style={styles.avatar}
+                  contentFit="cover"
+                />
+              )}
+            </ImageBackground>
+            <View style={styles.userInfo}>
+              <View style={styles.nicknameRow}>
+                <Text style={styles.nickname}>
+                  {userInfo?.nickname || '暂未登录!'}
+                </Text>
+                {userInfo && (
+                  <TouchableOpacity onPress={() => handleMenuPress('/user-info')}>
+                    <Image
+                      source={{ uri: Images.mine.editIcon }}
+                      style={styles.editIcon}
+                      contentFit="contain"
+                    />
+                  </TouchableOpacity>
+                )}
+              </View>
+              {userInfo ? (
+                <View style={styles.idRow}>
+                  <TouchableOpacity
+                    style={styles.idItem}
+                    onPress={() => handleCopy(userInfo.id || '')}
+                  >
+                    <Text style={styles.idText}>ID:{userInfo.id}</Text>
+                    <Image
+                      source={{ uri: Images.mine.kaixinUserCopyIcon }}
+                      style={styles.copyIcon}
+                      contentFit="contain"
+                    />
+                  </TouchableOpacity>
+                  {userInfo.phone && (
+                    <Text style={styles.phoneText}>手机:{userInfo.phone}</Text>
+                  )}
+                </View>
+              ) : (
+                <Text style={styles.loginTip}>点此登录账号</Text>
+              )}
+            </View>
+          </TouchableOpacity>
+
+          {/* 数据统计 */}
+          <ImageBackground
+            source={{ uri: Images.mine.kaixinUserDataBg }}
+            style={styles.dataBox}
+            resizeMode="stretch"
+          >
+            <TouchableOpacity style={styles.dataItem} onPress={() => handleMenuPress('/coupon')}>
+              <Text style={styles.dataNum}>{showNumber('couponCount')}</Text>
+              <Text style={styles.dataLabel}>优惠券</Text>
+            </TouchableOpacity>
+            <TouchableOpacity style={styles.dataItem} onPress={() => handleMenuPress('/store')}>
+              <Text style={styles.dataNum}>{showNumber('inventoryCount')}</Text>
+              <Text style={styles.dataLabel}>仓库</Text>
+            </TouchableOpacity>
+            <TouchableOpacity style={styles.dataItem} onPress={() => handleMenuPress('/magic')}>
+              <Text style={styles.dataNum}>{showNumber('magicBalance')}</Text>
+              <Text style={styles.dataLabel}>果实</Text>
+            </TouchableOpacity>
+            <TouchableOpacity style={styles.dataItem} onPress={() => handleMenuPress('/box-list')}>
+              <Text style={styles.dataNum}>{showNumber('treasureBoxCount')}</Text>
+              <Text style={styles.dataLabel}>宝箱</Text>
+            </TouchableOpacity>
+          </ImageBackground>
+
+          {/* 功能入口 */}
+          <ImageBackground
+            source={{ uri: Images.mine.userSection1Bg }}
+            style={styles.funcBox}
+            resizeMode="stretch"
+          >
+            <View style={styles.funcList}>
+              {inviteShow && userInfo && (
+                <TouchableOpacity style={styles.funcItem} onPress={() => handleMenuPress('/invite')}>
+                  <Image source={{ uri: Images.mine.invite }} style={styles.funcIcon} contentFit="contain" />
+                  <Text style={styles.funcText}>邀新有礼</Text>
+                </TouchableOpacity>
+              )}
+              {showCredit && (
+                <TouchableOpacity style={styles.funcItem} onPress={() => handleMenuPress('/integral')}>
+                  <Image source={{ uri: Images.mine.kaixinintegral }} style={styles.funcIcon} contentFit="contain" />
+                  <Text style={styles.funcText}>签到领积分</Text>
+                </TouchableOpacity>
+              )}
+              <TouchableOpacity style={styles.funcItem} onPress={() => handleMenuPress('/message')}>
+                <Image source={{ uri: Images.mine.message }} style={styles.funcIcon} contentFit="contain" />
+                <Text style={styles.funcText}>系统消息</Text>
+              </TouchableOpacity>
+              <TouchableOpacity style={styles.funcItem} onPress={() => handleMenuPress('/orders')}>
+                <Image source={{ uri: Images.mine.kaixinorder }} style={styles.funcIcon} contentFit="contain" />
+                <Text style={styles.funcText}>宝箱订单</Text>
+              </TouchableOpacity>
+            </View>
+          </ImageBackground>
+
+          {/* 订单入口 */}
+          <ImageBackground
+            source={{ uri: Images.mine.userSection2Bg }}
+            style={styles.orderBox}
+            resizeMode="stretch"
+          >
+            <View style={styles.orderList}>
+              <TouchableOpacity style={styles.orderItem} onPress={() => handleMenuPress('/orders?tab=1')}>
+                <Image source={{ uri: Images.mine.order1 }} style={styles.orderImage} contentFit="contain" />
+              </TouchableOpacity>
+              <TouchableOpacity style={styles.orderItem} onPress={() => handleMenuPress('/orders?tab=4')}>
+                <Image source={{ uri: Images.mine.order2 }} style={styles.orderImage} contentFit="contain" />
+              </TouchableOpacity>
+            </View>
+          </ImageBackground>
+
+          {/* 备案信息 */}
+          {filingInfo && filingInfo.state !== 0 && (
+            <View style={styles.filingBox}>
+              <Text style={styles.filingText}>{filingInfo.data}</Text>
+            </View>
+          )}
+
+          {/* 底部占位 */}
+          <View style={{ height: 120 }} />
+        </ScrollView>
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+  },
+  background: {
+    flex: 1,
+  },
+  headerBg: {
+    position: 'absolute',
+    left: 0,
+    width: '100%',
+    height: 260,
+    zIndex: 1,
+  },
+  scrollView: {
+    flex: 1,
+    zIndex: 2,
+  },
+  header: {
+    alignItems: 'center',
+    paddingBottom: 10,
+  },
+  title: {
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: 'bold',
+    height: 40,
+    lineHeight: 40,
+  },
+  userBox: {
+    flexDirection: 'row',
+    paddingHorizontal: 22,
+    paddingVertical: 8,
+    marginHorizontal: 8,
+  },
+  avatarBox: {
+    width: 64,
+    height: 64,
+    borderRadius: 32,
+    overflow: 'hidden',
+    marginRight: 21,
+  },
+  avatar: {
+    width: '100%',
+    height: '100%',
+  },
+  userInfo: {
+    flex: 1,
+    justifyContent: 'center',
+  },
+  nicknameRow: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    marginBottom: 10,
+  },
+  nickname: {
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: '700',
+  },
+  editIcon: {
+    width: 67,
+    height: 23,
+  },
+  idRow: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  idItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginRight: 22,
+  },
+  idText: {
+    color: '#fff',
+    fontSize: 11,
+    fontWeight: 'bold',
+  },
+  copyIcon: {
+    width: 11,
+    height: 11,
+    marginLeft: 5,
+  },
+  phoneText: {
+    color: '#fff',
+    fontSize: 11,
+    fontWeight: 'bold',
+  },
+  loginTip: {
+    color: '#fff',
+    fontSize: 12,
+  },
+  dataBox: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    width: 360,
+    height: 57,
+    marginHorizontal: 'auto',
+    paddingHorizontal: 11,
+    alignSelf: 'center',
+  },
+  dataItem: {
+    width: '25%',
+    alignItems: 'center',
+    justifyContent: 'center',
+    paddingTop: 10,
+  },
+  dataNum: {
+    color: '#000',
+    fontSize: 16,
+    fontWeight: 'bold',
+  },
+  dataLabel: {
+    color: '#934800',
+    fontSize: 12,
+  },
+  funcBox: {
+    height: 115,
+    marginHorizontal: 0,
+    paddingTop: 20,
+    paddingHorizontal: 10,
+  },
+  funcList: {
+    flexDirection: 'row',
+    justifyContent: 'space-around',
+  },
+  funcItem: {
+    alignItems: 'center',
+  },
+  funcIcon: {
+    width: 59,
+    height: 57,
+  },
+  funcText: {
+    color: '#000',
+    fontSize: 12,
+  },
+  orderBox: {
+    marginHorizontal: 8,
+    paddingTop: 20,
+    paddingHorizontal: 16,
+    paddingBottom: 25,
+  },
+  orderList: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+  },
+  orderItem: {
+    width: 161,
+  },
+  orderImage: {
+    width: 161,
+    height: 75,
+  },
+  filingBox: {
+    alignItems: 'center',
+    marginTop: 20,
+    paddingHorizontal: 20,
+  },
+  filingText: {
+    color: '#fff',
+    fontSize: 14,
+    textAlign: 'center',
+  },
+});

+ 178 - 0
app/(tabs)/welfare.tsx

@@ -0,0 +1,178 @@
+import { Image } from 'expo-image';
+import { useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { getUserInfo, UserInfo } from '@/services/user';
+
+export default function WelfareScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const [showSection, setShowSection] = useState(true);
+  const [user, setUser] = useState<UserInfo | null>(null);
+
+  const loadUserInfo = useCallback(async () => {
+    try {
+      const info = await getUserInfo();
+      setUser(info);
+    } catch (error) {
+      console.error('获取用户信息失败:', error);
+    }
+  }, []);
+
+  useEffect(() => {
+    loadUserInfo();
+  }, [loadUserInfo]);
+
+  const handleRoomPress = (type: number) => {
+    if (type === 0) {
+      // 福利房间
+      router.push('/weal/room' as any);
+    } else if (type === 1) {
+      // 扭蛋机
+      router.push('/weal/catchDoll' as any);
+    } else {
+      // 祈愿
+      router.push('/weal/wish' as any);
+    }
+  };
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground
+        source={{ uri: Images.mine.kaixinMineBg }}
+        style={styles.background}
+        resizeMode="cover"
+      >
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top + 50 }]}>
+          <Text style={styles.title}>福利</Text>
+        </View>
+
+        {/* 标题区域 */}
+        <View style={styles.headSection}>
+          <Text style={styles.headTitle}>限时福利活动</Text>
+          <Text style={styles.headText}>限时活动,不定时开放</Text>
+        </View>
+
+        {/* 房间列表 */}
+        {showSection && (
+          <ScrollView 
+            style={styles.scrollView}
+            showsVerticalScrollIndicator={false}
+            contentContainerStyle={styles.scrollContent}
+          >
+            <TouchableOpacity
+              style={styles.roomItem}
+              onPress={() => handleRoomPress(0)}
+              activeOpacity={0.8}
+            >
+              <Image
+                source={{ uri: Images.welfare.kaixinRoom1 }}
+                style={styles.roomImage}
+                contentFit="contain"
+              />
+            </TouchableOpacity>
+
+            <TouchableOpacity
+              style={styles.roomItem}
+              onPress={() => handleRoomPress(1)}
+              activeOpacity={0.8}
+            >
+              <Image
+                source={{ uri: Images.welfare.kaixinRoom2 }}
+                style={styles.roomImage}
+                contentFit="contain"
+              />
+            </TouchableOpacity>
+
+            <TouchableOpacity
+              style={styles.roomItem}
+              onPress={() => handleRoomPress(2)}
+              activeOpacity={0.8}
+            >
+              <Image
+                source={{ uri: Images.welfare.kaixinRoom3 }}
+                style={styles.roomImage}
+                contentFit="contain"
+              />
+            </TouchableOpacity>
+          </ScrollView>
+        )}
+
+        {/* 底部占位 */}
+        <View style={{ height: 120 }} />
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#52504e',
+  },
+  background: {
+    flex: 1,
+  },
+  header: {
+    alignItems: 'center',
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    zIndex: 100,
+  },
+  title: {
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: 'bold',
+    height: 40,
+    lineHeight: 40,
+  },
+  headSection: {
+    width: '100%',
+    height: 192,
+    alignItems: 'center',
+    paddingTop: 90,
+  },
+  headTitle: {
+    fontWeight: 'bold',
+    fontSize: 40,
+    color: '#FDF685',
+    textShadowColor: '#E85801',
+    textShadowOffset: { width: 1, height: 2 },
+    textShadowRadius: 0,
+  },
+  headText: {
+    fontSize: 15,
+    color: '#E85801',
+    marginTop: 5,
+  },
+  scrollView: {
+    flex: 1,
+    marginTop: 15,
+  },
+  scrollContent: {
+    paddingHorizontal: 0,
+    alignItems: 'center',
+  },
+  roomItem: {
+    marginBottom: 15,
+  },
+  roomImage: {
+    width: 375,
+    height: 130,
+  },
+});

+ 16 - 7
app/_layout.tsx

@@ -3,6 +3,7 @@ import { Stack } from 'expo-router';
 import { StatusBar } from 'expo-status-bar';
 import 'react-native-reanimated';
 
+import { AuthProvider } from '@/contexts/AuthContext';
 import { useColorScheme } from '@/hooks/use-color-scheme';
 
 export const unstable_settings = {
@@ -13,12 +14,20 @@ export default function RootLayout() {
   const colorScheme = useColorScheme();
 
   return (
-    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
-      <Stack>
-        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
-        <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
-      </Stack>
-      <StatusBar style="auto" />
-    </ThemeProvider>
+    <AuthProvider>
+      <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
+        <Stack>
+          <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
+          <Stack.Screen name="login" options={{ headerShown: false }} />
+          <Stack.Screen name="product/[id]" options={{ headerShown: false }} />
+          <Stack.Screen name="address/index" options={{ headerShown: false }} />
+          <Stack.Screen name="address/edit" options={{ headerShown: false }} />
+          <Stack.Screen name="orders/index" options={{ headerShown: false }} />
+          <Stack.Screen name="orders/[tradeNo]" options={{ headerShown: false }} />
+          <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
+        </Stack>
+        <StatusBar style="auto" />
+      </ThemeProvider>
+    </AuthProvider>
   );
 }

+ 348 - 0
app/address/edit.tsx

@@ -0,0 +1,348 @@
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    Alert,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Switch,
+    Text,
+    TextInput,
+    TouchableOpacity,
+    View
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { addAddress, Address, getAddressList, updateAddress } from '@/services/address';
+
+export default function AddressEditScreen() {
+  const { id } = useLocalSearchParams<{ id?: string }>();
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+
+  const [loading, setLoading] = useState(!!id);
+  const [saving, setSaving] = useState(false);
+
+  const [contactName, setContactName] = useState('');
+  const [contactNo, setContactNo] = useState('');
+  const [province, setProvince] = useState('');
+  const [city, setCity] = useState('');
+  const [district, setDistrict] = useState('');
+  const [address, setAddress] = useState('');
+  const [isDefault, setIsDefault] = useState(false);
+
+  const loadData = useCallback(async () => {
+    if (!id) return;
+    setLoading(true);
+    try {
+      const list = await getAddressList();
+      const item = list.find((a: Address) => a.id === id);
+      if (item) {
+        setContactName(item.contactName);
+        setContactNo(item.contactNo);
+        setProvince(item.province);
+        setCity(item.city);
+        setDistrict(item.district || '');
+        setAddress(item.address);
+        setIsDefault(item.defaultFlag === 1);
+      }
+    } catch (error) {
+      console.error('加载地址失败:', error);
+    }
+    setLoading(false);
+  }, [id]);
+
+  useEffect(() => {
+    loadData();
+  }, [loadData]);
+
+  const handleSave = async () => {
+    if (!contactName.trim()) {
+      Alert.alert('提示', '请输入收货人姓名');
+      return;
+    }
+    if (!contactNo.trim()) {
+      Alert.alert('提示', '请输入手机号码');
+      return;
+    }
+    if (!province.trim() || !city.trim()) {
+      Alert.alert('提示', '请输入省市信息');
+      return;
+    }
+    if (!address.trim()) {
+      Alert.alert('提示', '请输入详细地址');
+      return;
+    }
+
+    setSaving(true);
+    try {
+      const params = {
+        contactName: contactName.trim(),
+        contactNo: contactNo.trim(),
+        province: province.trim(),
+        city: city.trim(),
+        district: district.trim(),
+        address: address.trim(),
+        defaultFlag: isDefault ? 1 : 0,
+      };
+
+      let success = false;
+      if (id) {
+        success = await updateAddress({ id, ...params } as Address);
+      } else {
+        success = await addAddress(params);
+      }
+
+      if (success) {
+        Alert.alert('成功', id ? '地址已更新' : '地址已添加', [
+          { text: '确定', onPress: () => router.back() },
+        ]);
+      }
+    } catch (error) {
+      console.error('保存地址失败:', error);
+      Alert.alert('错误', '保存失败');
+    }
+    setSaving(false);
+  };
+
+  const goBack = () => {
+    router.back();
+  };
+
+  if (loading) {
+    return (
+      <View style={styles.loadingContainer}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    );
+  }
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground
+        source={{ uri: Images.mine.kaixinMineBg }}
+        style={styles.background}
+        resizeMode="cover"
+      >
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={goBack}>
+            <Text style={styles.backText}>←</Text>
+          </TouchableOpacity>
+          <Text style={styles.headerTitle}>{id ? '编辑地址' : '新增地址'}</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+          <ImageBackground
+            source={{ uri: Images.common.itemBg }}
+            style={styles.formBg}
+            resizeMode="stretch"
+          >
+            <View style={styles.form}>
+              <View style={styles.formItem}>
+                <Text style={styles.label}>收货人</Text>
+                <TextInput
+                  style={styles.input}
+                  value={contactName}
+                  onChangeText={setContactName}
+                  placeholder="请输入收货人姓名"
+                  placeholderTextColor="#999"
+                />
+              </View>
+
+              <View style={styles.formItem}>
+                <Text style={styles.label}>手机号码</Text>
+                <TextInput
+                  style={styles.input}
+                  value={contactNo}
+                  onChangeText={setContactNo}
+                  placeholder="请输入手机号码"
+                  placeholderTextColor="#999"
+                  keyboardType="phone-pad"
+                />
+              </View>
+
+              <View style={styles.formItem}>
+                <Text style={styles.label}>省份</Text>
+                <TextInput
+                  style={styles.input}
+                  value={province}
+                  onChangeText={setProvince}
+                  placeholder="请输入省份"
+                  placeholderTextColor="#999"
+                />
+              </View>
+
+              <View style={styles.formItem}>
+                <Text style={styles.label}>城市</Text>
+                <TextInput
+                  style={styles.input}
+                  value={city}
+                  onChangeText={setCity}
+                  placeholder="请输入城市"
+                  placeholderTextColor="#999"
+                />
+              </View>
+
+              <View style={styles.formItem}>
+                <Text style={styles.label}>区/县</Text>
+                <TextInput
+                  style={styles.input}
+                  value={district}
+                  onChangeText={setDistrict}
+                  placeholder="请输入区/县(选填)"
+                  placeholderTextColor="#999"
+                />
+              </View>
+
+              <View style={styles.formItem}>
+                <Text style={styles.label}>详细地址</Text>
+                <TextInput
+                  style={[styles.input, styles.textArea]}
+                  value={address}
+                  onChangeText={setAddress}
+                  placeholder="请输入详细地址"
+                  placeholderTextColor="#999"
+                  multiline
+                  numberOfLines={3}
+                />
+              </View>
+
+              <View style={styles.switchItem}>
+                <Text style={styles.switchLabel}>设为默认地址</Text>
+                <Switch value={isDefault} onValueChange={setIsDefault} />
+              </View>
+            </View>
+          </ImageBackground>
+          <View style={{ height: 100 }} />
+        </ScrollView>
+
+        {/* 底部保存按钮 */}
+        <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]}>
+          <TouchableOpacity
+            style={[styles.saveBtn, saving && styles.saveBtnDisabled]}
+            onPress={handleSave}
+            disabled={saving}
+            activeOpacity={0.8}
+          >
+            <ImageBackground
+              source={{ uri: Images.common.loginBtn }}
+              style={styles.saveBtnBg}
+              resizeMode="stretch"
+            >
+              <Text style={styles.saveBtnText}>{saving ? '保存中...' : '保存'}</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+        </View>
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+  },
+  background: {
+    flex: 1,
+  },
+  loadingContainer: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 15,
+    paddingBottom: 10,
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backText: {
+    color: '#fff',
+    fontSize: 24,
+  },
+  headerTitle: {
+    color: '#fff',
+    fontSize: 18,
+    fontWeight: '600',
+  },
+  placeholder: {
+    width: 40,
+  },
+  scrollView: {
+    flex: 1,
+  },
+  formBg: {
+    margin: 15,
+    padding: 15,
+    borderRadius: 12,
+    overflow: 'hidden',
+  },
+  form: {},
+  formItem: {
+    marginBottom: 15,
+  },
+  label: {
+    color: '#333',
+    fontSize: 14,
+    marginBottom: 8,
+  },
+  input: {
+    backgroundColor: 'rgba(255,255,255,0.8)',
+    borderRadius: 8,
+    paddingHorizontal: 12,
+    paddingVertical: 12,
+    color: '#333',
+    fontSize: 14,
+  },
+  textArea: {
+    height: 80,
+    textAlignVertical: 'top',
+  },
+  switchItem: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingTop: 10,
+  },
+  switchLabel: {
+    color: '#333',
+    fontSize: 14,
+  },
+  bottomBar: {
+    paddingHorizontal: 15,
+    paddingTop: 10,
+  },
+  saveBtn: {
+    height: 50,
+    overflow: 'hidden',
+  },
+  saveBtnBg: {
+    width: '100%',
+    height: '100%',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  saveBtnDisabled: {
+    opacity: 0.6,
+  },
+  saveBtnText: {
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: '600',
+  },
+});

+ 317 - 0
app/address/index.tsx

@@ -0,0 +1,317 @@
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    Alert,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { Address, deleteAddress, getAddressList, updateAddress } from '@/services/address';
+
+export default function AddressListScreen() {
+  const { type } = useLocalSearchParams<{ type?: string }>();
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+
+  const [loading, setLoading] = useState(true);
+  const [list, setList] = useState<Address[]>([]);
+
+  const loadData = useCallback(async () => {
+    setLoading(true);
+    try {
+      const data = await getAddressList();
+      setList(data);
+    } catch (error) {
+      console.error('加载地址失败:', error);
+    }
+    setLoading(false);
+  }, []);
+
+  useEffect(() => {
+    loadData();
+  }, [loadData]);
+
+  // 选择地址
+  const selectAddress = (item: Address) => {
+    if (type === '1') {
+      // 从结算页面来,选择后返回
+      router.back();
+      // 通过全局事件传递选中的地址
+    }
+  };
+
+  // 设为默认
+  const setDefault = async (item: Address) => {
+    try {
+      await updateAddress({ ...item, defaultFlag: 1 });
+      loadData();
+    } catch (error) {
+      console.error('设置默认地址失败:', error);
+    }
+  };
+
+  // 删除地址
+  const handleDelete = (item: Address) => {
+    Alert.alert('提示', '确定删除该地址吗?', [
+      { text: '取消', style: 'cancel' },
+      {
+        text: '删除',
+        style: 'destructive',
+        onPress: async () => {
+          try {
+            await deleteAddress(item.id);
+            loadData();
+          } catch (error) {
+            console.error('删除地址失败:', error);
+          }
+        },
+      },
+    ]);
+  };
+
+  // 编辑地址
+  const editAddress = (item: Address) => {
+    router.push(`/address/edit?id=${item.id}` as any);
+  };
+
+  // 新增地址
+  const addNewAddress = () => {
+    router.push('/address/edit' as any);
+  };
+
+  const goBack = () => {
+    router.back();
+  };
+
+  if (loading) {
+    return (
+      <View style={styles.loadingContainer}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    );
+  }
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground
+        source={{ uri: Images.mine.kaixinMineBg }}
+        style={styles.background}
+        resizeMode="cover"
+      >
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={goBack}>
+            <Text style={styles.backText}>←</Text>
+          </TouchableOpacity>
+          <Text style={styles.headerTitle}>收货地址</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+          {list.length === 0 ? (
+            <View style={styles.emptyContainer}>
+              <Text style={styles.emptyText}>暂无收货地址</Text>
+            </View>
+          ) : (
+            list.map((item) => (
+              <TouchableOpacity
+                key={item.id}
+                style={styles.addressItem}
+                onPress={() => selectAddress(item)}
+              >
+                <ImageBackground
+                  source={{ uri: Images.common.itemBg }}
+                  style={styles.addressItemBg}
+                  resizeMode="stretch"
+                >
+                  <View style={styles.addressInfo}>
+                    <View style={styles.nameRow}>
+                      <Text style={styles.name}>{item.contactName}</Text>
+                      <Text style={styles.phone}>{item.contactNo}</Text>
+                      {item.defaultFlag === 1 && (
+                        <View style={styles.defaultTag}>
+                          <Text style={styles.defaultText}>默认</Text>
+                        </View>
+                      )}
+                    </View>
+                    <Text style={styles.addressDetail}>
+                      {item.province}{item.city}{item.district}{item.address}
+                    </Text>
+                  </View>
+                  <View style={styles.actions}>
+                    {item.defaultFlag !== 1 && (
+                      <TouchableOpacity style={styles.actionBtn} onPress={() => setDefault(item)}>
+                        <Text style={styles.actionText}>设为默认</Text>
+                      </TouchableOpacity>
+                    )}
+                    <TouchableOpacity style={styles.actionBtn} onPress={() => editAddress(item)}>
+                      <Text style={styles.actionText}>编辑</Text>
+                    </TouchableOpacity>
+                    <TouchableOpacity style={styles.actionBtn} onPress={() => handleDelete(item)}>
+                      <Text style={[styles.actionText, styles.deleteText]}>删除</Text>
+                    </TouchableOpacity>
+                  </View>
+                </ImageBackground>
+              </TouchableOpacity>
+            ))
+          )}
+          <View style={{ height: 100 }} />
+        </ScrollView>
+
+        {/* 底部新增按钮 */}
+        <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]}>
+          <TouchableOpacity style={styles.addBtn} onPress={addNewAddress} activeOpacity={0.8}>
+            <ImageBackground
+              source={{ uri: Images.common.loginBtn }}
+              style={styles.addBtnBg}
+              resizeMode="stretch"
+            >
+              <Text style={styles.addBtnText}>+ 新增收货地址</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+        </View>
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+  },
+  background: {
+    flex: 1,
+  },
+  loadingContainer: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 15,
+    paddingBottom: 10,
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backText: {
+    color: '#fff',
+    fontSize: 24,
+  },
+  headerTitle: {
+    color: '#fff',
+    fontSize: 18,
+    fontWeight: '600',
+  },
+  placeholder: {
+    width: 40,
+  },
+  scrollView: {
+    flex: 1,
+    paddingHorizontal: 15,
+  },
+  emptyContainer: {
+    paddingTop: 100,
+    alignItems: 'center',
+  },
+  emptyText: {
+    color: '#999',
+    fontSize: 14,
+  },
+  addressItem: {
+    marginTop: 10,
+  },
+  addressItemBg: {
+    padding: 15,
+    borderRadius: 12,
+    overflow: 'hidden',
+  },
+  addressInfo: {
+    borderBottomWidth: 1,
+    borderBottomColor: 'rgba(0,0,0,0.1)',
+    paddingBottom: 12,
+  },
+  nameRow: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  name: {
+    color: '#333',
+    fontSize: 16,
+    fontWeight: '600',
+  },
+  phone: {
+    color: '#333',
+    fontSize: 14,
+    marginLeft: 15,
+  },
+  defaultTag: {
+    backgroundColor: '#ff6b00',
+    borderRadius: 10,
+    paddingHorizontal: 8,
+    paddingVertical: 2,
+    marginLeft: 10,
+  },
+  defaultText: {
+    color: '#fff',
+    fontSize: 10,
+  },
+  addressDetail: {
+    color: '#666',
+    fontSize: 13,
+    marginTop: 8,
+    lineHeight: 18,
+  },
+  actions: {
+    flexDirection: 'row',
+    justifyContent: 'flex-end',
+    paddingTop: 12,
+  },
+  actionBtn: {
+    paddingHorizontal: 12,
+    paddingVertical: 6,
+  },
+  actionText: {
+    color: '#666',
+    fontSize: 13,
+  },
+  deleteText: {
+    color: '#ff4d4f',
+  },
+  bottomBar: {
+    paddingHorizontal: 15,
+    paddingTop: 10,
+  },
+  addBtn: {
+    height: 50,
+    overflow: 'hidden',
+  },
+  addBtnBg: {
+    width: '100%',
+    height: '100%',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  addBtnText: {
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: '600',
+  },
+});

+ 376 - 0
app/login.tsx

@@ -0,0 +1,376 @@
+import { useRouter } from 'expo-router';
+import React, { useEffect, useRef, useState } from 'react';
+import {
+    Alert,
+    ImageBackground,
+    KeyboardAvoidingView,
+    Platform,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TextInput,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { login, sendVerifyCode } from '@/services/user';
+
+export default function LoginScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+
+  const [phone, setPhone] = useState('');
+  const [verifyCode, setVerifyCode] = useState('');
+  const [agreeFlag, setAgreeFlag] = useState(false);
+  const [countdown, setCountdown] = useState(0);
+  const [disabled, setDisabled] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const [tips, setTips] = useState('获取验证码');
+
+  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
+
+  useEffect(() => {
+    return () => {
+      if (timerRef.current) {
+        clearInterval(timerRef.current);
+      }
+    };
+  }, []);
+
+  // 验证手机号
+  const isChinesePhoneNumber = (phoneNumber: string) => {
+    const phoneNumberPattern = /^1[3-9]\d{9}$/;
+    return phoneNumberPattern.test(phoneNumber);
+  };
+
+  // 开始倒计时
+  const startCountdown = () => {
+    setDisabled(true);
+    let count = 60;
+    setTips(`${count}s后重新获取`);
+    
+    timerRef.current = setInterval(() => {
+      count--;
+      if (count > 0) {
+        setTips(`${count}s后重新获取`);
+      } else {
+        resetCountdown();
+      }
+    }, 1000);
+  };
+
+  // 重置倒计时
+  const resetCountdown = () => {
+    setDisabled(false);
+    setTips('获取验证码');
+    if (timerRef.current) {
+      clearInterval(timerRef.current);
+      timerRef.current = null;
+    }
+  };
+
+  // 获取验证码
+  const handleGetVerifyCode = async () => {
+    if (disabled) return;
+    
+    if (phone && isChinesePhoneNumber(phone)) {
+      try {
+        const res = await sendVerifyCode(phone, 'LOGIN');
+        if (res) {
+          Alert.alert('提示', '验证码已发送');
+          startCountdown();
+        }
+      } catch (error) {
+        Alert.alert('错误', '获取验证码失败,请重试');
+      }
+    } else {
+      Alert.alert('提示', '请输入正确的手机号');
+    }
+  };
+
+  // 登录
+  const handleLogin = async () => {
+    if (!agreeFlag) {
+      Alert.alert('提示', '请您先阅读并同意用户协议和隐私政策');
+      return;
+    }
+    
+    if (phone && isChinesePhoneNumber(phone) && verifyCode) {
+      setLoading(true);
+      try {
+        const result = await login({
+          loginWay: 'MOBILE',
+          mobile: phone,
+          verifycode: verifyCode,
+        });
+        
+        if (result.success) {
+          // TODO: 如果 needInfo 为 true,跳转到完善信息页面
+          // if (result.needInfo) {
+          //   router.replace('/user-info');
+          //   return;
+          // }
+          router.back();
+        } else {
+          Alert.alert('错误', '登录失败,请检查验证码');
+        }
+      } catch (error) {
+        Alert.alert('错误', '登录失败');
+      }
+      setLoading(false);
+    } else {
+      Alert.alert('提示', '请输入手机号和验证码');
+    }
+  };
+
+  const goBack = () => {
+    router.back();
+  };
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground
+        source={{ uri: Images.common.loginBg }}
+        style={styles.background}
+        resizeMode="cover"
+      >
+        <KeyboardAvoidingView
+          style={styles.keyboardView}
+          behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
+        >
+          <ScrollView
+            contentContainerStyle={styles.scrollContent}
+            keyboardShouldPersistTaps="handled"
+            showsVerticalScrollIndicator={false}
+          >
+            {/* 底部表单区域 */}
+            <View style={[styles.bottom, { paddingBottom: insets.bottom + 70 }]}>
+              {/* 表单 */}
+              <View style={styles.form}>
+                {/* 手机号输入 */}
+                <View style={styles.formItem}>
+                  <Text style={styles.label}>手机号</Text>
+                  <TextInput
+                    style={styles.input}
+                    value={phone}
+                    onChangeText={setPhone}
+                    placeholder="手机号"
+                    placeholderTextColor="rgba(255,255,255,0.5)"
+                    keyboardType="phone-pad"
+                    maxLength={11}
+                  />
+                </View>
+                <View style={styles.divider} />
+
+                {/* 验证码输入 */}
+                <View style={styles.formItem}>
+                  <Text style={styles.label}>验证码</Text>
+                  <TextInput
+                    style={[styles.input, styles.codeInput]}
+                    value={verifyCode}
+                    onChangeText={setVerifyCode}
+                    placeholder="请填写验证码"
+                    placeholderTextColor="rgba(255,255,255,0.5)"
+                    keyboardType="number-pad"
+                    maxLength={6}
+                  />
+                  <TouchableOpacity
+                    style={[styles.verifyBtn, disabled && styles.verifyBtnDisabled]}
+                    onPress={handleGetVerifyCode}
+                    disabled={disabled}
+                  >
+                    <Text style={[styles.verifyBtnText, disabled && styles.verifyBtnTextDisabled]}>
+                      {tips}
+                    </Text>
+                  </TouchableOpacity>
+                </View>
+                <View style={styles.divider} />
+              </View>
+
+              {/* 协议勾选 */}
+              <TouchableOpacity
+                style={styles.agree}
+                onPress={() => setAgreeFlag(!agreeFlag)}
+              >
+                <View style={[styles.radio, agreeFlag && styles.radioChecked]}>
+                  {agreeFlag && <View style={styles.radioInner} />}
+                </View>
+                <Text style={styles.agreeText}>
+                  我已阅读并同意
+                  <Text style={styles.linkText}>《用户协议》</Text>
+                  跟
+                  <Text style={styles.linkText}>《隐私政策》</Text>
+                </Text>
+              </TouchableOpacity>
+
+              {/* 按钮区域 */}
+              <View style={styles.btnArea}>
+                <TouchableOpacity
+                  style={[styles.btn, loading && styles.btnDisabled]}
+                  onPress={handleLogin}
+                  disabled={loading}
+                  activeOpacity={0.8}
+                >
+                  <ImageBackground
+                    source={{ uri: Images.common.loginBtn }}
+                    style={styles.loginBtnBg}
+                    resizeMode="stretch"
+                  >
+                    <Text style={styles.btnText}>
+                      {loading ? '登录中...' : '登录'}
+                    </Text>
+                  </ImageBackground>
+                </TouchableOpacity>
+
+                <TouchableOpacity style={styles.btnBack} onPress={goBack}>
+                  <Text style={styles.btnBackText}>返回</Text>
+                </TouchableOpacity>
+              </View>
+            </View>
+          </ScrollView>
+        </KeyboardAvoidingView>
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+  },
+  background: {
+    flex: 1,
+  },
+  keyboardView: {
+    flex: 1,
+  },
+  scrollContent: {
+    flexGrow: 1,
+    justifyContent: 'flex-end',
+  },
+  bottom: {
+    width: '100%',
+  },
+  form: {
+    paddingHorizontal: 25,
+  },
+  formItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 12,
+  },
+  label: {
+    color: '#fff',
+    fontSize: 14,
+    width: 50,
+  },
+  input: {
+    flex: 1,
+    color: '#fff',
+    fontSize: 14,
+    paddingVertical: 8,
+    outlineStyle: 'none',
+  } as any,
+  codeInput: {
+    flex: 1,
+  },
+  divider: {
+    height: 1,
+    backgroundColor: 'rgba(255,255,255,0.2)',
+  },
+  verifyBtn: {
+    backgroundColor: '#000',
+    borderRadius: 4,
+    paddingHorizontal: 10,
+    paddingVertical: 5,
+    minWidth: 80,
+    alignItems: 'center',
+  },
+  verifyBtnDisabled: {
+    backgroundColor: '#ccc',
+  },
+  verifyBtnText: {
+    color: '#fff',
+    fontSize: 12,
+  },
+  verifyBtnTextDisabled: {
+    color: '#666',
+  },
+  agree: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginTop: 25,
+    paddingHorizontal: 25,
+  },
+  radio: {
+    width: 16,
+    height: 16,
+    borderRadius: 8,
+    borderWidth: 1,
+    borderColor: 'rgba(255,255,255,0.5)',
+    marginRight: 6,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  radioChecked: {
+    borderColor: '#8b3dff',
+    backgroundColor: '#8b3dff',
+  },
+  radioInner: {
+    width: 6,
+    height: 6,
+    borderRadius: 3,
+    backgroundColor: '#fff',
+  },
+  agreeText: {
+    color: '#fff',
+    fontSize: 12,
+  },
+  linkText: {
+    color: '#8b3dff',
+  },
+  btnArea: {
+    paddingTop: 15,
+    alignItems: 'center',
+  },
+  btn: {
+    width: 234,
+    height: 45,
+    overflow: 'hidden',
+  },
+  loginBtnBg: {
+    width: '100%',
+    height: '100%',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  btnDisabled: {
+    opacity: 0.6,
+  },
+  btnText: {
+    color: '#fff',
+    fontSize: 14,
+    fontWeight: '600',
+  },
+  btnBack: {
+    width: 234,
+    height: 35,
+    backgroundColor: '#fff',
+    borderRadius: 25,
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginTop: 10,
+    borderWidth: 1,
+    borderColor: '#fff',
+  },
+  btnBackText: {
+    color: '#888',
+    fontSize: 12,
+  },
+});

+ 375 - 0
app/orders/[tradeNo].tsx

@@ -0,0 +1,375 @@
+import { Image } from 'expo-image';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    Alert,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { confirmReceive, getOrderDetail, OrderDetail, payOrder } from '@/services/mall';
+
+export default function OrderDetailScreen() {
+  const { tradeNo } = useLocalSearchParams<{ tradeNo: string }>();
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+
+  const [loading, setLoading] = useState(true);
+  const [data, setData] = useState<OrderDetail | null>(null);
+
+  const loadData = useCallback(async () => {
+    if (!tradeNo) return;
+    setLoading(true);
+    try {
+      const detail = await getOrderDetail(tradeNo);
+      setData(detail);
+    } catch (error) {
+      console.error('加载订单详情失败:', error);
+    }
+    setLoading(false);
+  }, [tradeNo]);
+
+  useEffect(() => {
+    loadData();
+  }, [loadData]);
+
+  // 去支付
+  const handlePay = async () => {
+    if (!data) return;
+    try {
+      const result = await payOrder(data.tradeNo, 'ALIPAY');
+      if (result?.paySuccess) {
+        Alert.alert('成功', '支付成功');
+        loadData();
+      }
+    } catch (error) {
+      console.error('支付失败:', error);
+      Alert.alert('错误', '支付失败');
+    }
+  };
+
+  // 确认收货
+  const handleReceive = () => {
+    Alert.alert('提示', '确认已收到商品?', [
+      { text: '取消', style: 'cancel' },
+      {
+        text: '确认',
+        onPress: async () => {
+          try {
+            await confirmReceive(tradeNo!);
+            Alert.alert('成功', '确认收货成功');
+            loadData();
+          } catch (error) {
+            console.error('确认收货失败:', error);
+          }
+        },
+      },
+    ]);
+  };
+
+  const goBack = () => {
+    router.back();
+  };
+
+  if (loading) {
+    return (
+      <View style={styles.loadingContainer}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    );
+  }
+
+  if (!data) {
+    return (
+      <View style={styles.loadingContainer}>
+        <Text style={styles.errorText}>订单不存在</Text>
+      </View>
+    );
+  }
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground
+        source={{ uri: Images.mine.kaixinMineBg }}
+        style={styles.background}
+        resizeMode="cover"
+      >
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={goBack}>
+            <Text style={styles.backText}>←</Text>
+          </TouchableOpacity>
+          <Text style={styles.headerTitle}>订单详情</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+          {/* 订单状态 */}
+          <View style={styles.statusSection}>
+            <Text style={styles.statusText}>{data.statusText}</Text>
+          </View>
+
+          {/* 收货地址 */}
+          {data.address && (
+            <ImageBackground
+              source={{ uri: Images.common.itemBg }}
+              style={styles.section}
+              resizeMode="stretch"
+            >
+              <Text style={styles.sectionTitle}>收货信息</Text>
+              <View style={styles.addressInfo}>
+                <Text style={styles.addressName}>
+                  {data.address.contactName} {data.address.contactNo}
+                </Text>
+                <Text style={styles.addressDetail}>
+                  {data.address.province}{data.address.city}{data.address.district}{data.address.address}
+                </Text>
+              </View>
+            </ImageBackground>
+          )}
+
+          {/* 商品信息 */}
+          <ImageBackground
+            source={{ uri: Images.common.itemBg }}
+            style={styles.section}
+            resizeMode="stretch"
+          >
+            <Text style={styles.sectionTitle}>商品信息</Text>
+            <View style={styles.goodsItem}>
+              <Image source={data.goodsCover} style={styles.goodsImage} contentFit="cover" />
+              <View style={styles.goodsInfo}>
+                <Text style={styles.goodsName} numberOfLines={2}>{data.goodsName}</Text>
+                <View style={styles.goodsBottom}>
+                  <Text style={styles.goodsPrice}>¥{data.paymentAmount}</Text>
+                  <Text style={styles.goodsQty}>x{data.quantity}</Text>
+                </View>
+              </View>
+            </View>
+          </ImageBackground>
+
+          {/* 订单信息 */}
+          <ImageBackground
+            source={{ uri: Images.common.itemBg }}
+            style={styles.section}
+            resizeMode="stretch"
+          >
+            <Text style={styles.sectionTitle}>订单信息</Text>
+            <View style={styles.infoRow}>
+              <Text style={styles.infoLabel}>订单编号</Text>
+              <Text style={styles.infoValue}>{data.tradeNo}</Text>
+            </View>
+            <View style={styles.infoRow}>
+              <Text style={styles.infoLabel}>下单时间</Text>
+              <Text style={styles.infoValue}>{data.createTime}</Text>
+            </View>
+            <View style={styles.infoRow}>
+              <Text style={styles.infoLabel}>实付金额</Text>
+              <Text style={[styles.infoValue, styles.priceText]}>¥{data.paymentAmount}</Text>
+            </View>
+          </ImageBackground>
+
+          <View style={{ height: 100 }} />
+        </ScrollView>
+
+        {/* 底部操作栏 */}
+        {(data.status === 1 || data.status === 3) && (
+          <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]}>
+            {data.status === 1 && (
+              <TouchableOpacity style={styles.payBtn} onPress={handlePay} activeOpacity={0.8}>
+                <ImageBackground
+                  source={{ uri: Images.common.loginBtn }}
+                  style={styles.payBtnBg}
+                  resizeMode="stretch"
+                >
+                  <Text style={styles.payBtnText}>去支付</Text>
+                </ImageBackground>
+              </TouchableOpacity>
+            )}
+            {data.status === 3 && (
+              <TouchableOpacity style={styles.payBtn} onPress={handleReceive} activeOpacity={0.8}>
+                <ImageBackground
+                  source={{ uri: Images.common.loginBtn }}
+                  style={styles.payBtnBg}
+                  resizeMode="stretch"
+                >
+                  <Text style={styles.payBtnText}>确认收货</Text>
+                </ImageBackground>
+              </TouchableOpacity>
+            )}
+          </View>
+        )}
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+  },
+  background: {
+    flex: 1,
+  },
+  loadingContainer: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  errorText: {
+    color: '#999',
+    fontSize: 16,
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 15,
+    paddingBottom: 10,
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backText: {
+    color: '#fff',
+    fontSize: 24,
+  },
+  headerTitle: {
+    color: '#fff',
+    fontSize: 18,
+    fontWeight: '600',
+  },
+  placeholder: {
+    width: 40,
+  },
+  scrollView: {
+    flex: 1,
+  },
+  statusSection: {
+    backgroundColor: '#ff6b00',
+    padding: 20,
+    marginHorizontal: 15,
+    borderRadius: 12,
+    alignItems: 'center',
+  },
+  statusText: {
+    color: '#fff',
+    fontSize: 18,
+    fontWeight: '600',
+  },
+  section: {
+    marginTop: 10,
+    marginHorizontal: 15,
+    padding: 15,
+    borderRadius: 12,
+    overflow: 'hidden',
+  },
+  sectionTitle: {
+    color: '#333',
+    fontSize: 15,
+    fontWeight: '600',
+    marginBottom: 12,
+  },
+  addressInfo: {},
+  addressName: {
+    color: '#333',
+    fontSize: 15,
+    fontWeight: '500',
+  },
+  addressDetail: {
+    color: '#666',
+    fontSize: 13,
+    marginTop: 6,
+    lineHeight: 18,
+  },
+  goodsItem: {
+    flexDirection: 'row',
+  },
+  goodsImage: {
+    width: 80,
+    height: 80,
+    borderRadius: 8,
+    backgroundColor: '#fff',
+  },
+  goodsInfo: {
+    flex: 1,
+    marginLeft: 12,
+    justifyContent: 'space-between',
+  },
+  goodsName: {
+    color: '#333',
+    fontSize: 14,
+    lineHeight: 20,
+  },
+  goodsBottom: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+  },
+  goodsPrice: {
+    color: '#ff6b00',
+    fontSize: 16,
+    fontWeight: '600',
+  },
+  goodsQty: {
+    color: '#666',
+    fontSize: 13,
+  },
+  infoRow: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    paddingVertical: 8,
+  },
+  infoLabel: {
+    color: '#666',
+    fontSize: 13,
+  },
+  infoValue: {
+    color: '#333',
+    fontSize: 13,
+  },
+  priceText: {
+    color: '#ff6b00',
+    fontWeight: '600',
+  },
+  bottomBar: {
+    position: 'absolute',
+    bottom: 0,
+    left: 0,
+    right: 0,
+    flexDirection: 'row',
+    justifyContent: 'flex-end',
+    paddingHorizontal: 15,
+    paddingTop: 10,
+    backgroundColor: 'rgba(0,0,0,0.3)',
+  },
+  payBtn: {
+    width: 120,
+    height: 45,
+    overflow: 'hidden',
+  },
+  payBtnBg: {
+    width: '100%',
+    height: '100%',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  payBtnText: {
+    color: '#fff',
+    fontSize: 15,
+    fontWeight: '600',
+  },
+});

+ 338 - 0
app/orders/index.tsx

@@ -0,0 +1,338 @@
+import { Image } from 'expo-image';
+import { useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    ImageBackground,
+    RefreshControl,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { getOrders, OrderItem } from '@/services/mall';
+
+const tabs = [
+  { label: '全部', value: 'all' },
+  { label: '待付款', value: 'to_pay' },
+  { label: '待补款', value: 'to_payLeft' },
+  { label: '待发货', value: 'to_delivery' },
+  { label: '待收货', value: 'to_receive' },
+  { label: '已完成', value: 'complete' },
+  { label: '未完成', value: 'uncomplete' },
+];
+
+export default function OrdersScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+
+  const [loading, setLoading] = useState(true);
+  const [refreshing, setRefreshing] = useState(false);
+  const [activeTab, setActiveTab] = useState(0);
+  const [orders, setOrders] = useState<OrderItem[]>([]);
+  const [page, setPage] = useState(1);
+
+  const loadData = useCallback(async (tab?: number, refresh = false) => {
+    if (refresh) {
+      setRefreshing(true);
+      setPage(1);
+    } else {
+      setLoading(true);
+    }
+    try {
+      const data = await getOrders(refresh ? 1 : page, 10, tab);
+      if (data?.records) {
+        setOrders(refresh ? data.records : [...orders, ...data.records]);
+      }
+    } catch (error) {
+      console.error('加载订单失败:', error);
+    }
+    setLoading(false);
+    setRefreshing(false);
+  }, [page, orders]);
+
+  useEffect(() => {
+    loadData(tabs[activeTab].value, true);
+  }, [activeTab]);
+
+  const onRefresh = () => {
+    loadData(tabs[activeTab].value, true);
+  };
+
+  const switchTab = (index: number) => {
+    if (index !== activeTab) {
+      setActiveTab(index);
+      setOrders([]);
+    }
+  };
+
+  const goToDetail = (tradeNo: string) => {
+    router.push(`/orders/${tradeNo}` as any);
+  };
+
+  const goBack = () => {
+    router.back();
+  };
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground
+        source={{ uri: Images.mine.kaixinMineBg }}
+        style={styles.background}
+        resizeMode="cover"
+      >
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={goBack}>
+            <Text style={styles.backText}>←</Text>
+          </TouchableOpacity>
+          <Text style={styles.headerTitle}>我的订单</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        {/* Tab 栏 */}
+        <View style={styles.tabBar}>
+          {tabs.map((tab, index) => (
+            <TouchableOpacity
+              key={index}
+              style={[styles.tabItem, activeTab === index && styles.tabItemActive]}
+              onPress={() => switchTab(index)}
+            >
+              <Text style={[styles.tabText, activeTab === index && styles.tabTextActive]}>
+                {tab.label}
+              </Text>
+            </TouchableOpacity>
+          ))}
+        </View>
+
+        <ScrollView
+          style={styles.scrollView}
+          showsVerticalScrollIndicator={false}
+          refreshControl={
+            <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#fff" />
+          }
+        >
+          {loading && orders.length === 0 ? (
+            <View style={styles.loadingContainer}>
+              <ActivityIndicator size="large" color="#fff" />
+            </View>
+          ) : orders.length === 0 ? (
+            <View style={styles.emptyContainer}>
+              <Text style={styles.emptyText}>暂无订单</Text>
+            </View>
+          ) : (
+            orders.map((item) => (
+              <TouchableOpacity
+                key={item.tradeNo}
+                style={styles.orderItem}
+                onPress={() => goToDetail(item.tradeNo)}
+              >
+                <ImageBackground
+                  source={{ uri: Images.common.itemBg }}
+                  style={styles.orderItemBg}
+                  resizeMode="stretch"
+                >
+                  <View style={styles.orderHeader}>
+                    <Text style={styles.orderNo}>订单号:{item.tradeNo}</Text>
+                    <Text style={styles.orderStatus}>{item.statusText}</Text>
+                  </View>
+                  <View style={styles.orderContent}>
+                    <Image source={item.goodsCover} style={styles.goodsImage} contentFit="cover" />
+                    <View style={styles.goodsInfo}>
+                      <Text style={styles.goodsName} numberOfLines={2}>{item.goodsName}</Text>
+                      <View style={styles.goodsBottom}>
+                        <Text style={styles.goodsPrice}>¥{item.paymentAmount}</Text>
+                        <Text style={styles.goodsQty}>x{item.quantity}</Text>
+                      </View>
+                    </View>
+                  </View>
+                  <View style={styles.orderFooter}>
+                    <Text style={styles.orderTime}>{item.createTime}</Text>
+                    <View style={styles.orderActions}>
+                      {item.status === 1 && (
+                        <TouchableOpacity style={styles.actionBtn}>
+                          <Text style={styles.actionBtnText}>去支付</Text>
+                        </TouchableOpacity>
+                      )}
+                      {item.status === 3 && (
+                        <TouchableOpacity style={styles.actionBtn}>
+                          <Text style={styles.actionBtnText}>确认收货</Text>
+                        </TouchableOpacity>
+                      )}
+                    </View>
+                  </View>
+                </ImageBackground>
+              </TouchableOpacity>
+            ))
+          )}
+          <View style={{ height: 20 }} />
+        </ScrollView>
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+  },
+  background: {
+    flex: 1,
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 15,
+    paddingBottom: 10,
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backText: {
+    color: '#fff',
+    fontSize: 24,
+  },
+  headerTitle: {
+    color: '#fff',
+    fontSize: 18,
+    fontWeight: '600',
+  },
+  placeholder: {
+    width: 40,
+  },
+  tabBar: {
+    flexDirection: 'row',
+    backgroundColor: 'rgba(255,255,255,0.1)',
+    marginHorizontal: 15,
+    borderRadius: 8,
+    paddingVertical: 5,
+  },
+  tabItem: {
+    flex: 1,
+    alignItems: 'center',
+    paddingVertical: 8,
+  },
+  tabItemActive: {
+    borderBottomWidth: 2,
+    borderBottomColor: '#ff6b00',
+  },
+  tabText: {
+    color: '#999',
+    fontSize: 14,
+  },
+  tabTextActive: {
+    color: '#ff6b00',
+    fontWeight: '600',
+  },
+  scrollView: {
+    flex: 1,
+    paddingHorizontal: 15,
+  },
+  loadingContainer: {
+    paddingTop: 100,
+    alignItems: 'center',
+  },
+  emptyContainer: {
+    paddingTop: 100,
+    alignItems: 'center',
+  },
+  emptyText: {
+    color: '#999',
+    fontSize: 14,
+  },
+  orderItem: {
+    marginTop: 10,
+  },
+  orderItemBg: {
+    borderRadius: 12,
+    overflow: 'hidden',
+  },
+  orderHeader: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    padding: 12,
+    borderBottomWidth: 1,
+    borderBottomColor: 'rgba(0,0,0,0.1)',
+  },
+  orderNo: {
+    color: '#666',
+    fontSize: 12,
+  },
+  orderStatus: {
+    color: '#ff6b00',
+    fontSize: 13,
+    fontWeight: '500',
+  },
+  orderContent: {
+    flexDirection: 'row',
+    padding: 12,
+  },
+  goodsImage: {
+    width: 80,
+    height: 80,
+    borderRadius: 8,
+    backgroundColor: '#fff',
+  },
+  goodsInfo: {
+    flex: 1,
+    marginLeft: 12,
+    justifyContent: 'space-between',
+  },
+  goodsName: {
+    color: '#333',
+    fontSize: 14,
+    lineHeight: 20,
+  },
+  goodsBottom: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+  },
+  goodsPrice: {
+    color: '#ff6b00',
+    fontSize: 16,
+    fontWeight: '600',
+  },
+  goodsQty: {
+    color: '#666',
+    fontSize: 13,
+  },
+  orderFooter: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    padding: 12,
+    borderTopWidth: 1,
+    borderTopColor: 'rgba(0,0,0,0.1)',
+  },
+  orderTime: {
+    color: '#999',
+    fontSize: 12,
+  },
+  orderActions: {
+    flexDirection: 'row',
+  },
+  actionBtn: {
+    paddingHorizontal: 16,
+    paddingVertical: 6,
+    backgroundColor: '#ff6b00',
+    borderRadius: 15,
+    marginLeft: 10,
+  },
+  actionBtnText: {
+    color: '#fff',
+    fontSize: 13,
+  },
+});

+ 378 - 0
app/product/[id].tsx

@@ -0,0 +1,378 @@
+import { Image } from 'expo-image';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import {
+    ActivityIndicator,
+    Dimensions,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { CheckoutModal } from '@/components/product/CheckoutModal';
+import { Images } from '@/constants/images';
+import { getGoodsDetail, GoodsDetail, previewSubmit } from '@/services/mall';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+export default function ProductDetailScreen() {
+  const { id, subjectId } = useLocalSearchParams<{ id: string; subjectId?: string }>();
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const checkoutRef = useRef<any>(null);
+
+  const [loading, setLoading] = useState(true);
+  const [data, setData] = useState<GoodsDetail | null>(null);
+
+  // 加载商品详情
+  const loadData = useCallback(async () => {
+    if (!id) return;
+    setLoading(true);
+    try {
+      const detail = await getGoodsDetail(id, subjectId);
+      setData(detail);
+    } catch (error) {
+      console.error('加载商品详情失败:', error);
+    }
+    setLoading(false);
+  }, [id, subjectId]);
+
+  useEffect(() => {
+    loadData();
+  }, [loadData]);
+
+  // 显示结算弹窗
+  const showCheckout = async () => {
+    if (!data) return;
+    try {
+      const preview = await previewSubmit({
+        goodsId: id!,
+        quantity: 1,
+        subjectId,
+      });
+      if (preview) {
+        checkoutRef.current?.show(preview);
+      }
+    } catch (error) {
+      console.error('预提交失败:', error);
+    }
+  };
+
+  // 返回
+  const goBack = () => {
+    router.back();
+  };
+
+  if (loading) {
+    return (
+      <View style={styles.loadingContainer}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    );
+  }
+
+  if (!data) {
+    return (
+      <View style={styles.loadingContainer}>
+        <Text style={styles.errorText}>商品不存在</Text>
+      </View>
+    );
+  }
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground
+        source={{ uri: Images.mine.kaixinMineBg }}
+        style={styles.background}
+        resizeMode="cover"
+      >
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={goBack}>
+            <Text style={styles.backText}>←</Text>
+          </TouchableOpacity>
+          <Text style={styles.headerTitle}>商品详情</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+          {/* 商品图片 */}
+          <View style={styles.imageWrapper}>
+            <Image source={data.spu.cover} style={styles.coverImage} contentFit="cover" />
+          </View>
+
+          {/* 商品信息 */}
+          <ImageBackground
+            source={{ uri: Images.common.itemBg }}
+            style={styles.infoSection}
+            resizeMode="stretch"
+          >
+            <View style={styles.priceRow}>
+              <Text style={styles.currency}>¥</Text>
+              <Text style={styles.price}>{data.subjectPrice || data.price}</Text>
+              {data.sellType === 2 && (
+                <View style={styles.presellTag}>
+                  <Text style={styles.presellText}>预售</Text>
+                </View>
+              )}
+            </View>
+            <Text style={styles.name}>{data.spu.name}</Text>
+          </ImageBackground>
+
+          {/* 商品详情图片 */}
+          {data.spu.images && data.spu.images.length > 0 && (
+            <ImageBackground
+              source={{ uri: Images.common.itemBg }}
+              style={styles.detailSection}
+              resizeMode="stretch"
+            >
+              <Text style={styles.sectionTitle}>商品详情</Text>
+              {data.spu.images.map((img, index) => (
+                <Image key={index} source={img} style={styles.detailImage} contentFit="contain" />
+              ))}
+            </ImageBackground>
+          )}
+
+          {/* 推荐商品 */}
+          {data.recommendedMallGoods && data.recommendedMallGoods.length > 0 && (
+            <ImageBackground
+              source={{ uri: Images.common.itemBg }}
+              style={styles.recommendSection}
+              resizeMode="stretch"
+            >
+              <Text style={styles.sectionTitle}>推荐商品</Text>
+              <ScrollView horizontal showsHorizontalScrollIndicator={false}>
+                {data.recommendedMallGoods.map((item) => (
+                  <TouchableOpacity
+                    key={item.id}
+                    style={styles.recommendItem}
+                    onPress={() => router.push(`/product/${item.id}` as any)}
+                  >
+                    <Image source={item.cover} style={styles.recommendImage} contentFit="cover" />
+                    <Text style={styles.recommendName} numberOfLines={1}>{item.name}</Text>
+                    <Text style={styles.recommendPrice}>¥{item.price}</Text>
+                  </TouchableOpacity>
+                ))}
+              </ScrollView>
+            </ImageBackground>
+          )}
+
+          <View style={styles.bottomSpace} />
+        </ScrollView>
+
+        {/* 底部购买栏 */}
+        <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]}>
+          <TouchableOpacity style={styles.serviceBtn}>
+            <Text style={styles.serviceBtnText}>客服</Text>
+          </TouchableOpacity>
+          <TouchableOpacity style={styles.buyBtn} onPress={showCheckout} activeOpacity={0.8}>
+            <ImageBackground
+              source={{ uri: Images.common.loginBtn }}
+              style={styles.buyBtnBg}
+              resizeMode="stretch"
+            >
+              <Text style={styles.buyBtnText}>
+                {data.sellType === 2 ? '支付定金' : '立即购买'}
+              </Text>
+            </ImageBackground>
+          </TouchableOpacity>
+        </View>
+      </ImageBackground>
+
+      {/* 结算弹窗 */}
+      <CheckoutModal
+        ref={checkoutRef}
+        data={data}
+        goodsId={id!}
+        subjectId={subjectId}
+      />
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+  },
+  background: {
+    flex: 1,
+  },
+  loadingContainer: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  errorText: {
+    color: '#999',
+    fontSize: 16,
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 15,
+    paddingBottom: 10,
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backText: {
+    color: '#fff',
+    fontSize: 24,
+  },
+  headerTitle: {
+    color: '#fff',
+    fontSize: 18,
+    fontWeight: '600',
+  },
+  placeholder: {
+    width: 40,
+  },
+  scrollView: {
+    flex: 1,
+  },
+  imageWrapper: {
+    width: SCREEN_WIDTH,
+    height: SCREEN_WIDTH,
+    backgroundColor: '#fff',
+  },
+  coverImage: {
+    width: '100%',
+    height: '100%',
+  },
+  infoSection: {
+    padding: 15,
+    marginHorizontal: 10,
+    marginTop: 10,
+    borderRadius: 12,
+    overflow: 'hidden',
+  },
+  priceRow: {
+    flexDirection: 'row',
+    alignItems: 'baseline',
+  },
+  currency: {
+    color: '#ff6b00',
+    fontSize: 14,
+  },
+  price: {
+    color: '#ff6b00',
+    fontSize: 28,
+    fontWeight: 'bold',
+  },
+  presellTag: {
+    backgroundColor: '#8b3dff',
+    borderRadius: 12,
+    paddingHorizontal: 10,
+    paddingVertical: 3,
+    marginLeft: 10,
+  },
+  presellText: {
+    color: '#fff',
+    fontSize: 12,
+  },
+  name: {
+    color: '#333',
+    fontSize: 16,
+    marginTop: 10,
+    lineHeight: 22,
+  },
+  detailSection: {
+    marginTop: 10,
+    marginHorizontal: 10,
+    padding: 15,
+    borderRadius: 12,
+    overflow: 'hidden',
+  },
+  sectionTitle: {
+    color: '#333',
+    fontSize: 16,
+    fontWeight: '600',
+    marginBottom: 15,
+  },
+  detailImage: {
+    width: SCREEN_WIDTH - 50,
+    height: 300,
+    marginBottom: 10,
+  },
+  recommendSection: {
+    marginTop: 10,
+    marginHorizontal: 10,
+    padding: 15,
+    borderRadius: 12,
+    overflow: 'hidden',
+  },
+  recommendItem: {
+    width: 120,
+    marginRight: 10,
+  },
+  recommendImage: {
+    width: 120,
+    height: 120,
+    borderRadius: 8,
+    backgroundColor: '#fff',
+  },
+  recommendName: {
+    color: '#333',
+    fontSize: 12,
+    marginTop: 8,
+  },
+  recommendPrice: {
+    color: '#ff6b00',
+    fontSize: 14,
+    fontWeight: '600',
+    marginTop: 4,
+  },
+  bottomSpace: {
+    height: 100,
+  },
+  bottomBar: {
+    position: 'absolute',
+    bottom: 0,
+    left: 0,
+    right: 0,
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 15,
+    paddingTop: 10,
+    backgroundColor: 'rgba(0,0,0,0.3)',
+  },
+  serviceBtn: {
+    paddingHorizontal: 25,
+    paddingVertical: 12,
+    backgroundColor: 'rgba(255,255,255,0.9)',
+    borderRadius: 25,
+  },
+  serviceBtnText: {
+    color: '#333',
+    fontSize: 14,
+  },
+  buyBtn: {
+    flex: 1,
+    marginLeft: 15,
+    height: 45,
+    overflow: 'hidden',
+  },
+  buyBtnBg: {
+    width: '100%',
+    height: '100%',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  buyBtnText: {
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: '600',
+  },
+});

+ 112 - 0
components/CustomTabBar.tsx

@@ -0,0 +1,112 @@
+import { Images } from '@/constants/images';
+import { Image } from 'expo-image';
+import { usePathname, useRouter } from 'expo-router';
+import React from 'react';
+import { Dimensions, ImageBackground, StyleSheet, TouchableOpacity, View } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+const tabList = [
+  {
+    name: '首页',
+    route: '/',
+    img: Images.tabs.home,
+    active: Images.tabs.homeActive,
+  },
+  {
+    name: '开箱',
+    route: '/box',
+    img: Images.tabs.box,
+    active: Images.tabs.boxActive,
+  },
+  {
+    name: '福利',
+    route: '/welfare',
+    img: Images.tabs.welfare,
+    active: Images.tabs.welfareActive,
+  },
+  {
+    name: '我的',
+    route: '/mine',
+    img: Images.tabs.mine,
+    active: Images.tabs.mineActive,
+  },
+];
+
+export function CustomTabBar() {
+  const router = useRouter();
+  const pathname = usePathname();
+  const insets = useSafeAreaInsets();
+
+  const getTabIndex = () => {
+    if (pathname === '/' || pathname === '/index') return 0;
+    if (pathname === '/box') return 1;
+    if (pathname === '/welfare') return 2;
+    if (pathname === '/mine') return 3; 
+    return 0;
+  };
+
+  const currentIndex = getTabIndex();
+
+  const handlePress = (index: number) => {
+    const route = tabList[index].route;
+    router.replace(route as any);
+  };
+
+  // 计算底部安全区域高度
+  const bottomPadding = insets.bottom;
+
+  return (
+    <View style={[styles.wrapper, { paddingBottom: bottomPadding }]}>
+      <ImageBackground
+        style={styles.container}
+        resizeMode="cover"
+      >
+        <View style={styles.center}>
+          {tabList.map((item, index) => (
+            <TouchableOpacity
+              key={index}
+              style={styles.item}
+              activeOpacity={0.8}
+              onPress={() => handlePress(index)}
+            >
+              <Image
+                source={currentIndex === index ? item.active : item.img}
+                style={styles.icon}
+                contentFit="contain"
+              />
+            </TouchableOpacity>
+          ))}
+        </View>
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  wrapper: {
+    position: 'absolute',
+    bottom: 0,
+    left: 0,
+    right: 0,
+    zIndex: 999,
+  },
+  container: {
+    width: SCREEN_WIDTH,
+    height: 78,
+  },
+  center: {
+    flex: 1,
+    flexDirection: 'row',
+  },
+  item: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  icon: {
+    width: '100%',
+    height: '100%',
+  },
+});

+ 108 - 0
components/home/Banner.tsx

@@ -0,0 +1,108 @@
+import { BannerItem } from '@/services/base';
+import { Image } from 'expo-image';
+import React, { useEffect, useRef, useState } from 'react';
+import { Dimensions, FlatList, StyleSheet, TouchableOpacity, View } from 'react-native';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+// 小程序 banner 高度 432rpx,宽度100%
+const BANNER_HEIGHT = 216; // 432rpx / 2
+
+interface BannerProps {
+  data: BannerItem[];
+  onPress?: (item: BannerItem) => void;
+}
+
+export function Banner({ data, onPress }: BannerProps) {
+  const [activeIndex, setActiveIndex] = useState(0);
+  const flatListRef = useRef<FlatList<BannerItem>>(null);
+  const autoPlayRef = useRef<ReturnType<typeof setInterval> | null>(null);
+
+  useEffect(() => {
+    if (data.length <= 1) return;
+
+    autoPlayRef.current = setInterval(() => {
+      const nextIndex = (activeIndex + 1) % data.length;
+      flatListRef.current?.scrollToIndex({ index: nextIndex, animated: true });
+      setActiveIndex(nextIndex);
+    }, 5000);
+
+    return () => {
+      if (autoPlayRef.current) clearInterval(autoPlayRef.current);
+    };
+  }, [activeIndex, data.length]);
+
+  const renderItem = ({ item }: { item: BannerItem }) => (
+    <TouchableOpacity activeOpacity={0.9} onPress={() => onPress?.(item)}>
+      <Image
+        source={item.cover}
+        style={styles.bannerImage}
+        contentFit="cover"
+      />
+    </TouchableOpacity>
+  );
+
+  const onScroll = (event: any) => {
+    const offsetX = event.nativeEvent.contentOffset.x;
+    const index = Math.round(offsetX / SCREEN_WIDTH);
+    if (index !== activeIndex && index >= 0 && index < data.length) {
+      setActiveIndex(index);
+    }
+  };
+
+  return (
+    <View style={styles.container}>
+      <FlatList
+        ref={flatListRef}
+        data={data}
+        renderItem={renderItem}
+        keyExtractor={(item, index) => item.id || String(index)}
+        horizontal
+        pagingEnabled
+        showsHorizontalScrollIndicator={false}
+        onScroll={onScroll}
+        scrollEventThrottle={16}
+        getItemLayout={(_, index) => ({
+          length: SCREEN_WIDTH,
+          offset: SCREEN_WIDTH * index,
+          index,
+        })}
+      />
+      <View style={styles.dots}>
+        {data.map((_, index) => (
+          <View key={index} style={[styles.dot, activeIndex === index && styles.dotActive]} />
+        ))}
+      </View>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    width: '100%',
+    height: BANNER_HEIGHT,
+    marginTop: 11,
+    overflow: 'hidden',
+  },
+  bannerImage: {
+    width: SCREEN_WIDTH,
+    height: BANNER_HEIGHT,
+  },
+  dots: {
+    position: 'absolute',
+    bottom: 10,
+    left: 0,
+    right: 0,
+    flexDirection: 'row',
+    justifyContent: 'center',
+  },
+  dot: {
+    width: 6,
+    height: 6,
+    borderRadius: 3,
+    backgroundColor: 'rgba(255,255,255,0.5)',
+    marginHorizontal: 3,
+  },
+  dotActive: {
+    backgroundColor: '#fff',
+  },
+});

+ 123 - 0
components/home/GoodsCard.tsx

@@ -0,0 +1,123 @@
+import { Images } from '@/constants/images';
+import { GoodsItem } from '@/services/mall';
+import { Image } from 'expo-image';
+import React from 'react';
+import { Dimensions, ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+// 小程序: width = (screenWidth - 18) / 2, height = 504rpx = 252pt
+const CARD_WIDTH = (SCREEN_WIDTH - 18) / 2;
+const CARD_HEIGHT = 252;
+// 图片区域: 340rpx = 170pt, 356rpx = 178pt
+const IMG_BOX_WIDTH = 170;
+const IMG_BOX_HEIGHT = 178;
+
+interface GoodsCardProps {
+  data: GoodsItem;
+  onPress?: (item: GoodsItem) => void;
+}
+
+export function GoodsCard({ data, onPress }: GoodsCardProps) {
+  return (
+    <TouchableOpacity activeOpacity={0.8} onPress={() => onPress?.(data)}>
+      <ImageBackground
+        source={{ uri: Images.home.cellGoodsBg }}
+        style={[styles.cell, { width: CARD_WIDTH }]}
+        resizeMode="cover"
+      >
+        <View style={[styles.content, { width: CARD_WIDTH - 10 }]}>
+          <View style={styles.imgBox}>
+            <Image source={data.cover} style={styles.image} contentFit="contain" />
+            {/* 图片边框装饰 */}
+            <ImageBackground
+              source={{ uri: Images.home.imgBoxBorder }}
+              style={styles.imgBoxBorder}
+              resizeMode="center"
+            />
+          </View>
+          <View style={styles.textBox}>
+            <Text style={styles.name} numberOfLines={1}>
+              {data.name}
+            </Text>
+            <View style={styles.priceRow}>
+              <Text style={styles.currency}>¥</Text>
+              <Text style={styles.price}>{data.price}</Text>
+              {data.sellType === 2 && (
+                <View style={styles.presellTag}>
+                  <Text style={styles.presellText}>预售</Text>
+                </View>
+              )}
+            </View>
+          </View>
+        </View>
+      </ImageBackground>
+    </TouchableOpacity>
+  );
+}
+
+const styles = StyleSheet.create({
+  cell: {
+    height: CARD_HEIGHT,
+    marginBottom: 12,
+  },
+  content: {
+    overflow: 'hidden',
+    borderRadius: 8,
+  },
+  imgBox: {
+    width: IMG_BOX_WIDTH,
+    height: IMG_BOX_HEIGHT,
+    borderRadius: 7,
+    overflow: 'hidden',
+    paddingHorizontal: 6,
+    alignSelf: 'center',
+  },
+  image: {
+    width: '100%',
+    height: '100%',
+  },
+  imgBoxBorder: {
+    position: 'absolute',
+    left: 0,
+    top: 0,
+    width: '100%',
+    height: '100%',
+  },
+  textBox: {
+    paddingHorizontal: 9,
+    marginTop: 6,
+  },
+  name: {
+    fontSize: 12,
+    color: '#fff',
+  },
+  priceRow: {
+    flexDirection: 'row',
+    alignItems: 'baseline',
+    marginTop: 3,
+  },
+  currency: {
+    fontSize: 10,
+    color: '#fff',
+  },
+  price: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+  },
+  presellTag: {
+    width: 64,
+    height: 22,
+    lineHeight: 22,
+    borderRadius: 11,
+    marginLeft: 8,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: 'rgba(255,255,255,0.2)',
+  },
+  presellText: {
+    fontSize: 12,
+    color: '#fff',
+    textAlign: 'center',
+  },
+});

+ 46 - 0
components/home/GoodsList.tsx

@@ -0,0 +1,46 @@
+import { GoodsItem } from '@/services/mall';
+import React from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+import { GoodsCard } from './GoodsCard';
+
+interface GoodsListProps {
+  data: GoodsItem[];
+  onItemPress?: (item: GoodsItem) => void;
+}
+
+export function GoodsList({ data, onItemPress }: GoodsListProps) {
+  if (!data || data.length === 0) {
+    return (
+      <View style={styles.empty}>
+        <Text style={styles.emptyText}>暂无商品</Text>
+      </View>
+    );
+  }
+
+  return (
+    <View style={styles.container}>
+      {data.map((item) => (
+        <GoodsCard key={item.id} data={item} onPress={onItemPress} />
+      ))}
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+    justifyContent: 'space-between',
+    marginHorizontal: 7,
+    marginTop: 10,
+    paddingBottom: 100,
+  },
+  empty: {
+    paddingVertical: 50,
+    alignItems: 'center',
+  },
+  emptyText: {
+    color: '#666',
+    fontSize: 14,
+  },
+});

+ 73 - 0
components/home/IPFilter.tsx

@@ -0,0 +1,73 @@
+import { Images } from '@/constants/images';
+import { IPItem } from '@/services/award';
+import React from 'react';
+import { ImageBackground, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+// 小程序 item 宽度 174rpx = 87pt, 高度 84rpx = 42pt
+const ITEM_WIDTH = 87;
+const ITEM_HEIGHT = 42;
+
+interface IPFilterProps {
+  data: IPItem[];
+  activeIndex: number;
+  onSelect?: (item: IPItem, index: number) => void;
+}
+
+export function IPFilter({ data, activeIndex, onSelect }: IPFilterProps) {
+  return (
+    <View style={styles.container}>
+      <ScrollView
+        horizontal
+        showsHorizontalScrollIndicator={false}
+        contentContainerStyle={styles.content}
+      >
+        {data.map((item, index) => (
+          <TouchableOpacity
+            key={item.id || index}
+            activeOpacity={0.7}
+            onPress={() => onSelect?.(item, index)}
+            style={styles.itemWrapper}
+          >
+            <ImageBackground
+              source={{ uri: activeIndex === index ? Images.home.typeBgOn : Images.home.typeBg }}
+              style={styles.item}
+              resizeMode="cover"
+            >
+              <Text style={styles.text} numberOfLines={1}>
+                {item.name}
+              </Text>
+            </ImageBackground>
+          </TouchableOpacity>
+        ))}
+      </ScrollView>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    width: '100%',
+    paddingTop: 10,
+  },
+  content: {
+    paddingHorizontal: 8,
+  },
+  itemWrapper: {
+    marginHorizontal: 8,
+  },
+  item: {
+    width: ITEM_WIDTH,
+    height: ITEM_HEIGHT,
+    justifyContent: 'center',
+    alignItems: 'center',
+    paddingTop: 3,
+  },
+  text: {
+    fontWeight: 'bold',
+    fontSize: 16,
+    color: '#fff',
+    textShadowColor: '#000',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 1,
+  },
+});

+ 64 - 0
components/home/QuickEntry.tsx

@@ -0,0 +1,64 @@
+import { TabItem } from '@/services/base';
+import { Image } from 'expo-image';
+import React from 'react';
+import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+// 小程序 item 宽度 162rpx = 81pt
+const ITEM_WIDTH = 81;
+
+interface QuickEntryProps {
+  data: TabItem[];
+  onPress?: (item: TabItem) => void;
+}
+
+export function QuickEntry({ data, onPress }: QuickEntryProps) {
+  return (
+    <View style={styles.container}>
+      <View style={styles.center}>
+        {data.map((item, index) => (
+          <TouchableOpacity
+            key={index}
+            style={styles.item}
+            activeOpacity={0.7}
+            onPress={() => onPress?.(item)}
+          >
+            <View style={styles.itemBox}>
+              <Image source={item.cover} style={styles.icon} contentFit="contain" />
+              <Text style={styles.title}>{item.title}</Text>
+            </View>
+          </TouchableOpacity>
+        ))}
+      </View>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    width: '100%',
+    marginTop: 10,
+    paddingHorizontal: 10,
+  },
+  center: {
+    width: '100%',
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+    justifyContent: 'flex-start',
+  },
+  item: {
+    width: ITEM_WIDTH,
+    alignItems: 'center',
+  },
+  itemBox: {
+    alignItems: 'center',
+  },
+  icon: {
+    width: ITEM_WIDTH,
+    height: ITEM_WIDTH,
+  },
+  title: {
+    fontSize: 10,
+    fontWeight: '300',
+    color: 'rgba(255,255,255,0.59)',
+  },
+});

+ 79 - 0
components/home/SearchBar.tsx

@@ -0,0 +1,79 @@
+import { Images } from '@/constants/images';
+import { Image } from 'expo-image';
+import React, { useState } from 'react';
+import { StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
+
+interface SearchBarProps {
+  onSearch?: (keyword: string) => void;
+}
+
+export function SearchBar({ onSearch }: SearchBarProps) {
+  const [keyword, setKeyword] = useState('');
+
+  const handleSearch = () => {
+    onSearch?.(keyword);
+  };
+
+  return (
+    <View style={styles.container}>
+      {/* Logo - 134rpx = 67pt, 50rpx = 25pt */}
+      <View style={styles.logo}>
+        <Image source={Images.home.portrait} style={styles.logoImage} contentFit="contain" />
+      </View>
+      {/* 搜索框 - 328rpx = 164pt, 56rpx = 28pt */}
+      <View style={styles.searchBox}>
+        <TouchableOpacity onPress={handleSearch}>
+          <Image source={Images.home.search} style={styles.searchIcon} contentFit="contain" />
+        </TouchableOpacity>
+        <TextInput
+          style={styles.input}
+          placeholder="搜索"
+          placeholderTextColor="#C4C3C3"
+          value={keyword}
+          onChangeText={setKeyword}
+          onSubmitEditing={handleSearch}
+          returnKeyType="search"
+        />
+      </View>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 15,
+    paddingVertical: 10,
+  },
+  logo: {
+    width: 67,
+    height: 25,
+    marginRight: 20,
+  },
+  logoImage: {
+    width: '100%',
+    height: '100%',
+  },
+  searchBox: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: 'rgba(255,255,255,0.38)',
+    borderRadius: 180,
+    paddingHorizontal: 12,
+    height: 28,
+    width: 164,
+  },
+  searchIcon: {
+    width: 15,
+    height: 15,
+    marginRight: 5,
+  },
+  input: {
+    width: 100,
+    color: '#fff',
+    fontSize: 12,
+    padding: 0,
+  },
+});

+ 505 - 0
components/product/CheckoutModal.tsx

@@ -0,0 +1,505 @@
+import { Image } from 'expo-image';
+import { useRouter } from 'expo-router';
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import {
+    Alert,
+    Modal,
+    ScrollView,
+    StyleSheet,
+    Switch,
+    Text,
+    TextInput,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Address, getDefaultAddress } from '@/services/address';
+import { GoodsDetail, previewSubmit, PreviewSubmitResult, submitOrder } from '@/services/mall';
+
+interface CheckoutModalProps {
+  data: GoodsDetail;
+  goodsId: string;
+  subjectId?: string;
+}
+
+export interface CheckoutModalRef {
+  show: (preview: PreviewSubmitResult) => void;
+  close: () => void;
+}
+
+export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
+  ({ data, goodsId, subjectId }, ref) => {
+    const router = useRouter();
+    const insets = useSafeAreaInsets();
+
+    const [visible, setVisible] = useState(false);
+    const [preview, setPreview] = useState<PreviewSubmitResult | null>(null);
+    const [address, setAddress] = useState<Address | null>(null);
+    const [quantity, setQuantity] = useState(1);
+    const [phone, setPhone] = useState('');
+    const [checked, setChecked] = useState(true);
+    const [useWallet, setUseWallet] = useState(false);
+    const [payType, setPayType] = useState<'alipay' | 'wxpay'>('alipay');
+    const [submitting, setSubmitting] = useState(false);
+
+    useImperativeHandle(ref, () => ({
+      show: (previewData: PreviewSubmitResult) => {
+        setPreview(previewData);
+        setVisible(true);
+        loadAddress();
+      },
+      close: () => {
+        setVisible(false);
+      },
+    }));
+
+    // 加载默认地址
+    const loadAddress = async () => {
+      try {
+        const addr = await getDefaultAddress();
+        setAddress(addr);
+      } catch (error) {
+        console.error('加载地址失败:', error);
+      }
+    };
+
+    // 计算最终价格
+    const lastPrice = preview
+      ? data.sellType === 2
+        ? preview.depositAmount || 0
+        : preview.paymentAmount
+      : data.price;
+
+    // 增加数量
+    const increment = async () => {
+      if (subjectId) return; // 秒杀不能改数量
+      if (quantity >= 5) return;
+      const newQty = quantity + 1;
+      setQuantity(newQty);
+      // 重新获取价格
+      const newPreview = await previewSubmit({ goodsId, quantity: newQty, subjectId });
+      if (newPreview) setPreview(newPreview);
+    };
+
+    // 减少数量
+    const decrement = async () => {
+      if (quantity <= 1) return;
+      const newQty = quantity - 1;
+      setQuantity(newQty);
+      const newPreview = await previewSubmit({ goodsId, quantity: newQty, subjectId });
+      if (newPreview) setPreview(newPreview);
+    };
+
+    // 提交订单
+    const handlePay = async () => {
+      if (!address) {
+        Alert.alert('提示', '请选择收货地址');
+        return;
+      }
+      if (data.sellType === 2 && !phone) {
+        Alert.alert('提示', '请填写尾款提醒手机号');
+        return;
+      }
+      if (!checked) {
+        Alert.alert('提示', '请勾选协议');
+        return;
+      }
+
+      setSubmitting(true);
+      try {
+        const paymentType = useWallet ? 'WALLET' : payType === 'alipay' ? 'ALIPAY' : 'WXPAY';
+        const result = await submitOrder({
+          goodsId: data.id,
+          paymentType,
+          addressBookId: address.id,
+          quantity,
+          restNotifyMobile: phone,
+          subjectId,
+        });
+
+        if (result) {
+          if (result.paySuccess) {
+            setVisible(false);
+            Alert.alert('成功', '支付成功', [
+              { text: '查看订单', onPress: () => router.push('/orders' as any) },
+            ]);
+          } else {
+            // 需要跳转支付
+            Alert.alert('提示', '请在支付页面完成支付');
+          }
+        }
+      } catch (error) {
+        console.error('提交订单失败:', error);
+        Alert.alert('错误', '提交订单失败');
+      }
+      setSubmitting(false);
+    };
+
+    // 选择地址
+    const selectAddress = () => {
+      setVisible(false);
+      router.push('/address?type=1' as any);
+    };
+
+    return (
+      <Modal visible={visible} transparent animationType="slide">
+        <View style={styles.overlay}>
+          <TouchableOpacity style={styles.mask} onPress={() => setVisible(false)} />
+          <View style={[styles.content, { paddingBottom: insets.bottom + 20 }]}>
+            {/* 标题 */}
+            <View style={styles.header}>
+              <Text style={styles.title}>确认订单</Text>
+              <TouchableOpacity onPress={() => setVisible(false)}>
+                <Text style={styles.closeBtn}>×</Text>
+              </TouchableOpacity>
+            </View>
+
+            <ScrollView style={styles.scrollContent} showsVerticalScrollIndicator={false}>
+              {/* 商品信息 */}
+              <View style={styles.goodsInfo}>
+                <Image source={data.cover} style={styles.goodsImage} contentFit="cover" />
+                <View style={styles.goodsDetail}>
+                  <Text style={styles.goodsName} numberOfLines={2}>{data.name}</Text>
+                  <View style={styles.priceRow}>
+                    <Text style={styles.price}>¥{data.subjectPrice || data.price}</Text>
+                    {data.sellType === 2 && (
+                      <Text style={styles.deposit}>定金:¥{data.deposit}</Text>
+                    )}
+                  </View>
+                  {/* 数量选择 */}
+                  <View style={styles.quantityRow}>
+                    <TouchableOpacity style={styles.qtyBtn} onPress={decrement}>
+                      <Text style={styles.qtyBtnText}>-</Text>
+                    </TouchableOpacity>
+                    <Text style={styles.qtyNum}>{quantity}</Text>
+                    <TouchableOpacity
+                      style={[styles.qtyBtn, subjectId ? styles.qtyBtnDisabled : null]}
+                      onPress={increment}
+                    >
+                      <Text style={styles.qtyBtnText}>+</Text>
+                    </TouchableOpacity>
+                  </View>
+                </View>
+              </View>
+
+              {/* 优惠券 */}
+              <View style={styles.row}>
+                <Text style={styles.rowLabel}>优惠券</Text>
+                <Text style={styles.rowValue}>-¥{preview?.couponAmount || 0}</Text>
+              </View>
+
+              {/* 钱包支付 */}
+              {preview?.cash && (
+                <View style={styles.row}>
+                  <Text style={styles.rowLabel}>
+                    使用钱包支付 <Text style={styles.balance}>(余额:¥{preview.cash.balance})</Text>
+                  </Text>
+                  <Switch value={useWallet} onValueChange={setUseWallet} />
+                </View>
+              )}
+
+              {/* 支付方式 */}
+              {!useWallet && (
+                <View style={styles.payTypeSection}>
+                  <TouchableOpacity
+                    style={styles.payTypeItem}
+                    onPress={() => setPayType('alipay')}
+                  >
+                    <Text style={styles.payTypeName}>支付宝</Text>
+                    <View style={[styles.radio, payType === 'alipay' && styles.radioActive]} />
+                  </TouchableOpacity>
+                  <TouchableOpacity
+                    style={styles.payTypeItem}
+                    onPress={() => setPayType('wxpay')}
+                  >
+                    <Text style={styles.payTypeName}>微信支付</Text>
+                    <View style={[styles.radio, payType === 'wxpay' && styles.radioActive]} />
+                  </TouchableOpacity>
+                </View>
+              )}
+
+              {/* 尾款手机号 */}
+              {data.sellType === 2 && (
+                <View style={styles.phoneRow}>
+                  <Text style={styles.phoneLabel}>
+                    <Text style={styles.required}>*</Text> 尾款提醒手机号
+                  </Text>
+                  <TextInput
+                    style={styles.phoneInput}
+                    value={phone}
+                    onChangeText={setPhone}
+                    placeholder="尾款支付提醒短信将发至此号码"
+                    placeholderTextColor="#999"
+                    keyboardType="phone-pad"
+                  />
+                </View>
+              )}
+
+              {/* 收货地址 */}
+              <TouchableOpacity style={styles.addressRow} onPress={selectAddress}>
+                {address ? (
+                  <View style={styles.addressInfo}>
+                    <Text style={styles.addressName}>
+                      {address.contactName} {address.contactNo}
+                    </Text>
+                    <Text style={styles.addressDetail}>
+                      {address.province}{address.city}{address.district}{address.address}
+                    </Text>
+                  </View>
+                ) : (
+                  <Text style={styles.noAddress}>
+                    <Text style={styles.required}>*</Text> 请填写收货地址
+                  </Text>
+                )}
+                <Text style={styles.arrow}>›</Text>
+              </TouchableOpacity>
+
+              {/* 协议 */}
+              <View style={styles.agreementRow}>
+                <Text style={styles.agreementText}>同意《平台服务协议》详情</Text>
+                <Switch value={checked} onValueChange={setChecked} />
+              </View>
+            </ScrollView>
+
+            {/* 支付按钮 */}
+            <TouchableOpacity
+              style={[styles.payBtn, submitting && styles.payBtnDisabled]}
+              onPress={handlePay}
+              disabled={submitting}
+            >
+              <Text style={styles.payBtnText}>
+                ¥{lastPrice} 立即支付
+              </Text>
+            </TouchableOpacity>
+          </View>
+        </View>
+      </Modal>
+    );
+  }
+);
+
+const styles = StyleSheet.create({
+  overlay: {
+    flex: 1,
+    justifyContent: 'flex-end',
+  },
+  mask: {
+    ...StyleSheet.absoluteFillObject,
+    backgroundColor: 'rgba(0,0,0,0.5)',
+  },
+  content: {
+    backgroundColor: '#fff',
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    maxHeight: '80%',
+  },
+  header: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    alignItems: 'center',
+    paddingVertical: 15,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+  },
+  title: {
+    fontSize: 16,
+    fontWeight: '600',
+    color: '#333',
+  },
+  closeBtn: {
+    position: 'absolute',
+    right: 15,
+    fontSize: 28,
+    color: '#999',
+  },
+  scrollContent: {
+    paddingHorizontal: 15,
+  },
+  goodsInfo: {
+    flexDirection: 'row',
+    paddingVertical: 15,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+  },
+  goodsImage: {
+    width: 90,
+    height: 90,
+    borderRadius: 8,
+    backgroundColor: '#f5f5f5',
+  },
+  goodsDetail: {
+    flex: 1,
+    marginLeft: 12,
+    justifyContent: 'space-between',
+  },
+  goodsName: {
+    fontSize: 14,
+    color: '#333',
+    lineHeight: 20,
+  },
+  priceRow: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  price: {
+    fontSize: 18,
+    fontWeight: 'bold',
+    color: '#ff6b00',
+  },
+  deposit: {
+    fontSize: 12,
+    color: '#8b3dff',
+    marginLeft: 10,
+    backgroundColor: 'rgba(139,61,255,0.1)',
+    paddingHorizontal: 8,
+    paddingVertical: 2,
+    borderRadius: 10,
+  },
+  quantityRow: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  qtyBtn: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    backgroundColor: '#f5f5f5',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  qtyBtnDisabled: {
+    opacity: 0.3,
+  },
+  qtyBtnText: {
+    fontSize: 18,
+    color: '#333',
+  },
+  qtyNum: {
+    fontSize: 16,
+    color: '#333',
+    marginHorizontal: 15,
+  },
+  row: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingVertical: 15,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+  },
+  rowLabel: {
+    fontSize: 14,
+    color: '#333',
+  },
+  rowValue: {
+    fontSize: 14,
+    color: '#ff6b00',
+  },
+  balance: {
+    color: '#ff6b00',
+  },
+  payTypeSection: {
+    paddingVertical: 10,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+  },
+  payTypeItem: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingVertical: 10,
+  },
+  payTypeName: {
+    fontSize: 14,
+    color: '#333',
+  },
+  radio: {
+    width: 20,
+    height: 20,
+    borderRadius: 10,
+    borderWidth: 2,
+    borderColor: '#ddd',
+  },
+  radioActive: {
+    borderColor: '#ff6b00',
+    backgroundColor: '#ff6b00',
+  },
+  phoneRow: {
+    paddingVertical: 15,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+  },
+  phoneLabel: {
+    fontSize: 14,
+    color: '#333',
+    marginBottom: 10,
+  },
+  required: {
+    color: '#dd524d',
+  },
+  phoneInput: {
+    height: 40,
+    backgroundColor: '#f5f5f5',
+    borderRadius: 8,
+    paddingHorizontal: 12,
+    fontSize: 14,
+  },
+  addressRow: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingVertical: 15,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+  },
+  addressInfo: {
+    flex: 1,
+  },
+  addressName: {
+    fontSize: 14,
+    color: '#333',
+    fontWeight: '500',
+  },
+  addressDetail: {
+    fontSize: 12,
+    color: '#666',
+    marginTop: 4,
+  },
+  noAddress: {
+    fontSize: 14,
+    color: '#999',
+  },
+  arrow: {
+    fontSize: 20,
+    color: '#999',
+    marginLeft: 10,
+  },
+  agreementRow: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingVertical: 15,
+  },
+  agreementText: {
+    fontSize: 14,
+    color: '#666',
+  },
+  payBtn: {
+    marginHorizontal: 15,
+    marginTop: 15,
+    height: 50,
+    backgroundColor: '#ff6b00',
+    borderRadius: 25,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  payBtnDisabled: {
+    opacity: 0.6,
+  },
+  payBtnText: {
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: '600',
+  },
+});

+ 115 - 0
constants/images.ts

@@ -0,0 +1,115 @@
+// 图片资源配置 - 对应 supermart-mini 的 ossurl.js
+const CDN_BASE = 'https://cdn.acetoys.cn/kai_xin_ma_te/supermart';
+const CDN_SUPERMART = 'https://cdn.acetoys.cn/supermart';
+
+export const Images = {
+  common: {
+    indexBg: `${CDN_BASE}/common/indexBg.png`,
+    awardBg: `${CDN_BASE}/common/awardBg.png`,
+    wealBg: `${CDN_BASE}/common/wealBg.png`,
+    tabsBg: `${CDN_BASE}/common/tab_bottom.png`,
+    loginBg: `${CDN_BASE}/common/loginBg.png`,
+    loginBtn: `${CDN_BASE}/common/loginBug.png`,
+    commonBg: `${CDN_BASE}/common/commonBg.png`,
+    defaultAvatar: `${CDN_BASE}/common/noavatar.png`,
+    topBtn: `${CDN_BASE}/common/topBtn.png`,
+    // 新增图片资源
+    itemBg: `${CDN_BASE}/common/itemBg.png`,
+    windowBg: `${CDN_BASE}/common/windowBg.png`,
+    windowBg2: `${CDN_BASE}/common/windowBg2.png`,
+    windowTitle: `${CDN_BASE}/common/windowTitle.png`,
+    windowClose: `${CDN_BASE}/common/windowClose.png`,
+    windowInputBg: `${CDN_BASE}/common/windowInputBg.png`,
+    closeBut: `${CDN_BASE}/common/closeBut.png`,
+    windBg: `${CDN_BASE}/common/windBg.png`,
+    butBgH: `${CDN_BASE}/common/butBgH.png`,
+    butBgL: `${CDN_BASE}/common/butBgL.png`,
+    butBgHui: `${CDN_BASE}/common/butBgHui.png`,
+    butBgQL: `${CDN_BASE}/common/butBgQL.png`,
+    butBgV: `${CDN_BASE}/common/butBgV.png`,
+    commonBtnBg: `${CDN_BASE}/common/commonBtnBg.png`,
+    awardDetailBg: `${CDN_BASE}/common/awardDetailBg.png`,
+    posterBg: `${CDN_BASE}/common/posterBg.png`,
+    userShare: `${CDN_BASE}/common/userShare.jpg`,
+    pullNewUserBg: `${CDN_BASE}/common/pullNewUserBg.png`,
+    kaixinTabbarBg: `${CDN_BASE}/common/tabbarBg.png`,
+    kaixinTabbarBgAct: `${CDN_BASE}/common/tabbarBgAct.png`,
+    kaixinTabbarArrow: `${CDN_BASE}/common/tabbarActArrow.png`,
+    kaixinTabbar2Bg: `${CDN_BASE}/common/tabbar2Bg.png`,
+    kaixinTabbar2BgAct: `${CDN_BASE}/common/tabbar2BgAct.png`,
+    qijiCancelBtnBg: `${CDN_BASE}/common/qijiCancelBtnBg.png`,
+    qijiClose: `${CDN_SUPERMART}/box/qiji_close.png`,
+    qijiDialogTitBg: `${CDN_SUPERMART}/common/qiji_dialogTitBg.png`,
+  },
+  home: {
+    portrait: `${CDN_BASE}/home/portrait.png`,
+    search: `${CDN_BASE}/home/search.png`,
+    search2: `${CDN_BASE}/home/search2.png`,
+    typeBg: `${CDN_BASE}/home/typeBg.png`,
+    typeBgOn: `${CDN_BASE}/home/typeBgOn.png`,
+    cellGoodsBg: `${CDN_BASE}/home/cellGoodsBg.png`,
+    imgBoxBorder: `${CDN_BASE}/home/imgBoxBorder.png`,
+  },
+  tabs: {
+    home: `${CDN_BASE}/common/tabs1.png`,
+    homeActive: `${CDN_BASE}/common/tabs11.png`,
+    box: `${CDN_BASE}/common/tabs2.png`,
+    boxActive: `${CDN_BASE}/common/tabs22.png`,
+    welfare: `${CDN_BASE}/common/tabs3.png`,
+    welfareActive: `${CDN_BASE}/common/tabs33.png`,
+    mine: `${CDN_BASE}/common/tabs4.png`,
+    mineActive: `${CDN_BASE}/common/tabs44.png`,
+  },
+  box: {
+    awardMainImg: `${CDN_BASE}/box/awardMainImg.png`,
+    goodsItemBg: `${CDN_BASE}/box/goodsItemBg.png`,
+    barrageItem: `${CDN_BASE}/box/barrageItem.png`,
+    type1: `${CDN_BASE}/box/type1.png`,
+    type1On: `${CDN_BASE}/box/type1On.png`,
+    type2: `${CDN_BASE}/box/type2.png`,
+    type2On: `${CDN_BASE}/box/type2On.png`,
+    type3: `${CDN_BASE}/box/type3.png`,
+    type3On: `${CDN_BASE}/box/type3On.png`,
+    type4: `${CDN_BASE}/box/type4.png`,
+    type4On: `${CDN_BASE}/box/type4On.png`,
+    type5: `${CDN_BASE}/box/type5.png`,
+    type5On: `${CDN_BASE}/box/type5On.png`,
+    sortAmount: `${CDN_BASE}/box/sortAmount.png`,
+    sortAmountOnT: `${CDN_BASE}/box/sortAmountOnT.png`,
+    sortAmountOnB: `${CDN_BASE}/box/sortAmountOnB.png`,
+  },
+  welfare: {
+    kaixinWelfareBg: `${CDN_BASE}/welfare/kaixinWelfareBg.png`,
+    kaixinRoomBg: `${CDN_BASE}/welfare/kaixinRoomBg.png`,
+    kaixinRoom1: `${CDN_BASE}/welfare/kaixinRoom1.png`,
+    kaixinRoom2: `${CDN_BASE}/welfare/kaixinRoom2.png`,
+    kaixinRoom3: `${CDN_BASE}/welfare/kaixinRoom3.png`,
+    wealTitle: `${CDN_BASE}/welfare/wealTitle.png`,
+    indexItem1: `${CDN_BASE}/welfare/indexItem1.png`,
+    indexItem2: `${CDN_BASE}/welfare/indexItem2.png`,
+    indexItem3: `${CDN_BASE}/welfare/indexItem3.png`,
+  },
+  mine: {
+    kaixinMineBg: `${CDN_BASE}/mine/kaixinMineBg.png`,
+    kaixinMineHeadBg: `${CDN_BASE}/mine/kaixinMineHeadBg.png`,
+    kaixinUserDataBg: `${CDN_BASE}/mine/kaixinUserDataBg.png`,
+    kaixinUserCopyIcon: `${CDN_BASE}/mine/kaixinUserCopyIcon.png`,
+    editIcon: `${CDN_BASE}/mine/editIcon.png`,
+    userSection1Bg: `${CDN_BASE}/mine/userSection1Bg.png`,
+    userSection2Bg: `${CDN_BASE}/mine/userSection2Bg.png`,
+    invite: `${CDN_BASE}/mine/invite.png`,
+    kaixinintegral: `${CDN_BASE}/mine/kaixinintegral.png`,
+    message: `${CDN_BASE}/mine/message.png`,
+    kaixinorder: `${CDN_BASE}/mine/kaixinorder.png`,
+    order1: `${CDN_BASE}/mine/order1.png`,
+    order2: `${CDN_BASE}/mine/order2.png`,
+  },
+  // 地址相关
+  address: {
+    itemBg: `${CDN_BASE}/common/itemBg.png`,
+  },
+  // 订单相关
+  order: {
+    itemBg: `${CDN_BASE}/common/itemBg.png`,
+  },
+};

+ 69 - 0
contexts/AuthContext.tsx

@@ -0,0 +1,69 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+
+import { getToken } from '@/services/http';
+import { getUserInfo, UserInfo } from '@/services/user';
+
+interface AuthContextType {
+  isLoggedIn: boolean;
+  user: UserInfo | null;
+  loading: boolean;
+  refreshUser: () => Promise<void>;
+  logout: () => void;
+}
+
+const AuthContext = createContext<AuthContextType>({
+  isLoggedIn: false,
+  user: null,
+  loading: true,
+  refreshUser: async () => {},
+  logout: () => {},
+});
+
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+  const [user, setUser] = useState<UserInfo | null>(null);
+  const [loading, setLoading] = useState(true);
+
+  const refreshUser = async () => {
+    try {
+      const token = getToken();
+      if (token) {
+        const info = await getUserInfo();
+        setUser(info);
+      } else {
+        setUser(null);
+      }
+    } catch (error) {
+      console.error('获取用户信息失败:', error);
+      setUser(null);
+    }
+  };
+
+  const logout = () => {
+    setUser(null);
+    // 清除token在http服务中处理
+  };
+
+  useEffect(() => {
+    const init = async () => {
+      await refreshUser();
+      setLoading(false);
+    };
+    init();
+  }, []);
+
+  return (
+    <AuthContext.Provider
+      value={{
+        isLoggedIn: !!user,
+        user,
+        loading,
+        refreshUser,
+        logout,
+      }}
+    >
+      {children}
+    </AuthContext.Provider>
+  );
+}
+
+export const useAuth = () => useContext(AuthContext);

+ 11 - 0
package-lock.json

@@ -13,6 +13,7 @@
         "@react-navigation/elements": "^2.6.3",
         "@react-navigation/native": "^7.1.8",
         "expo": "~54.0.30",
+        "expo-clipboard": "~8.0.8",
         "expo-constants": "~18.0.12",
         "expo-font": "~14.0.10",
         "expo-haptics": "~15.0.8",
@@ -6075,6 +6076,16 @@
         "react-native": "*"
       }
     },
+    "node_modules/expo-clipboard": {
+      "version": "8.0.8",
+      "resolved": "https://registry.npmmirror.com/expo-clipboard/-/expo-clipboard-8.0.8.tgz",
+      "integrity": "sha512-VKoBkHIpZZDJTB0jRO4/PZskHdMNOEz3P/41tmM6fDuODMpqhvyWK053X0ebspkxiawJX9lX33JXHBCvVsTTOA==",
+      "peerDependencies": {
+        "expo": "*",
+        "react": "*",
+        "react-native": "*"
+      }
+    },
     "node_modules/expo-constants": {
       "version": "18.0.12",
       "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.12.tgz",

+ 3 - 2
package.json

@@ -16,6 +16,7 @@
     "@react-navigation/elements": "^2.6.3",
     "@react-navigation/native": "^7.1.8",
     "expo": "~54.0.30",
+    "expo-clipboard": "~8.0.8",
     "expo-constants": "~18.0.12",
     "expo-font": "~14.0.10",
     "expo-haptics": "~15.0.8",
@@ -31,11 +32,11 @@
     "react-dom": "19.1.0",
     "react-native": "0.81.5",
     "react-native-gesture-handler": "~2.28.0",
-    "react-native-worklets": "0.5.1",
     "react-native-reanimated": "~4.1.1",
     "react-native-safe-area-context": "~5.6.0",
     "react-native-screens": "~4.16.0",
-    "react-native-web": "~0.21.0"
+    "react-native-web": "~0.21.0",
+    "react-native-worklets": "0.5.1"
   },
   "devDependencies": {
     "@types/react": "~19.1.0",

+ 99 - 0
services/address.ts

@@ -0,0 +1,99 @@
+// 地址服务 - 对应 supermart-mini/service/address.js
+import { get, postL } from './http';
+
+const apis = {
+  LIST: '/api/addressBook',
+  ADD: '/api/addressBook/add',
+  DELETE: '/api/addressBook/delete',
+  DEFAULT: '/api/addressBook/getDefault',
+  UPDATE: '/api/addressBook/update',
+  UPDATE_ORDER_ADDRESS: '/api/mallOrder/updateAddress',
+  GET_AREA: '/api/area',
+};
+
+export interface Address {
+  id: string;
+  contactName: string;
+  contactNo: string;
+  province: string;
+  city: string;
+  district: string;
+  address: string;
+  location?: string;
+  defaultFlag?: number;
+}
+
+export interface AddAddressParams {
+  contactName: string;
+  contactNo: string;
+  province: string;
+  city: string;
+  district?: string;
+  address: string;
+  location?: string;
+  defaultFlag?: number;
+}
+
+export interface AreaItem {
+  id: string;
+  name: string;
+  pid?: number;
+}
+
+// 获取默认地址
+export const getDefaultAddress = async (): Promise<Address | null> => {
+  const res = await get<Address>(apis.DEFAULT);
+  if (res.data && res.data.id) {
+    return res.data;
+  }
+  return null;
+};
+
+// 获取地址列表
+export const getAddressList = async (size = 100): Promise<Address[]> => {
+  const res = await get<{ records: Address[] }>(apis.LIST, { size });
+  return res.data?.records || [];
+};
+
+// 添加地址
+export const addAddress = async (params: AddAddressParams): Promise<boolean> => {
+  const res = await postL(apis.ADD, {
+    ...params,
+    district: params.district || '',
+  });
+  return res.success;
+};
+
+// 删除地址
+export const deleteAddress = async (id: string): Promise<boolean> => {
+  const res = await postL(`${apis.DELETE}/${id}`);
+  return res.success;
+};
+
+// 更新地址(设为默认)
+export const updateAddress = async (address: Address): Promise<boolean> => {
+  const res = await postL(apis.UPDATE, address);
+  return res.success;
+};
+
+// 更新订单地址
+export const updateOrderAddress = async (params: { tradeNo: string; addressBookId: string }) => {
+  const res = await postL(apis.UPDATE_ORDER_ADDRESS, params);
+  return res;
+};
+
+// 获取地区数据
+export const getArea = async (pid?: number): Promise<AreaItem[]> => {
+  const res = await get<AreaItem[]>(apis.GET_AREA, pid ? { pid } : { pid: 1 });
+  return res.data || [];
+};
+
+export default {
+  getDefaultAddress,
+  getAddressList,
+  addAddress,
+  deleteAddress,
+  updateAddress,
+  updateOrderAddress,
+  getArea,
+};

+ 32 - 0
services/api.ts

@@ -0,0 +1,32 @@
+// API 服务层 - 整合所有服务
+export * from './address';
+export * from './award';
+export * from './base';
+export * from './config';
+export * from './http';
+export * from './mall';
+export * from './user';
+
+// 重新导出类型
+export type { AddAddressParams, Address, AreaItem } from './address';
+export type { IPItem, PoolItem } from './award';
+export type { BannerItem, PageConfig, TabItem } from './base';
+export type { GoodsDetail, GoodsItem, GoodsListParams, OrderDetail, OrderItem } from './mall';
+export type { LoginParams, ParamConfig, UserInfo } from './user';
+
+// 默认导出所有服务
+import addressService from './address';
+import awardService from './award';
+import baseService from './base';
+import mallService from './mall';
+import userService from './user';
+
+export const services = {
+  mall: mallService,
+  award: awardService,
+  base: baseService,
+  address: addressService,
+  user: userService,
+};
+
+export default services;

+ 392 - 0
services/award.ts

@@ -0,0 +1,392 @@
+// 奖池服务 - 对应 supermart-mini/service/award.js
+import { get, post, postL } from './http';
+
+const apis = {
+  INDEX: '/api/app/index/magicMartIndex',
+  IP_LIST: '/api/spu/basic/listWorksLimit',
+  MAGIC_INDEX: '/api/app/index/magicBoxIndex',
+  LIST: '/api/luckPool/list',
+  HORSE: '/api/luckPool/horseRaceLampByIndex',
+  DETAIL: '/api/luckPool/detail',
+  PRODUCTS: '/api/luckPool/prize',
+  HORSE_DETAIL: '/api/luckPool/horseRaceLampByDetail',
+  BUY_RECORD: '/api/luckPool/buyRecord',
+  VERSION: '/api/luckPool/version',
+  PREVIEW: '/api/luckOrder/preSubmit',
+  APPLY: '/api/luckOrder/submit',
+  APPLY_TRY: '/api/luckOrder/demo',
+  APPLY_RESULT: '/api/luckOrder/luckResult',
+  ORDERS: '/api/luckOrder/pageMy',
+  STORE: '/api/luckInventory',
+  STORE_MOVE_SAFE: '/api/luckInventory/addToSafe',
+  STORE_OUT_SAFE: '/api/luckInventory/moveFromSafe',
+  CONVERT_PREVIEW: '/api/luckExchangeOrder/preSubmit',
+  CONVERT_APPLY: '/api/luckExchangeOrder/submit',
+  CONVERT_LIST: '/api/luckExchangeOrder/pageMy',
+  CONVERT_ALL_PREVIEW: '/api/luckExchangeOrder/preOneKeySubmit',
+  CONVERT_ALL: '/api/luckExchangeOrder/oneKeySubmit',
+  TAKE_PREVIEW: '/api/luckPickupOrder/preSubmit',
+  TAKE_APPLY: '/api/luckPickupOrder/submit',
+  TAKE_LIST: '/api/luckPickupOrder/pageMy',
+  TAKE_PAY_ORDER: '/api/luckPickupOrder/pay',
+  POOL_IN: '/api/luckPool/participate',
+  POOL_OUT: '/api/luckPool/unparticipate',
+  PACKAGES: '/api/luckPickupOrder/expressInfo',
+  EXPRESS: '/api/luckPickupOrder/expressDetail',
+  KING: '/api/luckKing/getTheLatestLuckKingRecord',
+  KING_USER: '/api/luckKing/listBillboard',
+  KING_GOODS: '/api/luckKing/listGoods',
+  CHECK_PAYMENT_STATUS: '/api/luckOrder/checkPaymentStatus',
+  GENERATE_PAYMENT_LINK: '/api/luckOrder/generatePaymentLink',
+  KING_PRE: '/api/luckKing/getThePrev',
+  BOX_LIST: '/api/luckBox',
+  BOX_DETAIL: '/api/luckBox/detail',
+  BOX_PRE: '/api/luckBox/getThePrev',
+  BOX_NEXT: '/api/luckBox/getTheNext',
+  UNAVAILABLE_SEAT_NUMBERS: '/api/luckBox/getUnavaliableSeatNumbers',
+  BOX_LOCK: '/api/luckBox/lock',
+  BOX_UN_LOCK: '/api/luckBox/unlock',
+  FEEDBACK_LIST: '/api/app/feedback/list',
+  FEEDBACK_SUBMIT: '/api/app/feedback/submit',
+};
+
+export interface IPItem {
+  id: string;
+  name: string;
+  cover?: string;
+}
+
+export interface PoolItem {
+  id: string;
+  name: string;
+  cover: string;
+  price: number;
+  mode?: string;
+  type?: number;
+  status?: number;
+}
+
+// 获取首页数据
+export const getIndex = async (type = 0) => {
+  const res = await get(apis.INDEX, { type });
+  return res.data;
+};
+
+// 获取 IP 列表
+export const getIPList = async (limit = 200): Promise<IPItem[]> => {
+  const res = await get<IPItem[]>(apis.IP_LIST, { limit });
+  return res.data || [];
+};
+
+// 获取魔盒首页
+export const getMagicIndex = async () => {
+  const res = await get(apis.MAGIC_INDEX);
+  return res.data;
+};
+
+// 获取奖池列表
+export const getPoolList = async (params: {
+  current: number;
+  size: number;
+  mode?: string;
+  type?: number;
+  worksId?: string;
+  keyword?: string;
+  priceMin?: number;
+  priceMax?: number;
+  priceSort?: number;
+}) => {
+  // 处理 mode 参数
+  let processedMode = params.mode;
+  if (processedMode && processedMode.startsWith('UNLIMITED')) {
+    processedMode = 'UNLIMITED';
+  }
+  const res = await post<PoolItem[]>(apis.LIST, { ...params, mode: processedMode });
+  return res;
+};
+
+// 获取跑马灯数据
+export const getHorse = async () => {
+  const res = await post(apis.HORSE);
+  return res.success ? res.data : null;
+};
+
+// 获取奖池详情
+export const getPoolDetail = async (poolId: string) => {
+  const res = await get(apis.DETAIL, { poolId });
+  return res.data;
+};
+
+// 获取奖池商品
+export const getPoolProducts = async (poolId: string) => {
+  const res = await get(apis.PRODUCTS, { poolId });
+  return res.data;
+};
+
+// 获取详情跑马灯
+export const getDetailHorse = async (poolId: string, size = 50) => {
+  const res = await post(apis.HORSE_DETAIL, { poolId, size });
+  return res.success ? res.data : null;
+};
+
+// 获取购买记录
+export const getBuyRecord = async (poolId: string, lastId?: string, level?: number, size = 200) => {
+  const res = await post(apis.BUY_RECORD, { poolId, lastId, level, size });
+  return res.data;
+};
+
+// 获取版本信息
+export const getVersion = async (poolId: string, size = 20) => {
+  const res = await post(apis.VERSION, { poolId, size });
+  return res.data;
+};
+
+// 试玩
+export const tryDemo = async (poolId: string, quantity: number) => {
+  const res = await postL(apis.APPLY_TRY, { poolId, quantity });
+  return res.data;
+};
+
+// 预提交订单
+export const previewOrder = async (poolId: string, quantity?: number, boxNumber?: string, seatNumbers?: number[], packFlag?: boolean) => {
+  const param: any = { poolId };
+  if (quantity) param.quantity = quantity;
+  if (boxNumber) param.boxNumber = boxNumber;
+  if (seatNumbers && seatNumbers.length > 0) param.seatNumbers = seatNumbers;
+  if (packFlag) param.packFlag = packFlag;
+  const res = await postL(apis.PREVIEW, param);
+  return res.data;
+};
+
+// 提交订单
+export const applyOrder = async (poolId: string, quantity: number, paymentType: string, boxNumber?: string, seatNumbers?: number[], packFlag?: boolean) => {
+  const param: any = { poolId, quantity, paymentType };
+  if (boxNumber) param.boxNumber = boxNumber;
+  if (seatNumbers && seatNumbers.length > 0) param.seatNumbers = seatNumbers;
+  if (packFlag) param.packFlag = packFlag;
+  const res = await postL(apis.APPLY, param);
+  return res.data;
+};
+
+// 获取抽奖结果
+export const getApplyResult = async (tradeNo: string) => {
+  const res = await get(apis.APPLY_RESULT, { tradeNo });
+  return res.data;
+};
+
+// 检查支付状态
+export const checkPaymentStatus = async (tradeNo: string) => {
+  const res = await get(apis.CHECK_PAYMENT_STATUS, { tradeNo });
+  return res.data;
+};
+
+// 获取盲盒订单列表
+export const getAwardOrders = async (current: number, size: number, tab?: string) => {
+  const res = await post(apis.ORDERS, { current, size, tab });
+  return res.data;
+};
+
+// 获取仓库列表
+export const getStore = async (current: number, size: number, safeFlag?: number, tab?: string) => {
+  const res = await post(apis.STORE, { current, size, status: 0, tab, safeFlag });
+  return res.data;
+};
+
+// 移入保险箱
+export const moveToSafeStore = async (inventoryIds: string[]) => {
+  const res = await postL(apis.STORE_MOVE_SAFE, { inventoryIds });
+  return res.success;
+};
+
+// 移出保险箱
+export const moveOutSafeStore = async (inventoryIds: string[]) => {
+  const res = await postL(apis.STORE_OUT_SAFE, { inventoryIds });
+  return res.success;
+};
+
+// 兑换预览
+export const convertPreview = async (inventoryIds: string[]) => {
+  const res = await postL(apis.CONVERT_PREVIEW, { inventoryIds });
+  return res;
+};
+
+// 兑换申请
+export const convertApply = async (inventoryIds: string[]) => {
+  const res = await postL(apis.CONVERT_APPLY, { inventoryIds });
+  return res.success;
+};
+
+// 兑换列表
+export const getConvertList = async (current: number, size: number) => {
+  const res = await post(apis.CONVERT_LIST, { current, size });
+  return res.data;
+};
+
+// 一键兑换预览
+export const convertAllPreview = async () => {
+  const res = await postL(apis.CONVERT_ALL_PREVIEW);
+  return res.data;
+};
+
+// 一键兑换
+export const convertAll = async (levels: number[]) => {
+  const res = await postL(apis.CONVERT_ALL, { levels });
+  return res.success;
+};
+
+// 提货预览
+export const takePreview = async (inventoryIds: string[], addressBookId: string) => {
+  const res = await postL(apis.TAKE_PREVIEW, { inventoryIds, addressBookId });
+  return res.data;
+};
+
+// 提货申请
+export const takeApply = async (inventoryIds: string[], addressBookId: string, paymentType: string) => {
+  const res = await postL(apis.TAKE_APPLY, { inventoryIds, addressBookId, paymentType });
+  return res.data;
+};
+
+// 提货列表
+export const getTakeList = async (current: number, size: number) => {
+  const res = await post(apis.TAKE_LIST, { current, size });
+  return res.data;
+};
+
+// 提货支付
+export const takePayOrder = async (inventoryIds: string[], addressBookId: string, paymentType: string, tradeNo?: string) => {
+  const res = await postL(apis.TAKE_PAY_ORDER, { inventoryIds, addressBookId, paymentType, tradeNo });
+  return res.data;
+};
+
+// 参与奖池
+export const poolIn = async (poolId: string) => {
+  const res = await get(apis.POOL_IN, { poolId });
+  return res.success;
+};
+
+// 退出奖池
+export const poolOut = async (poolId?: string) => {
+  const param: any = {};
+  if (poolId) param.poolId = poolId;
+  const res = await get(apis.POOL_OUT, param);
+  return res.success;
+};
+
+// 获取包裹信息
+export const getAwardPackages = async (tradeNo: string) => {
+  const res = await get(apis.PACKAGES, { tradeNo });
+  return res.data;
+};
+
+// 获取物流详情
+export const getAwardExpress = async (tradeNo: string, packageId: string) => {
+  const res = await get(apis.EXPRESS, { tradeNo, packageId });
+  return res.data;
+};
+
+// 获取欧皇信息
+export const getKing = async (poolId: string) => {
+  const res = await get(apis.KING, { poolId });
+  return res.success ? res.data : null;
+};
+
+// 获取欧皇榜单
+export const getKingUser = async (poolId: string) => {
+  const res = await get(apis.KING_USER, { poolId });
+  return res.data;
+};
+
+// 获取欧皇商品
+export const getKingGoods = async (poolId: string) => {
+  const res = await get(apis.KING_GOODS, { poolId });
+  return res.data;
+};
+
+// 获取上一期欧皇
+export const getPreKing = async (poolId: string) => {
+  const res = await get(apis.KING_PRE, { poolId });
+  return res.data;
+};
+
+// 获取盒子列表
+export const getBoxList = async (poolId: string, level?: number, current?: number, size?: number) => {
+  const res = await post(apis.BOX_LIST, { poolId, current, size, level });
+  return res.data;
+};
+
+// 获取盒子详情
+export const getBoxDetail = async (poolId: string, boxNumber?: string) => {
+  const param: any = { poolId };
+  if (boxNumber) param.boxNumber = boxNumber;
+  const res = await get(apis.BOX_DETAIL, param);
+  return res.data;
+};
+
+// 获取弹幕列表
+export const getFeedbackList = async () => {
+  const res = await get(apis.FEEDBACK_LIST);
+  return res;
+};
+
+// 发送弹幕
+export const submitFeedback = async (content: string) => {
+  const res = await postL(apis.FEEDBACK_SUBMIT, { content });
+  return res;
+};
+
+// 获取仓库商品详情
+export const getLuckDetail = async (id: string) => {
+  const res = await get('/api/luckInventory/detail', { id });
+  return res.data;
+};
+
+// 获取仓库商品总数
+export const getSumInventory = async () => {
+  const res = await get('/api/luckInventory');
+  return res.data;
+};
+
+export default {
+  getIndex,
+  getIPList,
+  getMagicIndex,
+  getPoolList,
+  getHorse,
+  getPoolDetail,
+  getPoolProducts,
+  getDetailHorse,
+  getBuyRecord,
+  getVersion,
+  tryDemo,
+  previewOrder,
+  applyOrder,
+  getApplyResult,
+  checkPaymentStatus,
+  getAwardOrders,
+  getStore,
+  moveToSafeStore,
+  moveOutSafeStore,
+  convertPreview,
+  convertApply,
+  getConvertList,
+  convertAllPreview,
+  convertAll,
+  takePreview,
+  takeApply,
+  getTakeList,
+  takePayOrder,
+  poolIn,
+  poolOut,
+  getAwardPackages,
+  getAwardExpress,
+  getKing,
+  getKingUser,
+  getKingGoods,
+  getPreKing,
+  getBoxList,
+  getBoxDetail,
+  getFeedbackList,
+  submitFeedback,
+  getLuckDetail,
+  getSumInventory,
+};

+ 68 - 0
services/base.ts

@@ -0,0 +1,68 @@
+// 基础服务 - 对应 supermart-mini/service/base.js
+import { get, post } from './http';
+
+const apis = {
+  PAGE_CONFIG: '/api/app/page/getByPageId',
+  MESSAGE: '/api/app/message',
+  TRACK: '/api/track',
+  FEEDBACK: '/api/app/feedback/submit',
+  PARAM_CONFIG: '/param/paramConfig',
+};
+
+export interface BannerItem {
+  id: string;
+  cover: string;
+  path?: { url: string };
+}
+
+export interface TabItem {
+  title: string;
+  cover: string;
+  path?: { url: string };
+}
+
+export interface PageConfig {
+  components: Array<{
+    elements: any[];
+  }>;
+}
+
+// 获取页面配置
+export const getPageConfig = async (pageId: string): Promise<PageConfig | null> => {
+  const res = await get<PageConfig>(apis.PAGE_CONFIG, { pageId });
+  return res.data;
+};
+
+// 获取消息列表
+export const getMessages = async (current: number, size: number, type?: string) => {
+  const params: any = { current, size };
+  if (type) params.type = type;
+  const res = await get(apis.MESSAGE, params);
+  return res.success ? res.data : null;
+};
+
+// 获取参数配置
+export const getParamConfig = async (code: string) => {
+  const res = await get(apis.PARAM_CONFIG, { code });
+  return res.data;
+};
+
+// 提交反馈
+export const submitFeedback = async (data: any) => {
+  const res = await post(apis.FEEDBACK, data);
+  return res.data;
+};
+
+// 追踪
+export const track = async () => {
+  const res = await get(apis.TRACK);
+  return res.data;
+};
+
+export default {
+  getPageConfig,
+  getMessages,
+  getParamConfig,
+  submitFeedback,
+  track,
+};

+ 14 - 0
services/config.ts

@@ -0,0 +1,14 @@
+// API 配置
+export const SERVICE_URL = 'https://mm.acetoys.cn';
+export const APP_VERSION = '1.0.0';
+export const WX_APP_ID = 'wxf3b4d740916d81be';
+
+// 通用请求头
+export const COMMON_HEADER = {
+  os: 5, // iOS
+  version: APP_VERSION,
+  deviceCode: '',
+  bundleId: 'com.wonderful.mart',
+  channel: 'wonderful',
+  clientId: WX_APP_ID,
+};

+ 144 - 0
services/http.ts

@@ -0,0 +1,144 @@
+// HTTP 请求封装
+import { router } from 'expo-router';
+
+import { COMMON_HEADER, SERVICE_URL } from './config';
+
+export const SUCCESS_CODE = 0;
+export const AUTH_INVALID = 401;
+export const AUTH_INVALID_2 = 403;
+
+interface RequestOptions {
+  loading?: boolean;
+  showMsg?: boolean;
+  method?: 'GET' | 'POST';
+  token?: string;
+}
+
+interface ApiResponse<T = any> {
+  code: number;
+  msg: string;
+  data: T;
+  success: boolean;
+  count?: number;
+}
+
+// 存储 token
+let authToken: string | null = null;
+
+export const setToken = (token: string | null) => {
+  authToken = token;
+};
+
+export const getToken = () => authToken;
+
+// 处理认证失败
+const handleAuthError = () => {
+  setToken(null);
+  // 跳转到登录页
+  router.push('/login');
+};
+
+// 基础请求方法
+export const request = async <T = any>(
+  url: string,
+  data: any = {},
+  options: RequestOptions = {}
+): Promise<ApiResponse<T>> => {
+  const { method = 'POST' } = options;
+
+  const headers: Record<string, string> = {
+    'Content-Type': 'application/json',
+    track: JSON.stringify(COMMON_HEADER),
+  };
+
+  if (authToken) {
+    headers.Authentication = authToken;
+  }
+
+  const fullUrl = url.startsWith('http') ? url : `${SERVICE_URL}${url}`;
+
+  try {
+    const config: RequestInit = {
+      method,
+      headers,
+    };
+
+    if (method === 'GET') {
+      const params = new URLSearchParams();
+      Object.keys(data).forEach((key) => {
+        if (data[key] !== undefined && data[key] !== null) {
+          params.append(key, String(data[key]));
+        }
+      });
+      const queryString = params.toString();
+      const requestUrl = queryString ? `${fullUrl}?${queryString}` : fullUrl;
+      
+      const response = await fetch(requestUrl, config);
+      const result = await response.json();
+      
+      // 处理 401/403 认证失败
+      if (result.code === AUTH_INVALID || result.code === AUTH_INVALID_2) {
+        handleAuthError();
+        return {
+          code: result.code,
+          msg: '登录已过期,请重新登录',
+          data: null as any,
+          success: false,
+        };
+      }
+      
+      return {
+        ...result,
+        success: result.code == SUCCESS_CODE, // 使用 == 兼容字符串 "0" 和数字 0
+      };
+    } else {
+      config.body = JSON.stringify(data);
+      const response = await fetch(fullUrl, config);
+      const result = await response.json();
+      
+      // 处理 401/403 认证失败
+      if (result.code === AUTH_INVALID || result.code === AUTH_INVALID_2) {
+        handleAuthError();
+        return {
+          code: result.code,
+          msg: '登录已过期,请重新登录',
+          data: null as any,
+          success: false,
+        };
+      }
+      
+      return {
+        ...result,
+        success: result.code == SUCCESS_CODE, // 使用 == 兼容字符串 "0" 和数字 0
+      };
+    }
+  } catch (error) {
+    console.error('Request error:', error);
+    return {
+      code: -1,
+      msg: '网络异常',
+      data: null as any,
+      success: false,
+    };
+  }
+};
+
+// GET 请求
+export const get = <T = any>(url: string, params: any = {}, options: RequestOptions = {}) => {
+  return request<T>(url, params, { ...options, method: 'GET' });
+};
+
+// POST 请求
+export const post = <T = any>(url: string, data: any = {}, options: RequestOptions = {}) => {
+  return request<T>(url, data, { ...options, method: 'POST' });
+};
+
+// 带 loading 的 GET 请求
+export const getL = <T = any>(url: string, params: any = {}, options: RequestOptions = {}) => {
+  return get<T>(url, params, { ...options, loading: true });
+};
+
+// 带 loading 的 POST 请求
+export const postL = <T = any>(url: string, data: any = {}, options: RequestOptions = {}) => {
+  return post<T>(url, data, { ...options, loading: true });
+};

+ 226 - 0
services/mall.ts

@@ -0,0 +1,226 @@
+// 商城服务 - 对应 supermart-mini/service/mall.js
+import { get, post, postL } from './http';
+
+const apis = {
+  CATEGORY: '/api/spu/basic/listCategoryLimit',
+  LIST: '/api/mallGoods',
+  DETAIL: '/api/mallGoods/detail',
+  DETAIL_BY_SPU: '/api/mallGoods/detailBySpuId',
+  PREVIEW_SUBMIT: '/api/mallOrder/preSubmit',
+  ORDERS: '/api/mallOrder/pageMy',
+  ORDER_DETAIL: '/api/mallOrder/detail',
+  APPLY: '/api/mallOrder/submit',
+  ORDER_PAY: '/api/mallOrder/pay',
+  RECEIVE: '/api/mallOrder/receive',
+  PACKAGES: '/api/deliveryOrder/expressInfo',
+  EXPRESS: '/api/deliveryOrder/expressDetail',
+  UPDATE_PHONE: '/api/mallOrder/updateRestNotifyMobile',
+  SPIKE_LIST: '/api/activity/mallSubject/subjectGoodsPage',
+  SPIKE_TIMER: '/api/activity/mallSubject/subject',
+};
+
+export interface GoodsItem {
+  id: string;
+  name: string;
+  price: number;
+  cover: string;
+  sellType?: number;
+  worksId?: string;
+  categoryId?: string;
+}
+
+export interface GoodsListParams {
+  current: number;
+  size: number;
+  keyword?: string;
+  worksId?: string;
+  categoryId?: string;
+  sellType?: string;
+}
+
+export interface GoodsDetail {
+  id: string;
+  name: string;
+  price: number;
+  cover: string;
+  sellType?: number;
+  sellFlag?: number;
+  sellTime?: string;
+  deposit?: number;
+  subjectPrice?: number;
+  spu: {
+    id: string;
+    name: string;
+    cover: string;
+    images?: string[];
+    description?: string;
+  };
+  recommendedLuckPool?: any[];
+  recommendedMallGoods?: GoodsItem[];
+}
+
+export interface PreviewSubmitParams {
+  goodsId: string;
+  quantity: number;
+  subjectId?: string;
+}
+
+export interface PreviewSubmitResult {
+  paymentAmount: number;
+  depositAmount?: number;
+  expressAmount?: number;
+  couponAmount?: number;
+  type?: number;
+  cash?: {
+    balance: number;
+  };
+}
+
+export interface OrderSubmitParams {
+  goodsId: string;
+  paymentType: string;
+  addressBookId: string;
+  quantity: number;
+  restNotifyMobile?: string;
+  subjectId?: string;
+}
+
+export interface OrderItem {
+  tradeNo: string;
+  status: number;
+  statusText: string;
+  goodsName: string;
+  goodsCover: string;
+  quantity: number;
+  paymentAmount: number;
+  createTime: string;
+}
+
+export interface OrderDetail {
+  tradeNo: string;
+  status: number;
+  statusText: string;
+  goodsName: string;
+  goodsCover: string;
+  quantity: number;
+  paymentAmount: number;
+  createTime: string;
+  address?: {
+    contactName: string;
+    contactNo: string;
+    province: string;
+    city: string;
+    district: string;
+    address: string;
+  };
+}
+
+// 获取商品分类
+export const getCategories = async (limit = 5) => {
+  const res = await get(apis.CATEGORY, { limit });
+  return res.data;
+};
+
+// 获取商品列表
+export const getGoodsList = async (params: GoodsListParams) => {
+  const res = await post<GoodsItem[]>(apis.LIST, params);
+  return res.data || [];
+};
+
+// 获取商品详情
+export const getGoodsDetail = async (goodsId: string, subjectId?: string): Promise<GoodsDetail | null> => {
+  const params: any = { goodsId };
+  if (subjectId) params.subjectId = subjectId;
+  const res = await get<GoodsDetail>(apis.DETAIL, params);
+  return res.data;
+};
+
+// 通过 SPU ID 获取商品详情
+export const getGoodsDetailBySpu = async (spuId: string): Promise<GoodsDetail | null> => {
+  const res = await get<GoodsDetail>(apis.DETAIL_BY_SPU, { goodsId: '', spuId });
+  return res.data;
+};
+
+// 预提交订单(获取价格信息)
+export const previewSubmit = async (params: PreviewSubmitParams): Promise<PreviewSubmitResult | null> => {
+  const res = await postL<PreviewSubmitResult>(apis.PREVIEW_SUBMIT, params);
+  return res.data;
+};
+
+// 提交订单
+export const submitOrder = async (params: OrderSubmitParams) => {
+  const res = await postL(apis.APPLY, params);
+  return res.data;
+};
+
+// 获取订单列表
+export const getOrders = async (current: number, size: number, tab?: string) => {
+  const res = await post<{ records: OrderItem[]; total: number }>(apis.ORDERS, { current, size, tab });
+  return res.data;
+};
+
+// 获取订单详情
+export const getOrderDetail = async (tradeNo: string): Promise<OrderDetail | null> => {
+  const res = await get<OrderDetail>(apis.ORDER_DETAIL, { tradeNo });
+  return res.data;
+};
+
+// 订单支付
+export const payOrder = async (tradeNo: string, paymentType: string) => {
+  const res = await postL(apis.ORDER_PAY, { tradeNo, paymentType });
+  return res.data;
+};
+
+// 确认收货
+export const confirmReceive = async (tradeNo: string) => {
+  const res = await postL(apis.RECEIVE, { tradeNo });
+  return res.success;
+};
+
+// 获取包裹信息
+export const getPackages = async (tradeNo: string) => {
+  const res = await get(apis.PACKAGES, { tradeNo });
+  return res.data;
+};
+
+// 获取物流详情
+export const getExpress = async (tradeNo: string, packageId: string) => {
+  const res = await get(apis.EXPRESS, { tradeNo, packageId });
+  return res.data;
+};
+
+// 更新尾款提醒手机号
+export const updatePhone = async (params: { tradeNo: string; restNotifyMobile: string }) => {
+  const res = await postL(apis.UPDATE_PHONE, params);
+  return res;
+};
+
+// 获取秒杀商品列表
+export const getSpikeList = async (params: any) => {
+  const res = await postL(apis.SPIKE_LIST, params);
+  return res.data;
+};
+
+// 获取秒杀活动信息
+export const getSpikeTimer = async (subjectId: string) => {
+  const res = await get(`${apis.SPIKE_TIMER}/${subjectId}`);
+  return res.data;
+};
+
+export default {
+  getCategories,
+  getGoodsList,
+  getGoodsDetail,
+  getGoodsDetailBySpu,
+  previewSubmit,
+  submitOrder,
+  getOrders,
+  getOrderDetail,
+  payOrder,
+  confirmReceive,
+  getPackages,
+  getExpress,
+  updatePhone,
+  getSpikeList,
+  getSpikeTimer,
+};

+ 202 - 0
services/user.ts

@@ -0,0 +1,202 @@
+// 用户服务 - 对应 supermart-mini/service/user.js
+import { get, postL, setToken } from './http';
+
+const apis = {
+  LOGIN: '/api/account/login',
+  USER_INFO: '/api/myProfile/get',
+  UPDATE_INFO: '/api/myProfile/basicInfo/update',
+  UPDATE_AVATAR: '/api/myProfile/avatar/update',
+  UPDATE_NICKNAME: '/api/myProfile/nickname/update',
+  REAL_NAME: '/api/authentication/submit',
+  SEND_CODE: '/api/verifycode/send',
+  PARAM_CONFIG: '/param/paramConfig',
+  WALLET_AMOUNT: '/api/wallet/ranking/walletAmount',
+  LOGOFF: '/api/removeAccount/submit',
+  GET_HIDE: '/api/luckOrder/hide',
+  SIGN_IN: '/credit/signIn',
+  CREDIT_RECORD: '/credit/selectCreditRecord',
+  CREDIT_SHOW: '/credit_show',
+  ISLAND_SIGN_IN: '/api/activity/activityIsland/signIn',
+  DISPLAY_REWARDS: '/api/activity/activityIsland/displayRewards',
+  ACTIVITY_TA_LIST: '/api/activity/activityTa/list',
+  ACTIVITY_TA_LEVEL: '/api/activity/activityTa/level',
+  CLAIM_DAILY_REWARD: '/api/wallet/recharge/claimDailyReward',
+  NEW_USER_NUM: '/api/wallet/ranking/newUserNum',
+};
+
+export interface UserInfo {
+  id: string;
+  nickname: string;
+  avatar: string;
+  phone?: string;
+  realName?: string;
+  idNum?: string;
+  balance?: number;
+}
+
+export interface LoginParams {
+  loginWay: string;
+  mobile: string;
+  verifycode: string;
+}
+
+export interface ParamConfig {
+  code: string;
+  data: string;
+  state: number;
+}
+
+export interface LoginResult {
+  success: boolean;
+  needInfo?: boolean;
+}
+
+// 账号登录
+export const login = async (params: LoginParams): Promise<LoginResult> => {
+  const res = await postL<{ tokenInfo: { tokenValue: string }; needPerfectProfile?: boolean }>(apis.LOGIN, params);
+  if (res.success && res.data?.tokenInfo?.tokenValue) {
+    setToken(res.data.tokenInfo.tokenValue);
+    return {
+      success: true,
+      needInfo: res.data.needPerfectProfile,
+    };
+  }
+  return { success: false };
+};
+
+// 获取用户信息
+export const getUserInfo = async (): Promise<UserInfo | null> => {
+  const res = await get<UserInfo>(apis.USER_INFO);
+  return res.data;
+};
+
+// 更新用户信息
+export const updateUserInfo = async (params: Partial<UserInfo>): Promise<boolean> => {
+  const res = await postL(apis.UPDATE_INFO, params);
+  return res.success;
+};
+
+// 更新头像
+export const updateAvatar = async (avatar: string): Promise<boolean> => {
+  const res = await postL(apis.UPDATE_AVATAR, { avatar });
+  return res.success;
+};
+
+// 更新昵称
+export const updateNickname = async (nickname: string): Promise<boolean> => {
+  const res = await postL(apis.UPDATE_NICKNAME, { nickname });
+  return res.success;
+};
+
+// 实名认证
+export const realNameAuth = async (idName: string, idNum: string): Promise<boolean> => {
+  const res = await postL(apis.REAL_NAME, { idName, idNum });
+  return res.success;
+};
+
+// 发送验证码
+export const sendVerifyCode = async (mobile: string, scene = 'LOGIN'): Promise<boolean> => {
+  const res = await postL(apis.SEND_CODE, { mobile, scene });
+  return res.success;
+};
+
+// 获取参数配置
+export const getParamConfig = async (code: string): Promise<ParamConfig | null> => {
+  const res = await get<ParamConfig>(apis.PARAM_CONFIG, { code });
+  return res.data;
+};
+
+// 获取钱包余额
+export const getWalletAmount = async () => {
+  const res = await get(apis.WALLET_AMOUNT);
+  return res;
+};
+
+// 注销账号
+export const logoff = async (): Promise<boolean> => {
+  const res = await postL(apis.LOGOFF);
+  return res.success;
+};
+
+// 获取隐藏状态 (1: 没消费,0: 消费)
+export const getHide = async () => {
+  const res = await get(apis.GET_HIDE);
+  return res.data;
+};
+
+// 签到
+export const signIn = async () => {
+  const res = await postL(apis.SIGN_IN);
+  return res;
+};
+
+// 岛屿签到
+export const islandSignIn = async () => {
+  const res = await postL(apis.ISLAND_SIGN_IN);
+  return res;
+};
+
+// 获取积分记录
+export const getCreditRecord = async (params?: any) => {
+  const res = await get(apis.CREDIT_RECORD, params);
+  return res;
+};
+
+// 获取积分显示配置
+export const creditShow = async () => {
+  const res = await get(apis.CREDIT_SHOW);
+  return res;
+};
+
+// 获取展示奖励
+export const displayRewards = async () => {
+  const res = await get(apis.DISPLAY_REWARDS);
+  return res;
+};
+
+// 获取活动列表
+export const activityTaList = async () => {
+  const res = await get(apis.ACTIVITY_TA_LIST);
+  return res;
+};
+
+// 获取活动等级
+export const activityTaLevel = async (params?: any) => {
+  const res = await get(apis.ACTIVITY_TA_LEVEL, params);
+  return res;
+};
+
+// 领取每日奖励
+export const claimDailyReward = async (params?: any) => {
+  const res = await postL(apis.CLAIM_DAILY_REWARD, params);
+  return res;
+};
+
+// 获取新用户数量
+export const getNewUserNum = async (params?: any) => {
+  const res = await getL(apis.NEW_USER_NUM, params);
+  return res;
+};
+
+export default {
+  login,
+  getUserInfo,
+  updateUserInfo,
+  updateAvatar,
+  updateNickname,
+  realNameAuth,
+  sendVerifyCode,
+  getParamConfig,
+  getWalletAmount,
+  logoff,
+  getHide,
+  signIn,
+  islandSignIn,
+  getCreditRecord,
+  creditShow,
+  displayRewards,
+  activityTaList,
+  activityTaLevel,
+  claimDailyReward,
+  getNewUserNum,
+};

+ 1 - 0
supermart-mini

@@ -0,0 +1 @@
+Subproject commit c62ab0c791a2456f663690e802500f6c4fb4d878