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

- 添加首页轮播图组件和相关 API
- 实现文章详情页面和相关 API
- 更新底部导航栏,支持工作台、推荐和我的三个标签页
- 移除 react-native-copilot 依赖- 调整主栈路由,支持新功能
这个提交包含在:
xuqm 2025-09-01 16:49:47 +08:00
父节点 7d19bba4b8
当前提交 35acc240ab
共有 51 个文件被更改,包括 4005 次插入472 次删除

查看文件

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

文件差异因一行或多行过长而隐藏

查看文件

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

查看文件

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

查看文件

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

查看文件

@ -11,12 +11,18 @@ import {
} from '@common/constants'; } from '@common/constants';
import HeaderBackImage from '@common/components/HeaderBackImage.tsx'; import HeaderBackImage from '@common/components/HeaderBackImage.tsx';
import MainViewScreen from '../screens/main/MainViewScreen'; 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() { export function MainStack() {
// const navigation = useNavigation<NavigationProp<MainParamList>>();
return ( return (
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{
@ -29,14 +35,22 @@ export function MainStack() {
}, },
headerBackImage: HeaderBackImage, headerBackImage: HeaderBackImage,
}} }}
initialRouteName="MainView" initialRouteName="MainTab"
> >
<Stack.Group <Stack.Group
screenOptions={{ screenOptions={{
...TransitionPresets.SlideFromRightIOS, ...TransitionPresets.SlideFromRightIOS,
}} }}
> >
<Stack.Screen
name="MainTab"
component={MainTab}
options={{headerShown: false}}
/>
<Stack.Screen name="MainView" component={MainViewScreen} /> <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>
<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.Group>
</Stack.Navigator> </Stack.Navigator>
); );

175
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}
/>
);
}

查看文件

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

查看文件

