panhui vor 5 Jahren
Ursprung
Commit
1e116d85b6

+ 42 - 41
.eslintrc.json

@@ -1,43 +1,44 @@
 {
-    "extends": ["airbnb", "prettier"],
-    "plugins": ["prettier"],
-    "parser": "babel-eslint",
-    "env": {
-        "browser": true,
-        "es6": true
-    },
-    "globals": {
-        "__DEV__": "readonly"
-    },
-    "rules": {
-        "prettier/prettier": "error",
-        "no-use-before-define": [
-            "error",
-            { "functions": true, "classes": true, "variables": false }
-        ],
-        "react/jsx-indent": [2, 2, { "checkAttributes": false }],
-        "no-else-return": 0,
-        "import/no-unresolved": 0,
-        "no-unused-vars": [
-            "error",
-            { "vars": "local", "varsIgnorePattern": "WebBrowser" }
-        ],
-        "no-nested-ternary": 0,
-        "react/no-array-index-key": 0,
-        "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
-        "no-unused-expressions": [
-            "error",
-            { "allowShortCircuit": true, "allowTernary": true }
-        ],
-        "no-param-reassign": 0,
-        "react/prop-types": [0, { "ignore": ["navigation"] }],
-        "react/jsx-props-no-spreading": [
-            1,
-            {
-                "html": "ignore",
-                "exceptions": ["Icon", "props", "Avatar", "Text"]
-            }
-        ],
-        "react/jsx-curly-newline": "off"
-    }
+  "extends": ["airbnb", "prettier"],
+  "plugins": ["prettier"],
+  "parser": "babel-eslint",
+  "env": {
+    "browser": true,
+    "es6": true
+  },
+  "globals": {
+    "__DEV__": "readonly"
+  },
+  "rules": {
+    "prettier/prettier": "error",
+    "no-use-before-define": [
+      "error",
+      { "functions": true, "classes": true, "variables": false }
+    ],
+    "react/jsx-indent": [2, 2, { "checkAttributes": false }],
+    "no-else-return": 0,
+    "import/no-unresolved": 0,
+    "no-unused-vars": [
+      "error",
+      { "vars": "local", "varsIgnorePattern": "WebBrowser" }
+    ],
+    "no-nested-ternary": 0,
+    "react/no-array-index-key": 0,
+    "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
+    "no-unused-expressions": [
+      "error",
+      { "allowShortCircuit": true, "allowTernary": true }
+    ],
+    "no-param-reassign": 0,
+    "react/prop-types": [0, { "ignore": ["navigation"] }],
+    "react/jsx-props-no-spreading": [
+      1,
+      {
+        "html": "ignore",
+        "exceptions": ["Icon", "props", "Avatar", "Text"]
+      }
+    ],
+    "react/jsx-curly-newline": "off",
+    "react/jsx-one-expression-per-line": [0]
+  }
 }

+ 11 - 11
App.js

