diff --git a/MeAgent/ios/Podfile.lock b/MeAgent/ios/Podfile.lock
index cc030802..3b144b08 100644
--- a/MeAgent/ios/Podfile.lock
+++ b/MeAgent/ios/Podfile.lock
@@ -5,6 +5,9 @@ PODS:
- ExpoModulesCore
- EXConstants (16.0.2):
- ExpoModulesCore
+ - EXImageLoader (4.7.0):
+ - ExpoModulesCore
+ - React-Core
- EXNotifications (0.28.19):
- ExpoModulesCore
- Expo (51.0.39):
@@ -21,6 +24,8 @@ PODS:
- ExpoModulesCore
- ExpoFont (12.0.10):
- ExpoModulesCore
+ - ExpoImagePicker (15.1.0):
+ - ExpoModulesCore
- ExpoKeepAwake (13.0.2):
- ExpoModulesCore
- ExpoLinearGradient (13.0.2):
@@ -78,6 +83,7 @@ PODS:
- hermes-engine (0.74.5):
- hermes-engine/Pre-built (= 0.74.5)
- hermes-engine/Pre-built (0.74.5)
+ - lottie-ios (4.5.2)
- RCT-Folly (2024.01.01.00):
- boost
- DoubleConversion
@@ -1283,6 +1289,9 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
+ - RNKLineView (1.0.0):
+ - lottie-ios (~> 4.5.0)
+ - React
- RNReanimated (3.10.1):
- DoubleConversion
- glog
@@ -1336,6 +1345,7 @@ DEPENDENCIES:
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- EXApplication (from `../node_modules/expo-application/ios`)
- EXConstants (from `../node_modules/expo-constants/ios`)
+ - EXImageLoader (from `../node_modules/expo-image-loader/ios`)
- EXNotifications (from `../node_modules/expo-notifications/ios`)
- Expo (from `../node_modules/expo`)
- ExpoAsset (from `../node_modules/expo-asset/ios`)
@@ -1344,6 +1354,7 @@ DEPENDENCIES:
- ExpoDevice (from `../node_modules/expo-device/ios`)
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
- ExpoFont (from `../node_modules/expo-font/ios`)
+ - ExpoImagePicker (from `../node_modules/expo-image-picker/ios`)
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
- ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
@@ -1407,6 +1418,7 @@ DEPENDENCIES:
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
+ - RNKLineView (from `../node_modules/react-native-kline-view`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- RNSVG (from `../node_modules/react-native-svg`)
@@ -1414,6 +1426,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
+ - lottie-ios
- SocketRocket
EXTERNAL SOURCES:
@@ -1425,6 +1438,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-application/ios"
EXConstants:
:path: "../node_modules/expo-constants/ios"
+ EXImageLoader:
+ :path: "../node_modules/expo-image-loader/ios"
EXNotifications:
:path: "../node_modules/expo-notifications/ios"
Expo:
@@ -1441,6 +1456,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-file-system/ios"
ExpoFont:
:path: "../node_modules/expo-font/ios"
+ ExpoImagePicker:
+ :path: "../node_modules/expo-image-picker/ios"
ExpoKeepAwake:
:path: "../node_modules/expo-keep-awake/ios"
ExpoLinearGradient:
@@ -1564,6 +1581,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-masked-view/masked-view"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
+ RNKLineView:
+ :path: "../node_modules/react-native-kline-view"
RNReanimated:
:path: "../node_modules/react-native-reanimated"
RNScreens:
@@ -1578,6 +1597,7 @@ SPEC CHECKSUMS:
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
EXApplication: ec862905fdab3a15bf6bd8ca1a99df7fc02d7762
EXConstants: 89d35611505a8ce02550e64e43cd05565da35f9a
+ EXImageLoader: 1fe96c70cdc78bedc985ec4b1fab5dd8e67dc38b
EXNotifications: 6ce128c0d3d3d161cd68bfd07d593db40e140396
Expo: ed0a748eb6be0efd2c3df7f6de3f3158a14464c9
ExpoAsset: 286fee7ba711ce66bf20b315e68106b13b8629fc
@@ -1586,6 +1606,7 @@ SPEC CHECKSUMS:
ExpoDevice: 84b3ed79df1234c17edfbf335f6ecf3c636f74de
ExpoFileSystem: 2988caaf68b7cb706e36d382829d99811d9d76a5
ExpoFont: 38dddf823e32740c2a9f37c926a33aeca736b5c4
+ ExpoImagePicker: 7038c1740853c170002c491de63f9f9780c8139b
ExpoKeepAwake: dd02e65d49f1cfd9194640028ae2857e536eb1c9
ExpoLinearGradient: 4c44b3803b441724874b232e6520b51ca6a50db1
ExpoModulesCore: 9ac73e2f60e0ea1d30137ca96cfc8c2aa34ef2b2
@@ -1595,6 +1616,7 @@ SPEC CHECKSUMS:
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
glog: fdfdfe5479092de0c4bdbebedd9056951f092c4f
hermes-engine: 8c1577f3fdb849cbe7729c2e7b5abc4b845e88f8
+ lottie-ios: 96784afc26ea031d3e2b6cae342a4b8915072489
RCT-Folly: 5dc73daec3476616d19e8a53f0156176f7b55461
RCTDeprecation: 3afceddffa65aee666dafd6f0116f1d975db1584
RCTRequired: ec1239bc9d8bf63e10fb92bd8b26171a9258e0c1
@@ -1647,6 +1669,7 @@ SPEC CHECKSUMS:
RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11
RNCMaskedView: de80352547bd4f0d607bf6bab363d826822bd126
RNGestureHandler: 326e35460fb6c8c64a435d5d739bea90d7ed4e49
+ RNKLineView: bb63410106d30c7e3a7967c638ef682f9415b2a2
RNReanimated: def444e044c354f38bb0a5926a8583ba19d944c1
RNScreens: a2d8a2555b4653d7a19706eb172f855657ac30d7
RNSVG: 0e7deccab0678200815104223aadd5ca734dd41d
diff --git a/MeAgent/ios/app.xcodeproj/project.pbxproj b/MeAgent/ios/app.xcodeproj/project.pbxproj
index 59461e5a..6db72d96 100644
--- a/MeAgent/ios/app.xcodeproj/project.pbxproj
+++ b/MeAgent/ios/app.xcodeproj/project.pbxproj
@@ -280,6 +280,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios/LottiePrivacyInfo.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -291,6 +292,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/LottiePrivacyInfo.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
diff --git a/MeAgent/package.json b/MeAgent/package.json
index da111949..93519ecc 100644
--- a/MeAgent/package.json
+++ b/MeAgent/package.json
@@ -29,6 +29,7 @@
"expo-constants": "~16.0.2",
"expo-device": "~6.0.2",
"expo-font": "~12.0.10",
+ "expo-image-picker": "~15.1.0",
"expo-linear-gradient": "~13.0.2",
"expo-modules-core": "~1.12.24",
"expo-notifications": "~0.28.19",
diff --git a/MeAgent/src/screens/Community/ChannelDetail.js b/MeAgent/src/screens/Community/ChannelDetail.js
index 110e7c83..59fa4830 100644
--- a/MeAgent/src/screens/Community/ChannelDetail.js
+++ b/MeAgent/src/screens/Community/ChannelDetail.js
@@ -13,6 +13,8 @@ import {
TextInput,
Image,
Dimensions,
+ Alert,
+ ActivityIndicator,
} from 'react-native';
import {
Box,
@@ -28,6 +30,7 @@ import {
import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch, useSelector } from 'react-redux';
+import * as ImagePicker from 'expo-image-picker';
import {
fetchMessages,
@@ -35,6 +38,7 @@ import {
addMessage,
} from '../../store/slices/communitySlice';
import { useCommunitySocket } from '../../hooks/useCommunitySocket';
+import { uploadService } from '../../services/communityService';
// 消息分组:按日期
const groupMessagesByDate = (messages) => {
@@ -170,6 +174,8 @@ const ChannelDetail = ({ route, navigation }) => {
const [inputText, setInputText] = useState('');
const [isSending, setIsSending] = useState(false);
+ const [selectedImages, setSelectedImages] = useState([]); // { uri, mimeType }
+ const [isUploadingImages, setIsUploadingImages] = useState(false);
// WebSocket 连接
const {
@@ -211,6 +217,47 @@ const ChannelDetail = ({ route, navigation }) => {
}
}, [isConnected, channel?.id, startTyping]);
+ // 选择图片
+ const handlePickImage = useCallback(async () => {
+ const MAX_IMAGES = 9;
+ if (selectedImages.length >= MAX_IMAGES) {
+ Alert.alert('提示', `最多添加${MAX_IMAGES}张图片`);
+ return;
+ }
+
+ const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
+ if (!permissionResult.granted) {
+ Alert.alert('提示', '需要访问相册权限才能选择图片');
+ return;
+ }
+
+ try {
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
+ allowsEditing: false,
+ quality: 0.8,
+ allowsMultipleSelection: true,
+ selectionLimit: MAX_IMAGES - selectedImages.length,
+ });
+
+ if (!result.canceled && result.assets?.length > 0) {
+ const newImages = result.assets.map((asset) => ({
+ uri: asset.uri,
+ mimeType: asset.mimeType,
+ }));
+ setSelectedImages((prev) => [...prev, ...newImages].slice(0, MAX_IMAGES));
+ }
+ } catch (error) {
+ console.warn('[ChannelDetail] 选择图片失败:', error);
+ Alert.alert('错误', '选择图片失败,请重试');
+ }
+ }, [selectedImages.length]);
+
+ // 删除选中的图片
+ const handleRemoveImage = useCallback((index) => {
+ setSelectedImages((prev) => prev.filter((_, idx) => idx !== index));
+ }, []);
+
// 设置导航标题
useEffect(() => {
navigation.setOptions({
@@ -263,8 +310,12 @@ const ChannelDetail = ({ route, navigation }) => {
// 发送消息
const handleSend = useCallback(async () => {
- const content = inputText.trim();
- if (!content || isSending || !channel?.id) return;
+ const textContent = inputText.trim();
+ const hasImages = selectedImages.length > 0;
+
+ // 必须有文字或图片才能发送
+ if (!textContent && !hasImages) return;
+ if (isSending || !channel?.id) return;
setIsSending(true);
Keyboard.dismiss();
@@ -273,24 +324,63 @@ const ChannelDetail = ({ route, navigation }) => {
stopTyping(channel.id);
try {
+ let finalContent = textContent;
+
+ // 如果有图片,先上传
+ if (hasImages) {
+ setIsUploadingImages(true);
+ const uploadedUrls = [];
+
+ for (const image of selectedImages) {
+ try {
+ const result = await uploadService.uploadImage(image);
+ if (result.url) {
+ uploadedUrls.push(result.url);
+ }
+ } catch (error) {
+ console.warn('[ChannelDetail] 图片上传失败:', error);
+ }
+ }
+
+ setIsUploadingImages(false);
+
+ // 将图片 URL 转换为 Markdown 格式追加到内容后
+ if (uploadedUrls.length > 0) {
+ const imageMarkdown = uploadedUrls
+ .map((url) => ``)
+ .join('\n');
+ finalContent = finalContent
+ ? finalContent + '\n\n' + imageMarkdown
+ : imageMarkdown;
+ } else if (!textContent) {
+ // 没有文字且图片全部上传失败
+ Alert.alert('发送失败', '图片上传失败,请重试');
+ setIsSending(false);
+ return;
+ }
+ }
+
await dispatch(
sendMessage({
channelId: channel.id,
- data: { content },
+ data: { content: finalContent },
})
).unwrap();
setInputText('');
+ setSelectedImages([]);
// 滚动到底部
setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: true });
}, 100);
} catch (error) {
console.error('发送消息失败:', error);
+ Alert.alert('发送失败', '消息发送失败,请重试');
} finally {
setIsSending(false);
+ setIsUploadingImages(false);
}
- }, [dispatch, channel?.id, inputText, isSending, stopTyping]);
+ }, [dispatch, channel?.id, inputText, selectedImages, isSending, stopTyping]);
// 渲染日期分隔符
const renderDateDivider = (date) => (
@@ -466,73 +556,114 @@ const ChannelDetail = ({ route, navigation }) => {
);
};
+ // 渲染选中的图片预览
+ const renderSelectedImages = () => {
+ if (selectedImages.length === 0) return null;
+
+ return (
+
+
+ {selectedImages.map((image, index) => (
+
+
+ {isUploadingImages && (
+
+
+
+ )}
+ handleRemoveImage(index)}
+ hitSlop={10}
+ >
+
+
+
+ ))}
+
+
+ );
+ };
+
// 渲染输入框
- const renderInput = () => (
-
- {renderTypingIndicator()}
-
- {/* 附件按钮 */}
-
-
-
+ const renderInput = () => {
+ const canSend = inputText.trim() || selectedImages.length > 0;
- {/* 输入框 */}
-
-
-
-
- {/* 发送按钮 */}
-
- {isSending ? (
-
- ) : (
+ return (
+
+ {renderTypingIndicator()}
+ {renderSelectedImages()}
+
+ {/* 图片选择按钮 */}
+
0 ? 'primary.500' : 'gray.500'}
/>
- )}
-
-
-
- );
+
+
+ {/* 输入框 */}
+
+
+
+
+ {/* 发送按钮 */}
+
+ {isSending ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+ };
return (
{
const [tags, setTags] = useState([]);
const [customTag, setCustomTag] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
+ const [images, setImages] = useState([]); // { uri, uploading, uploaded, url, error }
+ const [isUploadingImage, setIsUploadingImage] = useState(false);
const TITLE_MAX = 100;
const CONTENT_MAX = 5000;
const TAGS_MAX = 5;
+ const IMAGES_MAX = 9;
// 添加标签
const handleAddTag = useCallback((tag) => {
@@ -81,6 +88,138 @@ const CreatePost = ({ route, navigation }) => {
setCustomTag('');
}, [customTag, handleAddTag]);
+ // 选择图片
+ const handlePickImage = useCallback(async () => {
+ if (images.length >= IMAGES_MAX) {
+ Alert.alert('提示', `最多添加${IMAGES_MAX}张图片`);
+ return;
+ }
+
+ // 请求权限
+ const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
+ if (!permissionResult.granted) {
+ Alert.alert('提示', '需要访问相册权限才能选择图片');
+ return;
+ }
+
+ try {
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
+ allowsEditing: false,
+ quality: 0.8,
+ allowsMultipleSelection: true,
+ selectionLimit: IMAGES_MAX - images.length,
+ });
+
+ if (!result.canceled && result.assets?.length > 0) {
+ // 添加新图片到状态
+ const newImages = result.assets.map((asset) => ({
+ uri: asset.uri,
+ mimeType: asset.mimeType,
+ uploading: false,
+ uploaded: false,
+ url: null,
+ error: null,
+ }));
+ setImages((prev) => [...prev, ...newImages].slice(0, IMAGES_MAX));
+ }
+ } catch (error) {
+ console.warn('[CreatePost] 选择图片失败:', error);
+ Alert.alert('错误', '选择图片失败,请重试');
+ }
+ }, [images.length]);
+
+ // 上传单张图片
+ const uploadSingleImage = useCallback(async (imageIndex) => {
+ const image = images[imageIndex];
+ if (!image || image.uploaded || image.uploading) return;
+
+ setImages((prev) =>
+ prev.map((img, idx) =>
+ idx === imageIndex ? { ...img, uploading: true, error: null } : img
+ )
+ );
+
+ try {
+ const result = await uploadService.uploadImage(image);
+ setImages((prev) =>
+ prev.map((img, idx) =>
+ idx === imageIndex
+ ? { ...img, uploading: false, uploaded: true, url: result.url }
+ : img
+ )
+ );
+ } catch (error) {
+ console.warn('[CreatePost] 上传图片失败:', error);
+ setImages((prev) =>
+ prev.map((img, idx) =>
+ idx === imageIndex
+ ? { ...img, uploading: false, error: error.message || '上传失败' }
+ : img
+ )
+ );
+ }
+ }, [images]);
+
+ // 删除图片
+ const handleRemoveImage = useCallback((index) => {
+ setImages((prev) => prev.filter((_, idx) => idx !== index));
+ }, []);
+
+ // 重试上传
+ const handleRetryUpload = useCallback((index) => {
+ setImages((prev) =>
+ prev.map((img, idx) =>
+ idx === index ? { ...img, error: null, uploading: false, uploaded: false } : img
+ )
+ );
+ // 延迟触发上传
+ setTimeout(() => uploadSingleImage(index), 100);
+ }, [uploadSingleImage]);
+
+ // 上传所有未上传的图片,返回上传结果
+ const uploadAllImages = useCallback(async (imagesToUpload) => {
+ const results = [];
+
+ for (let i = 0; i < imagesToUpload.length; i++) {
+ const image = imagesToUpload[i];
+ if (image.uploaded && image.url) {
+ results.push({ success: true, url: image.url });
+ continue;
+ }
+
+ try {
+ setImages((prev) =>
+ prev.map((img, idx) =>
+ idx === i ? { ...img, uploading: true, error: null } : img
+ )
+ );
+
+ const result = await uploadService.uploadImage(image);
+
+ setImages((prev) =>
+ prev.map((img, idx) =>
+ idx === i ? { ...img, uploading: false, uploaded: true, url: result.url } : img
+ )
+ );
+
+ results.push({ success: true, url: result.url });
+ } catch (error) {
+ console.warn('[CreatePost] 上传图片失败:', error);
+
+ setImages((prev) =>
+ prev.map((img, idx) =>
+ idx === i ? { ...img, uploading: false, error: error.message || '上传失败' } : img
+ )
+ );
+
+ results.push({ success: false, error: error.message });
+ }
+ }
+
+ return results;
+ }, []);
+
// 验证表单
const validateForm = useCallback(() => {
if (!title.trim()) {
@@ -102,20 +241,15 @@ const CreatePost = ({ route, navigation }) => {
return true;
}, [title, content]);
- // 提交帖子
- const handleSubmit = useCallback(async () => {
- if (!validateForm() || isSubmitting) return;
-
- setIsSubmitting(true);
- Keyboard.dismiss();
-
+ // 实际提交帖子
+ const submitPost = useCallback(async (finalContent) => {
try {
await dispatch(
createPost({
channelId: channel.id,
data: {
title: title.trim(),
- content: content.trim(),
+ content: finalContent,
tags,
},
})
@@ -132,7 +266,64 @@ const CreatePost = ({ route, navigation }) => {
} finally {
setIsSubmitting(false);
}
- }, [dispatch, channel.id, title, content, tags, validateForm, isSubmitting, navigation]);
+ }, [dispatch, channel.id, title, tags, navigation]);
+
+ // 提交帖子
+ const handleSubmit = useCallback(async () => {
+ if (!validateForm() || isSubmitting) return;
+
+ setIsSubmitting(true);
+ Keyboard.dismiss();
+
+ try {
+ let finalContent = content.trim();
+ let uploadResults = [];
+
+ // 如果有图片,先上传所有图片
+ if (images.length > 0) {
+ setIsUploadingImage(true);
+ uploadResults = await uploadAllImages(images);
+ setIsUploadingImage(false);
+
+ // 获取成功上传的图片 URL
+ const successUrls = uploadResults
+ .filter((r) => r.success && r.url)
+ .map((r) => r.url);
+
+ if (successUrls.length > 0) {
+ // 将图片转换为 Markdown 格式追加到内容后面
+ const imageMarkdown = successUrls
+ .map((url) => ``)
+ .join('\n');
+ finalContent = finalContent + '\n\n' + imageMarkdown;
+ }
+
+ // 检查是否有上传失败的图片
+ const failedCount = uploadResults.filter((r) => !r.success).length;
+ if (failedCount > 0) {
+ Alert.alert(
+ '部分图片上传失败',
+ `${failedCount}张图片上传失败,是否继续发布?`,
+ [
+ { text: '取消', style: 'cancel', onPress: () => setIsSubmitting(false) },
+ {
+ text: '继续发布',
+ onPress: async () => {
+ await submitPost(finalContent);
+ },
+ },
+ ]
+ );
+ return;
+ }
+ }
+
+ await submitPost(finalContent);
+ } catch (error) {
+ Alert.alert('发布失败', error.message || '请稍后重试');
+ setIsSubmitting(false);
+ }
+ }, [content, images, validateForm, isSubmitting, uploadAllImages, submitPost]);
// 设置导航
React.useEffect(() => {
@@ -263,6 +454,77 @@ const CreatePost = ({ route, navigation }) => {
/>
+ {/* 图片选择 */}
+
+
+
+ 图片
+
+
+ {images.length}/{IMAGES_MAX}
+
+
+
+ {/* 图片预览网格 */}
+
+ {images.map((image, index) => (
+
+
+ {/* 上传状态遮罩 */}
+ {image.uploading && (
+
+
+
+ )}
+ {/* 上传成功标记 */}
+ {image.uploaded && (
+
+
+
+ )}
+ {/* 上传失败标记 */}
+ {image.error && (
+ handleRetryUpload(index)}
+ >
+
+
+
+ 点击重试
+
+
+
+ )}
+ {/* 删除按钮 */}
+ handleRemoveImage(index)}
+ hitSlop={10}
+ >
+
+
+
+ ))}
+
+ {/* 添加图片按钮 */}
+ {images.length < IMAGES_MAX && (
+
+
+
+
+ 添加图片
+
+
+
+ )}
+
+
+
{/* 标签选择 */}
@@ -391,6 +653,52 @@ const styles = StyleSheet.create({
content: {
flexGrow: 1,
},
+ imagePreview: {
+ width: 80,
+ height: 80,
+ borderRadius: 8,
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
+ },
+ imageOverlay: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
+ borderRadius: 8,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ imageSuccessBadge: {
+ position: 'absolute',
+ bottom: 4,
+ right: 4,
+ backgroundColor: '#22C55E',
+ borderRadius: 10,
+ width: 16,
+ height: 16,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ imageDeleteBtn: {
+ position: 'absolute',
+ top: -6,
+ right: -6,
+ backgroundColor: '#0F172A',
+ borderRadius: 10,
+ },
+ addImageBtn: {
+ width: 80,
+ height: 80,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 0.2)',
+ borderStyle: 'dashed',
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
+ },
});
export default CreatePost;
diff --git a/MeAgent/src/screens/StockDetail/StockDetailScreen.js b/MeAgent/src/screens/StockDetail/StockDetailScreen.js
index bc467ccf..912de249 100644
--- a/MeAgent/src/screens/StockDetail/StockDetailScreen.js
+++ b/MeAgent/src/screens/StockDetail/StockDetailScreen.js
@@ -37,6 +37,7 @@ import {
selectCurrentStock,
selectMinuteData,
selectMinutePrevClose,
+ selectIsTrading,
selectKlineData,
selectChartType,
selectOrderBook,
@@ -68,6 +69,7 @@ const StockDetailScreen = () => {
const currentStock = useSelector(selectCurrentStock);
const minuteData = useSelector(selectMinuteData);
const minutePrevClose = useSelector(selectMinutePrevClose);
+ const isTrading = useSelector(selectIsTrading);
const klineData = useSelector(selectKlineData);
const chartType = useSelector(selectChartType);
const orderBook = useSelector(selectOrderBook);
@@ -185,6 +187,26 @@ const StockDetailScreen = () => {
}
}, [chartType, realtimeQuote, fallbackOrderBook, loadOrderBookFallback]);
+ // 交易时间内自动刷新分时数据和股票详情(每 30 秒)
+ useEffect(() => {
+ if (!isTrading || chartType !== 'minute' || !stockCode) {
+ return;
+ }
+
+ console.log('[StockDetailScreen] 交易时间,启动自动刷新');
+
+ const refreshInterval = setInterval(() => {
+ console.log('[StockDetailScreen] 自动刷新数据');
+ dispatch(fetchMinuteData(stockCode));
+ dispatch(fetchStockDetail(stockCode));
+ }, 30000); // 每 30 秒刷新一次
+
+ return () => {
+ console.log('[StockDetailScreen] 停止自动刷新');
+ clearInterval(refreshInterval);
+ };
+ }, [isTrading, chartType, stockCode, dispatch]);
+
// 切换图表类型
const handleChartTypeChange = useCallback((type) => {
dispatch(setChartType(type));
diff --git a/MeAgent/src/screens/StockDetail/components/MinuteChart.js b/MeAgent/src/screens/StockDetail/components/MinuteChart.js
index 11e6adea..24b42f09 100644
--- a/MeAgent/src/screens/StockDetail/components/MinuteChart.js
+++ b/MeAgent/src/screens/StockDetail/components/MinuteChart.js
@@ -47,8 +47,14 @@ const formatPrice = (price) => {
return Number(price).toFixed(2);
};
-// 格式化涨跌幅
-const formatPercent = (current, preClose) => {
+// 格式化涨跌幅(优先使用已计算的涨跌幅)
+const formatPercent = (current, preClose, changePct = null) => {
+ // 如果有已计算的涨跌幅,直接使用
+ if (changePct !== null && changePct !== undefined) {
+ const sign = changePct >= 0 ? '+' : '';
+ return `${sign}${Number(changePct).toFixed(2)}%`;
+ }
+ // 否则根据当前价和昨收价计算
if (!current || !preClose) return '0.00%';
const change = ((current - preClose) / preClose) * 100;
const sign = change >= 0 ? '+' : '';
@@ -156,6 +162,7 @@ const MinuteChart = memo(({ data = [], preClose, loading }) => {
time: d.time,
volume: d.volume,
avgPrice: d.avg_price || d.average_price,
+ changePct: d.change_pct, // 保存 API 返回的涨跌幅
};
});
@@ -302,7 +309,7 @@ const MinuteChart = memo(({ data = [], preClose, loading }) => {
涨跌
- {formatPercent(activePoint.price, chartData.preClose)}
+ {formatPercent(activePoint.price, chartData.preClose, activePoint.changePct)}
{activePoint.avgPrice && (
diff --git a/MeAgent/src/screens/Watchlist/WatchlistStockItem.js b/MeAgent/src/screens/Watchlist/WatchlistStockItem.js
index 66865345..ea2eb128 100644
--- a/MeAgent/src/screens/Watchlist/WatchlistStockItem.js
+++ b/MeAgent/src/screens/Watchlist/WatchlistStockItem.js
@@ -165,17 +165,21 @@ const WatchlistStockItem = memo(({
>
{stock_name || stock_code}
-
-
- {stock_code}
-
-
- {/* 价格和涨跌幅 */}
-
+
+ {stock_code}
+
+ {/* 现价和涨跌幅 */}
+
+ {formatPrice(current_price)}
+
+
{formatChange(change_percent)}
diff --git a/MeAgent/src/services/communityService.js b/MeAgent/src/services/communityService.js
index 480dfdd3..ab077a88 100644
--- a/MeAgent/src/services/communityService.js
+++ b/MeAgent/src/services/communityService.js
@@ -507,6 +507,50 @@ export const memberService = {
},
};
+/**
+ * 上传服务
+ */
+export const uploadService = {
+ /**
+ * 上传图片
+ * @param {object} imageAsset - expo-image-picker 返回的图片资产
+ * @returns {Promise