10 Commits 08c6e3c1ce ... 800522b483

Auteur SHA1 Bericht Datum
  zbb 800522b483 feat: 移植转赠功能 (迁移自 supermart-mini),更新版本至 1.0.6 (34) 2 weken geleden
  zbb d599195560 feat: 新人优惠券弹窗+新人活动检测 build 33 1 maand geleden
  zbb 6fa7600346 fix: 头像上传-缩小质量0.4+传token+显示错误详情 build 32 1 maand geleden
  zbb 054fd528a7 fix: 重写uploadFile去掉track header+添加调试日志 build 31 1 maand geleden
  zbb 4c28b3108f fix: 兑换按钮隐藏+仓库价格+兑换码去重+提现路径+头像URI兼容 build 30 1 maand geleden
  zbb 81768c4539 fix: 修复正确仓库页面(store)的赏品点击跳转详情 build 29 1 maand geleden
  zbb 28a8ca759d fix: 仓库赏品点击改用Pressable整体可点击 build 28 1 maand geleden
  zbb ac19edf0be debug: 仓库详情页添加调试日志和错误处理 1 maand geleden
  zbb 3e63ddab8f chore: bump buildNumber to 26 1 maand geleden
  zbb 798b19e6d5 feat: 新增仓库详情页、审核列表页及多处bug修复 1 maand geleden

+ 2 - 2
app.json

