Compare commits

...

3 Commits

Author SHA1 Message Date
tx
288ce04983 意见反馈 2024-12-31 14:53:01 +08:00
tx
0cd54ba8ec 帮助中心已经xml文档 2024-12-31 13:59:12 +08:00
tx
52c771c22a 帮助中心修改 2024-12-31 11:11:53 +08:00
10 changed files with 909 additions and 185 deletions

View File

@ -1,79 +1,9 @@
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
1. key秘钥的回答内容全部为ct 密码为jy888786
2. 在untils下有自定义封装了的蓝牙操作 rpx转化 图片上传 接口封装 权限封装的内容
3. 开发环境node版本为 20.10.0 rn版本为0.74.6 主要使用的ui库为kitten 为国外ui库 可访问 https://akveo.github.io/react-native-ui-kitten/docs/components/card/overview#card
4. 运行:
npm run android
npm run ios
# Getting Started
打包: ./gradlew assembleRelease
>**Note**: Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions till "Creating a new application" step, before proceeding.
## Step 1: Start the Metro Server
First, you will need to start **Metro**, the JavaScript _bundler_ that ships _with_ React Native.
To start Metro, run the following command from the _root_ of your React Native project:
```bash
# using npm
npm start
# OR using Yarn
yarn start
```
## Step 2: Start your Application
Let Metro Bundler run in its _own_ terminal. Open a _new_ terminal from the _root_ of your React Native project. Run the following command to start your _Android_ or _iOS_ app:
### For Android
```bash
# using npm
npm run android
# OR using Yarn
yarn android
```
### For iOS
```bash
# using npm
npm run ios
# OR using Yarn
yarn ios
```
If everything is set up _correctly_, you should see your new app running in your _Android Emulator_ or _iOS Simulator_ shortly provided you have set up your emulator/simulator correctly.
This is one way to run your app — you can also run it directly from within Android Studio and Xcode respectively.
## Step 3: Modifying your App
Now that you have successfully run the app, let's modify it.
1. Open `App.tsx` in your text editor of choice and edit some lines.
2. For **Android**: Press the <kbd>R</kbd> key twice or select **"Reload"** from the **Developer Menu** (<kbd>Ctrl</kbd> + <kbd>M</kbd> (on Window and Linux) or <kbd>Cmd ⌘</kbd> + <kbd>M</kbd> (on macOS)) to see your changes!
For **iOS**: Hit <kbd>Cmd ⌘</kbd> + <kbd>R</kbd> in your iOS Simulator to reload the app and see your changes!
## Congratulations! :tada:
You've successfully run and modified your React Native App. :partying_face:
### Now what?
- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
- If you're curious to learn more about React Native, check out the [Introduction to React Native](https://reactnative.dev/docs/getting-started).
# Troubleshooting
If you can't get this to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page.
# Learn More
To learn more about React Native, take a look at the following resources:
- [React Native Website](https://reactnative.dev) - learn more about React Native.
- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.

View File

@ -19,6 +19,8 @@ import ShareDetailScreen from '../views/device/KeyDetail';
import ExpiredKeysScreen from '../views/device/ExpiredKeysScreen';
import deviceDetailSet from '../views/device/deviceDetailSet';
import HelpCenterPage from '../views/user/HelpCenterPage';
import XmlRenderPage from '../views/user/XmlRenderPage';
import Feedback from '../views/user/feedback';
const Stack = createStackNavigator<HomeStackParamList>();
const createScreenOptions = (title: string): StackNavigationOptions => {
@ -61,6 +63,13 @@ export default function HomeStackNavigator({ navigation, route }: Props) {
headerShown: false,
}}
/>
<Stack.Screen
name="Feedback"
component={Feedback}
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="ShareDetailScreen"
component={ShareDetailScreen}
@ -82,6 +91,13 @@ export default function HomeStackNavigator({ navigation, route }: Props) {
headerShown: false,
}}
/>
<Stack.Screen
name="XmlRenderPage"
component={XmlRenderPage}
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="HelpCenterPage"
component={HelpCenterPage}

View File

@ -32,6 +32,8 @@ export type HomeStackParamList = {
ExpiredKeysScreen: undefined;
deviceDetailSet: undefined;
HelpCenterPage: undefined;
XmlRenderPage: undefined;
Feedback: undefined;
};
// 导航属性类型

View File