@@ -63,16 +63,16 @@ export default function App() {
   } else {
     return (
       <View style={styles.container}>
-        <UseAPIProvider
-          value={{
-            requestMethod: request,
-          }}
-        >
+        <Provider>
           <PaperProvider theme={theme}>
-            <Provider>
-              {Platform.OS !== 'ios' && (
-                <StatusBar translucent={false} backgroundColor="#FFC21C" />
-              )}
+            {Platform.OS !== 'ios' && (
+              <StatusBar translucent={false} backgroundColor="#FFC21C" />
+            )}
+            <UseAPIProvider
+              value={{
+                requestMethod: request,
+              }}
+            >
               <NavigationContainer ref={navigationRef}>
                 <Stack.Navigator
                   initRouteName="InitApp"
@@ -88,9 +88,9 @@ export default function App() {
                   <Stack.Screen name="Home" component={BottomTabNavigator} />
                 </Stack.Navigator>
               </NavigationContainer>
-            </Provider>
+            </UseAPIProvider>
           </PaperProvider>
-        </UseAPIProvider>
+        </Provider>
       </View>
     );
   }

+ 80 - 0
Utils/NumberUtils.js

@@ -0,0 +1,80 @@
+/* eslint-disable no-restricted-properties */
+export function accAdd(arg1, arg2) {
+  let r1;
+  let r2;
+
+  try {
+    r1 = arg1.toFixed(2).split('.')[1].length;
+  } catch (e) {
+    r1 = 0;
+  }
+
+  try {
+    r2 = arg2.toFixed(2).split('.')[1].length;
+  } catch (e) {
+    r2 = 0;
+  }
+
+  const m = Math.pow(10, Math.max(r1, r2));
+
+  return (arg1 * m + arg2 * m) / m;
+}
+
+// 减法函数
+
+export function Subtr(arg1, arg2) {
+  let r1;
+  let r2;
+
+  try {
+    r1 = arg1.toFixed(2).split('.')[1].length;
+  } catch (e) {
+    r1 = 0;
+  }
+
+  try {
+    r2 = arg2.toFixed(2).split('.')[1].length;
+  } catch (e) {
+    r2 = 0;
+  }
+
+  const m = Math.pow(10, Math.max(r1, r2)); // 动态控制精度长度
+
+  const n = r1 >= r2 ? r1 : r2;
+
+  return parseFloat(((arg1 * m - arg2 * m) / m).toFixed(n));
+}
+
+// 乘法函数
+
+export function accMul(arg1, arg2) {
+  let m = 0;
+  const s1 = arg1.toFixed(2);
+  const s2 = arg2.toFixed(2);
+
+  m += s1.split('.')[1].length;
+
+  m += s2.split('.')[1].length;
+
+  return (
+    (Number(s1.replace('.', '')) * Number(s2.replace('.', ''))) /
+    Math.pow(10, m)
+  );
+}
+
+// 除法函数
+
+export function accDiv(arg1, arg2) {
+  let t1 = 0;
+  let t2 = 0;
+
+  t1 = arg1.toFixed(2).split('.')[1].length;
+
+  t2 = arg2.toFixed(2).split('.')[1].length;
+
+  const r1 = Number(arg1.toFixed(2).replace('.', ''));
+
+  const r2 = Number(arg2.toFixed(2).replace('.', ''));
+
+  return (r1 / r2) * Math.pow(10, t2 - t1);
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 0
Utils/SvgUtilsNew.js


+ 36 - 0
Utils/TotastUtils.js

@@ -0,0 +1,36 @@
+import React from 'react';
+import { Modal } from '@ant-design/react-native';
+import Text from '../components/Text';
+
+export function alert(title, content, submitEvent) {
+  Modal.alert(
+    <Text size="s1" bold center>
+      {title || '提示'}
+    </Text>,
+    <Text type="error" center>
+      {content}
+    </Text>,
+    [
+      {
+        text: '取消',
+        onPress: () => console.log('cancel'),
+        style: { color: '#000', fontSize: 12, lineHeight: 30 },
+      },
+      {
+        text: '确定',
+        onPress: () => {
+          if (submitEvent) {
+            submitEvent();
+          }
+        },
+        style: { color: '#FFC21C', fontSize: 12, lineHeight: 30 },
+      },
+    ]
+  );
+}
+export function operation(list) {
+  Modal.operation([
+    { text: '标为未读', onPress: () => console.log('标为未读被点击了') },
+    { text: '置顶聊天', onPress: () => console.log('置顶聊天被点击了') },
+  ]);
+}

+ 29 - 9
components/Button.js

@@ -8,6 +8,7 @@ import {
   Text,
   Subheading,
 } from 'react-native-paper';
+import { ActivityIndicator, Flex } from '@ant-design/react-native';
 
 function MyButton(props) {
   const {
@@ -21,11 +22,15 @@ function MyButton(props) {
     block,
     style,
     left,
+    width,
+    height,
+    radius,
+    disabled,
+    loading,
   } = props;
-  let { fontColor } = props;
+  let { fontColor, color } = props;
   const { colors } = theme;
   let contentStyle = {};
-  let color = '';
   let mode = 'contained';
   let childNode;
   let dark = true;
@@ -33,6 +38,9 @@ function MyButton(props) {
   switch (type) {
     case 'primary':
       color = colors.primary;
+      if (disabled) {
+        color = '#8E8E8E';
+      }
       break;
     case 'info':
       if (outline || text) {
@@ -46,7 +54,7 @@ function MyButton(props) {
       color = colors.error;
       break;
     default:
-      color = colors.font;
+      color = color || colors.primary;
       break;
   }
 
@@ -61,7 +69,7 @@ function MyButton(props) {
 
   switch (size) {
     case 'mini':
-      contentStyle = { height: 22 };
+      contentStyle = { height: 25 };
       childNode = () => (
         <Text
           style={[
@@ -88,7 +96,7 @@ function MyButton(props) {
       );
       break;
     case 'normal':
-      contentStyle = { width: 120, height: 44 };
+      contentStyle = { width: width || 120, height: height || 44 };
       childNode = () => (
         <Paragraph
           style={[
@@ -102,7 +110,7 @@ function MyButton(props) {
       break;
     case 'large':
       // 16
-      contentStyle = { width: 120, height: 44 };
+      contentStyle = { width: width || 120, height: height || 44 };
       if (left) {
         contentStyle = { height: 44, paddingRight: 30 };
       }
@@ -118,7 +126,7 @@ function MyButton(props) {
       );
       break;
     default:
-      contentStyle = { width: 120, height: 30 };
+      contentStyle = { width: width || 120, height: height || 30 };
       childNode = () => (
         <Paragraph
           style={[
@@ -139,7 +147,12 @@ function MyButton(props) {
         mode={mode}
         color={color}
         contentStyle={contentStyle}
-        onPress={onPress}
+        onPress={() => {
+          if (!loading && onPress) {
+            onPress();
+          }
+        }}
+        disabled={!!disabled}
         style={{
           elevation: 0,
           shadowOffset: {
@@ -167,8 +180,15 @@ function MyButton(props) {
               height: 0,
             },
             shadowOpacity: 0,
+            borderRadius: radius || radius === 0 ? radius : 3,
+            paddingVertical: 0,
+          }}
+          disabled={!!disabled}
+          onPress={() => {
+            if (!loading && onPress) {
+              onPress();
+            }
           }}
-          onPress={onPress}
           compact={left}
         >
           {childNode()}

+ 58 - 0
components/Plus.js

@@ -0,0 +1,58 @@
+import * as React from 'react';
+import { Animated } from 'react-native';
+import { IconButton } from 'react-native-paper';
+import { Flex, ActivityIndicator } from '@ant-design/react-native';
+import { useAnimation } from 'react-native-animation-hooks';
+
+import Text from './Text';
+
+export default function PlusCom(props) {
+  const { num, minus, plusEvent, loading } = props;
+  const minusOpacity = useAnimation({
+    type: 'timing',
+    initialValue: 0,
+    duration: 100,
+    toValue: num ? 1 : 0,
+  });
+  return (
+    <>
+      <Flex
+        style={{
+          position: 'absolute',
+          right: 0,
+          bottom: 0,
+          zIndex: 2,
+          // transform: [{ translateY: 10 }],
+        }}
+      >
+        <Animated.View
+          style={{
+            opacity: minusOpacity,
+          }}
+        >
+          <Flex>
+            <IconButton
+              icon="minus-circle-outline"
+              color="#FFB11E"
+              size={20}
+              disabled={!num}
+              onPress={minus}
+            />
+
+            {loading ? (
+              <ActivityIndicator />
+            ) : (
+              <Text size="c2">{num || 0}</Text>
+            )}
+          </Flex>
+        </Animated.View>
+        <IconButton
+          icon="plus-circle"
+          color="#FFB11E"
+          size={20}
+          onPress={plusEvent}
+        />
+      </Flex>
+    </>
+  );
+}

+ 17 - 7
components/SvgIcon.js

@@ -1,11 +1,12 @@
 /* eslint-disable no-underscore-dangle */
 import * as React from 'react';
 import Svg, { Path } from 'react-native-svg';
-import { withTheme } from 'react-native-paper';
+import { View } from 'react-native';
+import { withTheme, Badge } from 'react-native-paper';
 import svgMap from '../Utils/SvgUtilsNew';
 
 function Icon(props) {
-  const { name, type, theme, fillAll, Flip } = props;
+  const { name, type, theme, fillAll, Flip, style, badge } = props;
   let { color, width, height } = props;
   const { colors } = theme;
   if (type) {
@@ -37,23 +38,32 @@ function Icon(props) {
   const pathComList = () => {
     return pathList.map((item, index) => {
       const pathProps = { ...item };
-      if ((fillAll || !item.strokeWidth) && color) {
+      if ((fillAll || item.changeFill) && color) {
         pathProps.fill = color;
       }
 
-      if (item.strokeWidth && color) {
+      if (item.strokeWidth && !item.changeFill && color) {
         pathProps.stroke = color;
       }
       delete pathProps.viewBox;
       delete pathProps.defaultWidth;
+      delete pathProps.changeFill;
       return <Path {...pathProps} key={index} transform={transform} />;
     });
   };
 
   return (
-    <Svg width={width} height={height} viewBox={viewBox}>
-      {pathComList()}
-    </Svg>
+    <View>
+      <Svg width={width} height={height} viewBox={viewBox} style={style}>
+        {pathComList()}
+      </Svg>
+
+      {badge > 0 && (
+        <Badge size={15} style={{ position: 'absolute', right: 0, top: -5 }}>
+          {badge}
+        </Badge>
+      )}
+    </View>
   );
 }
 

+ 69 - 64
components/Text.js

@@ -9,8 +9,8 @@ import {
 } from 'react-native-paper';
 
 function MyText(props) {
-  const { type, size, children, bold } = props;
-  let { color, theme, style, center } = props;
+  const { type, size, children, bold, center } = props;
+  let { color, theme, style } = props;
   const { colors } = theme;
   if (type) {
     color = colors[type];
@@ -25,6 +25,7 @@ function MyText(props) {
       },
     };
   }
+
   if (bold) {
     style = {
       ...style,
@@ -38,68 +39,72 @@ function MyText(props) {
     };
   }
 
-  switch (size) {
-    case 'h1':
-      // 24
-      return (
-        <Headline
-          theme={theme}
-          style={{ ...style }}
-          numberOfLines={1}
-          ellipsizeMode="tail"
-          align="center"
-        >
-          {children}
-        </Headline>
-      );
-    case 's1':
-      // 16
-      return (
-        <Subheading
-          theme={theme}
-          style={{ ...style }}
-          numberOfLines={1}
-          ellipsizeMode="tail"
-        >
-          {children}
-        </Subheading>
-      );
-    case 'c1':
-      // 12
-      return (
-        <Caption
-          theme={theme}
-          style={{ ...style }}
-          numberOfLines={1}
-          ellipsizeMode="tail"
-        >
-          {children}
-        </Caption>
-      );
-    case 'c2':
-      // 10
-      return (
-        <Text
-          theme={theme}
-          style={[{ fontSize: 10 }, { ...style }]}
-          numberOfLines={1}
-          ellipsizeMode="tail"
-        >
-          {children}
-        </Text>
-      );
-    default:
-      // 14
-      return (
-        <Paragraph
-          theme={theme}
-          numberOfLines={1}
-          ellipsizeMode="tail"
-          style={{ ...style }}
-        >
-          {children}
-        </Paragraph>
-      );
+  if (!!children) {
+    switch (size) {
+      case 'h1':
+        // 24
+        return (
+          <Headline
+            theme={theme}
+            style={{ ...style }}
+            numberOfLines={1}
+            ellipsizeMode="tail"
+            align="center"
+          >
+            {children}
+          </Headline>
+        );
+      case 's1':
+        // 16
+        return (
+          <Subheading
+            theme={theme}
+            style={{ ...style }}
+            numberOfLines={1}
+            ellipsizeMode="tail"
+          >
+            {children}
+          </Subheading>
+        );
+      case 'c1':
+        // 12
+        return (
+          <Caption
+            theme={theme}
+            style={{ ...style }}
+            numberOfLines={1}
+            ellipsizeMode="tail"
+          >
+            {children}
+          </Caption>
+        );
+      case 'c2':
+        // 10
+        return (
+          <Text
+            theme={theme}
+            style={[{ fontSize: 10 }, { ...style }]}
+            numberOfLines={1}
+            ellipsizeMode="tail"
+          >
+            {children}
+          </Text>
+        );
+      default:
+        // 14
+        return (
+          <Paragraph
+            theme={theme}
+            numberOfLines={1}
+            ellipsizeMode="tail"
+            style={{ ...style }}
+          >
+            {children}
+          </Paragraph>
+        );
+    }
+  } else {
+    return <></>;
   }
 }
 

+ 1 - 1
hooks/useCachedResources.js

@@ -23,7 +23,7 @@ export default function useCachedResources() {
       } catch (e) {
         // We might want to provide this error information to an error reporting service
         // eslint-disable-next-line no-console
-        console.warn(e);
+        // console.warn(e);
       } finally {
         setLoadingComplete(true);
         SplashScreen.hideAsync();

+ 13 - 0
package-lock.json

@@ -9677,6 +9677,11 @@
         "prop-types": "^15.7.2"
       }
     },
+    "react-native-animation-hooks": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/react-native-animation-hooks/-/react-native-animation-hooks-1.0.1.tgz",
+      "integrity": "sha512-s1o71m9sPpEYwFpHTdBouO50jzvyxIVB4MmNwfVGgPWkdL02zKOzyXcNuPJmoCuLIstUj5TBEwtnGk1WMFJx5A=="
+    },
     "react-native-collapsible": {
       "version": "1.5.2",
       "resolved": "https://registry.npmjs.org/react-native-collapsible/-/react-native-collapsible-1.5.2.tgz",
@@ -9789,6 +9794,14 @@
         "prop-types": "^15.5.6"
       }
     },
+    "react-native-sticky-parallax-header": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/react-native-sticky-parallax-header/-/react-native-sticky-parallax-header-0.2.1.tgz",
+      "integrity": "sha512-fbIySVvr4fa6b6JPHgtphAxqZyV8mmQIbpJkXwxuD89Qj/VjKpjhQfsnBDOJ3wDDvngs1PRcyVv7gs1lZYehEw==",
+      "requires": {
+        "prop-types": "^15.7.2"
+      }
+    },
     "react-native-svg": {
       "version": "11.0.1",
       "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-11.0.1.tgz",

+ 2 - 0
package.json

@@ -47,11 +47,13 @@
     "react": "16.9.0",
     "react-dom": "16.9.0",
     "react-native": "https://github.com/expo/react-native/archive/sdk-37.0.1.tar.gz",
+    "react-native-animation-hooks": "^1.0.1",
     "react-native-gesture-handler": "^1.6.1",
     "react-native-paper": "^3.10.1",
     "react-native-reanimated": "^1.7.1",
     "react-native-safe-area-context": "0.7.3",
     "react-native-screens": "~2.2.0",
+    "react-native-sticky-parallax-header": "^0.2.1",
     "react-native-svg": "11.0.1",
     "react-native-tab-view": "^2.14.2",
     "react-native-ui-lib": "^5.8.1",

+ 251 - 0
screens/Detail/Cart.jsx

@@ -0,0 +1,251 @@
+import * as WebBrowser from 'expo-web-browser';
+import * as React from 'react';
+import { StyleSheet, View, Image } from 'react-native';
+import { Flex } from '@ant-design/react-native';
+import { Modal, Portal, TouchableRipple, Badge } from 'react-native-paper';
+import { ScrollView } from 'react-native-gesture-handler';
+
+import { useCreation, useRequest, useMount } from '@umijs/hooks';
+
+import useModel from 'flooks';
+import Detail from './model';
+
+import Icon from '../../components/SvgIcon';
+import Text from '../../components/Text';
+import Button from '../../components/Button';
+import Plus from '../../components/Plus';
+
+import { accAdd, accMul } from '../../Utils/NumberUtils';
+import { alert } from '../../Utils/TotastUtils';
+
+export default function Cart() {
+  const {
+    id,
+    merchantInfo,
+    addCart,
+    getCart,
+    setCartRequest,
+    clearCart,
+    setCartMap,
+  } = useModel(Detail, ['id', 'merchantInfo']);
+
+  const [cartList, setcartList] = React.useState([]);
+
+  const { deliveryAmount, startingAmount } = merchantInfo;
+
+  const [showList, setshowList] = React.useState(false);
+
+  const cartRequest = useRequest(getCart, {
+    refreshDeps: [id],
+    onSuccess: (result) => {
+      const value = result || [];
+      setcartList(value);
+      setCartRequest(cartRequest);
+      if (value.length === 0) {
+        setshowList(false);
+      }
+    },
+  });
+
+  const num = useCreation(() => {
+    return cartList.reduce((total, item) => {
+      return accAdd(total, item.num);
+    }, 0);
+  }, [cartList]);
+
+  const cartMap = useCreation(() => {
+    const map = new Map([]);
+    cartList.forEach((item) => {
+      map.set(
+        item.goodsId,
+        map.has(item.goodsId) ? map.get(item.goodsId) + item.num : item.num
+      );
+    });
+    return map;
+  }, [cartList]);
+
+  React.useEffect(() => {
+    setCartMap(cartMap);
+  }, [cartMap]);
+
+  const price = useCreation(() => {
+    return cartList.reduce((total, item) => {
+      return accAdd(total, accMul(item.num, item.goodsRealPrice));
+    }, 0);
+  }, [num]);
+
+  const canSubmit = useCreation(() => {
+    if (price >= startingAmount && num > 0) {
+      return true;
+    } else {
+      return false;
+    }
+  }, [num, startingAmount, price]);
+
+  const goodsList = (list) => {
+    return list.map((item) => {
+      const { goods } = item;
+      return (
+        <Flex key={item.id} style={styles.item}>
+          <Image
+            style={styles.icon2}
+            resizeMode="cover"
+            source={{ uri: goods.img }}
+          />
+          <Flex.Item style={styles.goodsMain}>
+            <Flex align="stretch">
+              <Text>{goods.name}</Text>
+              <Text type="error" style={{ marginLeft: 5 }}>
+                ¥{item.goodsRealPrice || item.goodsPrice}
+              </Text>
+            </Flex>
+            <Text size="c2" type="info">
+              {item.specification}
+            </Text>
+            <Plus
+              num={item.num}
+              minus={() => {}}
+              loading={cartRequest.loading}
+              plusEvent={() => {
+                addCart(item.goodsId, item.specificationId.join(','), 1);
+              }}
+            />
+          </Flex.Item>
+        </Flex>
+      );
+    });
+  };
+
+  return (
+    <>
+      <Portal>
+        <View style={styles.main}>
+          <Flex style={styles.bg}>
+            <TouchableRipple
+              style={styles.iconTouch}
+              disabled={num === 0}
+              onPress={() => setshowList(true)}
+            >
+              <Icon
+                type={num > 0 ? 'primary' : ''}
+                name="orderCart"
+                width={49}
+                height={53}
+                badge={num}
+              />
+            </TouchableRipple>
+
+            <Flex.Item style={styles.center}>
+              <Text
+                size="s1"
+                style={{
+                  lineHeight: 18,
+                  marginVertical: 0,
+                }}
+                color={num === 0 ? '#B4B4B4' : '#fff'}
+              >
+                {num === 0 ? '未选购商品' : `¥${price}`}
+              </Text>
+              <Text size="c2" type="info">
+                另需配送费¥{deliveryAmount || 0}
+              </Text>
+            </Flex.Item>
+
+            <Button
+              size="large"
+              type="primary"
+              width={106}
+              height={37}
+              disabled={!canSubmit}
+              radius={0}
+              fontColor={!canSubmit ? '#B4B4B4' : '#fff'}
+              onPress={() => {}}
+              loading={cartRequest.loading}
+            >
+              {!canSubmit ? `¥${startingAmount}起送` : '去结算'}
+            </Button>
+          </Flex>
+        </View>
+        <Modal
+          animationType="slide-up"
+          visible={showList}
+          onDismiss={() => setshowList(false)}
+          contentContainerStyle={styles.contentContainerStyle}
+        >
+          <Flex justify="between" style={styles.top}>
+            <Text>已选商品</Text>
+            <TouchableRipple
+              disabled={num === 0}
+              onPress={() => {
+                alert('', '确认要清空购物车吗?', clearCart);
+              }}
+            >
+              <Icon name="delete" type="info" width={20} height={20} />
+            </TouchableRipple>
+          </Flex>
+
+          <ScrollView contentContainerStyle={styles.scroll}>
+            {goodsList(cartList)}
+          </ScrollView>
+        </Modal>
+      </Portal>
+    </>
+  );
+}
+
+const styles = StyleSheet.create({
+  main: {
+    position: 'absolute',
+    left: 0,
+    right: 0,
+    bottom: 0,
+    height: 53,
+    zIndex: 2,
+    justifyContent: 'flex-end',
+  },
+  bg: {
+    height: 37,
+    backgroundColor: 'rgba(120, 120, 120, 1)',
+  },
+  iconTouch: {
+    marginLeft: 15,
+    alignSelf: 'flex-end',
+  },
+  center: {
+    paddingHorizontal: 10,
+  },
+  contentContainerStyle: {
+    backgroundColor: '#fff',
+    position: 'absolute',
+    bottom: 37,
+    left: 0,
+    right: 0,
+    justifyContent: 'flex-start',
+    paddingHorizontal: 20,
+    paddingTop: 20,
+    paddingBottom: 30,
+  },
+  icon2: {
+    width: 40,
+    height: 40,
+    borderRadius: 3,
+  },
+  goodsMain: {
+    marginLeft: 10,
+  },
+  top: {
+    paddingBottom: 8,
+    borderBottomWidth: 1,
+    borderColor: '#E5E5E5',
+  },
+  item: {
+    paddingTop: 10,
+    overflow: 'hidden',
+  },
+  scroll: {
+    flexGrow: 1,
+    paddingBottom: 15,
+    borderBottomWidth: 1,
+    borderColor: '#e5e5e5',
+  },
+});

+ 67 - 0
screens/Detail/Classification.jsx

@@ -0,0 +1,67 @@
+import * as WebBrowser from 'expo-web-browser';
+import * as React from 'react';
+import { StyleSheet, View, FlatList } from 'react-native';
+
+import { useRequest } from '@umijs/hooks';
+
+import useModel from 'flooks';
+import Detail from './model';
+
+import Text from '../../components/Text';
+
+function Item({ name }) {
+  return (
+    <View style={styles.item}>
+      <Text center size="c1">
+        {name}
+      </Text>
+    </View>
+  );
+}
+export default function Classification() {
+  const { id } = useModel(Detail, ['id']);
+
+  const [classifications, setclassifications] = React.useState([]);
+
+  const recommendRequest = useRequest(
+    () => {
+      const params = {
+        query: {
+          merchantId: id,
+          page: 0,
+          size: 100,
+        },
+      };
+      const urls = Object.keys(params).map((item) => {
+        return `${item}=${encodeURI(JSON.stringify(params[item]))}`;
+      });
+      return `/classification/all?${urls.join('&')}`;
+    },
+    {
+      refreshDeps: [id],
+      onSuccess: (result) => {
+        setclassifications(result.content);
+      },
+    }
+  );
+  // classification;
+  return (
+    <FlatList
+      style={styles.left}
+      data={classifications}
+      renderItem={({ item }) => <Item name={item.name} />}
+      keyExtractor={(item) => item.id}
+    />
+  );
+}
+
+const styles = StyleSheet.create({
+  left: {
+    width: 95,
+    maxWidth: 95,
+  },
+  item: {
+    paddingHorizontal: 15,
+    paddingVertical: 10,
+  },
+});

+ 18 - 15
screens/Detail/Header.jsx

@@ -1,29 +1,24 @@
 import * as React from 'react';
-import { ImageBackground, StyleSheet } from 'react-native';
+import { StyleSheet, View } from 'react-native';
 import { StatusBar } from 'expo-status-bar';
-import Constants from 'expo-constants';
 import { Appbar } from 'react-native-paper';
 import useModel from 'flooks';
 import Detail from './model';
 import { goBack } from '../../navigation/RootNavigation';
 
 export default function Header() {
-  const { merchantInfo } = useModel(Detail);
-
-  const { banner } = merchantInfo;
+  const { heardColor } = useModel(Detail, ['heardColor']);
   return (
     <>
-      <StatusBar backgroundColor="transparent" style="light" translucent />
-      <ImageBackground
-        style={{
-          height: 118 + Constants.statusBarHeight,
-        }}
-        resizeMode="cover"
-        source={{ uri: banner }}
-      >
+      <StatusBar
+        backgroundColor={heardColor || 'transparent'}
+        style="light"
+        translucent
+      />
+      <View style={styles.view}>
         <Appbar.Header
           dark
-          theme={{ colors: { primary: 'transparent' } }}
+          theme={{ colors: { primary: heardColor || 'transparent' } }}
           style={styles.header}
         >
           <Appbar.BackAction onPress={goBack} />
@@ -31,7 +26,7 @@ export default function Header() {
           <Appbar.Action icon="magnify" />
           <Appbar.Action icon="share-variant" />
         </Appbar.Header>
-      </ImageBackground>
+      </View>
     </>
   );
 }
@@ -45,4 +40,12 @@ const styles = StyleSheet.create({
     },
     shadowOpacity: 0,
   },
+  view: {
+    // backgroundColor: 'rgb(242, 242, 242)',
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    zIndex: 2,
+  },
 });

+ 113 - 36
screens/Detail/MerchantDetailScreen.jsx

@@ -1,19 +1,32 @@
 import * as WebBrowser from 'expo-web-browser';
 import * as React from 'react';
-import { StyleSheet, View, Image, Dimensions } from 'react-native';
+import {
+  ImageBackground,
+  StyleSheet,
+  Animated,
+  Dimensions,
+  View,
+} from 'react-native';
+import { Flex } from '@ant-design/react-native';
 import { ScrollView } from 'react-native-gesture-handler';
+import Constants from 'expo-constants';
 import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
-import Icon from 'react-native-vector-icons/Ionicons';
-import { useRoute } from '@react-navigation/native';
+
+import { useRoute, useNavigation } from '@react-navigation/native';
+import { useAnimation } from 'react-native-animation-hooks';
+
 import useModel from 'flooks';
 import Detail from './model'; // detail模块通用方法
 import Text from '../../components/Text';
+import Button from '../../components/Button';
 
 import Header from './Header';
 import Center from './Center';
 
 import Order from './Order';
 import Comment from './Comment';
+import Cart from './Cart'; // order 页面的选购
+import SelectSpecification from './SelectSpecification'; // order 页面的选购
 
 const Tab = createMaterialTopTabNavigator();
 
@@ -21,67 +34,131 @@ const { height, width } = Dimensions.get('window');
 
 export default function MerchantDetail() {
   const route = useRoute();
+  const navigation = useNavigation();
   const { params } = route;
 
-  const { init, merchantInfo } = useModel(Detail);
+  const { init, merchantInfo, setHeaderColor } = useModel(Detail, ['id']);
+
+  const { banner } = merchantInfo;
+
+  const [tabName, setTabName] = React.useState('order');
+  const [tabTop, settabTop] = React.useState(0);
 
-  const { logo } = merchantInfo;
   React.useEffect(() => {
     if (params.merchantId) {
       init(params.merchantId);
+      setHeaderColor('');
+      settabTop(0);
     }
   }, [params]);
 
   return (
     <>
-      <ScrollView>
-        <Header />
+      <Header />
+      <ScrollView
+        nestedScrollEnabled
+        stickyHeaderIndices={[2]}
+        scrollEventThrottle={0}
+        onScroll={({ nativeEvent }) => {
+          const { contentOffset } = nativeEvent || {};
+
+          if (contentOffset.y >= 80) {
+            setHeaderColor('#FFC21C');
+          } else {
+            setHeaderColor('');
+          }
+
+          if (contentOffset.y >= 200) {
+            settabTop(Constants.statusBarHeight + 56);
+          } else {
+            settabTop(0);
+          }
+        }}
+      >
+        <ImageBackground
+          style={{
+            height: 118 + Constants.statusBarHeight,
+          }}
+          resizeMode="cover"
+          source={{ uri: banner }}
+        />
         <Center />
+        <View
+          style={{
+            zIndex: 3,
+            backgroundColor: '#EEEEEE',
+          }}
+        >
+          <Flex
+            style={{
+              marginTop: tabTop,
+            }}
+          >
+            <Flex.Item>
+              <Button
+                color={tabName !== 'order' && '#000'}
+                text
+                size="large"
+                onPress={() => {
+                  navigation.navigate('order');
+                  setTabName('order');
+                }}
+              >
+                点餐
+              </Button>
+            </Flex.Item>
+            <Flex.Item>
+              <Button
+                color={tabName !== 'comment' && '#000'}
+                text
+                size="large"
+                onPress={() => {
+                  navigation.navigate('comment');
+                  setTabName('comment');
+                }}
+              >
+                评价
+              </Button>
+            </Flex.Item>
+            <Flex.Item>
+              <Button
+                color={tabName !== 'comment' && '#000'}
+                text
+                size="large"
+                onPress={() => {
+                  navigation.navigate('order');
+                }}
+              >
+                商家
+              </Button>
+            </Flex.Item>
+          </Flex>
+        </View>
         <Tab.Navigator
+          tabBarVisible={false}
+          backBehavior="initialRoute"
+          initialRouteName="order"
           tabBarOptions={{
-            activeTintColor: '#FFC750',
-            inactiveTintColor: '#000',
-            indicatorStyle: {
-              backgroundColor: '#FFC750',
-              height: 0,
-            },
-            labelStyle: {
-              fontSize: 16,
-            },
             style: {
-              backgroundColor: 'transparent',
-              height: 50,
-              elevation: 0,
-              shadowOffset: {
-                width: 0,
-                height: 0,
-              },
-              shadowOpacity: 0,
-              shadowRadius: 0,
-              borderBottomWidth: 1,
-              borderColor: '#eee',
+              height: 0,
+              overflow: 'hidden',
             },
           }}
-          backBehavior="initialRoute"
-          initialRouteName="order"
-          style={{ height }}
         >
           <Tab.Screen
             name="order"
-            options={{
-              title: '点餐',
-            }}
+            options={{ tabBarVisible: false }}
             component={Order}
           />
           <Tab.Screen
             name="comment"
-            options={{
-              title: '评价',
-            }}
+            options={{ tabBarVisible: false }}
             component={Comment}
           />
         </Tab.Navigator>
       </ScrollView>
+      <Cart />
+      <SelectSpecification />
     </>
   );
 }

+ 36 - 4
screens/Detail/Order.jsx

@@ -1,9 +1,41 @@
 import * as WebBrowser from 'expo-web-browser';
 import * as React from 'react';
-import { StyleSheet, View, Text } from 'react-native';
+import { StyleSheet, View, Text, Dimensions, FlatList } from 'react-native';
+import { Flex } from '@ant-design/react-native';
+import { ScrollView } from 'react-native-gesture-handler';
+import Constants from 'expo-constants';
 
-export default function LinksScreen() {
-  return <Text>11727</Text>;
+import useModel from 'flooks';
+import Detail from './model';
+
+import Recommend from './Recommend';
+import Classification from './Classification';
+
+const { height, width } = Dimensions.get('window');
+
+export default function Order() {
+  return (
+    <>
+      <Recommend />
+
+      <Flex
+        align="stretch"
+        style={{ height: height - Constants.statusBarHeight - 100 }}
+      >
+        <Classification />
+
+        <Flex.Item style={styles.right}>
+          <ScrollView>
+            <Text>2222</Text>
+          </ScrollView>
+        </Flex.Item>
+      </Flex>
+    </>
+  );
 }
 
-const styles = StyleSheet.create({});
+const styles = StyleSheet.create({
+  right: {
+    backgroundColor: '#fff',
+  },
+});

+ 91 - 0
screens/Detail/Recommend.jsx

@@ -0,0 +1,91 @@
+import * as WebBrowser from 'expo-web-browser';
+import * as React from 'react';
+import { StyleSheet, View } from 'react-native';
+import { Flex } from '@ant-design/react-native';
+import { Card, IconButton, Colors } from 'react-native-paper';
+
+import { useRequest } from '@umijs/hooks';
+
+import useModel from 'flooks';
+import Detail from './model';
+
+import Text from '../../components/Text';
+import Plus from '../../components/Plus';
+
+export default function Order() {
+  const { id, checkgoodsSpecification, getRecomend, cartMap } = useModel(
+    Detail,
+    ['id', 'cartMap']
+  );
+  const [recommendGoods, setRecommendGoods] = React.useState([]);
+  const recommendRequest = useRequest(getRecomend, {
+    refreshDeps: [id],
+    onSuccess: (result) => {
+      setRecommendGoods(result || []);
+    },
+  });
+
+  const goodsList = (list) => {
+    return list.map((item) => {
+      return (
+        <Flex.Item key={item.id} style={{ paddingHorizontal: 3 }}>
+          <Card elevation={2} style={styles.card}>
+            <Card.Cover
+              style={styles.image2}
+              resizeMode="cover"
+              source={{ uri: item.img }}
+            />
+            <Card.Content style={styles.content}>
+              <Text>{item.name}</Text>
+              <Text size="c1" type="info">
+                月售:
+                {item.totalSales}
+              </Text>
+              <Text size="s1" bold type="error">
+                ¥{item.amount}
+              </Text>
+
+              <Plus
+                num={cartMap.get(item.id) || 0}
+                minus={() => {}}
+                loading={recommendRequest.loading}
+                plusEvent={() => checkgoodsSpecification(item)}
+              />
+            </Card.Content>
+          </Card>
+        </Flex.Item>
+      );
+    });
+  };
+
+  return (
+    <>
+      {recommendGoods.length > 0 && (
+        <View style={styles.main}>
+          <Text size="s1" bold>
+            店主推荐
+          </Text>
+
+          <Flex>{goodsList(recommendGoods)}</Flex>
+        </View>
+      )}
+    </>
+  );
+}
+
+const styles = StyleSheet.create({
+  main: {
+    backgroundColor: '#fff',
+    padding: 15,
+  },
+  plus: {
+    position: 'absolute',
+    right: 0,
+    bottom: 0,
+  },
+  content: {
+    paddingHorizontal: 10,
+    paddingBottom: 5,
+    marginTop: 5,
+  },
+});

+ 223 - 0
screens/Detail/SelectSpecification.jsx

@@ -0,0 +1,223 @@
+import * as WebBrowser from 'expo-web-browser';
+import * as React from 'react';
+import { StyleSheet, View, Image } from 'react-native';
+import { ScrollView } from 'react-native-gesture-handler';
+import { Modal, Portal } from 'react-native-paper';
+import { Flex } from '@ant-design/react-native';
+import { useCreation, useMap } from '@umijs/hooks';
+
+import useModel from 'flooks';
+import Detail from './model';
+
+import Text from '../../components/Text';
+import Plus from '../../components/Plus';
+import Button from '../../components/Button';
+
+export default function SelectSpecification() {
+  const { showSelect, selectInfo, addCart, changeSelect } = useModel(Detail, [
+    'showSelect',
+    'selectInfo',
+  ]);
+  const {
+    id,
+    img,
+    name,
+    discountAmount,
+    amount,
+    specifications,
+  } = selectInfo || { specifications: [] };
+
+  // 全部的分类 一级包含二级形式 list
+  const selectspecifications = useCreation(() => {
+    return specifications.filter((item) => {
+      return !item.parent && item.children.length > 0;
+    });
+  }, [specifications]);
+
+  // 全部的一级分类的 Map
+  const classify1Map = useCreation(() => {
+    const map = new Map();
+    selectspecifications.forEach((item) => {
+      map.set(item.id, item);
+    });
+    return map;
+  }, [selectspecifications]);
+
+  // 选择模块
+  const [selectMap, selectMapEvent] = useMap([]);
+
+  React.useEffect(() => {
+    selectMapEvent.reset();
+  }, [id]);
+
+  // 全部选中的二级分类 list
+  const selectClassify2 = useCreation(() => {
+    const list = [...selectMap.values()].map((item) => {
+      return [...item.values()];
+    });
+    return list.flat();
+  }, [selectMap]);
+
+  // 全部选中的二级分类id list
+  const selectClassifyIds = useCreation(() => {
+    return selectClassify2.map((item) => {
+      return item.id;
+    });
+  }, [selectClassify2]);
+
+  // 未被选的一级分类
+  const notSelectClassify1 = useCreation(() => {
+    return [...classify1Map.values()].filter((item) => {
+      return !selectMap.get(item.id) || selectMap.get(item.id).size === 0;
+    });
+  }, [selectMap, classify1Map]);
+
+  // 总价
+  const totalAmount = useCreation(() => {
+    let money = discountAmount || amount;
+    selectClassify2.forEach((item) => {
+      money += item.amount || 0;
+    });
+    return money;
+  }, [discountAmount, amount, selectClassify2]);
+
+  const classify2 = (list) => {
+    return list.map((item, index) => {
+      const choosed = selectClassifyIds.indexOf(item.id) !== -1;
+      return (
+        <Button
+          key={item.id}
+          width="100%"
+          height={23}
+          color={choosed ? '#FFF5D8' : '#EEEEEE'}
+          fontColor={choosed ? '#FFC21C' : '#000000'}
+          size="mini"
+          style={{
+            width: '30%',
+            marginTop: 5,
+            marginRight: (index + 1) % 3 ? '5%' : 0,
+          }}
+          onPress={() => {
+            if (choosed) {
+              const selects = selectMapEvent.get(item.parent);
+              selects.delete(item.id);
+              selectMapEvent.set(item.parent, selects);
+            } else if (
+              classify1Map.get(item.parent).multiple &&
+              selectMapEvent.get(item.parent)
+            ) {
+              const selects = selectMapEvent.get(item.parent);
+              selects.set(item.id, item);
+              selectMapEvent.set(item.parent, selects);
+            } else {
+              selectMapEvent.set(item.parent, new Map([[item.id, item]]));
+            }
+          }}
+        >
+          {item.name}
+        </Button>
+      );
+    });
+  };
+
+  const classify1 = (list) => {
+    return list.map((item) => {
+      return (
+        <View key={item.id} style={styles.list}>
+          <Text>{item.name}</Text>
+          <Flex wrap="wrap">{classify2(item.children)}</Flex>
+        </View>
+      );
+    });
+  };
+
+  return (
+    <Portal>
+      <Modal
+        animationType="slide"
+        visible={showSelect}
+        onDismiss={() => changeSelect(false)}
+        contentContainerStyle={styles.contentContainerStyle}
+      >
+        <Flex align="stretch">
+          <Image style={styles.icon} resizeMode="cover" source={{ uri: img }} />
+          <Flex.Item style={styles.info}>
+            <Text size="s1" bold>
+              {name}
+            </Text>
+            {selectClassify2.length !== 0 && (
+              <Text size="c1" type="info">
+                已选择{' '}
+                {selectClassify2
+                  .map((item) => {
+                    return item.name;
+                  })
+                  .join('/')}
+              </Text>
+            )}
+
+            <Flex.Item />
+            <Text size="s1" type="error">
+              ¥{totalAmount}
+              {discountAmount !== null && (
+                <Text
+                  size="c1"
+                  type="info"
+                  style={{
+                    textDecorationLine: 'line-through',
+                    marginLeft: 10,
+                  }}
+                >
+                  ¥{amount}
+                </Text>
+              )}
+            </Text>
+          </Flex.Item>
+          {/* <Plus
+            plusEvent={() => {
+              addCart(id);
+            }}
+          /> */}
+        </Flex>
+
+        <ScrollView contentContainerStyle={{ flexGrow: 1 }}>
+          {classify1(selectspecifications)}
+        </ScrollView>
+
+        <Button
+          disabled={notSelectClassify1.length}
+          block
+          size="large"
+          type="primary"
+          onPress={() => addCart(id, selectClassifyIds.join(','), 1)}
+        >
+          选好了
+        </Button>
+      </Modal>
+    </Portal>
+  );
+}
+
+const styles = StyleSheet.create({
+  contentContainerStyle: {
+    backgroundColor: '#fff',
+    height: '70%',
+    position: 'absolute',
+    bottom: 0,
+    left: 0,
+    right: 0,
+    padding: 20,
+    justifyContent: 'flex-start',
+  },
+  icon: {
+    width: 80,
+    height: 80,
+    borderRadius: 3,
+  },
+  info: {
+    marginLeft: 8,
+  },
+  list: {
+    paddingTop: 15,
+  },
+});

+ 103 - 1
screens/Detail/model.js

@@ -4,14 +4,116 @@ import Toast from '../../flooks/Toast';
 const DetailModel = (now) => ({
   id: 0,
   merchantInfo: {},
+  cartMap: new Map([]),
+  showSelect: false,
+  selectInfo: null,
+  cartRequest: null,
+  heardColor: '',
   init(id) {
-    now({ id, merchantInfo: {} });
+    now({ id: 0, merchantInfo: {} });
     return request.get(`/merchant/getDTO/${id}`).then((res) => {
       now({
+        id,
         merchantInfo: res,
       });
     });
   },
+  setHeaderColor(color) {
+    now({
+      heardColor: color,
+    });
+  },
+  getRecomend() {
+    const { id } = now();
+    now({ recommendGoods: [] });
+    if (!id) {
+      return Promise.resolve();
+    }
+    return request
+      .get(`/goods/all`, {
+        params: {
+          query: {
+            recommend: true,
+            merchantId: id,
+          },
+        },
+      })
+      .then((res) => {
+        return Promise.resolve(res.content);
+      });
+  },
+  getCart() {
+    const { id } = now();
+    now({ cartList: [] });
+    if (!id) {
+      return Promise.resolve([]);
+    }
+    return request
+      .get('/shoppingCart/my/merchant', {
+        params: {
+          merchantId: id,
+        },
+      })
+      .then((res) => {
+        return Promise.resolve(res);
+      });
+  },
+  setCartMap(map) {
+    now({
+      cartMap: map,
+    });
+  },
+  changeSelect(show) {
+    now({
+      showSelect: show,
+    });
+  },
+  checkgoodsSpecification(goodsInfo) {
+    now({
+      selectInfo: goodsInfo,
+      showSelect: true,
+    });
+  },
+  addCart(goodsId, goodsSpecificationIds, num) {
+    const { warnning } = now(Toast);
+    const { cartRequest } = now();
+    return request
+      .post('/shoppingCart/cart', {
+        data: {
+          goodsId,
+          goodsSpecificationIds,
+          num,
+        },
+        requestType: 'form',
+      })
+      .then(() => {
+        now({ showSelect: false });
+        if (cartRequest) {
+          cartRequest.refresh();
+        }
+      })
+      .catch((e) => {
+        warnning(e.error);
+      });
+  },
+  setCartRequest(cartRequest) {
+    now({ cartRequest });
+  },
+  clearCart() {
+    const { warnning, success } = now(Toast);
+    const { cartRequest } = now();
+    return request
+      .get('/shoppingCart/clearCart')
+      .then(() => {
+        if (cartRequest) {
+          cartRequest.refresh();
+          success('清除成功');
+        }
+      })
+      .catch((e) => {
+        warnning(e.error);
+      });
+  },
 });
 
 export default DetailModel;

+ 1 - 1
screens/Main/HomeScreen.jsx

@@ -59,7 +59,7 @@ HomeScreen.navigationOptions = {
 
 const styles = StyleSheet.create({
   container: {
-    flex: 1,
+    flexGrow: 1,
     backgroundColor: '#fff',
   },
   contentStyle: {

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.