@ -0,0 +1,44 @@
import {useApi} from '@common/api/useApi';
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 bannerSchema = z.object({
id: z.string().optional().default(''), // id
imgUrl: z.string().url(), // 图片
jumpType: z.enum(['noSkip', 'link', 'activity', 'miniApp', 'hotNews']), // 轮播图跳转类型 noSkip无跳转 link链接跳转 activity活动跳转 miniApp应用跳转 hotNews 热点推荐
skipUrl: z.string().optional(), // jumpType 为 'link' 时的跳转链接
activityInfo: z.unknown(), // jumpType 为 'activity' 时的活动信息 这里暂时是 z.unknown() 直接带到旧模块
miniAppInfo: featureSchema.optional(), // jumpType 为 'miniApp' 时的活动信息 这里和精品服务那里的模型是一致的
articleId: z.string().optional(), // jumpType 为 'hotNews' 时的文章id
});
type Banner = z.infer<typeof bannerSchema>;
type Feature = z.infer<typeof featureSchema>;
const bannersSchema = z.array(bannerSchema).optional().default([]);
// 首页轮播图
// {"data":[{"clientId":"","collection":"app_banner","createTime":"2024-08-21 13:45:05","id":"{\"$oid\":\"66c57ee1986acc69b1dab1ee\"}","imgUrl":"https://tms-dev.oss-cn-beijing.aliyuncs.com/banner/headimg_0687ad243ffe43498e1bac2c16f47181.png","jumpType":"noSkip","linkType":"","skipUrl":"","sortNum":1,"title":"1234"},{"activityId":"c41d2fc4743f4093b0f7a0a49f81bc8f","activityInfo":{"activityId":"c41d2fc4743f4093b0f7a0a49f81bc8f","activityName":"活动99","activityPageImgUrl":"https://tms-dev.oss-cn-beijing.aliyuncs.com/banner/headimg_5f5f5918693d45369cdc8b1bbec48698.jpg","activityPrice":0,"activityPriceWithYuan":"0","activityWindowImgUrl":"https://tms-dev.oss-cn-beijing.aliyuncs.com/banner/headimg_022c001bbc444cd58304c7ff4cf4cc2c.jpg","activityfloatImgUrl":"https://tms-dev.oss-cn-beijing.aliyuncs.com/banner/headimg_10efc2e99e93468fb50404eb74370d91.png","collection":"app_activity","commodityTime":30,"createTime":"2022-03-26 16:57:04","del":0,"endTime":"2022-06-09 00:00:00","explain":"ok","id":"{\"$oid\":\"623ed560986acc03223d186d\"}","minAppVersion":"99","nSId":"012","price":100,"priceWithYuan":"1","startTime":"2022-03-26 00:00:00","templateId":"02fceff4348541f280f06b338b6349e1","updateTime":"2022-05-31 17:17:49"},"clientId":"","collection":"app_banner","createTime":"2024-08-21 13:45:05","id":"{\"$oid\":\"66c57ee1986acc69b1dab1ef\"}","imgUrl":"https://tms-dev.oss-cn-beijing.aliyuncs.com/banner/headimg_0c39dbf709304a5ca347a9cb1b2f8894.png","jumpType":"activity","linkType":"","skipUrl":"","sortNum":2,"title":"测试1"},{"clientId":"","collection":"app_banner","createTime":"2024-08-21 13:45:05","id":"{\"$oid\":\"66c57ee1986acc69b1dab1f0\"}","imgUrl":"https://tms-dev.oss-cn-beijing.aliyuncs.com/banner/headimg_bda974c012b844d9878b05b0e134849d.png","jumpType":"link","linkType":"https://","skipUrl":"https://dev.51trust.com/ketang/index.html?appId=Y_T_A2411062250","sortNum":3,"title":"医信课堂"},{"clientId":"","collection":"app_banner","createTime":"2024-08-21 13:45:05","id":"{\"$oid\":\"66c57ee1986acc69b1dab1f1\"}","imgUrl":"https://tms-dev.oss-cn-beijing.aliyuncs.com/banner/headimg_b5fa8ec8dc8c4326b45fb04c3f5c7969.jpg","jumpType":"link","linkType":"https://","skipUrl":"https://https://blog.csdn.net/kl222/article/details/84939135?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-84939135-blog-115694803.235^v43^pc_blog_bottom_relevance_base7&spm=1001.2101.3001.4242.1&utm_relevant_index=3","sortNum":4,"title":"掌上病历介绍"},{"clientId":"","collection":"app_banner","createTime":"2024-08-21 13:45:05","id":"{\"$oid\":\"66c57ee1986acc69b1dab1f2\"}","imgUrl":"https://tms-dev.oss-cn-beijing.aliyuncs.com/banner/headimg_a18ebcce9d2446c88e12c6675f02a5f9.jpg","jumpType":"miniApp","linkType":"","miniAppInfo":{"advertisePic":"https://tms-dev.oss-cn-beijing.aliyuncs.com/doctorHelper/advertPic_Y_T_A2310053073.png","allowApply":true,"cornerTagUrl":"","createTime":"2023-08-30 10:37:06","icon":"https://tms-dev.oss-cn-beijing.aliyuncs.com/doctorHelper/docIcon_Y_T_A2310053073.png","link":"ywxApp://FollowUpCenter","name":"随访中心","note":"随访中心","phone":"18811061787","recommend":true,"serviceType":4,"sort":0,"status":1,"subscribeType":1,"uniqueId":"Y_T_A2310053073","updateTime":"2024-08-30 15:15:32"},"nsId":"Y_T_A2310053073","skipUrl":"","sortNum":5,"title":"标题"},{"articleId":"2","clientId":"","collection":"app_banner","createTime":"2024-08-21 13:45:05","id":"{\"$oid\":\"66c57ee1986acc69b1dab1f3\"}","imgUrl":"https://tms-dev.oss-cn-beijing.aliyuncs.com/banner/headimg_e26da466ca454f0592321fa7df1c18be.png","jumpType":"noSkip","linkType":"","skipUrl":"","sortNum":6,"title":"标题"}],"message":"success","status":"0"}
const useBannerList = () => {
return useApi('/am/v3/banner/list', 'GET', {}, bannersSchema, {
automatic: true,
loadingDelay: 500,
});
};
export type {Banner, Feature};
export {useBannerList};

查看文件

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

查看文件

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

查看文件

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

查看文件

@ -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};

查看文件

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

查看文件

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

查看文件

@ -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';

查看文件

@ -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};

查看文件

@ -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};

查看文件

@ -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};

查看文件

@ -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};

查看文件

@ -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};

查看文件

@ -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};

查看文件

@ -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};

查看文件

@ -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};

查看文件

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

查看文件

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

查看文件

@ -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%' },
});

查看文件

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

查看文件

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

查看文件

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

查看文件

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

查看文件

@ -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};

查看文件

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

查看文件

@ -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 };

查看文件

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

查看文件

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

查看文件

@ -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};

查看文件

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

查看文件

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

查看文件

之前

宽度:  |  高度:  |  大小: 9.0 KiB

之后

宽度:  |  高度:  |  大小: 9.0 KiB

查看文件

之前

宽度:  |  高度:  |  大小: 30 KiB

之后

宽度:  |  高度:  |  大小: 30 KiB

查看文件

之前

宽度:  |  高度:  |  大小: 50 KiB

之后

宽度:  |  高度:  |  大小: 50 KiB

查看文件

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

查看文件

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

查看文件

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

查看文件

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

查看文件

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

查看文件

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

查看文件

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

719
yarn.lock

文件差异内容过多而无法显示 加载差异