ソースを参照

feat(app): 新增首页轮播图和文章详情功能

- 添加首页轮播图组件和相关 API
- 实现文章详情页面和相关 API
- 更新底部导航栏,支持工作台、推荐和我的三个标签页
- 移除 react-native-copilot 依赖- 调整主栈路由,支持新功能
xuqm 4 日 前
コミット
35acc240ab
51 ファイル変更3954 行追加416 行削除
  1. 1 1
      android/gradle.properties
  2. 1 0
      babel.config.js
  3. 1 1
      bundle/android/common/common.android.bundle
  4. 6 1
      package.json
  5. 0 6
      src/app/hooks/useLogout.ts
  6. 34 0
      src/app/routes/MainParamList.ts
  7. 24 4
      src/app/routes/MainStack.tsx
  8. 175 0
      src/app/routes/MainTab.tsx
  9. 1 0
      src/app/screens/api/index.ts
  10. 34 0
      src/app/screens/api/useBannerList.ts
  11. 98 0
      src/app/screens/components/HomeCarousel.tsx
  12. 326 0
      src/app/screens/home/articleDetail/ArticleDetailScreen.tsx
  13. 1 0
      src/app/screens/home/articleDetail/api/index.ts
  14. 29 0
      src/app/screens/home/articleDetail/api/useArticleDetail.tsx
  15. 154 0
      src/app/screens/home/articleShare/ArticleShareScreen.tsx
  16. 604 0
      src/app/screens/home/home/HomeScreen.tsx
  17. 8 0
      src/app/screens/home/home/api/index.ts
  18. 39 0
      src/app/screens/home/home/api/useArticleCategory.ts
  19. 33 0
      src/app/screens/home/home/api/useArticleList.ts
  20. 33 0
      src/app/screens/home/home/api/useArticleListByCategory.ts
  21. 24 0
      src/app/screens/home/home/api/useBadgeNum.ts
  22. 33 0
      src/app/screens/home/home/api/useFeatureList.ts
  23. 25 0
      src/app/screens/home/home/api/useNoticeList.ts
  24. 68 0
      src/app/screens/home/home/api/useSubServerCheck.ts
  25. 22 0
      src/app/screens/home/home/api/useUnreadRemind.ts
  26. 69 0
      src/app/screens/home/home/components/HeaderRight.tsx
  27. 87 0
      src/app/screens/home/home/components/HomeArticleItem.tsx
  28. 174 0
      src/app/screens/home/home/components/HomeArticlesView.tsx
  29. 87 0
      src/app/screens/home/home/components/HomeBulletin.tsx
  30. 148 0
      src/app/screens/home/home/components/HomeFeaturesView.tsx
  31. 181 0
      src/app/screens/home/home/components/HomeTodoView.tsx
  32. 155 0
      src/app/screens/home/home/components/HomeTopView.tsx
  33. 49 0
      src/app/screens/home/home/hooks/useHandleClickBannerListItem.ts
  34. 61 0
      src/app/screens/home/home/hooks/useHandleClickFeatureListItem.tsx
  35. 80 0
      src/app/screens/home/home/hooks/useHandleUnreadRemind.ts
  36. 147 0
      src/app/screens/mine/contactSupport/ContactSupportScreen.tsx
  37. 1 0
      src/app/screens/mine/contactSupport/api/index.ts
  38. 18 0
      src/app/screens/mine/contactSupport/api/useCustomrConfig.ts
  39. 400 0
      src/app/screens/mine/mine/MineScreen.tsx
  40. 82 0
      src/app/screens/mine/mine/components/FeatureButton.tsx
  41. 0 0
      src/common/assets/images/common_list_empty.png
  42. 0 0
      src/common/assets/images/common_list_empty@2x.png
  43. 0 0
      src/common/assets/images/common_list_empty@3x.png
  44. 5 1
      src/common/common.ts
  45. 58 0
      src/common/components/DataEmpty.tsx
  46. 40 0
      src/common/components/ListEmpty.tsx
  47. 18 0
      src/common/components/Separator.tsx
  48. 60 0
      src/common/components/TabBarIcon.tsx
  49. 9 3
      src/common/router/CommonParamList.ts
  50. 28 0
      src/common/screens/scan/ScanScreen.tsx
  51. 223 399
      yarn.lock

+ 1 - 1
android/gradle.properties

@@ -37,4 +37,4 @@ newArchEnabled=true
 # Use this property to enable or disable the Hermes JS engine.
 # If set to false, you will be using JSC instead.
 hermesEnabled=true
-#org.gradle.configuration-cache=true
+#org.gradle.configuration-cache=true

+ 1 - 0
babel.config.js

@@ -14,5 +14,6 @@ module.exports = {
         },
       },
     ],
+    'react-native-worklets/plugin',
   ],
 };

ファイルの差分が大きいため隠しています
+ 1 - 1
bundle/android/common/common.android.bundle


+ 6 - 1
package.json

@@ -28,6 +28,7 @@
     "@react-native-async-storage/async-storage": "^2.2.0",
     "@react-native-community/hooks": "^100.1.0",
     "@react-native/new-app-screen": "0.80.1",
+    "@react-navigation/bottom-tabs": "^7.4.7",
     "@react-navigation/native": "^7.1.14",
     "@react-navigation/stack": "^7.4.2",
     "@szyx-mobile/hooks": "^1.2.0",
@@ -36,18 +37,22 @@
     "patch-package": "^8.0.0",
     "react": "19.1.0",
     "react-native": "0.80.1",
-    "react-native-copilot": "^3.3.3",
     "react-native-device-info": "^14.0.4",
     "react-native-exit-app": "^2.0.0",
     "react-native-fs": "^2.20.0",
     "react-native-gesture-handler": "^2.27.2",
     "react-native-linear-gradient": "^2.8.3",
+    "react-native-reanimated": "^4.1.0",
+    "react-native-reanimated-carousel": "^4.0.3",
     "react-native-root-siblings": "^5.0.1",
     "react-native-safe-area-context": "^5.5.2",
     "react-native-spinkit": "^1.5.1",
     "react-native-storage": "^1.0.1",
+    "react-native-svg": "^15.12.1",
     "react-native-toast-message": "^2.3.3",
+    "react-native-view-shot": "^4.0.3",
     "react-native-webview": "^13.16.0",