@@ -2,7 +2,7 @@
   "expo": {
     "name": "艾斯",
     "slug": "asios",
-    "version": "1.0.4",
+    "version": "1.0.6",
     "orientation": "portrait",
     "icon": "./assets/images/icon.png",
     "scheme": ["asios", "alipay2021005175632205"],
@@ -12,7 +12,7 @@
       "supportsTablet": false,
       "bundleIdentifier": "com.asios",
       "appleTeamId": "Y9ZVX3FRX6",
-      "buildNumber": "23",
+      "buildNumber": "34",
       "infoPlist": {
         "CFBundleDisplayName": "艾斯潮盒",
         "ITSAppUsesNonExemptEncryption": false,

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

@@ -1,7 +1,10 @@
 import { Barrage } from "@/components/Barrage";
+import { CouponModal } from "@/components/CouponModal";
 import { Colors } from "@/constants/Colors";
 import { Images } from "@/constants/images";
+import { CouponItem, getNewbieGiftBagInfo, listValidCoupon } from "@/services/activity";
 import { getFeedbackList, getPoolList, PoolItem } from "@/services/award";
+import { getToken } from "@/services/http";
 import { Image } from "expo-image";
 import { useRouter } from "expo-router";
 import React, { useCallback, useEffect, useMemo, useState } from "react";
@@ -140,6 +143,10 @@ export default function BoxScreen() {
   const [current, setCurrent] = useState(1);
   const [hasMore, setHasMore] = useState(true);
   const [barrageList, setBarrageList] = useState<BarrageItem[]>([]);
+  // 新人优惠券弹窗
+  const [couponVisible, setCouponVisible] = useState(false);
+  const [couponList, setCouponList] = useState<CouponItem[]>([]);
+  const [newPeopleChecked, setNewPeopleChecked] = useState(false);
 
   // 加载弹幕
   const loadBarrage = useCallback(async () => {
@@ -153,6 +160,37 @@ export default function BoxScreen() {
     }
   }, []);
 
+  // 新人大礼包检测(Vue: initNewPeople)
+  const initNewPeople = useCallback(async () => {
+    if (newPeopleChecked) return;
+    const token = getToken();
+    if (!token) return; // 未登录不检测
+    try {
+      const data = await getNewbieGiftBagInfo();
+      if (data) {
+        setNewPeopleChecked(true);
+        // Vue 原项目只是展示一张海报图片,这里跳过(无具体交互)
+      }
+    } catch (e) {
+      // 静默失败
+    }
+  }, [newPeopleChecked]);
+
+  // 优惠券检测(Vue: initCoupon)
+  const initCoupon = useCallback(async () => {
+    const token = getToken();
+    if (!token) return;
+    try {
+      const coupons = await listValidCoupon("LUCK");
+      if (coupons && coupons.length > 0) {
+        setCouponList(coupons);
+        setCouponVisible(true);
+      }
+    } catch (e) {
+      // 静默失败
+    }
+  }, []);
+
   const loadData = useCallback(
     async (isRefresh = false) => {
       if (loading) return;
@@ -190,6 +228,8 @@ export default function BoxScreen() {
   useEffect(() => {
     loadData(true);
     loadBarrage();
+    initNewPeople();
+    initCoupon();
   }, [typeIndex, priceSort]);
 
   // 执行搜索
@@ -366,6 +406,17 @@ export default function BoxScreen() {
           onEndReached={handleLoadMore}
           onEndReachedThreshold={0.3}
         />
+
+        {/* 新人优惠券弹窗 */}
+        <CouponModal
+          visible={couponVisible}
+          coupons={couponList}
+          onClose={() => setCouponVisible(false)}
+          onSuccess={() => {
+            setCouponList([]);
+            loadData(true);
+          }}
+        />
       </View>
     </View>
   );

+ 5 - 1
app/(tabs)/mine.tsx

@@ -37,6 +37,7 @@ export default function MineScreen() {
     data: string;
   } | null>(null);
   const [showWallet, setShowWallet] = useState(false);
+  const [showExchange, setShowExchange] = useState(false);
   const kefuRef = useRef<KefuPopupRef>(null);
 
   const loadData = useCallback(async () => {
@@ -58,6 +59,9 @@ export default function MineScreen() {
 
       const walletConfig = await getParamConfig("wallet_recharge_show");
       setShowWallet(walletConfig?.state === 1);
+
+      const exchangeConfig = await getParamConfig("harry_exchange_show");
+      setShowExchange(exchangeConfig?.state === 1);
     } catch (error) {
       console.error("获取数据失败:", error);
     }
@@ -380,7 +384,7 @@ export default function MineScreen() {
 
         {/* Menu Section */}
         <View style={styles.menuSection}>
-          <MenuCell onItemPress={handleMenuItemPress} showWallet={showWallet} />
+          <MenuCell onItemPress={handleMenuItemPress} showWallet={showWallet} showExchange={showExchange} />
         </View>
 
         {/* Footer Info */}

+ 1 - 0
app/cloud-warehouse/_layout.tsx

@@ -6,6 +6,7 @@ export default function StoreLayout() {
       <Stack.Screen name="index" />
       <Stack.Screen name="checkout" />
       <Stack.Screen name="packages" />
+      <Stack.Screen name="detail" />
     </Stack>
   );
 }

+ 298 - 0
app/cloud-warehouse/detail.tsx

@@ -0,0 +1,298 @@
+import { Image } from "expo-image";
+import { useLocalSearchParams, useRouter } from "expo-router";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import {
+    ActivityIndicator,
+    Alert,
+    ImageBackground,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+import { Images } from "@/constants/images";
+import { convertApply, convertPreview, getLuckDetail } from "@/services/award";
+
+const LEVEL_MAP: Record<string, { title: string; color: string }> = {
+  A: { title: "超神", color: "#FF3B30" },
+  B: { title: "欧皇", color: "#FF9500" },
+  C: { title: "隐藏", color: "#AF52DE" },
+  D: { title: "普通", color: "#8E8E93" },
+};
+
+const FROM_TYPE_MAP: Record<string, string> = {
+  MALL: "商城",
+  DISTRIBUTION: "分销",
+  LUCK: "奖池",
+  LUCK_PICKUP: "商品提货",
+  LUCK_EXCHANGE: "商品兑换",
+  SUBSTITUTE: "商品置换",
+  TRANSFER: "商品转赠",
+  LUCK_ROOM: "福利房",
+  LUCK_WHEEL: "魔天轮",
+  DOLL_MACHINE: "扭蛋",
+  RECHARGE: "充值",
+  OFFICIAL: "官方",
+  ACTIVITY: "活动",
+  LUCK_ACTIVITY: "奖池活动",
+  REDEEM_CODE_ACTIVITY: "兑换码活动",
+};
+
+export default function StoreDetailScreen() {
+  const params = useLocalSearchParams<{ id: string }>();
+  const id = params?.id;
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+
+  const [loading, setLoading] = useState(true);
+  const [item, setItem] = useState<any>(null);
+  const [converting, setConverting] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+
+  console.log('[仓库详情] 页面加载, params:', JSON.stringify(params), 'id:', id);
+
+  const loadData = useCallback(async () => {
+    console.log('[仓库详情] loadData 开始, id:', id);
+    if (!id) {
+      setError('未获取到商品ID');
+      setLoading(false);
+      return;
+    }
+    setLoading(true);
+    setError(null);
+    try {
+      const data = await getLuckDetail(id);
+      console.log('[仓库详情] getLuckDetail 返回:', data ? '有数据' : 'null');
+      setItem(data);
+      if (!data) {
+        setError('商品数据为空');
+      }
+    } catch (e: any) {
+      console.error("[仓库详情] 加载失败:", e);
+      setError(`加载失败: ${e?.message || '未知错误'}`);
+    }
+    setLoading(false);
+  }, [id]);
+
+  useEffect(() => {
+    loadData();
+  }, [loadData]);
+
+  const handleConvert = async () => {
+    if (!item || item.magicAmount <= 0) {
+      Alert.alert("提示", "不可兑换");
+      return;
+    }
+    setConverting(true);
+    try {
+      const preview = await convertPreview([item.id]);
+      if (preview?.data) {
+        Alert.alert(
+          "确认兑换",
+          `将兑换 ${item.spu?.name} 获得 ${preview.data?.totalMagicAmount || item.magicAmount} 果实`,
+          [
+            { text: "取消", style: "cancel" },
+            {
+              text: "确定",
+              onPress: async () => {
+                const success = await convertApply([item.id]);
+                if (success) {
+                  Alert.alert("提示", "兑换成功", [
+                    { text: "确定", onPress: () => router.back() },
+                  ]);
+                }
+              },
+            },
+          ],
+        );
+      }
+    } catch (e) {
+      Alert.alert("提示", "兑换失败");
+    }
+    setConverting(false);
+  };
+
+  if (loading) {
+    return (
+      <View style={styles.loadingBox}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    );
+  }
+
+  if (!item) {
+    return (
+      <View style={styles.loadingBox}>
+        <Text style={{ color: "#999", fontSize: 16 }}>{error || '商品不存在'}</Text>
+        <TouchableOpacity onPress={() => router.back()} style={{ marginTop: 20 }}>
+          <Text style={{ color: "#fff", fontSize: 14 }}>返回</Text>
+        </TouchableOpacity>
+      </View>
+    );
+  }
+
+  const levelInfo = LEVEL_MAP[item.level] || { title: "其他", color: "#999" };
+
+  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={() => router.back()}
+          >
+            <Text style={styles.backText}>←</Text>
+          </TouchableOpacity>
+          <Text style={styles.title}>仓库</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        {/* 商品信息 */}
+        <View style={styles.content}>
+          <View style={styles.detailCard}>
+            <Image
+              source={{ uri: item.spu?.cover }}
+              style={styles.coverImage}
+              contentFit="contain"
+            />
+            <Text style={[styles.levelTag, { color: levelInfo.color }]}>
+              {levelInfo.title}
+            </Text>
+            <Text style={styles.goodsName}>{item.spu?.name}</Text>
+            <View style={styles.sourceBox}>
+              <Text style={styles.sourceText}>
+                从{FROM_TYPE_MAP[item.fromRelationType] || "其他"}获得
+              </Text>
+            </View>
+          </View>
+        </View>
+
+        {/* 兑换按钮 */}
+        {item.magicAmount > 0 && (
+          <View
+            style={[
+              styles.bottomBar,
+              { paddingBottom: Math.max(insets.bottom, 20) },
+            ]}
+          >
+            <TouchableOpacity
+              style={styles.convertBtn}
+              onPress={handleConvert}
+              disabled={converting}
+              activeOpacity={0.8}
+            >
+              <ImageBackground
+                source={{ uri: Images.common.loginBtn }}
+                style={styles.convertBtnBg}
+                resizeMode="stretch"
+              >
+                <Text style={styles.convertBtnText}>兑换果实</Text>
+                <Text style={styles.convertBtnSub}>
+                  可兑换 {item.magicAmount} 果实
+                </Text>
+              </ImageBackground>
+            </TouchableOpacity>
+          </View>
+        )}
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: "#1a1a2e" },
+  background: { flex: 1 },
+  loadingBox: {
+    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: 20 },
+  title: { color: "#fff", fontSize: 16, fontWeight: "bold" },
+  placeholder: { width: 40 },
+
+  content: { flex: 1, paddingHorizontal: 20, paddingTop: 20 },
+  detailCard: {
+    backgroundColor: "rgba(255,255,255,0.95)",
+    borderRadius: 16,
+    padding: 24,
+    alignItems: "center",
+  },
+  coverImage: {
+    width: 180,
+    height: 180,
+    borderRadius: 10,
+    marginBottom: 16,
+  },
+  levelTag: {
+    fontSize: 16,
+    fontWeight: "bold",
+    marginBottom: 8,
+    textShadowColor: "#000",
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 0,
+  },
+  goodsName: {
+    fontSize: 16,
+    color: "#333",
+    fontWeight: "bold",
+    textAlign: "center",
+    marginBottom: 12,
+    lineHeight: 22,
+  },
+  sourceBox: {
+    backgroundColor: "rgba(0,0,0,0.05)",
+    borderRadius: 20,
+    paddingHorizontal: 16,
+    paddingVertical: 6,
+  },
+  sourceText: { fontSize: 13, color: "#666" },
+
+  bottomBar: {
+    paddingHorizontal: 30,
+    paddingTop: 10,
+    alignItems: "center",
+  },
+  convertBtn: {
+    width: "80%",
+    height: 60,
+  },
+  convertBtnBg: {
+    width: "100%",
+    height: "100%",
+    justifyContent: "center",
+    alignItems: "center",
+  },
+  convertBtnText: {
+    color: "#000",
+    fontSize: 16,
+    fontWeight: "bold",
+  },
+  convertBtnSub: {
+    color: "#735200",
+    fontSize: 11,
+  },
+});

+ 72 - 3
app/cloud-warehouse/index.tsx

@@ -7,6 +7,7 @@ import {
     FlatList,
     ImageBackground,
     Platform,
+    Pressable,
     RefreshControl,
     ScrollView,
     StatusBar,
@@ -24,7 +25,9 @@ import {
     moveOutSafeStore,
     moveToSafeStore,
 } from "@/services/award";
+import { getParamConfig } from "@/services/user";
 import CheckoutModal from "./components/CheckoutModal";
+import TransferModal from "../store/components/TransferModal";
 
 const LEVEL_MAP: Record<string, { title: string; color: string }> = {
   A: { title: "超神", color: "#FF3366" }, // Neon Pink
@@ -117,6 +120,8 @@ export default function StoreScreen() {
   const [hasMore, setHasMore] = useState(true);
   const [checkMap, setCheckMap] = useState<Record<string, StoreItem>>({});
   const [checkoutVisible, setCheckoutVisible] = useState(false);
+  const [transferVisible, setTransferVisible] = useState(false);
+  const [shareOnState, setShareOnState] = useState(0);
 
   const mainTabs = ["未使用", "保险柜", "已提货"];
 
@@ -163,6 +168,19 @@ export default function StoreScreen() {
     [mainTabIndex, levelTabIndex, loading, hasMore],
   );
 
+  // 获取转赠开关
+  useEffect(() => {
+    const fetchShareOn = async () => {
+      try {
+        const config = await getParamConfig('share_on');
+        setShareOnState(config?.state || 0);
+      } catch (e) {
+        console.error('获取转赠开关失败:', e);
+      }
+    };
+    fetchShareOn();
+  }, []);
+
   useEffect(() => {
     setPage(1);
     setList([]);
@@ -272,6 +290,21 @@ export default function StoreScreen() {
     handleRefresh();
   };
 
+  const handleTransfer = () => {
+    const selected = Object.values(checkMap);
+    if (selected.length === 0) {
+      showAlert("请至少选择一个商品!");
+      return;
+    }
+    setTransferVisible(true);
+  };
+
+  const handleTransferSuccess = () => {
+    setTransferVisible(false);
+    setCheckMap({});
+    handleRefresh();
+  };
+
   const showAlert = (msg: string) => {
     // @ts-ignore
     if (Platform.OS === "web") window.alert(msg);
@@ -294,7 +327,13 @@ export default function StoreScreen() {
         style={styles.cell}
         resizeMode="stretch"
       >
-        <View style={styles.cellContent}>
+        <Pressable
+          style={styles.cellContent}
+          onPress={() => {
+            console.log('[仓库] 点击商品详情, id:', item.id);
+            router.push({ pathname: '/cloud-warehouse/detail', params: { id: item.id } } as any);
+          }}
+        >
           <TouchableOpacity
             style={styles.cellHeader}
             onPress={() => canSelect && handleChoose(item)}
@@ -331,7 +370,9 @@ export default function StoreScreen() {
               />
             </TouchableOpacity>
           </TouchableOpacity>
-          <View style={styles.cellBody}>
+          <View
+            style={styles.cellBody}
+          >
             <ImageBackground
               source={{ uri: Images.mine.storeGoodsImgBg }}
               style={styles.goodsImgBg}
@@ -352,7 +393,7 @@ export default function StoreScreen() {
             </View>
             <Text style={styles.arrow}>{">"}</Text>
           </View>
-        </View>
+        </Pressable>
       </ImageBackground>
     );
   };
@@ -665,6 +706,17 @@ export default function StoreScreen() {
           </View>
         )}
 
+        {/* 转赠浮动按钮 */}
+        {mainTabIndex === 0 && shareOnState === 1 && list.length > 0 && (
+          <TouchableOpacity
+            style={styles.transferFloatBtn}
+            onPress={handleTransfer}
+            activeOpacity={0.8}
+          >
+            <Image source={{ uri: Images.mine.transferBut }} style={styles.transferFloatImg} />
+          </TouchableOpacity>
+        )}
+
         {/* 提货弹窗 */}
         <CheckoutModal
           visible={checkoutVisible}
@@ -672,6 +724,14 @@ export default function StoreScreen() {
           onClose={() => setCheckoutVisible(false)}
           onSuccess={handleCheckoutSuccess}
         />
+
+        {/* 转赠弹窗 */}
+        <TransferModal
+          visible={transferVisible}
+          selectedItems={Object.values(checkMap)}
+          onClose={() => setTransferVisible(false)}
+          onSuccess={handleTransferSuccess}
+        />
       </ImageBackground>
     </View>
   );
@@ -680,6 +740,15 @@ export default function StoreScreen() {
 const styles = StyleSheet.create({
   container: { flex: 1, backgroundColor: "#1a1a2e" },
   background: { flex: 1 },
+  transferFloatBtn: {
+    position: "absolute",
+    bottom: 200,
+    right: 10,
+    width: 48,
+    height: 50,
+    zIndex: 100,
+  },
+  transferFloatImg: { width: 48, height: 50 },
   headerBg: {
     position: "absolute",
     top: 0,

+ 1 - 0
app/dimension/_layout.tsx

@@ -11,6 +11,7 @@ export default function WealLayout() {
       <Stack.Screen name="store_choose" />
       <Stack.Screen name="catchDoll" />
       <Stack.Screen name="wish" />
+      <Stack.Screen name="audit_list" />
     </Stack>
   );
 }

+ 342 - 0
app/dimension/audit_list.tsx

@@ -0,0 +1,342 @@
+import { Image } from "expo-image";
+import { useLocalSearchParams, useRouter } from "expo-router";
+import React, { useCallback, useEffect, useState } from "react";
+import {
+    ActivityIndicator,
+    Alert,
+    FlatList,
+    ImageBackground,
+    RefreshControl,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+import { Images } from "@/constants/images";
+import {
+    roomAuditPass,
+    roomAuditRecord,
+    roomAuditUnpass,
+} from "@/services/dimension";
+
+interface AuditItem {
+  id: string;
+  avatar: string;
+  nickname: string;
+  auditStatus: number; // 0-待审核 1-拒绝 2-通过
+}
+
+const TABS = [
+  { title: "待审核", value: 0 },
+  { title: "审核通过", value: 2 },
+  { title: "审核不通过", value: 1 },
+];
+
+export default function AuditListScreen() {
+  const { id: roomId } = useLocalSearchParams<{ id: string }>();
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+
+  const [activeTab, setActiveTab] = useState(0);
+  const [list, setList] = useState<AuditItem[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [refreshing, setRefreshing] = useState(false);
+  const [page, setPage] = useState(1);
+  const [hasMore, setHasMore] = useState(true);
+
+  const loadData = useCallback(
+    async (pageNum: number, isRefresh = false) => {
+      if (loading && !isRefresh) return;
+      try {
+        if (pageNum === 1) setLoading(true);
+        const data = await roomAuditRecord(
+          pageNum,
+          20,
+          roomId as string,
+          TABS[activeTab].value,
+        );
+        const records = Array.isArray(data)
+          ? data
+          : data?.records || [];
+        if (records.length < 20) setHasMore(false);
+        if (pageNum === 1 || isRefresh) {
+          setList(records);
+        } else {
+          setList((prev) => [...prev, ...records]);
+        }
+      } catch (e) {
+        console.error("加载审核记录失败:", e);
+      } finally {
+        setLoading(false);
+        setRefreshing(false);
+      }
+    },
+    [roomId, activeTab, loading],
+  );
+
+  useEffect(() => {
+    setPage(1);
+    setList([]);
+    setHasMore(true);
+    loadData(1, true);
+  }, [activeTab]);
+
+  const handleRefresh = () => {
+    setRefreshing(true);
+    setPage(1);
+    setHasMore(true);
+    loadData(1, true);
+  };
+
+  const handleLoadMore = () => {
+    if (!loading && hasMore) {
+      const np = page + 1;
+      setPage(np);
+      loadData(np);
+    }
+  };
+
+  const handlePass = async (item: AuditItem, index: number) => {
+    try {
+      const res = await roomAuditPass(item.id);
+      if (res) {
+        const newList = [...list];
+        newList[index] = { ...item, auditStatus: 2 };
+        setList(newList);
+      }
+    } catch (e) {
+      Alert.alert("提示", "操作失败");
+    }
+  };
+
+  const handleReject = async (item: AuditItem, index: number) => {
+    try {
+      const res = await roomAuditUnpass(item.id);
+      if (res) {
+        const newList = [...list];
+        newList[index] = { ...item, auditStatus: 1 };
+        setList(newList);
+      }
+    } catch (e) {
+      Alert.alert("提示", "操作失败");
+    }
+  };
+
+  const renderItem = ({
+    item,
+    index,
+  }: {
+    item: AuditItem;
+    index: number;
+  }) => (
+    <View style={styles.cell}>
+      <Image
+        source={{ uri: item.avatar }}
+        style={styles.avatar}
+        contentFit="cover"
+      />
+      <Text style={styles.nickname} numberOfLines={1}>
+        {item.nickname}
+      </Text>
+      <View style={styles.actionArea}>
+        {item.auditStatus === 0 ? (
+          <>
+            <TouchableOpacity
+              style={[styles.btn, styles.rejectBtn]}
+              onPress={() => handleReject(item, index)}
+            >
+              <Text style={styles.btnText}>拒绝</Text>
+            </TouchableOpacity>
+            <TouchableOpacity
+              style={[styles.btn, styles.passBtn]}
+              onPress={() => handlePass(item, index)}
+            >
+              <Text style={styles.btnText}>通过</Text>
+            </TouchableOpacity>
+          </>
+        ) : (
+          <Text style={styles.statusText}>
+            {item.auditStatus === 2 ? "通过" : "拒绝"}
+          </Text>
+        )}
+      </View>
+    </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={() => router.back()}
+          >
+            <Text style={styles.backText}>←</Text>
+          </TouchableOpacity>
+          <Text style={styles.title}>审核</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        {/* Tab 切换 */}
+        <View style={styles.tabs}>
+          {TABS.map((tab, index) => {
+            const isActive = activeTab === index;
+            return (
+              <TouchableOpacity
+                key={index}
+                style={[styles.tabItem, isActive && styles.tabItemActive]}
+                onPress={() => setActiveTab(index)}
+              >
+                <Text
+                  style={[
+                    styles.tabText,
+                    isActive && styles.tabTextActive,
+                  ]}
+                >
+                  {tab.title}
+                </Text>
+                {isActive && <View style={styles.tabLine} />}
+              </TouchableOpacity>
+            );
+          })}
+        </View>
+
+        {/* 列表 */}
+        <FlatList
+          data={list}
+          renderItem={renderItem}
+          keyExtractor={(item, index) => item.id || index.toString()}
+          contentContainerStyle={styles.listContent}
+          refreshControl={
+            <RefreshControl
+              refreshing={refreshing}
+              onRefresh={handleRefresh}
+              tintColor="#fff"
+            />
+          }
+          onEndReached={handleLoadMore}
+          onEndReachedThreshold={0.3}
+          ListFooterComponent={
+            loading && list.length > 0 ? (
+              <ActivityIndicator
+                color="#fff"
+                style={{ marginVertical: 10 }}
+              />
+            ) : null
+          }
+          ListEmptyComponent={
+            !loading ? (
+              <View style={styles.emptyBox}>
+                <Text style={styles.emptyText}>暂无记录</Text>
+              </View>
+            ) : null
+          }
+        />
+      </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: 20 },
+  title: { color: "#fff", fontSize: 16, fontWeight: "bold" },
+  placeholder: { width: 40 },
+
+  // Tabs
+  tabs: {
+    flexDirection: "row",
+    paddingHorizontal: 10,
+    borderBottomWidth: 1,
+    borderBottomColor: "rgba(255,255,255,0.15)",
+  },
+  tabItem: {
+    flex: 1,
+    height: 44,
+    justifyContent: "center",
+    alignItems: "center",
+    position: "relative",
+  },
+  tabItemActive: {},
+  tabText: { color: "#999", fontSize: 14 },
+  tabTextActive: { color: "#fff", fontWeight: "bold" },
+  tabLine: {
+    position: "absolute",
+    bottom: 0,
+    width: "60%",
+    height: 2,
+    backgroundColor: "#e79018",
+  },
+
+  // List
+  listContent: { padding: 15, paddingBottom: 100 },
+  cell: {
+    flexDirection: "row",
+    alignItems: "center",
+    backgroundColor: "rgba(255,255,255,0.9)",
+    borderRadius: 8,
+    paddingHorizontal: 15,
+    paddingVertical: 12,
+    marginBottom: 10,
+  },
+  avatar: {
+    width: 40,
+    height: 40,
+    borderRadius: 4,
+    borderWidth: 2,
+    borderColor: "#000",
+    marginRight: 12,
+  },
+  nickname: {
+    flex: 1,
+    fontSize: 14,
+    color: "#333",
+    fontWeight: "500",
+  },
+  actionArea: {
+    flexDirection: "row",
+    alignItems: "center",
+  },
+  btn: {
+    width: 68,
+    height: 32,
+    borderRadius: 4,
+    justifyContent: "center",
+    alignItems: "center",
+    marginLeft: 8,
+  },
+  rejectBtn: { backgroundColor: "#ff4d4f" },
+  passBtn: { backgroundColor: "#52c41a" },
+  btnText: { color: "#fff", fontSize: 13, fontWeight: "bold" },
+  statusText: {
+    fontSize: 13,
+    color: "#666",
+    fontWeight: "500",
+  },
+
+  emptyBox: { alignItems: "center", paddingTop: 80 },
+  emptyText: { color: "#999", fontSize: 14 },
+});

+ 5 - 1
app/dimension/create_list.tsx

@@ -103,7 +103,11 @@ const CreateListScreen = () => {
                         <TouchableOpacity
                             style={styles.auditBtn}
                             onPress={() => {
-                                // router.push({ pathname: '/dimension/audit_list', params: { id: item.id } })
+                                if (item.status === 1 && item.participateMode === 1) {
+                                    router.push({ pathname: '/dimension/audit_list', params: { id: item.id } } as any);
+                                } else {
+                                    router.push({ pathname: '/dimension/detail', params: { id: item.id } } as any);
+                                }
                             }}
                         >
                             <Text style={styles.auditBtnText}>

+ 1 - 1
app/dimension/room.tsx

@@ -170,7 +170,7 @@ export default function RoomScreen() {
               />
             </ImageBackground>
             <View style={styles.funcBtns}>
-              <TouchableOpacity style={styles.funcItem} onPress={() => router.push('/dimension/create' as any)}>
+              <TouchableOpacity style={styles.funcItem} onPress={() => router.push('/dimension/create_list' as any)}>
                 <Image source={{ uri: Images.welfare.roomIcon0 }} style={styles.funcIcon} contentFit="contain" />
                 <Text style={styles.funcText}>创建</Text>
               </TouchableOpacity>

+ 1 - 1
app/feedback/index.tsx

@@ -98,7 +98,7 @@ export default function FeedbackScreen() {
             {/* 投诉提示 */}
             <View style={styles.complaintTip}>
               <Text style={styles.complaintTipText}>
-                此投诉为本小程序自有投诉渠道,非微信官方投诉渠道
+                此投诉为本平台自有投诉渠道
               </Text>
             </View>
 

+ 158 - 95
app/profile/index.tsx

@@ -1,7 +1,7 @@
-import { Image } from 'expo-image';
-import * as ImagePicker from 'expo-image-picker';
-import { useRouter } from 'expo-router';
-import React, { useEffect, useState } from 'react';
+import { Image } from "expo-image";
+import * as ImagePicker from "expo-image-picker";
+import { useRouter } from "expo-router";
+import React, { useEffect, useState } from "react";
 import {
     Alert,
     ImageBackground,
@@ -12,13 +12,19 @@ import {
     TextInput,
     TouchableOpacity,
     View,
-} from 'react-native';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
 
-import { Images } from '@/constants/images';
-import { useAuth } from '@/contexts/AuthContext';
-import { uploadFile } from '@/services/base';
-import { getUserInfo, updateAvatar, updateNickname, updateUserInfo } from '@/services/user';
+import { Images } from "@/constants/images";
+import { useAuth } from "@/contexts/AuthContext";
+import { uploadFile } from "@/services/base";
+import { getToken } from "@/services/http";
+import {
+    getUserInfo,
+    updateAvatar,
+    updateNickname,
+    updateUserInfo,
+} from "@/services/user";
 
 interface FormData {
   nickname: string;
@@ -30,10 +36,10 @@ export default function ProfileScreen() {
   const router = useRouter();
   const insets = useSafeAreaInsets();
   const { refreshUser } = useAuth();
-  
+
   const [formData, setFormData] = useState<FormData>({
-    nickname: '',
-    avatar: '',
+    nickname: "",
+    avatar: "",
     sex: 3,
   });
   const [loading, setLoading] = useState(false);
@@ -47,13 +53,13 @@ export default function ProfileScreen() {
       const res = await getUserInfo();
       if (res) {
         setFormData({
-          nickname: res.nickname || '',
-          avatar: res.avatar || '',
+          nickname: res.nickname || "",
+          avatar: res.avatar || "",
           sex: (res as any).sex || 3,
         });
       }
     } catch (error) {
-      console.error('获取用户信息失败:', error);
+      console.error("获取用户信息失败:", error);
     }
   };
 
@@ -63,79 +69,81 @@ export default function ProfileScreen() {
 
   const handleChooseAvatar = async () => {
     try {
-      const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
+      const permissionResult =
+        await ImagePicker.requestMediaLibraryPermissionsAsync();
       if (!permissionResult.granted) {
-        Alert.alert('提示', '需要相册权限才能选择头像');
+        Alert.alert("提示", "需要相册权限才能选择头像");
         return;
       }
 
       const result = await ImagePicker.launchImageLibraryAsync({
-        mediaTypes: ['images'],
+        mediaTypes: ["images"],
         allowsEditing: true,
         aspect: [1, 1],
-        quality: 0.8,
+        quality: 0.4, // 服务器限制1MB,降低质量确保不超限
       });
 
       if (!result.canceled && result.assets[0]) {
         const imageUri = result.assets[0].uri;
-        setFormData(prev => ({ ...prev, avatar: imageUri }));
+        setFormData((prev) => ({ ...prev, avatar: imageUri }));
       }
     } catch (error) {
-      console.error('选择头像失败:', error);
-      Alert.alert('提示', '选择头像失败');
+      console.error("选择头像失败:", error);
+      Alert.alert("提示", "选择头像失败");
     }
   };
 
   const handleSexChange = (sex: number) => {
-    setFormData(prev => ({ ...prev, sex }));
+    setFormData((prev) => ({ ...prev, sex }));
   };
 
   const handleSave = async () => {
     if (!formData.nickname?.trim()) {
-      Alert.alert('提示', '请输入昵称');
+      Alert.alert("提示", "请输入昵称");
       return;
     }
 
     try {
       setLoading(true);
-      
-      // 如果头像是本地文件(file://开头),需要先上传
+
+      // 如果头像是本地文件(非http开头),需要先上传
       let avatarUrl = formData.avatar;
-      if (formData.avatar && formData.avatar.startsWith('file://')) {
-        const uploadedUrl = await uploadFile(formData.avatar, 'avatar');
-        if (uploadedUrl) {
-          avatarUrl = uploadedUrl;
-          // 更新头像
+      console.log("[profile] 头像URI:", formData.avatar?.substring(0, 100));
+      if (formData.avatar && !formData.avatar.startsWith("http")) {
+        const token = getToken();
+        const uploadResult = await uploadFile(formData.avatar, "avatar", token || undefined);
+        if (typeof uploadResult === "string") {
+          avatarUrl = uploadResult;
           await updateAvatar(avatarUrl);
         } else {
-          Alert.alert('提示', '头像上传失败');
+          Alert.alert("头像上传失败", uploadResult.error);
           setLoading(false);
           return;
         }
       }
-      
+
       // 更新昵称
       const nicknameRes = await updateNickname(formData.nickname);
-      
+
       // 更新其他信息(性别等)
       const infoRes = await updateUserInfo({ sex: formData.sex } as any);
-      
+
       if (nicknameRes || infoRes) {
-        Alert.alert('提示', '保存成功', [
+        Alert.alert("提示", "保存成功", [
           {
-            text: '确定',
+            text: "确定",
             onPress: () => {
               refreshUser?.();
               router.back();
-            }
-          }
+            },
+          },
         ]);
       } else {
-        Alert.alert('提示', '保存失败');
+        Alert.alert("提示", "保存失败");
       }
     } catch (error) {
-      console.error('保存失败:', error);
-      Alert.alert('提示', '保存失败');
+      console.error("保存失败:", error);
+      Alert.alert("提示", "保存失败");
     } finally {
       setLoading(false);
     }
@@ -148,7 +156,7 @@ export default function ProfileScreen() {
       resizeMode="cover"
     >
       <StatusBar barStyle="light-content" />
-      
+
       {/* 顶部导航 */}
       <View style={[styles.header, { paddingTop: insets.top }]}>
         <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
@@ -158,9 +166,15 @@ export default function ProfileScreen() {
         <View style={styles.placeholder} />
       </View>
 
-      <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+      <ScrollView
+        style={styles.scrollView}
+        showsVerticalScrollIndicator={false}
+      >
         {/* 头像 */}
-        <TouchableOpacity style={styles.avatarSection} onPress={handleChooseAvatar}>
+        <TouchableOpacity
+          style={styles.avatarSection}
+          onPress={handleChooseAvatar}
+        >
           <View style={styles.avatarWrapper}>
             <Image
               source={{ uri: formData.avatar || Images.common.defaultAvatar }}
@@ -179,7 +193,9 @@ export default function ProfileScreen() {
             <TextInput
               style={styles.formInput}
               value={formData.nickname}
-              onChangeText={(text) => setFormData(prev => ({ ...prev, nickname: text }))}
+              onChangeText={(text) =>
+                setFormData((prev) => ({ ...prev, nickname: text }))
+              }
               placeholder="请输入昵称"
               placeholderTextColor="#999"
               maxLength={20}
@@ -191,31 +207,76 @@ export default function ProfileScreen() {
             <Text style={styles.formLabel}>性别</Text>
             <View style={styles.sexOptions}>
               <TouchableOpacity
-                style={[styles.sexOption, formData.sex === 1 && styles.sexOptionActive]}
+                style={[
+                  styles.sexOption,
+                  formData.sex === 1 && styles.sexOptionActive,
+                ]}
                 onPress={() => handleSexChange(1)}
               >
-                <View style={[styles.radioOuter, formData.sex === 1 && styles.radioOuterActive]}>
+                <View
+                  style={[
+                    styles.radioOuter,
+                    formData.sex === 1 && styles.radioOuterActive,
+                  ]}
+                >
                   {formData.sex === 1 && <View style={styles.radioInner} />}
                 </View>
-                <Text style={[styles.sexText, formData.sex === 1 && styles.sexTextActive]}>男</Text>
+                <Text
+                  style={[
+                    styles.sexText,
+                    formData.sex === 1 && styles.sexTextActive,
+                  ]}
+                >
+                  男
+                </Text>
               </TouchableOpacity>
               <TouchableOpacity
-                style={[styles.sexOption, formData.sex === 2 && styles.sexOptionActive]}
+                style={[
+                  styles.sexOption,
+                  formData.sex === 2 && styles.sexOptionActive,
+                ]}
                 onPress={() => handleSexChange(2)}
               >
-                <View style={[styles.radioOuter, formData.sex === 2 && styles.radioOuterActive]}>
+                <View
+                  style={[
+                    styles.radioOuter,
+                    formData.sex === 2 && styles.radioOuterActive,
+                  ]}
+                >
                   {formData.sex === 2 && <View style={styles.radioInner} />}
                 </View>
-                <Text style={[styles.sexText, formData.sex === 2 && styles.sexTextActive]}>女</Text>
+                <Text
+                  style={[
+                    styles.sexText,
+                    formData.sex === 2 && styles.sexTextActive,
+                  ]}
+                >
+                  女
+                </Text>
               </TouchableOpacity>
               <TouchableOpacity
-                style={[styles.sexOption, formData.sex === 3 && styles.sexOptionActive]}
+                style={[
+                  styles.sexOption,
+                  formData.sex === 3 && styles.sexOptionActive,
+                ]}
                 onPress={() => handleSexChange(3)}
               >
-                <View style={[styles.radioOuter, formData.sex === 3 && styles.radioOuterActive]}>
+                <View
+                  style={[
+                    styles.radioOuter,
+                    formData.sex === 3 && styles.radioOuterActive,
+                  ]}
+                >
                   {formData.sex === 3 && <View style={styles.radioInner} />}
                 </View>
-                <Text style={[styles.sexText, formData.sex === 3 && styles.sexTextActive]}>保密</Text>
+                <Text
+                  style={[
+                    styles.sexText,
+                    formData.sex === 3 && styles.sexTextActive,
+                  ]}
+                >
+                  保密
+                </Text>
               </TouchableOpacity>
             </View>
           </View>
@@ -232,7 +293,9 @@ export default function ProfileScreen() {
             style={styles.saveBtnBg}
             resizeMode="contain"
           >
-            <Text style={styles.saveBtnText}>{loading ? '保存中...' : '确定'}</Text>
+            <Text style={styles.saveBtnText}>
+              {loading ? "保存中..." : "确定"}
+            </Text>
           </ImageBackground>
         </TouchableOpacity>
       </ScrollView>
@@ -245,27 +308,27 @@ const styles = StyleSheet.create({
     flex: 1,
   },
   header: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'space-between',
+    flexDirection: "row",
+    alignItems: "center",
+    justifyContent: "space-between",
     paddingHorizontal: 10,
     height: 80,
   },
   backBtn: {
     width: 40,
     height: 40,
-    justifyContent: 'center',
-    alignItems: 'center',
+    justifyContent: "center",
+    alignItems: "center",
   },
   backIcon: {
     fontSize: 32,
-    color: '#fff',
-    fontWeight: 'bold',
+    color: "#fff",
+    fontWeight: "bold",
   },
   headerTitle: {
     fontSize: 16,
-    fontWeight: 'bold',
-    color: '#fff',
+    fontWeight: "bold",
+    color: "#fff",
   },
   placeholder: {
     width: 40,
@@ -275,7 +338,7 @@ const styles = StyleSheet.create({
     paddingHorizontal: 20,
   },
   avatarSection: {
-    alignItems: 'center',
+    alignItems: "center",
     paddingVertical: 30,
   },
   avatarWrapper: {
@@ -283,49 +346,49 @@ const styles = StyleSheet.create({
     height: 80,
     borderRadius: 40,
     borderWidth: 3,
-    borderColor: '#FFE996',
-    overflow: 'hidden',
+    borderColor: "#FFE996",
+    overflow: "hidden",
   },
   avatar: {
-    width: '100%',
-    height: '100%',
+    width: "100%",
+    height: "100%",
   },
   avatarTip: {
     marginTop: 10,
     fontSize: 12,
-    color: 'rgba(255,255,255,0.7)',
+    color: "rgba(255,255,255,0.7)",
   },
   formSection: {
-    backgroundColor: 'rgba(255,255,255,0.1)',
+    backgroundColor: "rgba(255,255,255,0.1)",
     borderRadius: 10,
     padding: 15,
   },
   formItem: {
-    flexDirection: 'row',
-    alignItems: 'center',
+    flexDirection: "row",
+    alignItems: "center",
     paddingVertical: 15,
     borderBottomWidth: 1,
-    borderBottomColor: 'rgba(255,255,255,0.2)',
+    borderBottomColor: "rgba(255,255,255,0.2)",
   },
   formLabel: {
     width: 60,
     fontSize: 14,
-    color: '#fff',
+    color: "#fff",
   },
   formInput: {
     flex: 1,
     fontSize: 14,
-    color: '#fff',
+    color: "#fff",
     padding: 0,
   },
   sexOptions: {
     flex: 1,
-    flexDirection: 'row',
-    alignItems: 'center',
+    flexDirection: "row",
+    alignItems: "center",
   },
   sexOption: {
-    flexDirection: 'row',
-    alignItems: 'center',
+    flexDirection: "row",
+    alignItems: "center",
     marginRight: 20,
   },
   sexOptionActive: {},
@@ -334,30 +397,30 @@ const styles = StyleSheet.create({
     height: 18,
     borderRadius: 9,
     borderWidth: 2,
-    borderColor: 'rgba(255,255,255,0.5)',
-    justifyContent: 'center',
-    alignItems: 'center',
+    borderColor: "rgba(255,255,255,0.5)",
+    justifyContent: "center",
+    alignItems: "center",
     marginRight: 6,
   },
   radioOuterActive: {
-    borderColor: '#FC7D2E',
+    borderColor: "#FC7D2E",
   },
   radioInner: {
     width: 10,
     height: 10,
     borderRadius: 5,
-    backgroundColor: '#FC7D2E',
+    backgroundColor: "#FC7D2E",
   },
   sexText: {
     fontSize: 14,
-    color: 'rgba(255,255,255,0.7)',
+    color: "rgba(255,255,255,0.7)",
   },
   sexTextActive: {
-    color: '#fff',
+    color: "#fff",
   },
   saveBtn: {
     marginTop: 50,
-    alignItems: 'center',
+    alignItems: "center",
   },
   saveBtnDisabled: {
     opacity: 0.6,
@@ -365,14 +428,14 @@ const styles = StyleSheet.create({
   saveBtnBg: {
     width: 280,
     height: 60,
-    justifyContent: 'center',
-    alignItems: 'center',
+    justifyContent: "center",
+    alignItems: "center",
   },
   saveBtnText: {
     fontSize: 16,
-    fontWeight: 'bold',
-    color: '#fff',
-    textShadowColor: '#000',
+    fontWeight: "bold",
+    color: "#fff",
+    textShadowColor: "#000",
     textShadowOffset: { width: 1, height: 1 },
     textShadowRadius: 2,
   },

+ 1 - 0
app/store/_layout.tsx

@@ -6,6 +6,7 @@ export default function StoreLayout() {
       <Stack.Screen name="index" />
       <Stack.Screen name="checkout" />
       <Stack.Screen name="packages" />
+      <Stack.Screen name="detail" />
     </Stack>
   );
 }

+ 341 - 0
app/store/components/TransferModal.tsx

@@ -0,0 +1,341 @@
+import { Image } from 'expo-image';
+import React, { useEffect, useState } from 'react';
+import {
+  Alert,
+  Modal,
+  Platform,
+  ScrollView,
+  StyleSheet,
+  Text,
+  TextInput,
+  TouchableOpacity,
+  View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { transferOrderSubmit } from '@/services/user';
+
+interface GroupedGoods {
+  total: number;
+  data: {
+    id: string;
+    cover: string;
+    spuId: string;
+    level: string;
+  };
+}
+
+interface TransferModalProps {
+  visible: boolean;
+  selectedItems: Array<{
+    id: string;
+    spu: { id: string; name: string; cover: string };
+    level: string;
+  }>;
+  onClose: () => void;
+  onSuccess: () => void;
+}
+
+export default function TransferModal({
+  visible,
+  selectedItems,
+  onClose,
+  onSuccess,
+}: TransferModalProps) {
+  const insets = useSafeAreaInsets();
+  const [recipientId, setRecipientId] = useState('');
+  const [checked, setChecked] = useState(true);
+  const [submitting, setSubmitting] = useState(false);
+  const [goodsList, setGoodsList] = useState<GroupedGoods[]>([]);
+
+  const showAlert = (msg: string) => {
+    if (Platform.OS === 'web') window.alert(msg);
+    else Alert.alert('提示', msg);
+  };
+
+  useEffect(() => {
+    if (visible && selectedItems.length > 0) {
+      // 合并同款商品
+      const goodsMap: Record<string, GroupedGoods> = {};
+      selectedItems.forEach((item) => {
+        const key = `${item.spu.id}_${item.level}`;
+        if (goodsMap[key]) {
+          goodsMap[key].total += 1;
+        } else {
+          goodsMap[key] = {
+            total: 1,
+            data: {
+              id: item.id,
+              cover: item.spu.cover,
+              spuId: item.spu.id,
+              level: item.level,
+            },
+          };
+        }
+      });
+      setGoodsList(Object.values(goodsMap));
+      setRecipientId('');
+    }
+  }, [visible, selectedItems]);
+
+  const handleSubmit = async () => {
+    if (!recipientId.trim()) {
+      showAlert('请输入受赠人ID');
+      return;
+    }
+    if (!checked) {
+      showAlert('请阅读并同意《转赠风险及责任声明》');
+      return;
+    }
+    if (submitting) return;
+    setSubmitting(true);
+
+    try {
+      const inventoryIds = selectedItems.map((item) => item.id);
+      const levels = selectedItems.map((item) => item.level);
+      const res: any = await transferOrderSubmit({
+        inventoryIds,
+        levels,
+        toUserShortId: recipientId.trim(),
+      });
+
+      if (res && res.success) {
+        showAlert('转赠成功');
+        onClose();
+        onSuccess();
+      } else {
+        showAlert(res?.msg || res?.message || '转赠失败');
+      }
+    } catch (err) {
+      console.error('转赠失败:', err);
+      showAlert('转赠失败,请稍后再试');
+    }
+    setSubmitting(false);
+  };
+
+  return (
+    <Modal
+      visible={visible}
+      transparent
+      animationType="slide"
+      onRequestClose={onClose}
+    >
+      <View style={styles.overlay}>
+        <TouchableOpacity
+          style={styles.overlayBg}
+          onPress={onClose}
+          activeOpacity={1}
+        />
+        <View style={[styles.container, { paddingBottom: insets.bottom + 20 }]}>
+          {/* 标题 */}
+          <View style={styles.header}>
+            <Text style={styles.title}>转赠</Text>
+            <TouchableOpacity style={styles.closeBtn} onPress={onClose}>
+              <Text style={styles.closeBtnText}>×</Text>
+            </TouchableOpacity>
+          </View>
+
+          {/* 商品列表 */}
+          <View style={styles.goodsSection}>
+            <ScrollView horizontal showsHorizontalScrollIndicator={false}>
+              {goodsList.map((goods, idx) => (
+                <View key={idx} style={styles.goodsItem}>
+                  <Image
+                    source={{ uri: goods.data.cover }}
+                    style={styles.goodsImg}
+                    contentFit="contain"
+                  />
+                  <View style={styles.goodsCount}>
+                    <Text style={styles.goodsCountText}>x{goods.total}</Text>
+                  </View>
+                </View>
+              ))}
+            </ScrollView>
+          </View>
+
+          {/* 受赠人ID输入 */}
+          <View style={styles.inputSection}>
+            <Text style={styles.inputLabel}>受赠人ID</Text>
+            <TextInput
+              style={styles.input}
+              value={recipientId}
+              onChangeText={setRecipientId}
+              placeholder="请输入受赠人ID"
+              placeholderTextColor="#999"
+              keyboardType="default"
+            />
+          </View>
+
+          {/* 转赠声明 */}
+          <View style={styles.agreementRow}>
+            <TouchableOpacity style={styles.agreementText} onPress={() => {}}>
+              <Text style={styles.agreementLabel}>已阅读并同意</Text>
+              <Text style={styles.agreementLink}>《转赠风险及责任声明》</Text>
+            </TouchableOpacity>
+            <TouchableOpacity
+              style={[styles.radio, checked && styles.radioChecked]}
+              onPress={() => setChecked(!checked)}
+            >
+              {checked && <Text style={styles.radioIcon}>✓</Text>}
+            </TouchableOpacity>
+          </View>
+
+          {/* 提交按钮 */}
+          <TouchableOpacity
+            style={styles.submitBtn}
+            onPress={handleSubmit}
+            disabled={submitting}
+            activeOpacity={0.7}
+          >
+            <Text style={styles.submitBtnText}>
+              {submitting ? '提交中...' : '立即转赠'}
+            </Text>
+          </TouchableOpacity>
+
+          <View style={styles.fill} />
+        </View>
+      </View>
+    </Modal>
+  );
+}
+
+const styles = StyleSheet.create({
+  overlay: { flex: 1, justifyContent: 'flex-end' },
+  overlayBg: {
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    bottom: 0,
+    backgroundColor: 'rgba(0,0,0,0.5)',
+  },
+  container: {
+    backgroundColor: '#fff',
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    paddingHorizontal: 14,
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    paddingTop: 22,
+    paddingBottom: 5,
+    position: 'relative',
+  },
+  title: { fontSize: 16, fontWeight: 'bold', color: '#000' },
+  closeBtn: {
+    position: 'absolute',
+    right: 0,
+    top: 15,
+    width: 24,
+    height: 24,
+    backgroundColor: '#ebebeb',
+    borderRadius: 12,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  closeBtnText: { fontSize: 18, color: '#a2a2a2', lineHeight: 20 },
+  goodsSection: { paddingVertical: 10 },
+  goodsItem: {
+    width: 79,
+    height: 103,
+    backgroundColor: '#fff',
+    borderRadius: 6,
+    marginRight: 8,
+    alignItems: 'center',
+    justifyContent: 'center',
+    position: 'relative',
+    borderWidth: 1,
+    borderColor: '#eaeaea',
+  },
+  goodsImg: { width: 73, height: 85 },
+  goodsCount: {
+    position: 'absolute',
+    top: 0,
+    right: 0,
+    backgroundColor: '#ff6b00',
+    borderRadius: 2,
+    paddingHorizontal: 4,
+    paddingVertical: 2,
+  },
+  goodsCountText: { color: '#fff', fontSize: 10, fontWeight: 'bold' },
+  inputSection: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    backgroundColor: '#f8f8f8',
+    borderRadius: 6,
+    paddingHorizontal: 10,
+    paddingVertical: 10,
+    marginVertical: 15,
+    shadowColor: '#000',
+    shadowOffset: { width: 0, height: 1 },
+    shadowOpacity: 0.05,
+    shadowRadius: 3,
+  },
+  inputLabel: {
+    fontSize: 14,
+    fontWeight: '500',
+    color: '#333',
+  },
+  input: {
+    flex: 1,
+    marginLeft: 10,
+    textAlign: 'right',
+    height: 30,
+    fontSize: 14,
+    color: '#666',
+  },
+  agreementRow: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingVertical: 10,
+  },
+  agreementText: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  agreementLabel: {
+    fontSize: 12,
+    color: '#333',
+  },
+  agreementLink: {
+    fontSize: 12,
+    color: '#ff9600',
+  },
+  radio: {
+    width: 20,
+    height: 20,
+    borderRadius: 10,
+    borderWidth: 2,
+    borderColor: '#ccc',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  radioChecked: {
+    backgroundColor: '#ff9600',
+    borderColor: '#ff9600',
+  },
+  radioIcon: {
+    color: '#fff',
+    fontSize: 12,
+    fontWeight: 'bold',
+  },
+  submitBtn: {
+    backgroundColor: '#ff9600',
+    borderRadius: 22,
+    height: 44,
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginTop: 5,
+  },
+  submitBtnText: {
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: 'bold',
+  },
+  fill: { height: 25 },
+});

+ 272 - 0
app/store/detail.tsx

@@ -0,0 +1,272 @@
+import { Image } from "expo-image";
+import { useLocalSearchParams, useRouter } from "expo-router";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import {
+    ActivityIndicator,
+    Alert,
+    ImageBackground,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+import { Images } from "@/constants/images";
+import { convertApply, convertPreview, getLuckDetail } from "@/services/award";
+
+const LEVEL_MAP: Record<string, { title: string; color: string }> = {
+  A: { title: "超神", color: "#FF3B30" },
+  B: { title: "欧皇", color: "#FF9500" },
+  C: { title: "隐藏", color: "#AF52DE" },
+  D: { title: "普通", color: "#8E8E93" },
+};
+
+const FROM_TYPE_MAP: Record<string, string> = {
+  MALL: "商城",
+  DISTRIBUTION: "分销",
+  LUCK: "奖池",
+  LUCK_PICKUP: "商品提货",
+  LUCK_EXCHANGE: "商品兑换",
+  SUBSTITUTE: "商品置换",
+  TRANSFER: "商品转赠",
+  LUCK_ROOM: "福利房",
+  LUCK_WHEEL: "魔天轮",
+  DOLL_MACHINE: "扭蛋",
+  RECHARGE: "充值",
+  OFFICIAL: "官方",
+  ACTIVITY: "活动",
+  LUCK_ACTIVITY: "奖池活动",
+  REDEEM_CODE_ACTIVITY: "兑换码活动",
+};
+
+export default function StoreDetailScreen() {
+  const params = useLocalSearchParams<{ id: string }>();
+  const id = params?.id;
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+
+  const [loading, setLoading] = useState(true);
+  const [item, setItem] = useState<any>(null);
+  const [converting, setConverting] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+
+  console.log('[仓库详情] 页面加载, params:', JSON.stringify(params), 'id:', id);
+
+  const loadData = useCallback(async () => {
+    console.log('[仓库详情] loadData 开始, id:', id);
+    if (!id) {
+      setError('未获取到商品ID');
+      setLoading(false);
+      return;
+    }
+    setLoading(true);
+    setError(null);
+    try {
+      const data = await getLuckDetail(id);
+      console.log('[仓库详情] getLuckDetail 返回:', data ? '有数据' : 'null');
+      setItem(data);
+      if (!data) {
+        setError('商品数据为空');
+      }
+    } catch (e: any) {
+      console.error("[仓库详情] 加载失败:", e);
+      setError(`加载失败: ${e?.message || '未知错误'}`);
+    }
+    setLoading(false);
+  }, [id]);
+
+  useEffect(() => {
+    loadData();
+  }, [loadData]);
+
+  const handleConvert = async () => {
+    if (!item || item.magicAmount <= 0) {
+      Alert.alert("提示", "不可兑换");
+      return;
+    }
+    setConverting(true);
+    try {
+      const preview = await convertPreview([item.id]);
+      if (preview?.data) {
+        Alert.alert(
+          "确认兑换",
+          `将兑换 ${item.spu?.name} 获得 ${preview.data?.totalMagicAmount || item.magicAmount} 果实`,
+          [
+            { text: "取消", style: "cancel" },
+            {
+              text: "确定",
+              onPress: async () => {
+                const success = await convertApply([item.id]);
+                if (success) {
+                  Alert.alert("提示", "兑换成功", [
+                    { text: "确定", onPress: () => router.back() },
+                  ]);
+                }
+              },
+            },
+          ],
+        );
+      }
+    } catch (e) {
+      Alert.alert("提示", "兑换失败");
+    }
+    setConverting(false);
+  };
+
+  if (loading) {
+    return (
+      <View style={styles.loadingBox}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    );
+  }
+
+  if (!item) {
+    return (
+      <View style={styles.loadingBox}>
+        <Text style={{ color: "#999", fontSize: 16 }}>{error || '商品不存在'}</Text>
+        <TouchableOpacity onPress={() => router.back()} style={{ marginTop: 20 }}>
+          <Text style={{ color: "#fff", fontSize: 14 }}>返回</Text>
+        </TouchableOpacity>
+      </View>
+    );
+  }
+
+  const levelInfo = LEVEL_MAP[item.level] || { title: "其他", color: "#999" };
+
+  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={() => router.back()}
+          >
+            <Text style={styles.backText}>←</Text>
+          </TouchableOpacity>
+          <Text style={styles.title}>仓库</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        {/* 商品信息 */}
+        <View style={styles.content}>
+          <View style={styles.detailCard}>
+            <Image
+              source={{ uri: item.spu?.cover }}
+              style={styles.coverImage}
+              contentFit="contain"
+            />
+            <Text style={[styles.levelTag, { color: levelInfo.color }]}>
+              {levelInfo.title}
+            </Text>
+            <Text style={styles.goodsName}>{item.spu?.name}</Text>
+            <View style={styles.sourceBox}>
+              <Text style={styles.sourceText}>
+                从{FROM_TYPE_MAP[item.fromRelationType] || "其他"}获得
+              </Text>
+            </View>
+          </View>
+        </View>
+
+        {/* 兑换按钮 - 已关闭 */}
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: "#1a1a2e" },
+  background: { flex: 1 },
+  loadingBox: {
+    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: 20 },
+  title: { color: "#fff", fontSize: 16, fontWeight: "bold" },
+  placeholder: { width: 40 },
+
+  content: { flex: 1, paddingHorizontal: 20, paddingTop: 20 },
+  detailCard: {
+    backgroundColor: "rgba(255,255,255,0.95)",
+    borderRadius: 16,
+    padding: 24,
+    alignItems: "center",
+  },
+  coverImage: {
+    width: 180,
+    height: 180,
+    borderRadius: 10,
+    marginBottom: 16,
+  },
+  levelTag: {
+    fontSize: 16,
+    fontWeight: "bold",
+    marginBottom: 8,
+    textShadowColor: "#000",
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 0,
+  },
+  goodsName: {
+    fontSize: 16,
+    color: "#333",
+    fontWeight: "bold",
+    textAlign: "center",
+    marginBottom: 12,
+    lineHeight: 22,
+  },
+  sourceBox: {
+    backgroundColor: "rgba(0,0,0,0.05)",
+    borderRadius: 20,
+    paddingHorizontal: 16,
+    paddingVertical: 6,
+  },
+  sourceText: { fontSize: 13, color: "#666" },
+
+  bottomBar: {
+    paddingHorizontal: 30,
+    paddingTop: 10,
+    alignItems: "center",
+  },
+  convertBtn: {
+    width: "80%",
+    height: 60,
+  },
+  convertBtnBg: {
+    width: "100%",
+    height: "100%",
+    justifyContent: "center",
+    alignItems: "center",
+  },
+  convertBtnText: {
+    color: "#000",
+    fontSize: 16,
+    fontWeight: "bold",
+  },
+  convertBtnSub: {
+    color: "#735200",
+    fontSize: 11,
+  },
+});

+ 69 - 4
app/store/index.tsx

@@ -7,6 +7,7 @@ import {
     FlatList,
     ImageBackground,
     Platform,
+    Pressable,
     RefreshControl,
     ScrollView,
     StatusBar,
@@ -24,7 +25,9 @@ import {
     moveOutSafeStore,
     moveToSafeStore,
 } from '@/services/award';
+import { getParamConfig } from '@/services/user';
 import CheckoutModal from './components/CheckoutModal';
+import TransferModal from './components/TransferModal';
 
 const LEVEL_MAP: Record<string, { title: string; color: string }> = {
   A: { title: '超神', color: '#ff0000' },  // 红色
@@ -61,7 +64,7 @@ interface StoreItem {
   safeFlag: number;
   magicAmount?: number;
   fromRelationType: string;
-  spu: { id: string; name: string; cover: string };
+  spu: { id: string; name: string; cover: string; marketPrice?: number };
 }
 
 interface PickupItem {
@@ -103,6 +106,8 @@ export default function StoreScreen() {
   const [hasMore, setHasMore] = useState(true);
   const [checkMap, setCheckMap] = useState<Record<string, StoreItem>>({});
   const [checkoutVisible, setCheckoutVisible] = useState(false);
+  const [transferVisible, setTransferVisible] = useState(false);
+  const [shareOnState, setShareOnState] = useState(0);
 
   const mainTabs = ['未使用', '保险柜', '已提货'];
 
@@ -146,6 +151,19 @@ export default function StoreScreen() {
     }
   }, [mainTabIndex, levelTabIndex, loading, hasMore]);
 
+  // 获取转赠开关
+  useEffect(() => {
+    const fetchShareOn = async () => {
+      try {
+        const config = await getParamConfig('share_on');
+        setShareOnState(config?.state || 0);
+      } catch (e) {
+        console.error('获取转赠开关失败:', e);
+      }
+    };
+    fetchShareOn();
+  }, []);
+
   useEffect(() => {
     setPage(1); setList([]); setHasMore(true); setCheckMap({});
     loadData(1, true);
@@ -219,6 +237,18 @@ export default function StoreScreen() {
     handleRefresh();
   };
 
+  const handleTransfer = () => {
+    const selected = Object.values(checkMap);
+    if (selected.length === 0) { showAlert('请至少选择一个商品!'); return; }
+    setTransferVisible(true);
+  };
+
+  const handleTransferSuccess = () => {
+    setTransferVisible(false);
+    setCheckMap({});
+    handleRefresh();
+  };
+
   const showAlert = (msg: string) => {
     if (Platform.OS === 'web') window.alert(msg);
     else Alert.alert('提示', msg);
@@ -230,7 +260,10 @@ export default function StoreScreen() {
     const canSelect = mainTabIndex === 1 || item.safeFlag !== 1;
     return (
       <ImageBackground source={{ uri: Images.mine.storeItemBg }} style={styles.cell} resizeMode="stretch">
-        <View style={styles.cellContent}>
+        <Pressable style={styles.cellContent} onPress={() => {
+          console.log('[仓库store] 点击商品详情, id:', item.id);
+          router.push({ pathname: '/store/detail', params: { id: item.id } } as any);
+        }}>
           <TouchableOpacity style={styles.cellHeader} onPress={() => canSelect && handleChoose(item)}>
             <View style={styles.headerLeft}>
               {canSelect && (
@@ -251,11 +284,14 @@ export default function StoreScreen() {
             </ImageBackground>
             <View style={styles.goodsInfo}>
               <Text style={styles.goodsName} numberOfLines={2}>{item.spu?.name}</Text>
+              {(item.spu?.marketPrice ?? 0) > 0 && (
+                <Text style={styles.goodsPrice}>¥{item.spu?.marketPrice}</Text>
+              )}
               <Text style={styles.goodsSource}>从{FROM_TYPE_MAP[item.fromRelationType] || '其他'}获得</Text>
             </View>
             <Text style={styles.arrow}>{'>'}</Text>
           </View>
-        </View>
+        </Pressable>
       </ImageBackground>
     );
   };
@@ -437,6 +473,17 @@ export default function StoreScreen() {
           </View>
         )}
         
+        {/* 转赠浮动按钮 */}
+        {mainTabIndex === 0 && shareOnState === 1 && list.length > 0 && (
+          <TouchableOpacity
+            style={styles.transferFloatBtn}
+            onPress={handleTransfer}
+            activeOpacity={0.8}
+          >
+            <Image source={{ uri: Images.mine.transferBut }} style={styles.transferFloatImg} />
+          </TouchableOpacity>
+        )}
+
         {/* 提货弹窗 */}
         <CheckoutModal
           visible={checkoutVisible}
@@ -444,6 +491,14 @@ export default function StoreScreen() {
           onClose={() => setCheckoutVisible(false)}
           onSuccess={handleCheckoutSuccess}
         />
+
+        {/* 转赠弹窗 */}
+        <TransferModal
+          visible={transferVisible}
+          selectedItems={Object.values(checkMap)}
+          onClose={() => setTransferVisible(false)}
+          onSuccess={handleTransferSuccess}
+        />
       </ImageBackground>
     </View>
   );
@@ -452,6 +507,15 @@ export default function StoreScreen() {
 const styles = StyleSheet.create({
   container: { flex: 1, backgroundColor: '#1a1a2e' },
   background: { flex: 1 },
+  transferFloatBtn: {
+    position: 'absolute',
+    bottom: 200,
+    right: 10,
+    width: 48,
+    height: 50,
+    zIndex: 100,
+  },
+  transferFloatImg: { width: 48, height: 50 },
   headerBg: { position: 'absolute', top: 0, left: 0, width: '100%', height: 160 },
   header: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 100, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 10, paddingBottom: 10 },
   backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
@@ -578,7 +642,8 @@ const styles = StyleSheet.create({
   goodsImgBg: { width: 65, height: 65, justifyContent: 'center', alignItems: 'center', marginRight: 12, padding: 7 },
   goodsImg: { width: '100%', height: '100%' },
   goodsInfo: { flex: 1, justifyContent: 'center', paddingRight: 8 },
-  goodsName: { fontSize: 15, color: '#333', fontWeight: 'bold', marginBottom: 6 },
+  goodsName: { fontSize: 15, color: '#333', fontWeight: 'bold', marginBottom: 4 },
+  goodsPrice: { fontSize: 14, color: '#ff6b00', fontWeight: 'bold', marginBottom: 4 },
   goodsDesc: { fontSize: 12, color: '#999' },
   arrowIcon: { fontSize: 18, color: '#ccc', marginLeft: 8 },
 

+ 196 - 0
components/CouponModal.tsx

@@ -0,0 +1,196 @@
+import React from "react";
+import {
+  Alert,
+  Modal,
+  ScrollView,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from "react-native";
+
+import { Colors } from "@/constants/Colors";
+import { batchReceiveCoupon, CouponItem } from "@/services/activity";
+
+interface Props {
+  visible: boolean;
+  coupons: CouponItem[];
+  onClose: () => void;
+  onSuccess: () => void;
+}
+
+export function CouponModal({ visible, coupons, onClose, onSuccess }: Props) {
+  const handleReceive = async () => {
+    try {
+      const ids = coupons.map((c) => c.id);
+      const ok = await batchReceiveCoupon(ids);
+      if (ok) {
+        Alert.alert("领取成功", "优惠券已发放到您的账户");
+        onSuccess();
+      }
+      onClose();
+    } catch (e) {
+      console.error("领取优惠券失败:", e);
+      onClose();
+    }
+  };
+
+  const formatTime = (val: string) => (val ? val.slice(0, 10) : "");
+
+  if (!coupons || coupons.length === 0) return null;
+
+  return (
+    <Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
+      <View style={styles.overlay}>
+        <View style={styles.dialog}>
+          {/* 标题 */}
+          <View style={styles.titleRow}>
+            <Text style={styles.title}>新人大礼包</Text>
+            <TouchableOpacity onPress={onClose} style={styles.closeBtn}>
+              <Text style={styles.closeBtnText}>✕</Text>
+            </TouchableOpacity>
+          </View>
+
+          {/* 优惠券列表 */}
+          <ScrollView style={styles.scrollArea} showsVerticalScrollIndicator={false}>
+            {coupons.map((item, index) => (
+              <View key={item.id || index} style={styles.couponCard}>
+                <View style={styles.couponLeft}>
+                  <Text style={styles.couponLabel}>福利优惠劵</Text>
+                </View>
+                <View style={styles.couponCenter}>
+                  <Text style={styles.couponAmount}>
+                    ¥ <Text style={styles.couponAmountNum}>{item.amount}</Text>
+                  </Text>
+                </View>
+                <View style={styles.couponRight}>
+                  <Text style={styles.couponCondition}>
+                    {item.fullAmount > 0 ? `满${item.fullAmount}可用` : "无门槛"}
+                  </Text>
+                  <Text style={styles.couponExpiry}>
+                    {formatTime(item.endTime)}过期
+                  </Text>
+                </View>
+              </View>
+            ))}
+          </ScrollView>
+
+          {/* 一键领取按钮 */}
+          <TouchableOpacity style={styles.receiveBtn} onPress={handleReceive} activeOpacity={0.8}>
+            <Text style={styles.receiveBtnText}>一键领取</Text>
+          </TouchableOpacity>
+        </View>
+      </View>
+    </Modal>
+  );
+}
+
+const styles = StyleSheet.create({
+  overlay: {
+    flex: 1,
+    backgroundColor: "rgba(0,0,0,0.6)",
+    justifyContent: "center",
+    alignItems: "center",
+  },
+  dialog: {
+    width: "85%",
+    maxHeight: "70%",
+    backgroundColor: Colors.darkCard,
+    borderRadius: 16,
+    borderWidth: 1,
+    borderColor: "rgba(0, 243, 255, 0.4)",
+    overflow: "hidden",
+  },
+  titleRow: {
+    flexDirection: "row",
+    justifyContent: "center",
+    alignItems: "center",
+    paddingVertical: 16,
+    paddingHorizontal: 16,
+    position: "relative",
+  },
+  title: {
+    fontSize: 22,
+    fontWeight: "bold",
+    color: "#fff",
+    textShadowColor: Colors.neonBlue,
+    textShadowRadius: 8,
+  },
+  closeBtn: {
+    position: "absolute",
+    right: 16,
+    top: 12,
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    backgroundColor: "rgba(255,255,255,0.1)",
+    justifyContent: "center",
+    alignItems: "center",
+  },
+  closeBtnText: {
+    color: "#fff",
+    fontSize: 14,
+  },
+  scrollArea: {
+    maxHeight: 250,
+    paddingHorizontal: 16,
+  },
+  couponCard: {
+    flexDirection: "row",
+    alignItems: "center",
+    backgroundColor: "rgba(255,255,255,0.05)",
+    borderRadius: 10,
+    marginBottom: 10,
+    padding: 14,
+    borderWidth: 1,
+    borderColor: "rgba(0, 243, 255, 0.15)",
+  },
+  couponLeft: {
+    marginRight: 12,
+  },
+  couponLabel: {
+    color: Colors.neonBlue,
+    fontSize: 11,
+    fontWeight: "bold",
+    writingDirection: "ltr",
+  },
+  couponCenter: {
+    marginRight: 12,
+  },
+  couponAmount: {
+    color: "#fff",
+    fontSize: 14,
+    fontWeight: "bold",
+  },
+  couponAmountNum: {
+    fontSize: 32,
+    fontWeight: "800",
+    color: "#fff",
+  },
+  couponRight: {
+    flex: 1,
+  },
+  couponCondition: {
+    color: "#fff",
+    fontSize: 13,
+    fontWeight: "500",
+  },
+  couponExpiry: {
+    color: Colors.textTertiary,
+    fontSize: 11,
+    marginTop: 3,
+  },
+  receiveBtn: {
+    marginHorizontal: 16,
+    marginVertical: 16,
+    backgroundColor: Colors.neonBlue,
+    borderRadius: 24,
+    paddingVertical: 14,
+    alignItems: "center",
+  },
+  receiveBtnText: {
+    color: "#000",
+    fontSize: 16,
+    fontWeight: "bold",
+  },
+});

+ 6 - 11
components/RegionPicker.tsx

@@ -51,19 +51,14 @@ export const RegionPicker: React.FC<RegionPickerProps> = ({
   const loadProvinces = async () => {
     setLoading(true);
     try {
-      // Assuming pid=0 or 1 for top level. service says pid=1 is default
-      const list = await getArea(0); // Try 0 first, typically 0 is root
-      if (list && list.length > 0) {
-          setProvinces(list);
-      } else {
-        // Fallback or retry with 1 if 0 returns empty/null?
-        // Service code: pid ? { pid } : { pid: 1 }. So if I pass 0, it passes {pid:0}.
-        // Let's assume typical tree structure. If fail, maybe try different PID or log error.
-        // Actually, let's try 0.
-         setProvinces(list || []);
+      // 省级节点的 pid 通常为 1(中国节点),先尝试 1,空则回退 0
+      let list = await getArea(1);
+      if (!list || list.length === 0) {
+        list = await getArea(0);
       }
+      setProvinces(list || []);
     } catch (e) {
-      console.error(e);
+      console.error('加载省份数据失败:', e);
     }
     setLoading(false);
   };

+ 12 - 7
components/mine/MenuCell.tsx

@@ -14,9 +14,10 @@ interface MenuItem {
 interface MenuCellProps {
   onItemPress: (type: string) => void;
   showWallet?: boolean;
+  showExchange?: boolean;
 }
 
-export function MenuCell({ onItemPress, showWallet = false }: MenuCellProps) {
+export function MenuCell({ onItemPress, showWallet = false, showExchange = false }: MenuCellProps) {
   const menuList: MenuItem[] = [
     ...(showWallet
       ? [
@@ -32,12 +33,16 @@ export function MenuCell({ onItemPress, showWallet = false }: MenuCellProps) {
       title: "全部订单",
       type: "2_0",
     },
-    // {
-    //   icon: Images.mine.exchangeIcon || "",
-    //   title: "兑换码",
-    //   tip: "10:00 ~ 18:00",
-    //   type: "6_1",
-    // },
+    ...(showExchange
+      ? [
+          {
+            icon: Images.mine.exchangeIcon || "",
+            title: "兑换码",
+            tip: "10:00 ~ 18:00",
+            type: "6_1",
+          },
+        ]
+      : []),
     {
       icon: Images.mine.customerService || "",
       title: "联系客服",

+ 1 - 0
constants/images.ts

@@ -232,6 +232,7 @@ export const Images = {
     typeSelectIconT: `${CDN_BASE}/mine/typeSelectIconT.png`,
     typeSelectIconB: `${CDN_BASE}/mine/typeSelectIconB.png`,
     checkAll: `${CDN_BASE}/mine/checkAll.png`,
+    transferBut: `${CDN_BASE}/mine/transferBut.png`,
   },
   // 地址相关
   address: {

+ 34 - 0
services/activity.ts

@@ -0,0 +1,34 @@
+// 活动服务 - 新人大礼包、优惠券等
+import { get, post, postL } from './http';
+
+const apis = {
+  NEWBIE_GIFT_BAG: '/api/activity/newbieGiftBag/info',
+  COUPON_LIST_VALID: '/api/coupon/listValidCoupon',
+  COUPON_BATCH_RECEIVE: '/api/coupon/batchReceive',
+};
+
+// 新人大礼包信息
+export const getNewbieGiftBagInfo = async () => {
+  const res = await get<any>(apis.NEWBIE_GIFT_BAG);
+  return res.success && res.data ? res.data : null;
+};
+
+// 获取可领优惠券列表
+export interface CouponItem {
+  id: string;
+  amount: number;
+  fullAmount: number;
+  endTime: string;
+  name?: string;
+}
+
+export const listValidCoupon = async (scene: string, targetObj?: any) => {
+  const res = await post<CouponItem[]>(apis.COUPON_LIST_VALID, { scene, targetObj });
+  return res.success && res.data ? res.data : [];
+};
+
+// 一键领取优惠券
+export const batchReceiveCoupon = async (ids: string[]) => {
+  const res = await postL<any>(apis.COUPON_BATCH_RECEIVE, { ids });
+  return res.success;
+};

+ 2 - 2
services/award.ts

@@ -50,7 +50,7 @@ const apis = {
   FEEDBACK_SUBMIT: '/api/app/feedback/submit',
   MY_BOXES: '/api/luck/treasure-box/my-boxes',
   OPEN_BOX: '/api/luck/treasure-box/open',
-  HARRY_EXCHANGE: '/api/redeemCode/exchange',
+  HARRY_EXCHANGE: '/api/redemptionCode/redeem',
   WIN_RECORDS: '/api/luck/treasure-box/win-records',
   COUNT_RECORDS: '/api/luckPool/countRecordsAfterLastLevel',
 };
@@ -421,7 +421,7 @@ export const getSumInventory = async () => {
 
 // 兑换码兑换
 export const harryExchange = async (params: { code: string }) => {
-  const res = await postL(apis.HARRY_EXCHANGE, params);
+  const res = await post(apis.HARRY_EXCHANGE, params, { silent: true });
   return res;
 };
 

+ 33 - 32
services/base.ts

@@ -1,5 +1,6 @@
 // 基础服务 - 对应 supermart-mini/service/base.js
-import { get, post } from "./http";
+import { Platform } from "react-native";
+import { get, getToken, post } from "./http";
 
 const apis = {
   PAGE_CONFIG: "/api/app/page/getByPageId",
@@ -81,60 +82,60 @@ export const track = async () => {
 export const uploadFile = async (
   fileUri: string,
   folder = "avatar",
-): Promise<string | null> => {
+  externalToken?: string,
+): Promise<string | { error: string }> => {
   try {
-    const formData = new FormData();
+    let processedUri = decodeURI(fileUri);
+    if (Platform.OS === "ios") {
+      if (!processedUri.startsWith("file://") && !processedUri.startsWith("http")) {
+        processedUri = `file://${processedUri}`;
+      }
+    }
 
-    // 获取文件名
-    const fileName = fileUri.split("/").pop() || "image.jpg";
+    const fileName = processedUri.split("/").pop() || `avatar_${Date.now()}.jpg`;
+    const fileExt = fileName.split(".").pop()?.toLowerCase() || "jpg";
+    const mimeType = fileExt === "png" ? "image/png" : "image/jpeg";
 
-    // 创建文件对象 (React Native 格式)
-    const fileObj = {
-      uri: fileUri,
-      type: "image/jpeg",
-      name: fileName,
-    } as any;
+    console.log("[uploadFile] uri:", processedUri.substring(0, 80), "name:", fileName);
 
-    formData.append("file", fileObj);
+    const formData = new FormData();
+    formData.append("file", {
+      uri: processedUri,
+      type: mimeType,
+      name: fileName,
+    } as any);
     formData.append("appId", "supermart-acetoys");
     formData.append("folder", folder);
 
-    // 使用http.ts中的getToken获取token
-    const { getToken } = require("./http");
-    const token = getToken();
+    const token = externalToken || getToken();
+    console.log("[uploadFile] token:", token ? token.substring(0, 10) + "..." : "空!");
 
     const response = await fetch("https://mm.acefig.com/api/oss/file/upload", {
       method: "POST",
       headers: {
-        // 不设置 Content-Type,让 fetch 自动处理 multipart/form-data 边界
         Authentication: token || "",
       },
       body: formData,
     });
 
-    // 检查响应状态
+    const responseText = await response.text();
+    console.log("[uploadFile] status:", response.status, "body:", responseText?.substring(0, 200));
+
     if (!response.ok) {
-      const errorText = await response.text();
-      console.error("上传响应状态:", response.status, "响应内容:", errorText);
-      return null;
+      return { error: `HTTP ${response.status}: ${responseText?.substring(0, 100)}` };
     }
-
-    const text = await response.text();
-    if (!text) {
-      console.error("上传响应为空");
-      return null;
+    if (!responseText) {
+      return { error: "服务器响应为空" };
     }
 
-    const result = JSON.parse(text);
-    // code 可能是数字或字符串
+    const result = JSON.parse(responseText);
     if ((result.code === 0 || result.code === "0") && result.data?.url) {
       return result.data.url;
     }
-    console.error("上传返回错误:", result);
-    return null;
-  } catch (error) {
-    console.error("上传文件失败:", error);
-    return null;
+    return { error: `code:${result.code} msg:${result.msg || JSON.stringify(result).substring(0, 80)}` };
+  } catch (error: any) {
+    console.error("[uploadFile] 异常:", error);
+    return { error: `异常: ${error?.message || String(error).substring(0, 100)}` };
   }
 };
 

+ 32 - 1
services/dimension.ts

@@ -1,4 +1,4 @@
-import { get, getL, post } from "./http";
+import { get, getL, post, postL } from "./http";
 
 const apis = {
   LIST: "/api/luckRoom",
@@ -109,6 +109,34 @@ export const createRoom = async (params: {
   }
 };
 
+// 获取审核记录列表
+export const roomAuditRecord = async (
+  current: number,
+  size: number,
+  roomId: string,
+  auditStatus: number,
+) => {
+  const res = await post(apis.ROOM_AUDIT_RECORD, {
+    current,
+    size,
+    roomId,
+    auditStatus,
+  });
+  return res.data;
+};
+
+// 审核通过
+export const roomAuditPass = async (id: string) => {
+  const res = await postL(apis.ROOM_AUDIT_PASS, { id });
+  return res.success;
+};
+
+// 审核拒绝
+export const roomAuditUnpass = async (id: string) => {
+  const res = await postL(apis.ROOM_AUDIT_UNPASS, { id });
+  return res.success;
+};
+
 export default {
   getWealList,
   getWealDetail,
@@ -120,6 +148,9 @@ export default {
   getDateTimeScope,
   getMyCreateList,
   createRoom,
+  roomAuditRecord,
+  roomAuditPass,
+  roomAuditUnpass,
 
   // 扭蛋机相关 API
   catchDollDetail: async () => {

+ 10 - 0
services/user.ts

@@ -195,6 +195,16 @@ export const getNewUserNum = async (params?: any) => {
   return res;
 };
 
+// 转赠订单提交
+export const transferOrderSubmit = async (params: {
+  inventoryIds: string[];
+  levels: string[];
+  toUserShortId: string;
+}): Promise<{ success: boolean; message?: string }> => {
+  const res = await postL('/api/transferOrder/submit', params);
+  return res;
+};
+
 export default {
   login,
   logout,

+ 2 - 2
services/wallet.ts

@@ -5,8 +5,8 @@ const apis = {
     COUPON: '/api/coupon/pageMyValidCoupon',
     BILL: '/api/wallet/bill',
     
-    WITHDRAW_PRE: '/api/wallet/withdrawPre',
-    WITHDRAW: '/api/wallet/withdraw',
+    WITHDRAW_PRE: '/api/wallet/withdraw/preInfo',
+    WITHDRAW: '/api/wallet/withdraw/submit',
     WITHDRAW_INFO: '/api/user/withdraw/info', // Bank info
     WITHDRAW_SAVE: '/api/user/withdraw/save',