Explorar o código

feat: 移植转赠功能 (迁移自 supermart-mini),更新版本至 1.0.6 (34)

zbb hai 2 semanas
pai
achega
800522b483
Modificáronse 6 ficheiros con 471 adicións e 2 borrados
  1. 2 2
      app.json
  2. 60 0
      app/cloud-warehouse/index.tsx
  3. 341 0
      app/store/components/TransferModal.tsx
  4. 57 0
      app/store/index.tsx
  5. 1 0
      constants/images.ts
  6. 10 0
      services/user.ts

+ 2 - 2
app.json

@@ -2,7 +2,7 @@
   "expo": {
     "name": "艾斯",
     "slug": "asios",
-    "version": "1.0.5",
+    "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": "33",
+      "buildNumber": "34",
       "infoPlist": {
         "CFBundleDisplayName": "艾斯潮盒",
         "ITSAppUsesNonExemptEncryption": false,

+ 60 - 0
app/cloud-warehouse/index.tsx

@@ -25,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
@@ -118,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 = ["未使用", "保险柜", "已提货"];
 
@@ -164,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([]);
@@ -273,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);
@@ -674,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}
@@ -681,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>
   );
@@ -689,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,

+ 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 },
+});

+ 57 - 0
app/store/index.tsx

@@ -25,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' },  // 红色
@@ -104,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 = ['未使用', '保险柜', '已提货'];
 
@@ -147,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);
@@ -220,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);
@@ -444,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}
@@ -451,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>
   );
@@ -459,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' },

+ 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: {

+ 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,