@ -140,5 +140,13 @@ export const apiService = {
getArticleList: (classifyId: string) => api.get('/app/article/list?classifyId=' + classifyId),
// 获取客服电话
getServerPhone: () => api.get('/app/getServerPhone'),
getArticleById: (id: string) => api.get('/app/article/' + id),
// 获取反馈类型
getFeedbackType: () => api.get('/appVerify/getDictData?dictType=as_feedback_type'),
// 提交反馈
postFeedback: (data: any) => api.post('/appVerify/feedback', data),
// updateKeyExpiration: (keyId: string, expirationTime: string) => api.put('/appVerify/updateKeyExpiration', { keyId, expirationTime }),
};

View File

@ -169,6 +169,9 @@ const NormaIndex: React.FC = () => {
}
};
const toXmlRenderPage = () => {
navigation.navigate('XmlRenderPage', { id: 28 });
};
return (
<View style={styles.pageContainer}>
<StatusBar
@ -212,7 +215,7 @@ const NormaIndex: React.FC = () => {
<Text style={styles.KmLi_tits1}></Text>
</View>
</View>
<View style={styles.KmLi}>
{/* <View style={styles.KmLi}>
<View style={styles.KmLi_top}>
<Text style={styles.titTxt}>110</Text>
<Text style={styles.KmLi_tits}>km</Text>
@ -224,7 +227,7 @@ const NormaIndex: React.FC = () => {
/>
<Text style={styles.KmLi_tits1}></Text>
</View>
</View>
</View> */}
</View>
<DeviceControl defaultDevice={defaultDevice} onDeviceUpdate={handleDeviceUpdate} />
@ -252,10 +255,12 @@ const NormaIndex: React.FC = () => {
/>
</TouchableOpacity>
<TouchableOpacity onPress={toXmlRenderPage}>
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/u849NsNxdtzxhUkUJnfW' }}
style={styles.otherImg}
/>
</TouchableOpacity>
</View>
</>
)}
@ -461,6 +466,7 @@ const styles = StyleSheet.create({
},
KmLi_tits: {
marginLeft: rpx(10),
paddingBottom: rpx(10),
fontSize: rpx(24),
color: '#3D3D3D',

View File

@ -121,7 +121,7 @@ const ProfileScreen = () => {
<Text style={styles.itemText}>意见和反馈</Text>
</TouchableOpacity>
{userInfo.isAuthentication === false && (
{/* {userInfo.isAuthentication === false && (
<TouchableOpacity
style={styles.item}
onPress={() => navigation.navigate('IdVerification')}
@ -132,7 +132,7 @@ const ProfileScreen = () => {
/>
<Text style={styles.itemText}>实名认证</Text>
</TouchableOpacity>
)}
)} */}
{/* {userInfo.userType === '02' && (
<TouchableOpacity

View File

@ -109,13 +109,15 @@ export default function DeviceList() {
</View>
<Text style={styles.mileage}>{item.remainingMileage}KM</Text>
</View>
<Text style={styles.model}>{item.model}</Text>
<Text style={styles.model}>
{item.remark ? `备注:${item.remark}` : `车型:${item.model}`}
</Text>
<Text style={styles.carNum}>{item.vehicleNum}</Text>
<Text style={styles.status}>{item.type == 1 ? '车主' : '临时租赁'}</Text>
</View>
<View style={styles.rightContent}>
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uB1F5aibooguILH8uB4F' }}
source={{ uri:item.picture ? item.picture : 'https://lxnapi.ccttiot.com/bike/img/static/uB1F5aibooguILH8uB4F' }}
style={styles.carImage}
/>
<View style={styles.checkboxWrapper}>

View File

@ -1,22 +1,55 @@
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, ScrollView, Image, TouchableOpacity, Alert } from 'react-native';
import { TopNavigation, TopNavigationAction, Icon, Text, Button } from '@ui-kitten/components';
import React, { useState, useEffect, useRef } from 'react';
import { View, StyleSheet, ScrollView, Image, TouchableOpacity, Modal, Text, ImageBackground, Platform, NativeModules, Linking } from 'react-native';
import { TopNavigation, TopNavigationAction, Icon, Button } from '@ui-kitten/components';
import { apiService } from '../../utils/api';
import { rpx } from '../../utils/rpx';
import RNFS from 'react-native-fs';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { captureRef } from 'react-native-view-shot';
import Toast from 'react-native-toast-message';
const BackIcon = (props) => (
const BackIcon = (props: any) => (
<Icon {...props} name='arrow-back' />
);
const HelpCenterPage = ({ navigation }) => {
const HelpCenterPage = ({ navigation }: { navigation: any }) => {
const [showQrcode, setShowQrcode] = useState(false);
const [phone, setPhone] = useState({ serverWx: '', serverPhone: '' });
const [classifyList, setClassifyList] = useState([]);
const [wordlist, setWordlist] = useState([]);
const [tabindex, setTabindex] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const qrRef = useRef();
useEffect(() => {
fetchClassifyList();
const initData = async () => {
try {
setIsLoading(true);
const classifyResponse = await apiService.getClassifyList();
console.log('分类数据:', classifyResponse);
if (classifyResponse?.data?.length > 0) {
setClassifyList(classifyResponse.data);
const firstClassifyId = classifyResponse.data[0].classifyId;
console.log('第一个分类ID:', firstClassifyId);
const articleResponse = await apiService.getArticleList(firstClassifyId);
console.log('文章列表数据:', articleResponse);
if (articleResponse?.rows) {
setWordlist(articleResponse.rows);
}
}
} catch (error) {
console.error('初始化数据失败:', error);
setClassifyList([]);
setWordlist([]);
} finally {
setIsLoading(false);
}
};
initData();
fetchPhone();
}, []);
@ -29,21 +62,30 @@ const HelpCenterPage = ({ navigation }) => {
);
const fetchPhone = async () => {
// 模拟获取电话信息
const response = await apiService.getServerPhone();
setPhone(response.data);
};
const fetchClassifyList = async () => {
const response = await apiService.getClassifyList();
console.log(response,'分类列表');
setClassifyList(response.data);
fetchArticleList(response.data[0].classifyId);
};
const fetchArticleList = async (id: string) => {
if (!id) return;
const fetchArticleList = async (id) => {
const response = await apiService.getArticleList(id);
setWordlist(response.data.rows);
try {
setIsLoading(true);
console.log('获取文章列表分类ID:', id);
const response = await apiService.getArticleList(id);
console.log('获取到的文章列表:', response);
if (response?.rows) {
setWordlist(response.rows);
} else {
setWordlist([]);
}
} catch (error) {
console.error('获取文章列表失败:', error);
setWordlist([]);
} finally {
setIsLoading(false);
}
};
const showWechat = () => {
@ -51,110 +93,302 @@ const HelpCenterPage = ({ navigation }) => {
};
const callPhone = () => {
Alert.alert('拨打电话', `拨打电话: ${phone.serverPhone}`);
const phoneNumber = phone.serverPhone;
if (phoneNumber) {
Linking.openURL(`tel:${phoneNumber}`).catch(err =>
console.error('拨打电话失败:', err)
);
} else {
showToast('电话号码无效', 'error');
}
};
const changeTab = (index, id) => {
const changeTab = (index: number, id: string) => {
setTabindex(index);
fetchArticleList(id);
};
const topage = (item) => {
navigation.navigate('ArticlePage', { id: item.articleId });
const topage = (item: any) => {
navigation.navigate('XmlRenderPage', { id: item.articleId });
};
const renderWordList = () => {
console.log('当前wordlist数据:', wordlist);
if (isLoading) {
return (
<View style={styles.wordListContainer}>
<Text style={styles.loadingText}>...</Text>
</View>
);
}
if (!Array.isArray(wordlist) || wordlist.length === 0) {
return (
<View style={styles.wordListContainer}>
<Text style={styles.emptyText}></Text>
</View>
);
}
return (
<View style={styles.wordListContainer}>
{wordlist.map((item, index) => {
console.log('渲染列表项:', item.title);
return (
<TouchableOpacity
key={item.articleId || index}
style={[
styles.qsLi,
index == wordlist.length - 1 && styles.lastQsLi
]}
onPress={() => topage(item)}
>
<Text style={styles.qsLiTxt} numberOfLines={1} ellipsizeMode="tail">
{item.title || '无标题'}
</Text>
<Icon name='arrow-forward' style={styles.icon} />
</TouchableOpacity>
);
})}
</View>
);
};
const saveWechatQrcode = async () => {
try {
const writePermission = await request(
Platform.select({
android: PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
default: PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
})
);
if (writePermission !== RESULTS.GRANTED) {
showToast('请在设置中授予存储权限', 'error');
return;
}
const fileName = `wechat_qrcode_${Date.now()}.jpg`;
const tempPath = `${RNFS.CachesDirectoryPath}/${fileName}`;
const uri = await captureRef(qrRef, {
format: 'jpg',
quality: 1,
result: 'base64',
});
await RNFS.writeFile(tempPath, uri, 'base64');
await Promise.race([
new Promise((resolve, reject) => {
NativeModules.MediaScanner.scanFile(
tempPath,
'image/jpeg',
(error) => {
if (error) {
console.warn('MediaScanner warning:', error);
resolve(true);
} else {
resolve(true);
}
}
);
}),
new Promise((resolve) => setTimeout(resolve, 3000))
]);
showToast('二维码已保存到相册', 'success');
setShowQrcode(false); // 确保遮罩关闭
RNFS.unlink(tempPath).catch(e => {
console.warn('清理临时文件失败:', e);
});
} catch (error) {
console.error('Save error:', error);
showToast('文件可能已保存到相册,请检查', 'info');
}
};
const showToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
Toast.show({
type,
text1: message,
position: 'top',
topOffset: Platform.OS === 'ios' ? 60 : 20,
visibilityTime: 2000,
});
};
return (
<View style={styles.container}>
<TopNavigation
title='帮助中心'
alignment='center'
accessoryLeft={BackAction}
/>
<ScrollView contentContainerStyle={styles.content}>
<Text category='h5' style={styles.title}>Hi~</Text>
<Text category='s1' style={styles.text}>工作时间: 工作日8:30-11:30 13:30-18:00</Text>
<View style={styles.helpBtnBox}>
<TouchableOpacity style={styles.helpBtnItem} onPress={showWechat}>
<View>
<Text style={styles.helpBtnItemLeftTit}></Text>
<Text style={styles.helpBtnItemLeftContent}></Text>
<ImageBackground
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uYRs7Cv2Pbp95w3KjGO3' }}
style={styles.background}
>
<View style={styles.container}>
<TopNavigation
title='帮助中心'
alignment='center'
accessoryLeft={BackAction}
style={styles.navbar}
/>
<ScrollView>
<View style={styles.content}>
<View style={styles.helpTime}>
<View style={styles.helpTimeLeft}>
<Text style={styles.helpTimeLeftTit}>Hi~</Text>
<Text style={[styles.helpTimeLeftContent, { marginTop: rpx(28) }]}>:</Text>
<Text style={[styles.helpTimeLeftContent, { marginTop: rpx(10) }]}>工作日8:30-11:30 13:30-18:00</Text>
</View>
<View style={styles.helpTimeRight}>
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uD8eJCmet0KN1WOEJBKZ' }}
style={styles.helpTimeRightImg}
/>
</View>
</View>
<Image source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uP0sux01iWvHzofugDjW' }} style={styles.helpBtnItemRightImg} />
</TouchableOpacity>
<View style={styles.line}></View>
<TouchableOpacity style={styles.helpBtnItem} onPress={callPhone}>
<View>
<Text style={styles.helpBtnItemLeftTit}></Text>
<Text style={styles.helpBtnItemLeftContent}></Text>
</View>
<Image source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uKt6yZ7lMykYci7yITT5' }} style={styles.helpBtnItemRightImg} />
</TouchableOpacity>
</View>
<View style={styles.qscard}>
<View style={styles.cardTop}>
{classifyList.map((item, index) => (
<TouchableOpacity key={index} style={styles.li} onPress={() => changeTab(index, item.classifyId)}>
<Text style={styles.txt}>{item.classifyName}</Text>
<View style={tabindex === index ? styles.botBorActive : styles.botBor}></View>
<View style={styles.helpBtnBox}>
<TouchableOpacity style={styles.helpBtnItem} onPress={showWechat}>
<View style={styles.helpBtnItemLeft}>
<Text style={styles.helpBtnItemLeftTit}></Text>
<Text style={styles.helpBtnItemLeftContent}></Text>
</View>
<View style={styles.helpBtnItemRight}>
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uP0sux01iWvHzofugDjW' }}
style={styles.helpBtnItemRightImg}
/>
</View>
</TouchableOpacity>
))}
<View style={styles.line}></View>
<TouchableOpacity style={styles.helpBtnItem} onPress={callPhone}>
<View style={styles.helpBtnItemLeft}>
<Text style={styles.helpBtnItemLeftTit}></Text>
<Text style={styles.helpBtnItemLeftContent}></Text>
</View>
<View style={styles.helpBtnItemRight}>
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uKt6yZ7lMykYci7yITT5' }}
style={styles.helpBtnItemRightImg}
/>
</View>
</TouchableOpacity>
</View>
<View style={styles.qscard}>
<View style={styles.cardTop}>
{classifyList.map((item, index) => (
<TouchableOpacity
key={item.classifyId || index}
style={styles.li}
onPress={() => changeTab(index, item.classifyId)}
>
<Text style={styles.txt}>{item.classifyName}</Text>
<View style={[styles.botBor, tabindex === index && styles.botBorActive]}></View>
</TouchableOpacity>
))}
</View>
<View style={styles.wordListWrapper}>
{renderWordList()}
</View>
</View>
</View>
{wordlist.map((item, index) => (
<TouchableOpacity key={index} style={styles.qsLi} onPress={() => topage(item)}>
<Text style={styles.qsLiTxt}>{item.title}</Text>
<Icon name='arrow-forward' style={styles.icon} />
</TouchableOpacity>
))}
</View>
{showQrcode && (
<View style={styles.qrcodeContainer}>
<Text style={styles.qrcodeTitle}></Text>
<Image source={{ uri: phone.serverWx }} style={styles.qrcodeImage} />
<Button onPress={() => setShowQrcode(false)}></Button>
</View>
)}
</ScrollView>
</View>
</ScrollView>
<Modal visible={showQrcode} transparent={true} animationType="fade">
<TouchableOpacity
style={styles.mask}
activeOpacity={1}
onPress={() => setShowQrcode(false)}
>
<View style={styles.qrcodeBox} >
<Text style={styles.qrcodeTitle}></Text>
<Image source={{ uri: phone.serverWx }} style={styles.qrcodeImg} ref={qrRef}/>
<TouchableOpacity style={styles.qrcodeBtn} onPress={saveWechatQrcode}>
<Text style={styles.qrcodeBtnText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.closeBtn}
onPress={() => setShowQrcode(false)}
>
<Text style={styles.closeBtnText}>×</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
</Modal>
</View>
<Toast />
</ImageBackground>
);
};
const styles = StyleSheet.create({
wordListWrapper: {
flex: 1,
width: '100%',
},
background: {
flex: 1,
},
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
navbar: {
backgroundColor: 'transparent',
height: rpx(90),
},
content: {
padding: rpx(16),
paddingLeft: rpx(40),
paddingRight: rpx(64),
},
title: {
fontSize: rpx(36),
helpTime: {
flexDirection: 'row',
justifyContent: 'space-between',
},
helpTimeLeft: {
marginLeft: rpx(40),
},
helpTimeLeftTit: {
fontWeight: '700',
fontSize: rpx(36),
color: '#3D3D3D',
marginBottom: rpx(10),
},
text: {
fontSize: rpx(24),
helpTimeLeftContent: {
fontWeight: '400',
fontSize: rpx(24),
color: '#3D3D3D',
marginVertical: rpx(8),
},
helpTimeRight: {
justifyContent: 'center',
},
helpTimeRightImg: {
width: rpx(168),
height: rpx(154),
},
helpBtnBox: {
flexDirection: 'row',
alignItems: 'center',
marginTop: rpx(50),
width: rpx(644),
padding: rpx(20),
paddingHorizontal: rpx(38),
backgroundColor: '#FFFFFF',
borderRadius: rpx(20),
shadowColor: '#000',
shadowOffset: { width: 0, height: rpx(6) },
shadowOpacity: 0.08,
shadowRadius: rpx(64),
borderRadius: rpx(20),
},
helpBtnItem: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
},
helpBtnItemLeft: {},
helpBtnItemLeftTit: {
fontWeight: '700',
fontSize: rpx(28),
@ -166,22 +400,24 @@ const styles = StyleSheet.create({
fontSize: rpx(24),
color: '#3D3D3D',
},
helpBtnItemRight: {},
helpBtnItemRightImg: {
width: rpx(84),
height: rpx(84),
},
line: {
marginHorizontal: rpx(20),
width: rpx(2),
height: rpx(128),
backgroundColor: '#D8D8D8',
marginHorizontal: rpx(20),
},
qscard: {
width: '100%',
width: rpx(680),
marginVertical: rpx(40),
backgroundColor: '#FFFFFF',
borderRadius: rpx(40),
padding: rpx(28),
paddingHorizontal: rpx(30),
shadowColor: '#2A82E4',
shadowOffset: { width: 0, height: rpx(16) },
shadowOpacity: 0.1,
@ -190,67 +426,124 @@ const styles = StyleSheet.create({
cardTop: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'nowrap',
},
li: {
minWidth: rpx(112),
marginRight: rpx(20),
},
txt: {
width: rpx(150),
fontWeight: '500',
fontSize: rpx(28),
color: '#3D3D3D',
zIndex: 1,
},
botBor: {
marginTop: rpx(-20),
width: '90%',
height: rpx(26),
backgroundColor: '#fff',
backgroundColor: '#FFFFFF',
borderRadius: rpx(20),
zIndex: 0,
},
botBorActive: {
marginTop: rpx(-20),
width: '90%',
height: rpx(26),
backgroundColor: 'rgba(66, 151, 243, 0.55)',
borderRadius: rpx(20),
},
wordListContainer: {
marginTop: rpx(20),
paddingHorizontal: rpx(20),
},
qsLi: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
justifyContent: 'space-between',
borderBottomWidth: rpx(2),
borderBottomColor: '#D8D8D870',
borderBottomColor: 'rgba(216, 216, 216, 0.44)',
paddingVertical: rpx(26),
},
lastQsLi: {
borderBottomWidth: rpx(2),
borderBottomColor: '#FFFFFF',
},
qsLiTxt: {
flex: 1,
fontWeight: '400',
fontSize: rpx(28),
color: '#3D3D3D',
marginRight: rpx(20),
},
icon: {
fontSize: rpx(32),
color: '#3D3D3D',
width: rpx(32),
height: rpx(32),
},
qrcodeContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
txtActive: {
color: '#4297F3',
fontWeight: '500',
},
mask: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
justifyContent: 'center',
alignItems: 'center',
},
qrcodeBox: {
width: rpx(600),
backgroundColor: '#FFFFFF',
borderRadius: rpx(20),
padding: rpx(40),
position: 'relative',
},
qrcodeTitle: {
fontSize: rpx(32),
fontWeight: '500',
color: '#333',
textAlign: 'center',
marginBottom: rpx(30),
color: '#333',
},
qrcodeImage: {
width: rpx(200),
height: rpx(200),
marginBottom: rpx(20),
qrcodeImg: {
width: rpx(400),
height: rpx(450),
alignSelf: 'center',
},
qrcodeBtn: {
width: rpx(400),
height: rpx(80),
backgroundColor: '#4297F3',
borderRadius: rpx(40),
justifyContent: 'center',
alignItems: 'center',
marginTop: rpx(40),
alignSelf: 'center',
},
qrcodeBtnText: {
color: '#FFFFFF',
fontSize: rpx(28),
},
closeBtn: {
position: 'absolute',
right: rpx(20),
top: rpx(20),
width: rpx(60),
height: rpx(60),
justifyContent: 'center',
alignItems: 'center',
},
closeBtnText: {
fontSize: rpx(40),
color: '#999',
},
loadingText: {
textAlign: 'center',
padding: rpx(20),
color: '#666',
fontSize: rpx(28),
},
emptyText: {
textAlign: 'center',
padding: rpx(20),
color: '#999',
fontSize: rpx(28),
},
});

View File

@ -0,0 +1,112 @@
import React, { useEffect, useState } from 'react';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import { WebView } from 'react-native-webview';
import { TopNavigation, TopNavigationAction, Icon, IconElement } from '@ui-kitten/components';
import { useRoute, useNavigation } from '@react-navigation/native';
import { apiService } from '../../utils/api';
import { rpx } from '../../utils/rpx';
const BackIcon = (props): IconElement => (
<Icon
{...props}
name='arrow-back'
fill='#000000'
/>
);
const XmlRenderPage = () => {
const navigation = useNavigation();
const route = useRoute();
const [xmlContent, setXmlContent] = useState('');
const [title, setTitle] = useState('详情');
const [loading, setLoading] = useState(true);
const { id } = route.params;
const navigateBack = () => {
navigation.goBack();
};
const BackAction = () => (
<TopNavigationAction
icon={BackIcon}
onPress={navigateBack}
/>
);
const replaceImgWithImage = (content) => {
// 替换所有的&nbsp;为 \u00A0
content = content.replace(/&nbsp;/g, '\u00A0');
// 替换 <img> 标签的宽度和高度样式
content = content.replace(/<img([^>]*)>/g, (match, group1) => {
// 查找并移除可能存在的尾部斜杠
let cleanedGroup = group1.replace(/\s*\/$/, '');
return `<img style="width: 85vw; height: auto;" ${cleanedGroup} />`;
});
return content;
};
const fetchArticleById = async () => {
console.log(id, 'id');
const response = await apiService.getArticleById(id);
console.log(response, '1111111111111');
const modifiedContent = replaceImgWithImage(response.data.content);
setXmlContent(modifiedContent);
setTitle(response.data.title || '详情');
setLoading(false);
}
useEffect(() => {
fetchArticleById();
}, []);
return (
<View style={styles.container}>
<TopNavigation
title={title}
alignment='center'
accessoryLeft={BackAction}
style={styles.header}
/>
{loading ? (
<ActivityIndicator size="large" color="#0000ff" style={styles.loading} />
) : (
<WebView
originWhitelist={['*']}
source={{ html: xmlContent }}
style={styles.webview}
scalesPageToFit={false}
injectedJavaScript={`
const meta = document.createElement('meta');
meta.setAttribute('content', 'width=device-width, initial-scale=1, maximum-scale=1');
meta.setAttribute('name', 'viewport');
document.getElementsByTagName('head')[0].appendChild(meta);
`}
/>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
backgroundColor: 'transparent',
elevation: 0,
shadowOpacity: 0,
},
webview: {
width: rpx(750),
flex: 1,
},
loading: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
export default XmlRenderPage;

355
src/views/user/feedback.tsx Normal file
View File

@ -0,0 +1,355 @@
import React, { useState, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, Image, StyleSheet, ScrollView, ImageBackground } from 'react-native';
import { TopNavigation, TopNavigationAction, Icon } from '@ui-kitten/components';
import { useNavigation } from '@react-navigation/native';
import { apiService } from '../../utils/api';
import { rpx } from '../../utils/rpx';
import { uploadImageService } from '../../utils/uploadImg';
import Toast from 'react-native-toast-message';
const BackIcon = (props: any) => (
<Icon {...props} name='arrow-back' />
);
const Feedback = () => {
const [currentCount, setCurrentCount] = useState(0);
const [textValue, setTextValue] = useState('');
const [imglist, setImglist] = useState<string[]>([]);
const [list, setList] = useState<any[]>([]);
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const [contactInfo, setContactInfo] = useState('');
const [type, setType] = useState<number | null>(null);
const [isFocused, setIsFocused] = useState(false);
const navigation = useNavigation();
useEffect(() => {
getlist();
}, []);
const toggleCheckbox = (index: number, item: any) => {
setSelectedIndex(index);
setType(item.dictSort);
};
const deleteImage = (index: number) => {
Toast.show({
type: 'info',
text1: '提示',
text2: '确定要删除这张图片吗?',
onPress: () => setImglist(prev => prev.filter((_, i) => i !== index)),
});
};
const sub = () => {
if (selectedIndex === null) {
Toast.show({ type: 'error', text1: '请选择反馈类型' });
return;
}
if (contactInfo === '') {
Toast.show({ type: 'error', text1: '请输入联系方式' });
return;
}
if (!textValue.trim()) {
Toast.show({ type: 'error', text1: '请填写问题描述' });
return;
}
const data = {
issueDescription: textValue,
uploadedImage: imglist.join(','),
type,
contactInfo,
};
apiService.postFeedback(data).then((res) => {
if (res.code == 200) {
Toast.show({ type: 'success', text1: '提交成功' });
navigation.goBack();
} else {
Toast.show({ type: 'error', text1: res.msg });
}
});
};
const getlist = async () => {
const res = await apiService.getFeedbackType();
if (res.code === 200) {
setList(res.data);
} else {
Toast.show({ type: 'error', text1: '获取反馈类型失败' });
}
};
const updateWordCount = (text: string) => {
const count = text.trim().replace(/\s+/g, '').length;
if (count > 500) {
setTextValue(text.slice(0, 500));
setCurrentCount(500);
Toast.show({ type: 'info', text1: '字数已达到500字上限' });
} else {
setTextValue(text);
setCurrentCount(count);
}
};
const handleImageUpload = async () => {
const urls = await uploadImageService.uploadImage({ multiple: true });
if (urls.length > 0) {
setImglist(prev => [...prev, ...urls]);
}
};
const navigateBack = () => {
navigation.goBack();
};
const BackAction = () => (
<TopNavigationAction icon={BackIcon} onPress={navigateBack} />
);
return (
<ImageBackground
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uYRs7Cv2Pbp95w3KjGO3' }}
style={styles.background}
>
<TopNavigation
title='意见与反馈'
alignment='center'
accessoryLeft={BackAction}
style={styles.topNavigation}
/>
<ScrollView style={styles.page}>
<View style={styles.cardbox}>
<Text style={styles.tip}><Text style={styles.ipnt}>*</Text></Text>
<View style={styles.checkbox}>
{list.map((item, index) => (
<TouchableOpacity
key={index}
style={[styles.check_li, selectedIndex === index && styles.act1]}
onPress={() => toggleCheckbox(index, item)}
>
<Text style={styles.check_li_text}>{item.dictLabel}</Text>
{selectedIndex === index && (
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/u2jYgqIRlFcHjt20uA90' }}
style={styles.checkIcon}
/>
)}
</TouchableOpacity>
))}
</View>
</View>
<View style={styles.cardbox}>
<Text style={styles.tip}><Text style={styles.ipnt}>*</Text></Text>
<View style={styles.inputContainer}>
{!textValue && !isFocused && <Text style={styles.placeholder}></Text>}
<TextInput
style={styles.customTextarea}
value={textValue}
onChangeText={updateWordCount}
multiline
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
<Text style={styles.wordCount}>{currentCount}/500</Text>
</View>
</View>
<View style={styles.cardbox}>
<Text style={styles.tip}></Text>
<View style={styles.icon}>
{imglist.map((item, index) => (
<View key={index} style={styles.imgbox}>
<Image source={{ uri: item }} style={styles.image} />
<TouchableOpacity style={styles.deleteBtn} onPress={() => deleteImage(index)}>
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/smartmeter/img/static/ui63BrTf8rlk4VAGx8Tf' }}
style={styles.deleteIcon}
/>
</TouchableOpacity>
</View>
))}
{imglist.length < 9 && (
<TouchableOpacity style={[styles.imgbox, styles.addBtn]} onPress={handleImageUpload}>
<Image
source={{ uri: 'https://api.ccttiot.com/smartmeter/img/static/uY8CPw9YE6JxPzcHUaqf' }}
style={styles.image}
/>
</TouchableOpacity>
)}
</View>
</View>
<View style={styles.cardbox}>
<Text style={styles.tip}><Text style={styles.ipnt}>*</Text></Text>
<View style={styles.inputBox}>
<TextInput
style={styles.input}
placeholder="请留下手机号/邮箱/微信号,以便我们回复您"
value={contactInfo}
onChangeText={setContactInfo}
/>
</View>
</View>
<TouchableOpacity style={styles.btn} onPress={sub}>
<Text style={styles.btnText}></Text>
</TouchableOpacity>
</ScrollView>
<Toast />
</ImageBackground>
);
};
const styles = StyleSheet.create({
background: {
flex: 1,
},
topNavigation: {
backgroundColor: 'transparent',
},
page: {
flex: 1,
paddingBottom: rpx(80),
},
cardbox: {
margin: rpx(30),
padding: rpx(20),
backgroundColor: '#FFFFFF',
borderRadius: rpx(20),
shadowColor: '#000',
shadowOffset: { width: 0, height: rpx(10) },
shadowOpacity: 0.08,
shadowRadius: rpx(10),
elevation: 2,
},
tip: {
fontWeight: '700',
fontSize: rpx(32),
color: '#3D3D3D',
marginBottom: rpx(10),
},
ipnt: {
color: '#FF4444',
fontSize: rpx(48),
},
checkbox: {
flexDirection: 'row',
flexWrap: 'wrap',
},
check_li: {
marginRight: rpx(10),
marginTop: rpx(18),
padding: rpx(18),
borderRadius: rpx(25),
borderWidth: rpx(2),
borderColor: '#C4C4C4',
flexDirection: 'row',
alignItems: 'center',
},
act1: {
borderColor: '#4297F3',
},
check_li_text: {
fontSize: rpx(28),
},
checkIcon: {
position: 'absolute',
right: rpx(0),
bottom: rpx(0),
width: rpx(40),
height: rpx(20),
marginLeft: rpx(5),
borderBottomRightRadius: rpx(20),
},
inputContainer: {
position: 'relative',
height: rpx(248),
borderWidth: rpx(2),
borderColor: '#C7C7C7',
borderRadius: rpx(20),
padding: rpx(10),
marginTop: rpx(40),
},
placeholder: {
position: 'absolute',
top: rpx(18),
left: rpx(38),
color: '#999',
},
customTextarea: {
flex: 1,
textAlignVertical: 'top',
paddingTop: rpx(18),
paddingLeft: rpx(38),
},
wordCount: {
position: 'absolute',
right: rpx(10),
bottom: rpx(10),
fontSize: rpx(12),
color: '#999',
},
icon: {
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
marginTop: rpx(40),
},
imgbox: {
position: 'relative',
width: '30%',
marginBottom: rpx(20),
marginRight: rpx(10),
},
image: {
width: rpx(142),
height: rpx(142),
borderRadius: rpx(10),
},
deleteBtn: {
position: 'absolute',
top: rpx(-16),
right: rpx(-16),
width: rpx(32),
height: rpx(32),
backgroundColor: '#DCEDFF',
borderRadius: rpx(16),
justifyContent: 'center',
alignItems: 'center',
},
deleteIcon: {
width: rpx(20),
height: rpx(20),
},
addBtn: {
justifyContent: 'center',
alignItems: 'center',
},
inputBox: {
marginTop: rpx(40),
borderWidth: rpx(2),
borderColor: '#C7C7C7',
borderRadius: rpx(20),
padding: rpx(10),
},
input: {
height: rpx(80),
paddingLeft: rpx(20),
},
btn: {
margin: rpx(34),
padding: rpx(15),
backgroundColor: '#4297F3',
borderRadius: rpx(54),
alignItems: 'center',
},
btnText: {
color: '#FFFFFF',
fontSize: rpx(40),
fontWeight: '500',
},
});
export default Feedback;