+    "react-native-worklets": "^0.5.0",
     "react-native-zip-archive": "^7.0.2"
   },
   "devDependencies": {

+ 0 - 6
src/app/hooks/useLogout.ts

@@ -1,6 +1,5 @@
 import { useMemo } from 'react';
 import { COMMONINFO_KEY, TOKEN_KEY, USERINFO_KEY } from '@common/constants';
-import { useCopilot } from 'react-native-copilot';
 import { getAllKeys, removeAllItems } from '@common/StorageHelper.ts';
 import { useAuth } from '@common/contexts/useAuth.ts';
 
@@ -16,8 +15,6 @@ const useLogout = (): {
     actions: { logout },
   } = useAuth();
 
-  const { stop } = useCopilot();
-
   const actions = useMemo(
     () => ({
       logout: async () => {
@@ -41,9 +38,6 @@ const useLogout = (): {
         await removeAllItems(keysToDelete);
         // --- 这部分内容等旧模块不再使用后就可以删除了 ---
 
-        stop().catch(() => {
-          return;
-        });
         await logout();
       },
     }),

+ 34 - 0
src/app/routes/MainParamList.ts

@@ -1,3 +1,37 @@
+import { NavigatorScreenParams } from '@react-navigation/native';
+
+export type MainTabParamList = {
+  Home: undefined; // 工作台
+  Recommend: undefined; // 推荐
+  Mine: undefined; // 我的
+};
 export type MainParamList = {
   MainView: undefined;
+
+  // 【工作台】
+  MainTab: NavigatorScreenParams<MainTabParamList>;
+  // 首页文章详情
+  ArticleDetail: {
+    // 文章 id
+    id: string;
+  };
+  // 文章图片预览
+  ArticleImagePreview: {
+    url: string;
+  };
+  // 分享页
+  ArticleShare: {
+    // 文章名称
+    name: string;
+    // 文章 url
+    url: string;
+    // 图片
+    thumbImage?: string;
+    // 摘要
+    summary?: string;
+  };
+
+  // 【我的】
+  // 咨询客服
+  ContactSupport: undefined;
 };

+ 24 - 4
src/app/routes/MainStack.tsx

@@ -11,12 +11,18 @@ import {
 } from '@common/constants';
 import HeaderBackImage from '@common/components/HeaderBackImage.tsx';
 import MainViewScreen from '../screens/main/MainViewScreen';
+import WebViewScreen from '@common/screens/webview/WebViewScreen.tsx';
+import ArticleDetailScreen from '@app/screens/home/articleDetail/ArticleDetailScreen.tsx';
+import ScanScreen from '@common/screens/scan/ScanScreen.tsx';
+import { CommonParamList } from '@common/router/CommonParamList.ts';
+import ContactSupportScreen from '@app/screens/mine/contactSupport/ContactSupportScreen.tsx';
+import ArticleShareScreen from '@app/screens/home/articleShare/ArticleShareScreen.tsx';
+import MainTab from '@app/routes/MainTab.tsx';
+// import ArticleImagePreviewScreen from '@app/screens/home/articleDetail/ArticleImagePreviewScreen.tsx';
 
-const Stack = createStackNavigator<MainParamList>();
+const Stack = createStackNavigator<MainParamList & CommonParamList>();
 
 export function MainStack() {
-  // const navigation = useNavigation<NavigationProp<MainParamList>>();
-
   return (
     <Stack.Navigator
       screenOptions={{
@@ -29,14 +35,22 @@ export function MainStack() {
         },
         headerBackImage: HeaderBackImage,
       }}
-      initialRouteName="MainView"
+      initialRouteName="MainTab"
     >
       <Stack.Group
         screenOptions={{
           ...TransitionPresets.SlideFromRightIOS,
         }}
       >
+        <Stack.Screen
+          name="MainTab"
+          component={MainTab}
+          options={{headerShown: false}}
+        />
         <Stack.Screen name="MainView" component={MainViewScreen} />
+        <Stack.Screen name="WebView" component={WebViewScreen} />
+        <Stack.Screen name="ArticleDetail" component={ArticleDetailScreen} />
+        <Stack.Screen name="Scan" component={ScanScreen} />
         {/* 普通栈路由 --> 写这里 */}
       </Stack.Group>
       <Stack.Group
@@ -46,6 +60,12 @@ export function MainStack() {
         }}
       >
         {/* 对话框形式路由 --> 写这里 */}
+        <Stack.Screen name="ContactSupport" component={ContactSupportScreen} />
+        {/*<Stack.Screen*/}
+        {/*  name="ArticleImagePreview"*/}
+        {/*  component={ArticleImagePreviewScreen}*/}
+        {/*/>*/}
+        <Stack.Screen name="ArticleShare" component={ArticleShareScreen} />
       </Stack.Group>
     </Stack.Navigator>
   );

+ 175 - 0
src/app/routes/MainTab.tsx

@@ -0,0 +1,175 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { DeviceEventEmitter, ImageSourcePropType } from 'react-native';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import {
+  DEVICE_ENENT_UPDATE_HOME_FEATURE_LIST_KEY,
+  HEADER_TINT_COLOR,
+  HEADER_TITLE_FONT_SIZE,
+  HEADER_TITLE_FONT_WEIGHT,
+  TAB_BAR_ACTIVE_TINT_COLOR,
+  TAB_BAR_INACTIVE_TINT_COLOR,
+} from '@common//constants';
+import { useAuth } from '@common/contexts/useAuth';
+import { MainTabParamList } from './MainParamList';
+import HomeScreen from '@app/screens/home/home/HomeScreen';
+import MineScreen from '@app/screens/mine/mine/MineScreen';
+import { Feature } from '@app/screens/home/home/api';
+import { useHandleClickFeatureListItem } from '@app/screens/home/home/hooks/useHandleClickFeatureListItem';
+import TabBarIcon from '@common/components/TabBarIcon.tsx';
+
+const Tab = createBottomTabNavigator<MainTabParamList>();
+
+export default function MainTab() {
+  const {
+    state: { userInfo },
+  } = useAuth();
+
+  // 【状态】
+
+  const [recommendFeature, setRecommendFeature] = useState<Feature | undefined>(
+    undefined,
+  ); // 当前推荐的服务
+
+  // 【通知监听】
+
+  useEffect(() => {
+    // 监听用户信息的更新
+    const listener = DeviceEventEmitter.addListener(
+      DEVICE_ENENT_UPDATE_HOME_FEATURE_LIST_KEY,
+      (feature?: Feature) => {
+        setRecommendFeature(feature);
+      },
+    );
+
+    return () => {
+      listener.remove();
+    };
+  }, []);
+
+  // 【点击事件】
+  const { handleClickFeatureListItem } = useHandleClickFeatureListItem();
+
+  // 【子组件】
+
+  const handleTabBarIcon = useCallback(
+    (props: {
+      focused: boolean;
+      size: number;
+      activeImage: ImageSourcePropType;
+      inactiveImage: ImageSourcePropType;
+      badge?: boolean;
+    }) => {
+      return (
+        <MainTabBarIcon
+          focused={props.focused}
+          size={props.size}
+          activeImage={props.activeImage}
+          inactiveImage={props.inactiveImage}
+          badge={props.badge}
+        />
+      );
+    },
+    [],
+  );
+
+  if (userInfo === undefined) {
+    return null;
+  }
+
+  return (
+    <Tab.Navigator
+      screenOptions={{
+        lazy: false, // 关闭懒加载
+        headerTitleAlign: 'center', // 安卓标题居中
+        tabBarActiveTintColor: TAB_BAR_ACTIVE_TINT_COLOR,
+        tabBarInactiveTintColor: TAB_BAR_INACTIVE_TINT_COLOR,
+        headerShadowVisible: false,
+        headerTintColor: HEADER_TINT_COLOR,
+        headerTitleStyle: {
+          fontSize: HEADER_TITLE_FONT_SIZE,
+          fontWeight: HEADER_TITLE_FONT_WEIGHT,
+        },
+      }}
+    >
+      <Tab.Screen
+        name="Home"
+        component={HomeScreen}
+        options={{
+          title: '工作台',
+          tabBarIcon: ({ focused, size }) => {
+            return handleTabBarIcon({
+              focused: focused,
+              size: size,
+              activeImage: require('@app/assets/images/common/tab_home_s.png'),
+              inactiveImage: require('@app/assets/images/common/tab_home.png'),
+            });
+          },
+        }}
+      />
+      {recommendFeature && (
+        <Tab.Screen
+          name="Recommend"
+          component={RecommendScreen}
+          options={{
+            title: recommendFeature.name,
+            tabBarIcon: ({ focused, size }) => {
+              return handleTabBarIcon({
+                focused: focused,
+                size: size,
+                activeImage: require('@app/assets/images/common/tab_recommend.png'),
+                inactiveImage: require('@app/assets/images/common/tab_recommend.png'),
+                badge: recommendFeature.badge,
+              });
+            },
+          }}
+          listeners={{
+            tabPress: e => {
+              e.preventDefault(); // 阻止默认行为
+              handleClickFeatureListItem(recommendFeature);
+            },
+          }}
+        />
+      )}
+      <Tab.Screen
+        name="Mine"
+        component={MineScreen}
+        options={{
+          title: '我的',
+          tabBarIcon: ({ focused, size }) => {
+            return handleTabBarIcon({
+              focused: focused,
+              size: size,
+              activeImage: require('@app/assets/images/common/tab_mine_s.png'),
+              inactiveImage: require('@app/assets/images/common/tab_mine.png'),
+            });
+          },
+        }}
+      />
+    </Tab.Navigator>
+  );
+}
+
+function RecommendScreen() {
+  return null;
+}
+
+// 图标
+function MainTabBarIcon(props: {
+  focused: boolean;
+  size: number;
+  inactiveImage: ImageSourcePropType;
+  activeImage: ImageSourcePropType;
+  badge?: boolean;
+}) {
+  return (
+    <TabBarIcon
+      focused={props.focused}
+      size={props.size}
+      activeImage={props.activeImage}
+      inactiveImage={props.inactiveImage}
+      // activeTintColor={TAB_BAR_ACTIVE_TINT_COLOR}
+      inactiveTintColor={TAB_BAR_INACTIVE_TINT_COLOR}
+      badge={props.badge}
+    />
+  );
+}

+ 1 - 0
src/app/screens/api/index.ts

@@ -0,0 +1 @@
+export * from './useBannerList';

ファイルの差分が大きいため隠しています
+ 34 - 0
src/app/screens/api/useBannerList.ts


+ 98 - 0
src/app/screens/components/HomeCarousel.tsx

@@ -0,0 +1,98 @@
+import React, {useState} from 'react';
+import {Image, StyleSheet, TouchableOpacity, View} from 'react-native';
+import Carousel from 'react-native-reanimated-carousel';
+
+const CAROUSEL_ASPECT_RATIO = 350 / 80; // 轮播图宽高比
+const INDEX_BACKGROUND_CURRENT = '#FFFFFF';
+const INDEX_BACKGROUND = 'rgba(255, 255, 255, 0.6)';
+
+type Props = {
+  width: number; // 轮播图宽度
+  data: {imgUrl: string}[]; // 数据的数组
+  onPress?: (index: number) => void; // 点击事件
+};
+
+/**
+ * @description: 首页轮播图
+ */
+export default function HomeCarousel(props: Props) {
+  const [currentIndex, setCurrentIndex] = useState(0);
+  return (
+    <View>
+      <Carousel
+        style={styles.carousel}
+        loop
+        width={props.width}
+        height={props.width / CAROUSEL_ASPECT_RATIO}
+        autoPlay={true}
+        data={props.data}
+        scrollAnimationDuration={3000}
+        onScrollEnd={index => {
+          setCurrentIndex(index);
+        }}
+        renderItem={({index}) => {
+          return (
+            <TouchableOpacity
+              onPress={() => {
+                props.onPress && props.onPress(index);
+              }}>
+              <Image
+                style={{
+                  width: props.width,
+                  height: props.width / CAROUSEL_ASPECT_RATIO,
+                }}
+                source={{uri: props.data[index].imgUrl}}
+              />
+            </TouchableOpacity>
+          );
+        }}
+      />
+      <View style={styles.indexView}>
+        <View style={styles.indexContent}>
+          {props.data.map((_item, index) => {
+            return (
+              <View
+                key={index}
+                style={[
+                  styles.indexItem,
+                  {
+                    backgroundColor:
+                      currentIndex === index
+                        ? INDEX_BACKGROUND_CURRENT
+                        : INDEX_BACKGROUND,
+                  },
+                ]}
+              />
+            );
+          })}
+        </View>
+      </View>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  carousel: {
+    borderRadius: 10,
+    backgroundColor: '#FFFFFF',
+  },
+  indexView: {
+    position: 'absolute',
+    bottom: 0,
+    left: 0,
+    right: 0,
+    height: 7.5,
+  },
+  indexContent: {
+    flex: 1,
+    flexDirection: 'row',
+    justifyContent: 'center',
+  },
+  indexItem: {
+    flexShrink: 1,
+    width: 7.5,
+    height: 2.5,
+    marginHorizontal: 1.5,
+    borderRadius: 2,
+  },
+});

+ 326 - 0
src/app/screens/home/articleDetail/ArticleDetailScreen.tsx

@@ -0,0 +1,326 @@
+import React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
+import {
+  ActivityIndicator,
+  DeviceEventEmitter,
+  Image,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+} from 'react-native';
+import { StackScreenProps } from '@react-navigation/stack';
+import WebView from 'react-native-webview';
+import Spinner from '@common/components/Spinner';
+import DataEmpty from '@common/components/DataEmpty';
+import { MainParamList } from '@app/routes/MainParamList';
+import { useArticleDetail } from './api';
+import { useAuth } from '@common/contexts/useAuth';
+import { useSubServerCheck } from '../home/api';
+import { showErrorMessage, showMessage } from '@common/ToastHelper.ts';
+
+type Props = StackScreenProps<MainParamList, 'ArticleDetail'>;
+
+// 服务协议/隐私声明页面
+export default function ArticleDetailScreen(props: Props) {
+  const { navigation, route } = props;
+
+  const {
+    state: { userInfo },
+  } = useAuth();
+  // const { width } = useWindowDimensions();
+  //
+  // const [visible, setVisible] = useState(false); // 预览视图是否可见
+  // const [imgUrl, setImgUrl] = useState(undefined); // 预览视图是否可见
+  //
+  // const [adKey, setAdKey] = useState<string | undefined>(undefined);
+
+
+  // 获取协议内容接口
+  const { response, error, loading, fetch } = useArticleDetail(route.params.id);
+
+  useEffect(() => {
+    fetch();
+  }, [fetch]);
+
+  useEffect(() => {
+    if (error && error.type !== 'Cancel') {
+      showErrorMessage(error.message);
+    }
+  }, [error]);
+
+  useEffect(() => {
+    const listener = DeviceEventEmitter.addListener(
+      'WeChat_Resp',
+      (resp?: any) => {
+        if (resp.type === 'SendMessageToWX.Resp' && resp.errCode === 0) {
+          showMessage('分享成功');
+        }
+      },
+    );
+
+    return () => {
+      listener.remove();
+    };
+  }, []);
+
+  const { loading: subServerCheckLoading, fetchAsync: subServerCheckFetch } =
+    useSubServerCheck();
+
+  // 导航栏右侧按钮
+  const HeaderRight = useCallback(() => {
+    return (
+      <TouchableOpacity
+        style={styles.headerRight}
+        onPress={() => {
+          if (response !== undefined) {
+            if (
+              response.shareUrl === undefined ||
+              response.shareUrl.length === 0
+            ) {
+              showErrorMessage('文章没有分享链接');
+              return;
+            }
+            navigation.navigate('ArticleShare', {
+              name: response.title,
+              url: response.shareUrl,
+              summary: response.summary,
+              // thumbImage: Image.resolveAssetSource(
+              //   // eslint-disable-next-line @typescript-eslint/no-var-requires
+              //   require('@app/assets/images/common/common_appicon.png'),
+              // ).uri,
+            });
+          }
+        }}
+      >
+        <Image
+          style={styles.headerRightImage}
+          source={require('@app/assets/images/home/home_share.png')}
+        />
+      </TouchableOpacity>
+    );
+  }, [navigation, response]);
+
+  useLayoutEffect(() => {
+    navigation.setOptions({
+      title: '热点资讯',
+      headerRight: HeaderRight,
+    });
+  }, [HeaderRight, navigation]);
+
+  const webViewRef = useRef<WebView>(null);
+
+  const onMessage = (event: any) => {
+    // 1 {\"appId\":\"101\",\"model\":\"init\",\"functionId\":\"4d349822-b5ee-48b8-8642-b1f92da7f2b1\"}
+    // 2 {\"model\":\"isYwxApp\",\"functionId\":\"39ad7558-b9e2-4950-b1fe-1bc6a8084f37\",\"appId\":\"101\"}
+    // 3 {\"model\":\"userInfo\",\"functionId\":\"329b5484-943e-4c23-85ed-9f0b497787cd\",\"appId\":\"101\"}
+
+    try {
+      const data = JSON.parse(event.nativeEvent.data);
+      if (data.type === 'imagePreview') {
+        // setVisible(true);
+        // setImgUrl(data.url);
+        // console.log('>>>>>>', data.url);
+        navigation.navigate('ArticleImagePreview', { url: data.url });
+        return;
+      }
+
+      if (data.appId || data.model === 'isYwxApp') {
+        if (data.model === 'init') {
+          // {"model":"init","functionId":"8769e533-2645-487b-9d84-80c98e384e05","value":{"appId":"101","openId":"eb4a3ec98d540c99q8834w68b8y9810068f"},"status":"0","message":"init:ok"}
+          const obj = {
+            model: data.model,
+            functionId: data.functionId,
+            value: {
+              appId: data.appId,
+              openId: getOpenIdByUserId(userInfo?.userId ?? ''),
+            },
+            status: '0',
+            message: `${data.model}:ok`,
+          };
+          const responseStr = JSON.stringify(obj);
+          const jsString = `(function() {window.SZYX_YWX_WebViewBridge && window.SZYX_YWX_WebViewBridge.onMessage(${responseStr});})()`;
+          webViewRef.current?.injectJavaScript(jsString);
+        } else if (data.model === 'isYwxApp') {
+          // {"model":"isYwxApp","functionId":"d54f705d-801a-42f7-ae07-64f30c616dc7","value":{"isYwxApp":true},"status":"0","message":"isYwxApp:ok"}
+          const obj = {
+            model: data.model,
+            functionId: data.functionId,
+            value: {
+              isYwxApp: true,
+            },
+            status: '0',
+            message: `${data.model}:ok`,
+          };
+          const responseStr = JSON.stringify(obj);
+          const jsString = `(function() {window.SZYX_YWX_WebViewBridge && window.SZYX_YWX_WebViewBridge.onMessage(${responseStr});})()`;
+          webViewRef.current?.injectJavaScript(jsString);
+        } else if (data.model === 'userInfo') {
+          // {"model":"userInfo","functionId":"8b41c37f-06dc-4de0-8959-0caa1dee90c9","value":{"openId":"eb4a3ec98d540c99q8834w68b8y9810068f","userName":"邓文龙","nickname":"邓文龙","clientList":[{"clientId":"2019022814080064","clientName":"医网信beta联调"}]},"status":"0","message":"userInfo:ok"}
+          subServerCheckFetch({
+            data: {
+              nsId: data.appId,
+            },
+          })
+            .then(res => {
+              const clientList = res.clientList;
+              const list: {
+                clientId: string;
+                clientName: string;
+              }[] = [];
+              clientList.map(value => {
+                if (value.subscribeStatus === 'serving') {
+                  list.push({
+                    clientId: value.clientId,
+                    clientName: value.clientName,
+                  });
+                }
+              });
+              const responseData = {
+                openId: getOpenIdByUserId(userInfo?.userId ?? ''),
+                userName: userInfo?.nickname,
+                nickname: userInfo?.nickname,
+                clientList: list,
+              };
+              const obj = {
+                model: data.model,
+                functionId: data.functionId,
+                value: responseData,
+                status: '0',
+                message: `${data.model}:ok`,
+              };
+              const responseStr = JSON.stringify(obj);
+              const jsString = `(function() {window.SZYX_YWX_WebViewBridge && window.SZYX_YWX_WebViewBridge.onMessage(${responseStr});})()`;
+              webViewRef.current?.injectJavaScript(jsString);
+            })
+            .catch(() => {
+              return;
+            });
+        } else if (data.model === 'closeWindows') {
+          // {\"model\":\"closeWindows\",\"functionId\":\"5c9f2161-1506-42f4-b5ec-8aa4742a038c\",\"appId\":\"101\"}
+          navigation.pop();
+        }
+      }
+    } catch (e) {
+      return;
+    }
+  };
+
+  if (loading) {
+    return <Spinner />;
+  }
+
+  if (!response) {
+    return (
+      <DataEmpty
+        reload={() => {
+          fetch();
+        }}
+      />
+    );
+  }
+
+  return (
+    <View style={styles.container}>
+      <WebView
+        ref={webViewRef}
+        style={styles.webview}
+        onMessage={onMessage}
+        onLoadEnd={() => {
+          // 移动端展示默认表格的时候,边框默认是不展示的,这里注入样式代码将表格每个单元格的边框展示出来
+          const injectStyles = `
+            var cells = document.querySelectorAll('table td, table th');
+            cells.forEach(function(cell) {
+            cell.style.border = '1px solid #000000';
+            cell.style.padding = '4px';
+            });
+          `;
+          webViewRef.current?.injectJavaScript(injectStyles);
+        }}
+        originWhitelist={['*']}
+        dataDetectorTypes={'none'}
+        // 增加了禁止缩放,图片宽度不超过屏幕宽度
+        source={{
+          html: `<html>
+                  <head>
+                    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
+                    <style>
+                      body { font-size: 16px; line-height: 1.6; padding: 10px; overflow-x: hidden; }
+                      img { max-width: 100%; height: auto; }
+                    </style>
+                  </head>
+                  <body>
+                    <h3>${response.title}</h3><p style="color:#666666;">${
+            response.publishTime ?? ''
+          }</p>${response.content}
+                    <script>
+  document.querySelectorAll("img").forEach(img => {
+    img.addEventListener("click", () => {
+      console.log(">>>>>>>>>>>>>>", img.src);
+      window.ReactNativeWebView.postMessage(JSON.stringify({ type: "imagePreview", url: img.src }));
+    });
+  });
+</script>
+                  </body>
+                </html>`,
+        }}
+        javaScriptEnabled={true}
+        startInLoadingState={true}
+        scalesPageToFit={false}
+        renderLoading={() => {
+          return (
+            <ActivityIndicator
+              style={styles.activityIndicator}
+              color="#3296f6"
+              size="large"
+              animating={true}
+            />
+          );
+        }}
+      />
+
+      {/*<ImageView*/}
+      {/*  navigation={navigation}*/}
+      {/*  images={[{ uri: imgUrl }]}*/}
+      {/*  imageIndex={0}*/}
+      {/*  visible={visible}*/}
+      {/*  onRequestClose={() => setVisible(false)}*/}
+      {/*/>*/}
+      {subServerCheckLoading && <Spinner />}
+    </View>
+  );
+}
+
+function getOpenIdByUserId(userId: string) {
+  if (userId) {
+    const array = userId.split('');
+    array.splice(8, 0, ['y', 'w', 'q'][0]);
+    array.splice(13, 0, ['y', 'w', 'q'][1]);
+    array.splice(18, 0, ['y', 'w', 'q'][2]);
+    array.reverse();
+    const openId = array.join('');
+    return openId;
+  }
+  return '';
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+  },
+  headerRight: {
+    paddingHorizontal: 16,
+    height: '100%',
+    justifyContent: 'center',
+  },
+  headerRightImage: {
+    height: 15,
+    width: 15,
+  },
+  webview: {
+    flex: 1,
+  },
+  activityIndicator: {
+    position: 'absolute',
+    alignSelf: 'center',
+    top: 100,
+  },
+});

+ 1 - 0
src/app/screens/home/articleDetail/api/index.ts

@@ -0,0 +1 @@
+export * from './useArticleDetail';

+ 29 - 0
src/app/screens/home/articleDetail/api/useArticleDetail.tsx

@@ -0,0 +1,29 @@
+import {useApi} from '@common/api/useApi.ts';
+import {z} from 'zod';
+
+const articleSchema = z.object({
+  content: z.string({
+    required_error: '文章内容不存在',
+    invalid_type_error: '文章内容类型错误',
+  }), // 文章内容
+  title: z.string(), // 文章标题
+  publishTime: z.string().optional(), // 发布时间
+  coverImgUrl: z.string().optional(), // 封面图
+  shareUrl: z.string().optional(), // 分享链接
+  summary: z.string().optional(), // 摘要
+});
+
+// 首页文章详情
+const useArticleDetail = (id: string) => {
+  return useApi(
+    '/am/v3/hotnews/article/detail',
+    'GET',
+    {
+      id: id,
+    },
+    articleSchema,
+    {},
+  );
+};
+
+export {useArticleDetail};

+ 154 - 0
src/app/screens/home/articleShare/ArticleShareScreen.tsx

@@ -0,0 +1,154 @@
+import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
+import {
+  Animated,
+  Image,
+  Pressable,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native';
+import { StackScreenProps } from '@react-navigation/stack';
+import { MainParamList } from '@app/routes/MainParamList';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import Separator from '@common/components/Separator';
+
+const CONTENT_HEIGHT = 200; // 卡片高度
+const DURATION = 300; // 弹出和收起的时间
+
+type Props = StackScreenProps<MainParamList, 'ArticleShare'>;
+
+// 分享弹窗页
+export default function ArticleShareScreen(props: Props) {
+  const { navigation, route } = props;
+  const safeAreaInsets = useSafeAreaInsets();
+
+  useLayoutEffect(() => {
+    navigation.setOptions({
+      title: '',
+    });
+  }, [navigation]);
+
+  const bottom = useRef(
+    new Animated.Value(-CONTENT_HEIGHT - safeAreaInsets.bottom),
+  );
+
+  const [shown, setShown] = useState<boolean>(true);
+
+  useEffect(() => {
+    if (shown === true) {
+      Animated.timing(bottom.current, {
+        toValue: 0,
+        duration: DURATION,
+        useNativeDriver: false,
+      }).start();
+    } else {
+      Animated.timing(bottom.current, {
+        toValue: -CONTENT_HEIGHT - safeAreaInsets.bottom,
+        duration: DURATION,
+        useNativeDriver: false,
+      }).start(() => {
+        navigation.pop();
+      });
+    }
+  }, [navigation, safeAreaInsets.bottom, shown]);
+
+  return (
+    <View style={styles.container}>
+      <Pressable
+        style={[StyleSheet.absoluteFill, styles.background]}
+        onPress={() => {
+          setShown(false);
+        }}
+      />
+      <Animated.View
+        style={[
+          styles.animated,
+          {
+            height: CONTENT_HEIGHT + safeAreaInsets.bottom,
+            bottom: bottom.current,
+          },
+        ]}
+      >
+        <View style={styles.content}>
+          <TouchableOpacity
+            style={styles.contentButton}
+            onPress={() => {
+              setShown(false);
+            }}
+          >
+            <Image
+              style={styles.buttonImage}
+              source={require('@app/assets/images/common/common_share_session.png')}
+            />
+            <Text style={styles.buttonText}>微信</Text>
+          </TouchableOpacity>
+          <TouchableOpacity
+            style={styles.contentButton}
+            onPress={() => {
+              setShown(false);
+            }}
+          >
+            <Image
+              style={styles.buttonImage}
+              source={require('@app/assets/images/common/common_share_timeline.png')}
+            />
+            <Text style={styles.buttonText}>朋友圈</Text>
+          </TouchableOpacity>
+        </View>
+        <Separator />
+        <TouchableOpacity
+          style={styles.bottomButton}
+          onPress={() => {
+            setShown(false);
+          }}
+        >
+          <Text style={styles.bottomButtonText}>取消</Text>
+        </TouchableOpacity>
+      </Animated.View>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    justifyContent: 'flex-end',
+  },
+  background: {
+    backgroundColor: 'rgba(21, 20, 35, 0.6)',
+  },
+  animated: {
+    backgroundColor: '#FFFFFF',
+    borderTopLeftRadius: 13,
+    borderTopRightRadius: 13,
+  },
+  content: {
+    height: 120,
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-evenly',
+  },
+  contentButton: {
+    alignItems: 'center',
+  },
+  buttonImage: {
+    width: 58,
+    height: 58,
+  },
+  buttonText: {
+    marginTop: 4,
+    color: '#242424',
+    fontSize: 12,
+  },
+  bottomButton: {
+    flex: 1,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  bottomButtonText: {
+    color: '#242424',
+    fontWeight: '600',
+    fontSize: 14,
+  },
+});

+ 604 - 0
src/app/screens/home/home/HomeScreen.tsx

@@ -0,0 +1,604 @@
+import React, {
+  useCallback,
+  useEffect,
+  useLayoutEffect,
+  useMemo,
+  useState,
+} from 'react';
+import {
+  DeviceEventEmitter,
+  FlatList,
+  Image,
+  NativeScrollPoint,
+  RefreshControl,
+  SafeAreaView,
+  StyleSheet,
+  Text,
+  useWindowDimensions,
+  View,
+} from 'react-native';
+import { CompositeScreenProps, useFocusEffect } from '@react-navigation/native';
+import { useHeaderHeight } from '@react-navigation/elements';
+import { StackScreenProps } from '@react-navigation/stack';
+import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
+import { MainParamList, MainTabParamList } from '@app/routes/MainParamList';
+import LinearGradient from 'react-native-linear-gradient';
+import HeaderRight from './components/HeaderRight';
+import HomeTopView from './components/HomeTopView';
+import HomeTodoView from './components/HomeTodoView';
+import HomeCarousel from '@app/screens/components/HomeCarousel';
+import HomeBulletin from './components/HomeBulletin';
+import { useBannerList } from '@app/screens/api';
+import {
+  Article,
+  useArticleCategory,
+  useArticleListByCategory,
+  useBadgeNum,
+  useFeatureList,
+  useNoticeList,
+} from './api';
+import HomeFeaturesView from './components/HomeFeaturesView';
+import Spinner from '@common/components/Spinner';
+import HomeArticlesView from './components/HomeArticlesView';
+import HomeArticleItem from './components/HomeArticleItem';
+import { DEVICE_ENENT_UPDATE_HOME_FEATURE_LIST_KEY } from '@common/constants';
+import { useHandleClickFeatureListItem } from './hooks/useHandleClickFeatureListItem';
+import { useHandleClickBannerListItem } from './hooks/useHandleClickBannerListItem';
+import { useAuth } from '@common/contexts/useAuth';
+import { useHandleUnreadRemind } from './hooks/useHandleUnreadRemind';
+import { debounce } from '@common/utils/commonUtils';
+import { showErrorMessage } from '@common/ToastHelper.ts';
+import ListEmpty from '@common/components/ListEmpty.tsx';
+
+const OPACITY_START = 1; // 导航栏起始透明度
+const OPACITY_END = 0; // 导航栏终点透明度
+const PADDING_HORIZONTAL = 12;
+const BACKGROUND_COLOR = '#F3F4F5'; // 背景色
+const ARTICLE_PAGE_SIZE = 10; // 热门推荐每页数量
+
+type Props = CompositeScreenProps<
+  BottomTabScreenProps<MainTabParamList, 'Home'>,
+  StackScreenProps<MainParamList, 'MainTab'>
+>;
+
+export default function HomeScreen(props: Props) {
+  const { navigation } = props;
+  const headerHeight = useHeaderHeight();
+  const { width } = useWindowDimensions();
+
+  const {
+    state: { userInfo },
+  } = useAuth();
+
+
+  // 【状态】
+  const [contentOffset, setContentOffset] = useState<NativeScrollPoint>({
+    x: 0,
+    y: 0,
+  });
+
+  // 【子组件】
+
+  // 创建导航栏背景
+  const createHeaderBackground = useCallback(() => {
+    return (
+      <View
+        style={[
+          styles.headerBackground,
+          {
+            opacity:
+              contentOffset.y <= 0
+                ? OPACITY_END
+                : contentOffset.y >= headerHeight
+                ? OPACITY_START
+                : contentOffset.y / headerHeight,
+          }, // 滑动 ScrollView 导航栏透明度逐渐变化
+        ]}
+      />
+    );
+  }, [contentOffset.y, headerHeight]);
+
+  const { unread, unreadRemindLoading, handleUnreadRemindFetch } =
+    useHandleUnreadRemind(); // 处理未读消息相关
+
+  useEffect(() => {
+    handleUnreadRemindFetch();
+  }, [handleUnreadRemindFetch]);
+
+  const createHeaderRight = useCallback(() => {
+    return (
+      <HeaderRight
+        badge={unread}
+        onPressLeftButton={() => {
+          debounce(() => {});
+        }}
+        onPressRightButton={() => {
+          debounce(() => {});
+          // navigation.navigate('Scan'); // 以后再更换新的扫一扫,工作量太大
+        }}
+      />
+    );
+  }, [unread]);
+
+  // 【副作用】
+
+  useLayoutEffect(() => {
+    navigation.setOptions({
+      headerTitle: '医网信',
+      headerTransparent: true,
+      headerBackground: () => createHeaderBackground(),
+      headerRight: () => createHeaderRight(),
+    });
+    return () => {
+    };
+  }, [createHeaderBackground, createHeaderRight, navigation]);
+
+  const {
+    response: bannerList,
+    error: bannerListError,
+    loading: bannerListLoading,
+    fetch: bannerListFetch,
+  } = useBannerList(); // 轮播图
+
+  useEffect(() => {
+    if (bannerListError && bannerListError.type !== 'Cancel') {
+      showErrorMessage(bannerListError.message);
+    }
+  }, [bannerListError]);
+
+  const {
+    response: noticeList,
+    error: noticeListError,
+    loading: noticeListLoading,
+    fetch: noticeListFetch,
+  } = useNoticeList(); // 公告
+
+  useEffect(() => {
+    if (noticeListError && noticeListError.type !== 'Cancel') {
+      showErrorMessage(noticeListError.message);
+    }
+  }, [noticeListError]);
+
+  const {
+    response: featureList,
+    error: featureListError,
+    loading: featureListLoading,
+    fetch: featureListFetch,
+  } = useFeatureList(); // 精品服务数组
+
+  useEffect(() => {
+    if (featureListError && featureListError.type !== 'Cancel') {
+      showErrorMessage(featureListError.message);
+    }
+  }, [featureListError]);
+
+  const {
+    response: articleCategory,
+    error: articleCategoryError,
+    loading: articleCategoryLoading,
+    fetch: articleCategoryFetch,
+  } = useArticleCategory(); // 热门推荐分类
+
+  useEffect(() => {
+    if (articleCategoryError && articleCategoryError.type !== 'Cancel') {
+      showErrorMessage(articleCategoryError.message);
+    }
+  }, [articleCategoryError]);
+
+  const [selectedCategoryCode, setSelectedCategoryCode] = useState<
+    number | undefined
+  >(undefined);
+
+  useEffect(() => {
+    if (articleCategory !== undefined && articleCategory.length > 0) {
+      const filteredArticleCategory = articleCategory.filter(
+        c => c.isDefault === true,
+      );
+      if (filteredArticleCategory.length > 0) {
+        setSelectedCategoryCode(filteredArticleCategory[0].code);
+      }
+    }
+  }, [articleCategory]);
+
+  const {
+    response: articleList,
+    error: articleListError,
+    loading: articleListLoading,
+    fetch: articleListFetch,
+  } = useArticleListByCategory(selectedCategoryCode ?? 0); // 热门推荐数组
+  const [displayArticleList, setDisplayArticleList] = useState<Article[]>([]); // 当前展示的热门推荐数组
+
+  useEffect(() => {
+    if (articleListError && articleListError.type !== 'Cancel') {
+      showErrorMessage(articleListError.message);
+    }
+  }, [articleListError]);
+
+  useEffect(() => {
+    if (selectedCategoryCode !== undefined) {
+      articleListFetch();
+    }
+  }, [articleListFetch, selectedCategoryCode]);
+
+  useEffect(() => {
+    if (articleList) {
+      setDisplayArticleList(articleList.slice(0, ARTICLE_PAGE_SIZE));
+    }
+  }, [articleList]);
+
+  const {
+    response: badgeNum,
+    error: badgeNumError,
+    fetch: badgeNumFetch,
+  } = useBadgeNum(); // 今日未处理数据和精品服务角标
+
+  // 对于首页角标数据,频繁调用
+  useFocusEffect(
+    useCallback(() => {
+      badgeNumFetch();
+    }, [badgeNumFetch]),
+  );
+
+  useEffect(() => {
+    if (badgeNumError && badgeNumError.type !== 'Cancel') {
+      showErrorMessage(badgeNumError.message);
+    }
+  }, [badgeNumError]);
+
+  // 下拉刷新
+  const handleRefresh = useCallback(() => {
+    bannerListFetch();
+    noticeListFetch();
+    featureListFetch();
+    articleCategoryFetch();
+    badgeNumFetch();
+    handleUnreadRemindFetch();
+  }, [
+    articleCategoryFetch,
+    badgeNumFetch,
+    bannerListFetch,
+    featureListFetch,
+    handleUnreadRemindFetch,
+    noticeListFetch,
+  ]);
+
+  // 【计算属性】
+
+  // 处理过的精品服务列表,增加了badge的状态
+  const computedFeatureList = useMemo(() => {
+    if (badgeNum === undefined || badgeNum.length === 0) {
+      return featureList;
+    }
+    return featureList?.map(feature => {
+      const filteredBadgeItems = badgeNum.filter(badgeItem => {
+        return badgeItem.nsId && badgeItem.nsId === feature.uniqueId;
+      });
+      let badge = false;
+      if (filteredBadgeItems.length > 0) {
+        badge = filteredBadgeItems[0].shouldRemind;
+      }
+      return { ...feature, badge: badge };
+    });
+  }, [badgeNum, featureList]); // 在原有精品服务功能数组的基础上增加了角标的是否展示
+
+  useEffect(() => {
+    const filteredFeatureList = computedFeatureList?.filter(feature => {
+      return feature.recommend === true;
+    });
+
+    // 当这个列表更新的更新时,发通知,tab栏会接收通知更新tab标签
+    if (filteredFeatureList !== undefined) {
+      DeviceEventEmitter.emit(
+        DEVICE_ENENT_UPDATE_HOME_FEATURE_LIST_KEY,
+        filteredFeatureList.length > 0 ? filteredFeatureList[0] : undefined, // 如果没有推荐的则传空
+      );
+    }
+  }, [computedFeatureList]);
+
+  // 今日未处理三个功能块未处理数
+  const computedBadgeNum = useMemo(() => {
+    if (badgeNum === undefined || badgeNum.length === 0) {
+      return {};
+    }
+    let yiwangqianNum = '0';
+    let suifangNum = '0';
+    let hospitalNum = '0';
+    for (let index = 0; index < badgeNum.length; index++) {
+      const badgeItem = badgeNum[index];
+      // 医网签id
+      if (badgeItem.nsId === '101') {
+        yiwangqianNum = badgeItem.num.toString();
+      }
+      // 随访id
+      if (badgeItem.nsId === 'Y_T_A2310053073') {
+        suifangNum = badgeItem.num.toString();
+      }
+      // 云诊室id
+      if (badgeItem.nsId === 'CLOUD_CLINIC') {
+        hospitalNum = badgeItem.num.toString();
+      }
+    }
+    return {
+      yiwangqianNum,
+      suifangNum,
+      hospitalNum,
+    };
+  }, [badgeNum]);
+
+  // 【点击事件方法】
+
+  const { subServerCheckLoading, handleClickFeatureListItem } =
+    useHandleClickFeatureListItem(); // 点击精品服务功能块
+
+  const handleClickBannerListItem = useHandleClickBannerListItem(
+    handleClickFeatureListItem,
+  ); // 点击轮播图
+
+  return (
+    <View
+      style={styles.container}
+      onLayout={() => {
+        // 今日未处理视图时引导的开始位置,当这个视图渲染好之后就可以开始了
+        // if (info.appTourVersion !== APP_TOUR_VERSION) {
+        //   start('今日未处理', scrollRef.current);
+        // }
+      }}
+    >
+      {/* 顶部背景 */}
+      <View style={styles.topBackground}>
+        <Image
+          style={styles.backgroundImage}
+          source={require('@app/assets/images/home/home_background.png')}
+        />
+      </View>
+      <FlatList
+        style={styles.list}
+        contentInsetAdjustmentBehavior="scrollableAxes"
+        showsVerticalScrollIndicator={false}
+        onScroll={event => {
+          setContentOffset(event.nativeEvent.contentOffset);
+        }}
+        data={displayArticleList}
+        renderItem={({ item, index }) => (
+          <View key={index}>
+            <HomeArticleItem
+              title={item.title}
+              summary={item.summary}
+              totalReadCount={item.totalReadCount}
+              coverImgUrl={item.coverImgUrl}
+              onPress={() => {
+                if (articleList && articleList.length > 0) {
+                  const article = articleList[index];
+                  if (article.id === undefined || article.id.length === 0) {
+                    return;
+                  }
+                  navigation.navigate('ArticleDetail', {
+                    id: article.id,
+                  });
+                }
+              }}
+            />
+          </View>
+        )}
+        ListEmptyComponent={ListEmpty}
+        ListHeaderComponent={
+          <SafeAreaView>
+            <View style={{ height: headerHeight }} />
+            {/* 医生信息视图 */}
+            <HomeTopView
+              isVerified={
+                userInfo?.realNameStatus === 'success'
+                  ? true
+                  : userInfo?.realNameStatus === undefined
+                  ? undefined
+                  : false
+              }
+              nickname={userInfo?.nickname}
+              picUrl={userInfo?.picUrl}
+              onPress={() => {
+                debounce(() => {});
+              }}
+            />
+            {/* 今日未处理视图 */}
+            <HomeTodoView
+              yiwangqianNum={computedBadgeNum.yiwangqianNum}
+              suifangNum={computedBadgeNum.suifangNum}
+              hospitalNum={computedBadgeNum.hospitalNum}
+              onPressYiwangqian={() => {
+                debounce(() => {
+                  const filteredFeatureList = computedFeatureList?.filter(
+                    feature => {
+                      return feature.uniqueId === '101';
+                    },
+                  );
+                  if (
+                    filteredFeatureList !== undefined &&
+                    filteredFeatureList.length > 0
+                  ) {
+                    handleClickFeatureListItem(filteredFeatureList[0]);
+                  }
+                });
+              }}
+              onPressSuifang={() => {
+                debounce(() => {
+                  const filteredFeatureList = computedFeatureList?.filter(
+                    feature => {
+                      return feature.uniqueId === 'Y_T_A2310053073';
+                    },
+                  );
+                  if (
+                    filteredFeatureList !== undefined &&
+                    filteredFeatureList.length > 0
+                  ) {
+                    handleClickFeatureListItem(filteredFeatureList[0]);
+                  }
+                });
+              }}
+              onPressHospital={() => {
+                debounce(() => {
+                  const filteredFeatureList = computedFeatureList?.filter(
+                    feature => {
+                      return feature.uniqueId === 'CLOUD_CLINIC';
+                    },
+                  );
+                  if (
+                    filteredFeatureList !== undefined &&
+                    filteredFeatureList.length > 0
+                  ) {
+                    handleClickFeatureListItem(filteredFeatureList[0]);
+                  }
+                });
+              }}
+            />
+            {/* 轮播图 */}
+            {bannerList && bannerList.length > 0 && (
+              <LinearGradient
+                style={styles.carousel}
+                start={{ x: 0.5, y: 0 }}
+                end={{ x: 0.5, y: 1 }}
+                colors={['#F8FDFF', BACKGROUND_COLOR]}
+              >
+                <HomeCarousel
+                  width={width - 2 * PADDING_HORIZONTAL}
+                  data={bannerList}
+                  onPress={(index: number) => {
+                    debounce(() => {
+                      if (bannerList && bannerList.length > 0) {
+                        handleClickBannerListItem(bannerList[index]);
+                      }
+                    });
+                  }}
+                />
+              </LinearGradient>
+            )}
+            {/* 公告栏 */}
+            {noticeList && noticeList.list.length > 0 && (
+              <View style={styles.bulletin}>
+                <HomeBulletin
+                  width={width - 2 * PADDING_HORIZONTAL}
+                  data={noticeList.list}
+                  onPress={index => {
+                    debounce(() => {});
+                  }}
+                />
+              </View>
+            )}
+            {/* 精品服务 */}
+            <HomeFeaturesView
+              features={computedFeatureList}
+              error={featureListError}
+              reload={() => {
+                featureListFetch();
+              }}
+              onPress={index => {
+                debounce(() => {
+                  if (computedFeatureList && computedFeatureList.length > 0) {
+                    handleClickFeatureListItem(computedFeatureList[index]);
+                  }
+                });
+              }}
+            />
+            {/* 热门推荐头部视图 */}
+            <HomeArticlesView
+              articleCategory={articleCategory}
+              selectedCode={selectedCategoryCode}
+              articleCategoryError={articleCategoryError}
+              onPressTag={code => {
+                setSelectedCategoryCode(code);
+              }}
+            />
+          </SafeAreaView>
+        }
+        ListFooterComponent={articleList === undefined ? FooterEmpty : Footer}
+        refreshing={false}
+        refreshControl={
+          <RefreshControl refreshing={false} onRefresh={handleRefresh} />
+        }
+        onRefresh={() => {
+          handleRefresh();
+        }}
+        onEndReachedThreshold={0.2}
+        onEndReached={() => {
+          if (articleList === undefined) {
+            return;
+          }
+          const startIndex = displayArticleList.length;
+          const endEndex = startIndex + ARTICLE_PAGE_SIZE;
+
+          if (startIndex < articleList.length) {
+            setDisplayArticleList(prev => [
+              ...prev,
+              ...articleList.slice(startIndex, endEndex),
+            ]);
+          }
+        }}
+      />
+      {(bannerListLoading ||
+        noticeListLoading ||
+        featureListLoading ||
+        articleCategoryLoading ||
+        articleListLoading ||
+        subServerCheckLoading ||
+        unreadRemindLoading) && <Spinner />}
+    </View>
+  );
+}
+
+function FooterEmpty() {
+  return <View style={styles.footerEmpty} />;
+}
+
+function Footer() {
+  return (
+    <View style={styles.footer}>
+      <Text>没有更多数据了</Text>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  alertText: {
+    textAlign: 'center',
+    color: '#999999',
+    fontSize: 12,
+  },
+  headerBackground: {
+    flex: 1,
+    backgroundColor: '#FFFFFF',
+  },
+  container: {
+    flex: 1,
+    backgroundColor: BACKGROUND_COLOR,
+  },
+  topBackground: {
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    height: 240,
+  },
+  backgroundImage: {
+    flex: 1,
+    width: '100%',
+  },
+  list: {
+    flex: 1,
+  },
+  carousel: {
+    paddingHorizontal: PADDING_HORIZONTAL,
+    paddingBottom: 6,
+  },
+  bulletin: {
+    paddingHorizontal: PADDING_HORIZONTAL,
+    paddingVertical: 4,
+    backgroundColor: BACKGROUND_COLOR,
+  },
+  footerEmpty: {
+    height: 25,
+  },
+  footer: {
+    height: 40,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+});

+ 8 - 0
src/app/screens/home/home/api/index.ts

@@ -0,0 +1,8 @@
+export * from './useNoticeList';
+export * from './useFeatureList';
+export * from './useArticleList';
+export * from './useBadgeNum';
+export * from './useSubServerCheck';
+export * from './useUnreadRemind';
+export * from './useArticleCategory';
+export * from './useArticleListByCategory';

+ 39 - 0
src/app/screens/home/home/api/useArticleCategory.ts

@@ -0,0 +1,39 @@
+import {z} from 'zod';
+import { useApi } from '@common/api/useApi.ts';
+
+// 文章分类 item
+const articleCategoryItemSchema = z.object({
+  code: z.number(), // 分类 code
+  name: z.string().optional().default(''), // 分类名称
+  isDefault: z.boolean(), // 图标
+});
+
+const articleCategorySchema = z
+  .array(articleCategoryItemSchema)
+  .transform(arr => {
+    return [
+      ...arr.filter(item => item.isDefault === true),
+      ...arr.filter(item => item.isDefault !== true),
+    ];
+  })
+  .optional()
+  .default([]);
+
+type ArticleCategoryItem = z.infer<typeof articleCategoryItemSchema>;
+
+// 查询文章分类
+const useArticleCategory = () => {
+  return useApi(
+    '/am/v3/hotnews/article/queryCategory',
+    'GET',
+    {},
+    articleCategorySchema,
+    {
+      automatic: true,
+      loadingDelay: 500,
+    },
+  );
+};
+
+export type {ArticleCategoryItem};
+export {useArticleCategory};

+ 33 - 0
src/app/screens/home/home/api/useArticleList.ts

@@ -0,0 +1,33 @@
+import {useApi} from '@common/api/useApi.ts';
+import {z} from 'zod';
+
+const articleSchema = z.object({
+  id: z.string().optional(), // 文章id
+  title: z.string().optional().default(''), // 文章标题
+  summary: z.string().optional().default(''), // 文章摘要
+  publishTime: z.string().optional(), // 发布时间
+  coverImgUrl: z.string().optional(), // 图片
+  totalReadCount: z.string().optional().default('0'), // 阅读量
+});
+
+const articlesSchema = z.array(articleSchema).optional().default([]);
+
+// 首页热门推荐
+// {"data":[{"coverImgUrl":"https://tms-dev.oss-cn-beijing.aliyuncs.com/khcrm_file/null_GNPJXNPOAC/WechatIMG1741.jpg","id":"4","publishTime":"2024-08-27 14:07:59","summary":"1","title":"点击快速反击脸上的肌肤 发了几点上课就放假了带手机客服端上来就分开了带手机来看风景的时刻了就分开的路","totalReadCount":"2"}],"message":"success","status":"0"}
+const useArticleList = () => {
+  return useApi(
+    '/am/v3/hotnews/article/list',
+    'POSTJSON',
+    {
+      startNum: 0,
+      endNum: 999,
+    },
+    articlesSchema,
+    {
+      automatic: true,
+      loadingDelay: 500,
+    },
+  );
+};
+
+export {useArticleList};

+ 33 - 0
src/app/screens/home/home/api/useArticleListByCategory.ts

@@ -0,0 +1,33 @@
+import {useApi} from '@common/api/useApi.ts';
+import {z} from 'zod';
+
+const articleSchema = z.object({
+  id: z.string().optional(), // 文章id
+  title: z.string().optional().default(''), // 文章标题
+  summary: z.string().optional().default(''), // 文章摘要
+  publishTime: z.string().optional(), // 发布时间
+  coverImgUrl: z.string().optional(), // 图片
+  totalReadCount: z.string().optional().default('0'), // 阅读量
+});
+
+const articlesSchema = z.array(articleSchema).optional().default([]);
+type Article = z.infer<typeof articleSchema>;
+
+// 首页根据分类查询热门推荐
+// {"data":[{"coverImgUrl":"https://tms-dev.oss-cn-beijing.aliyuncs.com/khcrm_file/null_GNPJXNPOAC/WechatIMG1741.jpg","id":"4","publishTime":"2024-08-27 14:07:59","summary":"1","title":"点击快速反击脸上的肌肤 发了几点上课就放假了带手机客服端上来就分开了带手机来看风景的时刻了就分开的路","totalReadCount":"2"}],"message":"success","status":"0"}
+const useArticleListByCategory = (code: number) => {
+  return useApi(
+    '/am/v3/hotnews/article/queryListByCategory',
+    'POSTJSON',
+    {
+      categoryCode: code,
+    },
+    articlesSchema,
+    {
+      loadingDelay: 500,
+    },
+  );
+};
+
+export type {Article};
+export {useArticleListByCategory};

+ 24 - 0
src/app/screens/home/home/api/useBadgeNum.ts

@@ -0,0 +1,24 @@
+import {useApi} from '@common/api/useApi.ts';
+import {z} from 'zod';
+
+const itemBadgeSchema = z.object({
+  nsId: z.string().optional(), // id // 功能 id
+  num: z.number().optional().default(0), // 未处理数量
+  shouldRemind: z.boolean().optional().default(false), // 是否红点提示
+});
+
+const itemBadgesSchema = z.array(itemBadgeSchema).optional().default([]);
+
+// 首页今日未处理数据以及精品服务角标数据
+// [{"nsId":"101","num":0,"shouldRemind":false},{"nsId":"E_CONTRACT","num":0,"shouldRemind":false},{"nsId":"CLOUD_CLINIC","num":0,"shouldRemind":false},{"$ref":"$.data[2]"}]
+const useBadgeNum = () => {
+  return useApi(
+    '/am/v1/subServer/cornerTag',
+    'POSTJSON',
+    {},
+    itemBadgesSchema,
+    {},
+  );
+};
+
+export {useBadgeNum};

+ 33 - 0
src/app/screens/home/home/api/useFeatureList.ts

@@ -0,0 +1,33 @@
+import {useApi} from '@common/api/useApi.ts';
+import {z} from 'zod';
+
+// 精品服务功能块
+const featureSchema = z.object({
+  uniqueId: z.string(), // 功能 id
+  name: z.string(), // 功能名称
+  icon: z.string().optional(), // 图标
+  link: z.string(), // 链接 http/https链接、ywxApp://TeamIndexView
+  recommend: z.boolean().optional().default(false), // 是否是主推功能
+  advertisePic: z.string().optional(), // 广告图?
+  phone: z.string().optional().default(''), // 电话
+  allowApply: z.boolean().optional().default(false), // 是否允许体验
+  subscribeType: z.number().optional(), // 订阅类型 0免费订阅 1厂商订阅 2用户订阅
+
+  badge: z.boolean().optional().default(false), // 注:当前接口没有这个字段,但是会根据 /am/v1/subServer/cornerTag 接口给该模型拼接上该字段,故这里添加一个可选字段并默认为false
+});
+
+const featuresSchema = z.array(featureSchema).optional().default([]);
+
+type Feature = z.infer<typeof featureSchema>;
+
+// 首页精品服务位置常用功能模块列表
+// {"advertisePic":"https://tms-dev.oss-cn-beijing.aliyuncs.com/doctorHelper/advertPic_E_CONTRACT.png?time=1694161704000?time=1694161744000?time=1710124742000?time=1711019040000","allowApply":false,"cornerTagUrl":"","createTime":"2021-06-28 18:30:33","icon":"https://tms-dev.oss-cn-beijing.aliyuncs.com/doctorHelper/docIcon_E_CONTRACT.png?time=1710124742000?time=1711019040000","link":"ywxApp://TeamIndexView","name":"电子合同","note":"电子合同","phone":"13122223333","recommend":false,"serviceType":2,"sort":2,"status":1,"subscribeId":"synForwardRecipe","subscribeType":0,"uniqueId":"E_CONTRACT","updateTime":"2024-03-21 19:04:00"}
+const useFeatureList = () => {
+  return useApi('/am/v4/subServer/baseList', 'POSTJSON', {}, featuresSchema, {
+    automatic: true,
+    loadingDelay: 500,
+  });
+};
+
+export type {Feature};
+export {useFeatureList};

+ 25 - 0
src/app/screens/home/home/api/useNoticeList.ts

@@ -0,0 +1,25 @@
+import {useApi} from '@common/api/useApi.ts';
+import {z} from 'zod';
+
+const noticeSchema = z
+  .object({
+    id: z.string().optional().default(''), // id
+    content: z.string().url(), // 公告内容
+  })
+  .passthrough(); // passthrough 的意思是除了 content 之外,其他的字段不做任何处理;目前这么写,将对象整体传给旧模块,后期重构可以去掉这个
+
+const noticesSchema = z.object({
+  list: z.array(noticeSchema).optional().default([]),
+});
+
+// 公告列表
+// {"collection":"sys_notice","content":"啊倒萨大苏打的","createTime":"2024-08-15 18:31:18","id":"{\"$oid\":\"66bdd8f6986acc13d359124f\"}","isDel":0,"readCount":0,"sendTime":"2024-08-15 18:31:20","status":1,"title":"公告新增标题","type":0,"updateTime":"2024-08-15 18:31:20"}
+const useNoticeList = () => {
+  return useApi('/am/v3/notice/list', 'POSTJSON', {}, noticesSchema, {
+    automatic: true,
+    loadingDelay: 500,
+    log: false,
+  });
+};
+
+export {useNoticeList};

+ 68 - 0
src/app/screens/home/home/api/useSubServerCheck.ts

@@ -0,0 +1,68 @@
+import {useApi} from '@common/api/useApi.ts';
+import {z} from 'zod';
+
+// 用户类型
+const SubscribeType = {
+  Free: 0, // 免费
+  Vendor: 1, // 面向厂商开启
+  Individual: 2, // 面向个人开启
+} as const;
+
+// 服务开通状态
+const SubscribeStatus = {
+  Serving: 1, // 已开通
+  Stop: 2, // 未开通
+} as const;
+
+const checkResultSchema = z.object({
+  checkResult: z.boolean(),
+  clientList: z
+    .array(
+      z.object({
+        clientId: z.string().min(1), // 厂商id
+        clientName: z.string().optional().default(''), // 厂商名
+        subscribeStatus: z.nativeEnum(SubscribeStatus).transform(val => {
+          switch (val) {
+            case SubscribeStatus.Serving:
+              return 'serving';
+            case SubscribeStatus.Stop:
+              return 'stop';
+          }
+        }), // 服务开通状态
+        cossConf: z
+          .object({
+            appId: z.string(),
+            servUrl: z.string(),
+          })
+          .nullish(),
+      }),
+    )
+    .optional()
+    .default([]), // 应该是厂商列表
+  subscribeType: z.nativeEnum(SubscribeType).transform(val => {
+    switch (val) {
+      case SubscribeType.Free:
+        return 'free';
+      case SubscribeType.Vendor:
+        return 'vendor';
+      case SubscribeType.Individual:
+        return 'individual';
+    }
+  }), // 订阅类型
+});
+
+// 检查精品服务功能块是否能进入
+// {"checkResult": true, "clientList": [{"clientId": "2015112716143758", "clientName": "数字医信开发", "subscribeStatus": 2}, {"clientId": "2024013118101247", "clientName": "dev随访客户真", "subscribeStatus": 2}, {"clientId": "2024062618140163", "clientName": "同和互联网医院", "subscribeStatus": 2}], "subscribeType": 1}
+const useSubServerCheck = () => {
+  return useApi(
+    '/am/v3/subServer/check',
+    'POSTJSON',
+    {
+      nsId: '', // 功能模块的id
+    },
+    checkResultSchema,
+    {},
+  );
+};
+
+export {useSubServerCheck};

+ 22 - 0
src/app/screens/home/home/api/useUnreadRemind.ts

@@ -0,0 +1,22 @@
+import {useApi} from '@common/api/useApi.ts';
+import {z} from 'zod';
+
+// 检查是否存在未读的消息
+// timeStamp 传一个 类似 2000-01-01 00:00:00 的字符串
+const useUnreadRemind = () => {
+  return useApi(
+    '/am/v3/notice/unreadRemind',
+    'POSTJSON',
+    {
+      timeStamp: '',
+    },
+    z.object({
+      unread: z.boolean().optional().default(false),
+    }),
+    {
+      loadingDelay: 500,
+    },
+  );
+};
+
+export {useUnreadRemind};

+ 69 - 0
src/app/screens/home/home/components/HeaderRight.tsx

@@ -0,0 +1,69 @@
+import React from 'react';
+import {Image, StyleSheet, TouchableOpacity, View} from 'react-native';
+
+type Props = {
+  badge?: boolean; // 是否有已读消息
+  onPressLeftButton?: () => void; // 点击左侧的按钮
+  onPressRightButton?: () => void; // 点击右侧的按钮
+};
+
+/**
+ * @description: 首页导航栏右侧按钮视图
+ */
+export default function HeaderRight(props: Props) {
+  return (
+    <View style={styles.contanier}>
+      <TouchableOpacity
+        style={styles.button}
+        onPress={() => {
+          props.onPressLeftButton && props.onPressLeftButton();
+        }}>
+        <View>
+          <Image
+            style={styles.image}
+            source={require('@app/assets/images/home/home_notifications.png')}
+          />
+          {props.badge === true && <View style={styles.badge} />}
+        </View>
+      </TouchableOpacity>
+      <TouchableOpacity
+        style={styles.button}
+        onPress={() => {
+          props.onPressRightButton && props.onPressRightButton();
+        }}>
+        <Image
+          style={styles.image}
+          source={require('@app/assets/images/home/home_scan.png')}
+        />
+      </TouchableOpacity>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  contanier: {
+    height: '100%',
+    marginRight: 15,
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  button: {
+    height: '100%',
+    paddingHorizontal: 10,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  image: {
+    height: 24,
+    width: 24,
+  },
+  badge: {
+    position: 'absolute',
+    width: 6,
+    height: 6,
+    backgroundColor: '#FF6500',
+    borderRadius: 3,
+    top: 0,
+    right: 0,
+  },
+});

+ 87 - 0
src/app/screens/home/home/components/HomeArticleItem.tsx

@@ -0,0 +1,87 @@
+import React from 'react';
+import {
+  Image,
+  SafeAreaView,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native';
+import Separator from '@common/components/Separator';
+
+type Props = {
+  title: string; // 标题
+  summary: string; // 摘要
+  totalReadCount: string; // 未读数
+  coverImgUrl?: string; // 图片
+  onPress?: () => void; // 点击 item
+};
+
+export default function HomeArticleItem(props: Props) {
+  return (
+    <SafeAreaView style={styles.container}>
+      <TouchableOpacity
+        style={styles.touch}
+        onPress={() => {
+          props.onPress && props.onPress();
+        }}>
+        <View style={styles.content}>
+          {props.coverImgUrl && props.coverImgUrl.length > 0 && (
+            <Image style={styles.image} source={{uri: props.coverImgUrl}} />
+          )}
+          <View style={styles.contentRight}>
+            <Text numberOfLines={1} style={styles.title}>
+              {props.title}
+            </Text>
+            <Text style={styles.summary} numberOfLines={3}>
+              {props.summary}
+            </Text>
+            <Text
+              numberOfLines={1}
+              style={styles.bottomText}>{`${props.totalReadCount}阅读`}</Text>
+          </View>
+        </View>
+        <Separator />
+      </TouchableOpacity>
+    </SafeAreaView>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#F3F4F5',
+  },
+  touch: {
+    flex: 1,
+    backgroundColor: '#FFFFFF',
+    marginHorizontal: 12,
+  },
+  content: {
+    flexDirection: 'row',
+    padding: 12,
+  },
+  image: {
+    width: 110,
+    height: 110,
+    borderRadius: 10,
+    marginRight: 12,
+  },
+  contentRight: {
+    flex: 1,
+  },
+  title: {
+    color: '#11102C',
+    fontSize: 14,
+    fontWeight: '500',
+  },
+  summary: {
+    flex: 1,
+    marginVertical: 10,
+    color: '#999999',
+  },
+  bottomText: {
+    color: '#666666',
+    fontSize: 13,
+  },
+});

+ 174 - 0
src/app/screens/home/home/components/HomeArticlesView.tsx

@@ -0,0 +1,174 @@
+import React from 'react';
+import {
+  Image,
+  ScrollView,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native';
+import { RequestError } from '@szyx-mobile/use-request';
+import DataEmpty from '@common/components/DataEmpty';
+
+type Props = {
+  articleCategory?: {
+    code: number; // 分类 code
+    name: string; // 分类名称
+  }[];
+  selectedCode?: number;
+  articleCategoryError?: RequestError;
+  articleCategoryReload?: () => void;
+  onPressTag?: (code: number) => void;
+};
+
+export default function HomeArticlesView(props: Props) {
+  if (
+    props.articleCategoryError === undefined &&
+    props.articleCategory &&
+    props.articleCategory.length === 0
+  ) {
+    return null;
+  }
+
+  return (
+    <View style={styles.contanier}>
+      <View style={styles.content}>
+        <View style={styles.header}>
+          <Image
+            style={styles.background}
+            source={require('@app/assets/images/home/home_section_background.png')}
+          />
+          <View style={styles.titleView}>
+            <Text style={styles.title} numberOfLines={1}>
+              热门推荐
+            </Text>
+          </View>
+          {props.articleCategoryError &&
+            props.articleCategoryError.type !== 'Cancel' && (
+              <View style={styles.tagEmpty}>
+                <DataEmpty
+                  reload={() => {
+                    props.articleCategoryReload &&
+                      props.articleCategoryReload();
+                  }}
+                />
+              </View>
+            )}
+          {props.articleCategoryError === undefined && props.articleCategory ? (
+            <ScrollView
+              style={styles.tagsView}
+              horizontal={true}
+              scrollEnabled={true}
+            >
+              {props.articleCategory.map((articleCategoryItem, index) => {
+                return (
+                  <TouchableOpacity
+                    key={`${articleCategoryItem.code}-${index}`}
+                    onPress={() => {
+                      props.onPressTag &&
+                        props.onPressTag(articleCategoryItem.code);
+                    }}
+                    style={[
+                      styles.tagButton,
+                      // eslint-disable-next-line react-native/no-inline-styles
+                      {
+                        marginRight:
+                          props.articleCategory?.length === index - 1 ? 0 : 10,
+                        backgroundColor:
+                          articleCategoryItem.code === props.selectedCode
+                            ? '#DCEAFF'
+                            : '#FFFFFF',
+                        borderColor:
+                          articleCategoryItem.code === props.selectedCode
+                            ? '#B4D8FA'
+                            : '#EEEEEE',
+                      },
+                    ]}
+                  >
+                    <Text
+                      style={[
+                        styles.tagText,
+                        // eslint-disable-next-line react-native/no-inline-styles
+                        {
+                          color:
+                            articleCategoryItem.code === props.selectedCode
+                              ? '#1C90FE'
+                              : '#1F1F1F',
+                          fontWeight:
+                            articleCategoryItem.code === props.selectedCode
+                              ? '600'
+                              : '400',
+                        },
+                      ]}
+                      numberOfLines={1}
+                    >
+                      {articleCategoryItem.name}
+                    </Text>
+                  </TouchableOpacity>
+                );
+              })}
+            </ScrollView>
+          ) : null}
+        </View>
+      </View>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  contanier: {
+    backgroundColor: '#F3F4F5',
+  },
+  content: {
+    flex: 1,
+    marginTop: 6,
+    marginHorizontal: 12,
+    backgroundColor: '#FFFFFF',
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10,
+    paddingBottom: 10,
+  },
+  header: {
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10,
+    borderColor: '#FFFFFF',
+    borderWidth: 1,
+    alignItems: 'flex-start',
+  },
+  background: {
+    position: 'absolute',
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10,
+    width: '100%',
+    height: '100%',
+  },
+  titleView: {
+    marginTop: 8,
+    padding: 12,
+  },
+  title: {
+    color: '#001846',
+    fontSize: 16,
+    fontWeight: '700',
+    alignSelf: 'flex-start',
+  },
+  tagsView: {
+    width: '100%',
+    paddingHorizontal: 12,
+  },
+  tagButton: {
+    backgroundColor: '#DCEAFF',
+    borderRadius: 5,
+    paddingHorizontal: 10,
+    paddingVertical: 7,
+    borderColor: '#B4D8FA',
+    borderWidth: 0.5,
+    marginVertical: 4,
+  },
+  tagText: {
+    fontSize: 12,
+    color: '#1C90FE',
+    fontWeight: '600',
+  },
+  tagEmpty: { width: '100%' },
+});

+ 87 - 0
src/app/screens/home/home/components/HomeBulletin.tsx

@@ -0,0 +1,87 @@
+import React from 'react';
+import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
+import Carousel from 'react-native-reanimated-carousel';
+
+const PADDING_HORIZONTAL = 0;
+const LOGO_WIDTH = 23;
+const ARROW_WIDTH = 6;
+const CAROUSEL_MARGIN_HORIZONTAL = 6;
+
+type Props = {
+  width: number; // 视图的宽度
+  data: {content: string}[]; // 数据的数组
+  onPress?: (index: number) => void; // 点击事件
+};
+
+export default function HomeBulletin(props: Props) {
+  return (
+    <View style={styles.contanier}>
+      <Image
+        style={styles.image}
+        source={require('@app/assets/images/home/home_bulletin.png')}
+      />
+      <Carousel
+        style={styles.carousel}
+        vertical={true}
+        loop
+        width={
+          props.width -
+          2 * PADDING_HORIZONTAL -
+          LOGO_WIDTH -
+          ARROW_WIDTH -
+          2 * CAROUSEL_MARGIN_HORIZONTAL
+        }
+        height={24}
+        autoPlay={true}
+        data={props.data}
+        scrollAnimationDuration={2000}
+        renderItem={({index}) => {
+          return (
+            <TouchableOpacity
+              style={styles.item}
+              onPress={() => {
+                props.onPress && props.onPress(index);
+              }}>
+              <Text numberOfLines={1} style={styles.itemText}>
+                {props.data[index].content}
+              </Text>
+            </TouchableOpacity>
+          );
+        }}
+      />
+      <Image
+        style={styles.arrow}
+        source={require('@app/assets/images/common/common_arrow_right_6.png')}
+      />
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  contanier: {
+    paddingHorizontal: PADDING_HORIZONTAL,
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  image: {
+    width: LOGO_WIDTH,
+    height: 12.5,
+  },
+  carousel: {
+    flex: 1,
+    marginHorizontal: CAROUSEL_MARGIN_HORIZONTAL,
+  },
+  item: {
+    flex: 1,
+    justifyContent: 'center',
+  },
+  itemText: {
+    fontSize: 12,
+    color: '#666666',
+  },
+  arrow: {
+    width: ARROW_WIDTH,
+    height: 10,
+    tintColor: '#BCBCBC',
+  },
+});

+ 148 - 0
src/app/screens/home/home/components/HomeFeaturesView.tsx

@@ -0,0 +1,148 @@
+import React from 'react';
+import {
+  Image,
+  ImageSourcePropType,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native';
+import DataEmpty from '@common/components/DataEmpty';
+import { RequestError } from '@szyx-mobile/use-request';
+
+const ITEM_WIDTH = '33.33%'; // 图标宽度
+
+type Props = {
+  features?: { name: string; icon?: string; badge?: boolean }[];
+  error?: RequestError;
+  reload?: () => void;
+  onPress?: (index: number) => void;
+};
+
+export default function HomeFeaturesView(props: Props) {
+  return (
+    <View style={styles.contanier}>
+      <View style={styles.header}>
+        <Image
+          style={styles.background}
+          source={require('@app/assets/images/home/home_section_background.png')}
+        />
+        <Text style={styles.title} numberOfLines={1}>
+          精品服务
+        </Text>
+      </View>
+      {props.error && props.error.type !== 'Cancel' && (
+        <DataEmpty
+          reload={() => {
+            props.reload && props.reload();
+          }}
+        />
+      )}
+      {props.error === undefined && props.features && (
+        <View style={styles.items}>
+          {props.features.map((feature, index) => {
+            return (
+              <FeatureButtons
+                key={index}
+                title={feature.name}
+                image={{ uri: feature.icon }}
+                badge={feature.badge}
+                onPress={() => {
+                  props.onPress && props.onPress(index);
+                }}
+              />
+            );
+          })}
+        </View>
+      )}
+    </View>
+  );
+}
+
+function FeatureButtons(props: {
+  image: ImageSourcePropType;
+  title: string;
+  badge?: boolean;
+  onPress?: () => void;
+}) {
+  return (
+    <TouchableOpacity
+      style={styles.item}
+      onPress={() => {
+        props.onPress && props.onPress();
+      }}
+    >
+      <View>
+        <Image style={styles.itemImage} source={props.image} />
+        {props.badge === true && <View style={styles.badge} />}
+      </View>
+      <Text style={styles.itemText} numberOfLines={1}>
+        {props.title}
+      </Text>
+    </TouchableOpacity>
+  );
+}
+
+const styles = StyleSheet.create({
+  contanier: {
+    backgroundColor: '#F3F4F5',
+  },
+  content: {
+    flex: 1,
+    marginVertical: 6,
+    marginHorizontal: 12,
+    backgroundColor: '#FFFFFF',
+    borderRadius: 10,
+    paddingBottom: 14,
+  },
+  header: {
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10,
+    borderColor: '#FFFFFF',
+    borderWidth: 1,
+  },
+  background: {
+    position: 'absolute',
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10,
+    width: '100%',
+    height: '100%',
+  },
+  title: {
+    color: '#001846',
+    fontSize: 16,
+    fontWeight: '700',
+    margin: 12,
+    marginTop: 20,
+  },
+  items: {
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+  },
+  item: {
+    width: ITEM_WIDTH,
+    minWidth: 70,
+    paddingVertical: 10,
+    alignItems: 'center',
+    paddingHorizontal: 4,
+  },
+  itemImage: {
+    width: 50,
+    height: 50,
+    marginBottom: 2,
+  },
+  badge: {
+    position: 'absolute',
+    top: 0,
+    right: 0,
+    height: 9,
+    width: 9,
+    backgroundColor: '#FF6500',
+    borderRadius: 4.5,
+  },
+  itemText: {
+    color: '#11102C',
+    fontSize: 15,
+    fontWeight: '500',
+  },
+});

+ 181 - 0
src/app/screens/home/home/components/HomeTodoView.tsx

@@ -0,0 +1,181 @@
+import React from 'react';
+import {
+  Image,
+  ImageSourcePropType,
+  Platform,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native';
+
+type Props = {
+  yiwangqianNum?: string; // 医网签未处理数量
+  suifangNum?: string; // 随访未处理数量
+  hospitalNum?: string; // 云诊室未处理数量
+  onPressYiwangqian?: () => void; // 点击医网签
+  onPressSuifang?: () => void; // 点击随访中心
+  onPressHospital?: () => void; // 点击云诊室
+};
+
+/**
+ * @description: 首页顶部医生代办视图
+ */
+export default function HomeTodoView(props: Props) {
+  return (
+    <View style={styles.container}>
+      <Image
+        style={styles.arrow}
+        source={require('@app/assets/images/home/home_todo_arrow.png')}
+      />
+      <View style={styles.content}>
+        {/* <Text style={styles.title}>今日未处理</Text> */}
+
+        <View style={styles.items}>
+          <TodoItem
+            title="医网签"
+            text="待签数量"
+            number={props.yiwangqianNum ?? '0'}
+            backgroundImage={require('@app/assets/images/home/home_todo_yiwangqian.png')}
+            onPress={() => {
+              props.onPressYiwangqian && props.onPressYiwangqian();
+            }}
+          />
+          <TodoItem
+            title="随访中心"
+            text="待入组人数"
+            number={props.suifangNum ?? '0'}
+            backgroundImage={require('@app/assets/images/home/home_todo_suifang.png')}
+            onPress={() => {
+              props.onPressSuifang && props.onPressSuifang();
+            }}
+          />
+          <TodoItem
+            title="云诊室"
+            text="待接诊人数"
+            number={props.hospitalNum ?? '0'}
+            backgroundImage={require('@app/assets/images/home/home_todo_hospital.png')}
+            onPress={() => {
+              props.onPressHospital && props.onPressHospital();
+            }}
+          />
+        </View>
+      </View>
+    </View>
+  );
+}
+
+function TodoItem(props: {
+  title: string; // 标题
+  text: string; // 中间文字
+  number: string; // 底部数字
+  backgroundImage: ImageSourcePropType; // 背景图片
+  onPress?: () => void; // 点击
+}) {
+  return (
+    <TouchableOpacity
+      style={styles.item}
+      onPress={() => {
+        props.onPress && props.onPress();
+      }}
+    >
+      <View style={styles.itemContent}>
+        <Image style={styles.itemBackground} source={props.backgroundImage} />
+        <View style={styles.itemTop}>
+          <View style={styles.itemTopBadge} />
+          <Text style={styles.itemTopText} numberOfLines={1}>
+            {props.title}
+          </Text>
+        </View>
+        <Text style={styles.itemNumber} numberOfLines={1}>
+          {props.number}
+        </Text>
+        <Text numberOfLines={1} style={styles.itemText}>
+          {props.text}
+        </Text>
+      </View>
+    </TouchableOpacity>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    marginTop: 10,
+  },
+  arrow: {
+    width: 69,
+    height: 12,
+    marginLeft: 12,
+    tintColor: '#F8FDFF',
+  },
+  content: {
+    padding: 6,
+    backgroundColor: '#F8FDFF',
+    borderTopLeftRadius: 12,
+    borderTopRightRadius: 12,
+  },
+  title: {
+    marginHorizontal: 6,
+    marginTop: 12,
+    marginBottom: 9,
+    fontSize: 16,
+    color: '#666666',
+    fontWeight: '700',
+  },
+  items: {
+    flexDirection: 'row',
+    padding: 3,
+  },
+  item: {
+    flex: 1,
+    margin: 3,
+    backgroundColor: '#FFFFFF',
+    borderRadius: 8,
+  },
+  itemContent: {
+    flex: 1,
+    borderRadius: 7,
+    justifyContent: 'center',
+    padding: 12,
+  },
+  itemBackground: {
+    position: 'absolute',
+    width: 58,
+    height: 58,
+    right: -7,
+    bottom: -7,
+  },
+  itemTop: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  itemTopBadge: {
+    backgroundColor: '#1D91FF',
+    height: 10,
+    width: 3,
+    borderRadius: 1.5,
+    marginRight: 4,
+  },
+  itemTopText: {
+    fontSize: 16,
+    fontWeight: '600',
+    color: '#000000',
+  },
+  itemText: {
+    fontSize: 14,
+    color: '#999999',
+    marginLeft: 7,
+  },
+  itemNumber: {
+    color: '#000000',
+    fontWeight: '700',
+    fontSize: 24,
+    fontFamily: Platform.select({
+      android: 'DINCondensedBold',
+      ios: 'DIN Condensed',
+    }),
+    marginTop: 16,
+    marginBottom: 3,
+    marginLeft: 7,
+  },
+});

+ 155 - 0
src/app/screens/home/home/components/HomeTopView.tsx

@@ -0,0 +1,155 @@
+import React, {memo} from 'react';
+import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
+
+const GREETINGSTRINGS = [
+  '不断进步,超越自己!',
+  '自信自强,砥砺前行!',
+  '积极向上,充满信心!',
+  '努力追求,终达顶峰!',
+  '自信满满,开启新篇!',
+  '挥洒汗水,拥抱成功!',
+  '勇敢追求,梦想必达!',
+  '须及春光,力追朝阳!',
+  '笃定自信,无畏前行!',
+  '积极进取,永不放弃!',
+];
+
+/**
+ * @description: 获取打招呼字符串
+ */
+function getGreeting(): string {
+  const currentHour = new Date().getHours();
+  if (currentHour >= 0 && currentHour < 5) {
+    return '凌晨好';
+  } else if (currentHour >= 5 && currentHour < 8) {
+    return '早上好';
+  } else if (currentHour >= 8 && currentHour < 12) {
+    return '上午好';
+  } else if (currentHour >= 12 && currentHour < 19) {
+    return '下午好';
+  } else {
+    return '晚上好';
+  }
+}
+
+/**
+ * @description: 从字符串数组中获取随机一个字符串
+ */
+function getRandomStringFromArray(strings: string[]): string {
+  const randomIndex = Math.floor(Math.random() * strings.length);
+  return strings[randomIndex];
+}
+
+type Props = {
+  nickname?: string; // 昵称
+  isVerified?: boolean; // 是否已实名
+  picUrl?: string; // 头像
+  onPress?: () => void; // 点击实名那里的按钮
+};
+
+/**
+ * @description: 首页顶部医生信息的视图
+ */
+function HomeTopView(props: Props) {
+  return (
+    <View style={styles.top}>
+      <TouchableOpacity
+        style={styles.topLeft}
+        onPress={() => {
+          props.onPress && props.onPress();
+        }}>
+        <View style={styles.avatar}>
+          <Image
+            style={styles.avatarImage}
+            source={
+              props.picUrl && props.picUrl.length > 0
+                ? {uri: props.picUrl}
+                : require('@app/assets/images/mine/mine_avatar.png')
+            }
+          />
+          {props.isVerified !== undefined && (
+            <View style={styles.avatarBadgeTouch}>
+              <Image
+                style={styles.avatarBadge}
+                source={
+                  props.isVerified
+                    ? require('@app/assets/images/common/common_name_authenticated.png')
+                    : require('@app/assets/images/common/common_name_unauthenticated.png')
+                }
+              />
+            </View>
+          )}
+        </View>
+      </TouchableOpacity>
+      <MemoizedHomeTopRightView nickname={props.nickname} />
+    </View>
+  );
+}
+
+export default memo(HomeTopView);
+
+/**
+ * @description: 右边文字视图
+ */
+function HomeTopRightView(props: {nickname?: string}) {
+  return (
+    <View style={styles.topRight}>
+      <Text style={styles.topTitleText} numberOfLines={1}>
+        {props.nickname ? `${props.nickname},${getGreeting()}` : '---'}
+      </Text>
+      <Text style={styles.topRightText} numberOfLines={2}>
+        {props.nickname ? `${getRandomStringFromArray(GREETINGSTRINGS)}` : ''}
+      </Text>
+    </View>
+  );
+}
+
+const MemoizedHomeTopRightView = memo(HomeTopRightView);
+
+const styles = StyleSheet.create({
+  top: {
+    height: 90,
+    flexDirection: 'row',
+  },
+  topLeft: {
+    justifyContent: 'center',
+  },
+  avatar: {
+    width: 73,
+    height: 73,
+    borderRadius: 37,
+    backgroundColor: '#FFFFFF',
+    marginHorizontal: 12,
+    alignItems: 'center',
+  },
+  avatarImage: {
+    width: 73,
+    height: 73,
+    borderRadius: 73,
+    borderWidth: 2,
+    borderColor: '#FFF5E9',
+  },
+  avatarBadgeTouch: {
+    width: 63,
+    height: 18,
+    top: -8,
+  },
+  avatarBadge: {
+    width: 63,
+    height: 18,
+  },
+  topRight: {
+    flex: 1,
+    marginRight: 12,
+    justifyContent: 'center',
+  },
+  topTitleText: {
+    fontSize: 19,
+    color: '#17171A',
+    fontWeight: '600',
+  },
+  topRightText: {
+    color: '#17171A',
+    marginTop: 14,
+  },
+});

+ 49 - 0
src/app/screens/home/home/hooks/useHandleClickBannerListItem.ts

@@ -0,0 +1,49 @@
+import {useCallback} from 'react';
+import {Banner, Feature} from '@app/screens/api';
+import {NavigationProp, useNavigation} from '@react-navigation/native';
+import {MainParamList} from '@app/routes/MainParamList';
+import { showErrorMessage } from '@common/ToastHelper.ts';
+
+const useHandleClickBannerListItem = (
+  handleClickFeatureListItem: (feature: Feature) => void,
+) => {
+  const navigation = useNavigation<NavigationProp<MainParamList>>();
+  // 点击轮播图
+  const handleClickBannerListItem = useCallback(
+    (banner: Banner) => {
+      // TODO: 处理 banner 版本是否低于手机app版本?
+      if (banner.jumpType === 'noSkip') {
+        return;
+      }
+      if (banner.jumpType === 'link') {
+        if (banner.skipUrl === undefined) {
+          showErrorMessage('链接配置错误,url没有配置');
+          return;
+        }
+        return;
+      }
+      if (banner.jumpType === 'activity') {
+        if (banner.activityInfo) {
+        }
+        return;
+      }
+      if (banner.jumpType === 'miniApp') {
+        if (banner.miniAppInfo) {
+          handleClickFeatureListItem(banner.miniAppInfo);
+        }
+        return;
+      }
+      if (banner.jumpType === 'hotNews') {
+        if (banner.articleId) {
+          navigation.navigate('ArticleDetail', {id: banner.articleId});
+        }
+        return;
+      }
+    },
+    [handleClickFeatureListItem, navigation],
+  );
+
+  return handleClickBannerListItem;
+};
+
+export {useHandleClickBannerListItem};

+ 61 - 0
src/app/screens/home/home/hooks/useHandleClickFeatureListItem.tsx

@@ -0,0 +1,61 @@
+import React, { useCallback } from 'react';
+import { StyleSheet, Text } from 'react-native';
+import { Feature, useSubServerCheck } from '../api';
+import Toast from 'react-native-toast-message';
+import Alert from '@common/components/Alert.tsx';
+import { useAuth } from '@common/contexts/useAuth.ts';
+
+// 点击精品模块
+const useHandleClickFeatureListItem = () => {
+  const { loading: subServerCheckLoading, fetchAsync: subServerCheckFetch } =
+    useSubServerCheck(); // 检查精品服务功能块是否可以进
+
+  const {
+    state: { userInfo },
+  } = useAuth();
+
+  // 点击精品服务功能块
+  const handleClickFeatureListItem = useCallback(
+    (feature: Feature) => {
+      // 只有实名可以进入
+      if (userInfo?.realNameStatus !== 'success') {
+        // 未实名的需要实名
+        Alert.show(
+          '立即实名认证',
+          <Text style={styles.alertText}>
+            {'您还未实名认证,请认证后再操作~'}
+          </Text>,
+          {
+            action: () => {},
+          },
+          {},
+        );
+        return;
+      }
+
+      subServerCheckFetch({
+        data: {
+          nsId: feature.uniqueId,
+        },
+      })
+        .then(r => {
+          console.log('厂商信息', r);
+        })
+        .catch(e => {
+          Toast.show(e.message);
+        });
+    },
+    [subServerCheckFetch, userInfo],
+  );
+  return { subServerCheckLoading, handleClickFeatureListItem };
+};
+
+export { useHandleClickFeatureListItem };
+
+const styles = StyleSheet.create({
+  alertText: {
+    textAlign: 'center',
+    color: '#999999',
+    fontSize: 12,
+  },
+});

+ 80 - 0
src/app/screens/home/home/hooks/useHandleUnreadRemind.ts

@@ -0,0 +1,80 @@
+import { useCallback, useEffect, useState } from 'react';
+import { useUnreadRemind } from '../api';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { DeviceEventEmitter } from 'react-native';
+import {
+  PERSONAL_NOTIFICATION,
+  PLATFORM_NOTIFICATION,
+} from '@common/constants';
+import { showErrorMessage } from '@common/ToastHelper.ts';
+
+const useHandleUnreadRemind = () => {
+  const [unread, setUnread] = useState(false);
+  const { loading: unreadRemindLoading, fetchAsync: unreadRemindFetch } =
+    useUnreadRemind();
+
+  const handleUnreadRemindFetch = useCallback(() => {
+    // 注意:这里取旧模块存储的最后已读时间,后续重构可能会删除或修改此处逻辑
+    AsyncStorage.getItem('readNoticeLastTime')
+      .then(value => {
+        const parsedValue = JSON.parse(value ?? '{}');
+        let time = parsedValue?.readNoticeLastTime;
+        if (time === null || time === undefined) {
+          time = '2000-01-01 00:00:00';
+        }
+        unreadRemindFetch({
+          data: {
+            timeStamp: time,
+          },
+        })
+          .then(r => {
+            setUnread(r.unread);
+          })
+          .catch(e => {
+            showErrorMessage(e.message);
+          });
+      })
+      .catch(e => {
+        showErrorMessage(e.message);
+      });
+  }, [unreadRemindFetch]);
+
+  // 【监听im】
+  // 监听系统公告
+  useEffect(() => {
+    const listener = DeviceEventEmitter.addListener(
+      PLATFORM_NOTIFICATION,
+      () => {
+        // 刷新铃铛接口
+        handleUnreadRemindFetch();
+      },
+    );
+
+    return () => {
+      listener.remove();
+    };
+  }, [handleUnreadRemindFetch]);
+
+  // 监听站内信
+  useEffect(() => {
+    const listener = DeviceEventEmitter.addListener(
+      PERSONAL_NOTIFICATION,
+      () => {
+        // 刷新铃铛接口
+        handleUnreadRemindFetch();
+      },
+    );
+
+    return () => {
+      listener.remove();
+    };
+  }, [handleUnreadRemindFetch]);
+
+  return {
+    unread,
+    unreadRemindLoading,
+    handleUnreadRemindFetch,
+  };
+};
+
+export { useHandleUnreadRemind };

+ 147 - 0
src/app/screens/mine/contactSupport/ContactSupportScreen.tsx

@@ -0,0 +1,147 @@
+import React, { useRef } from 'react';
+import {
+  Image,
+  ImageBackground,
+  Pressable,
+  StyleSheet,
+  Text,
+  View,
+} from 'react-native';
+import { StackScreenProps } from '@react-navigation/stack';
+import { MainParamList } from '@app/routes/MainParamList';
+import SubmitButton from '@common/components/SubmitButton';
+import { useCustomrConfig } from './api';
+import Spinner from '@common/components/Spinner';
+import DataEmpty from '@common/components/DataEmpty';
+import ViewShot from 'react-native-view-shot';
+
+type Props = StackScreenProps<MainParamList, 'ContactSupport'>;
+
+export default function ContactSupportScreen(props: Props) {
+  const {navigation} = props;
+
+  const shotRef = useRef<ViewShot>(null); // 保存到本地的视图的引用
+
+  // 【接口】
+  const {response, error, loading, fetch} = useCustomrConfig();
+
+  return (
+    <View style={styles.container}>
+      <Pressable
+        style={[StyleSheet.absoluteFill, styles.background]}
+        onPress={() => {
+          navigation.pop();
+        }}
+      />
+      <View style={styles.content}>
+        <ViewShot
+          ref={shotRef}
+          options={{
+            fileName: 'qrcode',
+            format: 'png',
+            quality: 1,
+          }}>
+          <ImageBackground
+            style={styles.imageBackground}
+            source={require('@app/assets/images/mine/mine_contact_support_background.png')}>
+            <View style={styles.header}>
+              <Image
+                style={styles.headerImage}
+                source={require('@app/assets/images/mine/mine_contact_support.png')}
+              />
+              <Text style={styles.headerTitle} numberOfLines={1}>
+                医网信小助手
+              </Text>
+            </View>
+            <Text style={styles.headerText} numberOfLines={1}>
+              服务时间:9:00-18:00
+            </Text>
+            <View style={styles.qrCodeView}>
+              {loading === true ? (
+                <Spinner />
+              ) : error !== undefined ? (
+                <DataEmpty
+                  reload={() => {
+                    fetch();
+                  }}
+                />
+              ) : response?.imgUrl ? (
+                <Image
+                  style={styles.qrCodeImage}
+                  source={{uri: response.imgUrl}}
+                />
+              ) : null}
+            </View>
+          </ImageBackground>
+        </ViewShot>
+        <SubmitButton
+          title="保存到相册"
+          theme="dark"
+          style={styles.submit}
+          onPress={() => {
+          }}
+        />
+      </View>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  background: {
+    backgroundColor: 'rgba(0, 0, 0, 0.7)',
+  },
+  content: {
+    width: '70%',
+  },
+  imageBackground: {
+    width: '100%',
+    aspectRatio: 260 / 357,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginLeft: 12,
+    marginRight: 18, // 背景图不是中心对称,内容需要向左偏移一些
+  },
+  headerImage: {
+    width: 17,
+    height: 17,
+    marginRight: 6,
+  },
+  headerTitle: {
+    color: '#1D91FF',
+    fontSize: 16,
+    fontWeight: '600',
+  },
+  headerText: {
+    color: '#999999',
+    fontSize: 12,
+    marginVertical: 12,
+    marginLeft: 12,
+    marginRight: 18, // 背景图不是中心对称,内容需要向左偏移一些
+  },
+  qrCodeView: {
+    aspectRatio: 1,
+    width: '80%',
+    borderRadius: 5,
+    borderColor: 'rgba(1, 137, 255, 0.14)',
+    borderWidth: 4.5,
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginRight: 6, // 背景图不是中心对称,内容需要向左偏移一些
+  },
+  qrCodeImage: {
+    width: '90%',
+    height: '90%',
+  },
+  submit: {
+    marginTop: 24,
+  },
+});

+ 1 - 0
src/app/screens/mine/contactSupport/api/index.ts

@@ -0,0 +1 @@
+export * from './useCustomrConfig';

+ 18 - 0
src/app/screens/mine/contactSupport/api/useCustomrConfig.ts

@@ -0,0 +1,18 @@
+import {useApi} from '@common/api/useApi.ts';
+import {z} from 'zod';
+
+// 客服信息
+// {"collection": "customer_config_info", "id": "{\"$oid\":\"66050aa9986acc61cd268da4\"}", "imgUrl": "https://tms-dev.oss-cn-beijing.aliyuncs.com/banner/headimg_40fa54e416eb439faf4dd95a2f4a9948.png", "phones": "18312341234,18012312332", "wx": "wx-123456633"}
+const useCustomrConfig = () => {
+  return useApi(
+    '/am/v1/feedback/getCustomerConfig',
+    'GET',
+    undefined,
+    z.object({
+      imgUrl: z.string().url(), // 二维码图片
+    }),
+    {automatic: true, loadingDelay: 500},
+  );
+};
+
+export {useCustomrConfig};

+ 400 - 0
src/app/screens/mine/mine/MineScreen.tsx

@@ -0,0 +1,400 @@
+import React, {
+  useCallback,
+  useEffect,
+  useLayoutEffect,
+  useRef,
+  useState,
+} from 'react';
+import {
+  Image,
+  NativeEventEmitter,
+  NativeModules,
+  RefreshControl,
+  SafeAreaView,
+  ScrollView,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native';
+import { CompositeScreenProps, useFocusEffect } from '@react-navigation/native';
+import { StackScreenProps } from '@react-navigation/stack';
+import { useHeaderHeight } from '@react-navigation/elements';
+import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
+import { MainParamList, MainTabParamList } from '@app/routes/MainParamList';
+import LinearGradient from 'react-native-linear-gradient';
+import FeatureButton from './components/FeatureButton';
+import { useAuth } from '@common/contexts/useAuth';
+import BottomSheet from '@common/components/BottomSheet';
+import useUpdateRef from '@app/hooks/useUpdateRef';
+import Spinner from '@common/components/Spinner';
+import { useHandleUnreadRemind } from '@app/screens/home/home/hooks/useHandleUnreadRemind';
+import { debounce } from '@common/utils/commonUtils';
+
+type Props = CompositeScreenProps<
+  BottomTabScreenProps<MainTabParamList, 'Mine'>,
+  StackScreenProps<MainParamList, 'MainTab'>
+>;
+
+export default function HomeScreen(props: Props) {
+  const { navigation } = props;
+  const headerHeight = useHeaderHeight();
+
+  const [adKey, setAdKey] = useState<string | undefined>(undefined);
+
+  useLayoutEffect(() => {
+    navigation.setOptions({
+      title: '我的',
+      headerTransparent: true,
+    });
+
+    const pushListener = new NativeEventEmitter(
+      NativeModules.innerModule,
+    ).addListener('adViewRefresh', key => {
+      if (key && key === 'MineScreen') {
+        console.log('*********************show', key);
+        setAdKey(new Date().getTime().toString());
+      }
+    });
+    return () => {
+      pushListener.remove();
+    };
+  }, [navigation]);
+
+  useFocusEffect(
+    useCallback(() => {
+      if (!adKey) {
+        setAdKey(new Date().getTime().toString());
+      }
+    }, [adKey]),
+  );
+
+  // 【引用】
+  const scrollRef = useRef<ScrollView>(null);
+
+  const {
+    state: { userInfo },
+    actions: { update },
+  } = useAuth();
+  const userInfoRef = useUpdateRef(userInfo);
+
+  const { unread, unreadRemindLoading, handleUnreadRemindFetch } =
+    useHandleUnreadRemind(); // 处理未读消息相关
+
+  useEffect(() => {
+    handleUnreadRemindFetch();
+  }, [handleUnreadRemindFetch]);
+
+  // 下拉刷新
+  const handleRefresh = useCallback(() => {
+    handleUnreadRemindFetch();
+  }, [handleUnreadRemindFetch]);
+
+  return (
+    <View style={styles.container}>
+      {/* 顶部背景 */}
+      <View style={styles.topBackground}>
+        <Image
+          style={styles.backgroundImage}
+          source={require('@app/assets/images/mine/mine_background.png')}
+        />
+      </View>
+      {/* 底部背景 */}
+      <View style={styles.bottomBackground}>
+        <Image
+          style={styles.bottomBackgroundImage}
+          source={require('@app/assets/images/mine/mine_background_logo.png')}
+        />
+        <View style={styles.bottomBackgroundTextView}>
+          <LinearGradient
+            style={styles.bottomBackgroundLine}
+            start={{ x: 0, y: 0.5 }}
+            end={{ x: 1, y: 0.5 }}
+            colors={['rgba(220, 220, 220, 0.45)', 'rgba(187, 187, 187, 0.45)']}
+          />
+          <Text style={styles.bottomBackgroundText} numberOfLines={1}>
+            名医首选互联网职业应用产品
+          </Text>
+          <LinearGradient
+            style={styles.bottomBackgroundLine}
+            start={{ x: 0, y: 0.5 }}
+            end={{ x: 1, y: 0.5 }}
+            colors={['rgba(187, 187, 187, 0.45)', 'rgba(220, 220, 220, 0.45)']}
+          />
+        </View>
+      </View>
+      {/* 主内容 */}
+      <ScrollView
+        ref={scrollRef}
+        style={styles.scroll}
+        scrollIndicatorInsets={{ right: 1 }}
+        showsVerticalScrollIndicator={false}
+        scrollEventThrottle={16}
+        refreshControl={
+          <RefreshControl refreshing={false} onRefresh={handleRefresh} />
+        }
+      >
+        {/* 顶部医生信息区域 */}
+        <View style={{ height: headerHeight }} />
+        <SafeAreaView>
+          <TouchableOpacity
+            style={styles.header}
+            onPress={() => {
+              debounce(() => {});
+            }}
+          >
+            <View style={styles.headerLeft}>
+              <View style={styles.avatar}>
+                <Image
+                  style={styles.avatarImage}
+                  source={
+                    userInfo?.picUrl && userInfo.picUrl.length > 0
+                      ? { uri: userInfo.picUrl }
+                      : require('@app/assets/images/mine/mine_avatar.png')
+                  }
+                />
+                <View style={styles.avatarBadgeTouch}>
+                  <Image
+                    style={styles.avatarBadge}
+                    source={
+                      userInfo?.realNameStatus === 'success'
+                        ? require('@app/assets/images/common/common_name_authenticated.png')
+                        : require('@app/assets/images/common/common_name_unauthenticated.png')
+                    }
+                  />
+                </View>
+              </View>
+            </View>
+            <View style={styles.headerRight}>
+              <View style={styles.headerTitle}>
+                <Text style={styles.headerTitleText} numberOfLines={1}>
+                  {`${userInfo?.nickname ?? ''}医生`}
+                </Text>
+                <Image
+                  style={styles.headerTitleArrow}
+                  source={require('@app/assets/images/common/common_arrow_right_6.png')}
+                />
+              </View>
+              <Text style={styles.headerRightText} numberOfLines={1}>
+                {userInfo?.phone
+                  ? `${userInfo?.phone.replace(
+                      /(\d{3})\d{4}(\d{4})/,
+                      '$1****$2',
+                    )}`
+                  : ''}
+              </Text>
+            </View>
+          </TouchableOpacity>
+          {/* 常用功能区域 */}
+          <View style={styles.middle}>
+            <Text style={styles.middleTitle} numberOfLines={1}>
+              常用功能
+            </Text>
+            <View style={styles.middleContent}>
+              <FeatureButton
+                title="我的医院"
+                image={require('@app/assets/images/mine/mine_feature_hospitals.png')}
+                onPress={() => {
+                  debounce(() => {});
+                }}
+              />
+              <FeatureButton
+                title="服务权益"
+                image={require('@app/assets/images/mine/mine_feature_privilege.png')}
+                onPress={() => {
+                  debounce(() => {});
+                }}
+              />
+              <FeatureButton
+                title="密码重置"
+                image={require('@app/assets/images/mine/mine_feature_password.png')}
+                onPress={() => {
+                  if (userInfo === undefined) {
+                    return;
+                  }
+                  debounce(() => {});
+                }}
+              />
+              {/* 只有用户包含冠新才有切换按钮 */}
+              {userInfo && userInfo.userType !== 'general' && (
+                <FeatureButton
+                  title="身份切换"
+                  image={require('@app/assets/images/mine/mine_feature_role.png')}
+                  onPress={() => {
+                    BottomSheet.show(
+                      [
+                        { id: 0, text: '医网信(当前)' },
+                        { id: 1, text: '国家医疗服务数据中心用户专版' },
+                      ],
+                      undefined,
+                      item => {
+                        if (userInfoRef.current === undefined) {
+                          return;
+                        }
+                        if (item.id === 1) {
+                          // 更新当前角色
+                          update({
+                            ...userInfoRef.current,
+                            currentUserType: 'guanxin',
+                          });
+                        }
+                      },
+                    );
+                  }}
+                />
+              )}
+              <FeatureButton
+                title="系统设置"
+                image={require('@app/assets/images/mine/mine_feature_settings.png')}
+                onPress={() => {
+                  debounce(() => {});
+                }}
+              />
+              <FeatureButton
+                title="咨询客服"
+                image={require('@app/assets/images/mine/mine_feature_service.png')}
+                onPress={() => {
+                  navigation.navigate('ContactSupport');
+                }}
+              />
+              <FeatureButton
+                title="消息通知"
+                image={require('@app/assets/images/mine/mine_feature_notifications.png')}
+                badge={unread}
+                onPress={() => {
+                  debounce(() => {});
+                }}
+              />
+            </View>
+          </View>
+        </SafeAreaView>
+      </ScrollView>
+      {unreadRemindLoading && <Spinner />}
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#F3F4F5',
+  },
+  topBackground: {
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    height: 260,
+  },
+  backgroundImage: {
+    flex: 1,
+    width: '100%',
+  },
+  bottomBackground: {
+    position: 'absolute',
+    bottom: 35,
+    left: 0,
+    right: 0,
+    alignItems: 'center',
+  },
+  bottomBackgroundImage: {
+    marginVertical: 8,
+  },
+  bottomBackgroundTextView: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  bottomBackgroundLine: {
+    height: 1,
+    width: 36,
+    borderRadius: 0.5,
+  },
+  bottomBackgroundText: {
+    color: 'rgba(133, 133, 133, 0.45)',
+    fontSize: 12,
+    marginHorizontal: 5,
+  },
+  scroll: {
+    flex: 1,
+  },
+  header: {
+    height: 90,
+    flexDirection: 'row',
+  },
+  headerLeft: {
+    justifyContent: 'center',
+  },
+  avatar: {
+    width: 73,
+    height: 73,
+    borderRadius: 37,
+    backgroundColor: '#FFFFFF',
+    marginHorizontal: 12,
+    alignItems: 'center',
+  },
+  avatarImage: {
+    width: 73,
+    height: 73,
+    borderRadius: 73,
+    borderWidth: 2,
+    borderColor: '#FFF5E9',
+  },
+  avatarBadgeTouch: {
+    width: 63,
+    height: 18,
+    top: -8,
+  },
+  avatarBadge: {
+    width: 63,
+    height: 18,
+  },
+  headerRight: {
+    flex: 1,
+    marginRight: 12,
+    justifyContent: 'center',
+  },
+  headerTitle: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  headerTitleText: {
+    flexShrink: 1,
+    fontSize: 19,
+    color: '#17171A',
+    fontWeight: '600',
+  },
+  headerTitleArrow: {
+    width: 6,
+    height: 10,
+    tintColor: '#17171A',
+    marginLeft: 8,
+  },
+  headerRightText: {
+    color: '#17171A',
+    marginTop: 14,
+  },
+  middle: {
+    margin: 12,
+    padding: 12,
+    backgroundColor: '#FFFFFF',
+    borderRadius: 10,
+  },
+  middleTitle: {
+    alignSelf: 'flex-start',
+    color: '#001846',
+    fontSize: 16,
+    fontWeight: '600',
+    marginBottom: 12,
+    marginTop: 4,
+  },
+  middleContent: {
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+    marginBottom: 10,
+  },
+  carousel: {
+    // marginHorizontal: PADDING_HORIZONTAL,
+    height: 120,
+    borderRadius: 6,
+  },
+});

+ 82 - 0
src/app/screens/mine/mine/components/FeatureButton.tsx

@@ -0,0 +1,82 @@
+import React from 'react';
+import {
+  Image,
+  ImageSourcePropType,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native';
+
+const ITEM_WIDTH = '25%'; // 图标宽度
+
+type Props = {
+  image: ImageSourcePropType;
+  title: string;
+  badge?: boolean;
+  onPress?: () => void;
+};
+
+export default function FeatureButton(props: Props) {
+  if (props.title === '咨询客服') {
+    return (
+      <TouchableOpacity
+        style={styles.container}
+        onPress={() => {
+          props.onPress && props.onPress();
+        }}
+      >
+        <View>
+          <Image style={styles.image} source={props.image} />
+          <Text style={styles.text} numberOfLines={1}>
+            {props.title}
+          </Text>
+        </View>
+      </TouchableOpacity>
+    );
+  }
+  return (
+    <TouchableOpacity
+      style={styles.container}
+      onPress={() => {
+        props.onPress && props.onPress();
+      }}
+    >
+      <View>
+        <Image style={styles.image} source={props.image} />
+        {props.badge === true && <View style={styles.badge} />}
+      </View>
+      <Text style={styles.text} numberOfLines={1}>
+        {props.title}
+      </Text>
+    </TouchableOpacity>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    width: ITEM_WIDTH,
+    minWidth: 50,
+    paddingVertical: 12,
+    alignItems: 'center',
+    paddingHorizontal: 4,
+  },
+  image: {
+    width: 24,
+    height: 24,
+    marginBottom: 6,
+  },
+  text: {
+    color: '#130700',
+    fontSize: 12,
+  },
+  badge: {
+    position: 'absolute',
+    width: 6,
+    height: 6,
+    backgroundColor: '#FF6500',
+    borderRadius: 3,
+    top: 0,
+    right: 0,
+  },
+});

+ 0 - 0
src/app/assets/images/common/common_list_empty.png → src/common/assets/images/common_list_empty.png


+ 0 - 0
src/app/assets/images/common/common_list_empty@2x.png → src/common/assets/images/common_list_empty@2x.png


+ 0 - 0
src/app/assets/images/common/common_list_empty@3x.png → src/common/assets/images/common_list_empty@3x.png


+ 5 - 1
src/common/common.ts

@@ -12,12 +12,16 @@ import 'react-native-device-info';
 // 应用间路由工具
 import '@common/NavigationHelper';
 import '@common/UpdateHelper.ts';
-// 弹出相关
+// 自定义组件
 import '@common/ToastHelper.ts';
 import '@common/components/Alert.tsx';
 import '@common/components/BottomSheet.tsx';
 import '@common/components/HeaderBackImage.tsx';
 import '@common/components/Spinner.tsx';
+import '@common/components/Separator.tsx';
+import '@common/components/DataEmpty.tsx';
+import '@common/components/ListEmpty.tsx';
+import '@common/components/TabBarIcon.tsx';
 // 工具
 import '@common/utils/commonUtils.ts';
 import '@common/utils/md5';

+ 58 - 0
src/common/components/DataEmpty.tsx

@@ -0,0 +1,58 @@
+import React, { JSX } from 'react';
+import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+type Props = {
+  title?: string;
+  info?: string;
+  reload: () => void;
+};
+
+export default function DataEmpty(props: Props): JSX.Element {
+  return (
+    <View style={styles.container}>
+      <Text style={styles.title}>{props.title ?? '网络加载失败'}</Text>
+      {props.info && <Text style={styles.info}>{props.info}</Text>}
+      <TouchableOpacity
+        style={styles.button}
+        onPress={() => {
+          props.reload();
+        }}
+      >
+        <Text style={styles.buttonText}>重新加载</Text>
+      </TouchableOpacity>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  title: {
+    textAlign: 'center',
+    color: '#4E4E4E',
+    fontSize: 15,
+  },
+  info: {
+    textAlign: 'center',
+    color: '#6B6B6B',
+    fontSize: 14,
+    marginVertical: 8,
+  },
+  button: {
+    width: 100,
+    height: 40,
+    borderWidth: 1,
+    borderColor: '#A9A9A9',
+    borderRadius: 8,
+    alignItems: 'center',
+    justifyContent: 'center',
+    margin: 28,
+  },
+  buttonText: {
+    color: '#2B2B2B',
+    fontSize: 14,
+  },
+});

+ 40 - 0
src/common/components/ListEmpty.tsx

@@ -0,0 +1,40 @@
+import React, { JSX } from 'react';
+import { Image, StyleSheet, Text, View } from 'react-native';
+
+type Props = {
+  text?: string;
+};
+
+export default function ListEmpty(props: Props): JSX.Element {
+  return (
+    <View style={styles.container}>
+      <Image
+        style={styles.image}
+        resizeMode="contain"
+        source={require('@common/assets/images/common_list_empty.png')}
+      />
+      <Text style={styles.text}>
+        {props.text && props.text.length > 0 ? props.text : '暂无数据'}
+      </Text>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginTop: 50,
+    marginBottom: 20,
+  },
+  image: {
+    width: 150,
+    height: 102,
+  },
+  text: {
+    textAlign: 'center',
+    color: '#6B6B75',
+    fontSize: 14,
+    marginTop: 15,
+  },
+});

+ 18 - 0
src/common/components/Separator.tsx

@@ -0,0 +1,18 @@
+import React from 'react';
+import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native';
+
+type Props = {
+  style?: StyleProp<ViewStyle>;
+};
+
+export default function Separator(props: Props) {
+  return <View style={[styles.separator, props.style]} />;
+}
+
+const styles = StyleSheet.create({
+  separator: {
+    backgroundColor: '#F6F8FB',
+    marginHorizontal: 12,
+    height: 0.5,
+  },
+});

+ 60 - 0
src/common/components/TabBarIcon.tsx

@@ -0,0 +1,60 @@
+import React from 'react';
+import {Image, ImageSourcePropType, StyleSheet, View} from 'react-native';
+import {TAB_BAR_ICON_SIZE} from '@common/constants';
+
+type Props = {
+  focused: boolean;
+  size: number;
+  activeImage: ImageSourcePropType;
+  inactiveImage: ImageSourcePropType;
+  activeTintColor?: string;
+  inactiveTintColor: string;
+  badge?: boolean;
+};
+
+export default function TabBarIcon(props: Props) {
+  return (
+    <View
+      style={[
+        styles.container,
+        {
+          width: props.size,
+          height: props.size,
+        },
+      ]}>
+      <Image
+        source={props.focused ? props.activeImage : props.inactiveImage}
+        style={
+          props.focused
+            ? {
+                height: TAB_BAR_ICON_SIZE,
+                width: TAB_BAR_ICON_SIZE,
+                tintColor: props.activeTintColor,
+              }
+            : {
+                height: TAB_BAR_ICON_SIZE,
+                width: TAB_BAR_ICON_SIZE,
+                tintColor: props.inactiveTintColor,
+              }
+        }
+      />
+      {props.badge === true && <View style={styles.badge} />}
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    alignItems: 'center',
+    justifyContent: 'flex-end',
+  },
+  badge: {
+    position: 'absolute',
+    width: 9,
+    height: 9,
+    right: 0,
+    top: 0,
+    borderRadius: 9,
+    backgroundColor: '#FF3500',
+  },
+});

+ 9 - 3
src/common/router/CommonParamList.ts

@@ -1,6 +1,12 @@
 export type CommonParamList = {
+  // 通用的 webview 页面
   WebView: {
-    url: string; // 地址
-    title?: string; // 标题
-  }; // 通用的 webview 页面
+    // 地址
+    url: string;
+    // 标题
+    title?: string;
+  };
+
+  // 扫一扫
+  Scan: undefined;
 };

+ 28 - 0
src/common/screens/scan/ScanScreen.tsx

@@ -0,0 +1,28 @@
+import React from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+import { StackScreenProps } from '@react-navigation/stack';
+import { CommonParamList } from '@common/router/CommonParamList.ts';
+
+type Props = StackScreenProps<CommonParamList, 'Scan'>;
+
+export default function ScanScreen(props: Props) {
+  return (
+    <View style={styles.container}>
+      <Text>扫一扫</Text>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+  },
+  webview: {
+    flex: 1,
+  },
+  activityIndicator: {
+    position: 'absolute',
+    alignSelf: 'center',
+    top: 100,
+  },
+});

ファイルの差分が大きいため隠しています
+ 223 - 399
yarn.lock


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません