feat(app): 新增首页轮播图和文章详情功能
- 添加首页轮播图组件和相关 API - 实现文章详情页面和相关 API - 更新底部导航栏,支持工作台、推荐和我的三个标签页 - 移除 react-native-copilot 依赖- 调整主栈路由,支持新功能
这个提交包含在:
父节点
7d19bba4b8
当前提交
35acc240ab
@ -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
|
||||
|
||||
@ -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-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": {
|
||||
|
||||
@ -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();
|
||||
},
|
||||
}),
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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
src/app/routes/MainTab.tsx
普通文件
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1
src/app/screens/api/index.ts
普通文件
1
src/app/screens/api/index.ts
普通文件
@ -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/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';
|
||||
|
||||
@ -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 = {
|
||||
// 通用的 webview 页面
|
||||
WebView: {
|
||||
url: string; // 地址
|
||||
title?: string; // 标题
|
||||
}; // 通用的 webview 页面
|
||||
// 地址
|
||||
url: string;
|
||||
// 标题
|
||||
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,
|
||||
},
|
||||
});
|
||||
正在加载...
在新工单中引用
屏蔽一个用户