feat: 更新教练页面和消息结构,支持附件功能
- 在教练页面中引入附件类型,支持图片、视频和文件的上传和展示 - 重构消息数据结构,确保消息包含附件信息 - 优化消息发送逻辑,支持发送包含图片的消息 - 更新界面样式,提升附件展示效果 - 删除不再使用的旧图标,替换为新的应用图标和启动画面
@@ -5,9 +5,9 @@ import { Text, TouchableOpacity, View } from 'react-native';
|
|||||||
|
|
||||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { ROUTES } from '@/constants/Routes';
|
||||||
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { ROUTES } from '@/constants/Routes';
|
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
@@ -16,6 +16,7 @@ export default function TabLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
initialRouteName="coach"
|
||||||
screenOptions={({ route }) => {
|
screenOptions={({ route }) => {
|
||||||
const routeName = route.name;
|
const routeName = route.name;
|
||||||
const isSelected = (routeName === 'index' && pathname === ROUTES.TAB_HOME) ||
|
const isSelected = (routeName === 'index' && pathname === ROUTES.TAB_HOME) ||
|
||||||
|
|||||||
@@ -35,10 +35,31 @@ import { ActionSheet } from '../../components/ui/ActionSheet';
|
|||||||
|
|
||||||
type Role = 'user' | 'assistant';
|
type Role = 'user' | 'assistant';
|
||||||
|
|
||||||
|
// 附件类型枚举
|
||||||
|
type AttachmentType = 'image' | 'video' | 'file';
|
||||||
|
|
||||||
|
// 附件数据结构
|
||||||
|
type MessageAttachment = {
|
||||||
|
id: string;
|
||||||
|
type: AttachmentType;
|
||||||
|
url: string;
|
||||||
|
localUri?: string; // 本地URI,用于上传中的显示
|
||||||
|
filename?: string;
|
||||||
|
size?: number;
|
||||||
|
duration?: number; // 视频时长(秒)
|
||||||
|
thumbnail?: string; // 视频缩略图
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
uploadProgress?: number; // 上传进度 0-1
|
||||||
|
uploadError?: string; // 上传错误信息
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重构后的消息数据结构
|
||||||
type ChatMessage = {
|
type ChatMessage = {
|
||||||
id: string;
|
id: string;
|
||||||
role: Role;
|
role: Role;
|
||||||
content: string;
|
content: string; // 文本内容
|
||||||
|
attachments?: MessageAttachment[]; // 附件列表
|
||||||
};
|
};
|
||||||
|
|
||||||
// 卡片类型常量定义
|
// 卡片类型常量定义
|
||||||
@@ -256,7 +277,15 @@ export default function CoachScreen() {
|
|||||||
const cached = await loadAiCoachSessionCache();
|
const cached = await loadAiCoachSessionCache();
|
||||||
if (isMounted && cached && Array.isArray(cached.messages) && cached.messages.length > 0) {
|
if (isMounted && cached && Array.isArray(cached.messages) && cached.messages.length > 0) {
|
||||||
setConversationId(cached.conversationId);
|
setConversationId(cached.conversationId);
|
||||||
setMessages(cached.messages.filter(msg => msg && typeof msg === 'object' && msg.role && msg.content) as ChatMessage[]);
|
// 确保缓存的消息符合新的 ChatMessage 结构
|
||||||
|
const validMessages = cached.messages
|
||||||
|
.filter(msg => msg && typeof msg === 'object' && msg.role && msg.content)
|
||||||
|
.map(msg => ({
|
||||||
|
...msg,
|
||||||
|
// 确保 attachments 字段存在,对于旧的缓存消息可能没有这个字段
|
||||||
|
attachments: (msg as any).attachments || undefined,
|
||||||
|
})) as ChatMessage[];
|
||||||
|
setMessages(validMessages);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isMounted) scrollToEnd();
|
if (isMounted) scrollToEnd();
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -463,7 +492,14 @@ export default function CoachScreen() {
|
|||||||
}
|
}
|
||||||
const mapped: ChatMessage[] = (detail.messages || [])
|
const mapped: ChatMessage[] = (detail.messages || [])
|
||||||
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
||||||
.map((m, idx) => ({ id: `${m.role}_${idx}_${Date.now()}`, role: m.role as Role, content: m.content || '' }));
|
.map((m, idx) => ({
|
||||||
|
id: `${m.role}_${idx}_${Date.now()}`,
|
||||||
|
role: m.role as Role,
|
||||||
|
content: m.content || '',
|
||||||
|
// 对于历史消息,暂时不包含附件信息,因为服务器端可能还没有返回附件数据
|
||||||
|
// 如果将来服务器支持返回附件信息,可以在这里添加映射逻辑
|
||||||
|
attachments: undefined,
|
||||||
|
}));
|
||||||
setConversationId(detail.conversationId);
|
setConversationId(detail.conversationId);
|
||||||
setMessages(mapped.length ? mapped : [{ id: 'm_welcome', role: 'assistant', content: generateWelcomeMessage() }]);
|
setMessages(mapped.length ? mapped : [{ id: 'm_welcome', role: 'assistant', content: generateWelcomeMessage() }]);
|
||||||
setHistoryVisible(false);
|
setHistoryVisible(false);
|
||||||
@@ -493,9 +529,9 @@ export default function CoachScreen() {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendStream(text: string) {
|
async function sendStream(text: string, imageUrls: string[] = []) {
|
||||||
const tokenExists = !!getAuthToken();
|
const tokenExists = !!getAuthToken();
|
||||||
try { console.log('[AI_CHAT][ui] send start', { tokenExists, conversationId, textPreview: text.slice(0, 50) }); } catch { }
|
try { console.log('[AI_CHAT][ui] send start', { tokenExists, conversationId, textPreview: text.slice(0, 50), imageUrls }); } catch { }
|
||||||
|
|
||||||
// 终止上一次未完成的流
|
// 终止上一次未完成的流
|
||||||
if (streamAbortRef.current) {
|
if (streamAbortRef.current) {
|
||||||
@@ -510,6 +546,7 @@ export default function CoachScreen() {
|
|||||||
const body = {
|
const body = {
|
||||||
conversationId: cid,
|
conversationId: cid,
|
||||||
messages: [...historyForServer, { role: 'user' as const, content: text }],
|
messages: [...historyForServer, { role: 'user' as const, content: text }],
|
||||||
|
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
||||||
stream: true,
|
stream: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -517,7 +554,19 @@ export default function CoachScreen() {
|
|||||||
const assistantId = `a_${Date.now()}`;
|
const assistantId = `a_${Date.now()}`;
|
||||||
const userMsgId = `u_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
const userMsgId = `u_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
|
||||||
const userMsg: ChatMessage = { id: userMsgId, role: 'user', content: text };
|
// 构建包含附件的用户消息
|
||||||
|
const attachments = imageUrls.map((url, index) => ({
|
||||||
|
id: `img_${Date.now()}_${index}`,
|
||||||
|
type: 'image' as AttachmentType,
|
||||||
|
url,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const userMsg: ChatMessage = {
|
||||||
|
id: userMsgId,
|
||||||
|
role: 'user',
|
||||||
|
content: text,
|
||||||
|
attachments: attachments.length > 0 ? attachments : undefined,
|
||||||
|
};
|
||||||
shouldAutoScrollRef.current = isAtBottom;
|
shouldAutoScrollRef.current = isAtBottom;
|
||||||
setMessages((m) => [...m, userMsg, { id: assistantId, role: 'assistant', content: '' }]);
|
setMessages((m) => [...m, userMsg, { id: assistantId, role: 'assistant', content: '' }]);
|
||||||
pendingAssistantIdRef.current = assistantId;
|
pendingAssistantIdRef.current = assistantId;
|
||||||
@@ -614,12 +663,10 @@ export default function CoachScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const urls = selectedImages.map(img => img.uploadedUrl).filter(Boolean);
|
const imageUrls = selectedImages.map(img => img.uploadedUrl).filter(Boolean) as string[];
|
||||||
const mdImages = urls.map((u) => ``).join('\n\n');
|
|
||||||
const composed = [trimmed, mdImages].filter(Boolean).join('\n\n');
|
|
||||||
setInput('');
|
setInput('');
|
||||||
setSelectedImages([]);
|
setSelectedImages([]);
|
||||||
await sendStream(composed);
|
await sendStream(trimmed, imageUrls);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Alert.alert('发送失败', e?.message || '消息发送失败,请稍后重试');
|
Alert.alert('发送失败', e?.message || '消息发送失败,请稍后重试');
|
||||||
}
|
}
|
||||||
@@ -706,6 +753,88 @@ export default function CoachScreen() {
|
|||||||
setSelectedImages((prev) => prev.filter((it) => it.id !== id));
|
setSelectedImages((prev) => prev.filter((it) => it.id !== id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 渲染单个附件
|
||||||
|
function renderAttachment(attachment: MessageAttachment, isUser: boolean) {
|
||||||
|
const { type, url, localUri, uploadProgress, uploadError, width, height, filename } = attachment;
|
||||||
|
|
||||||
|
if (type === 'image') {
|
||||||
|
const imageUri = url || localUri;
|
||||||
|
if (!imageUri) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View key={attachment.id} style={styles.attachmentContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
accessibilityRole="imagebutton"
|
||||||
|
onPress={() => setPreviewImageUri(imageUri)}
|
||||||
|
style={styles.imageAttachment}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri: imageUri }}
|
||||||
|
style={[
|
||||||
|
styles.attachmentImage,
|
||||||
|
width && height ? { aspectRatio: width / height } : {}
|
||||||
|
]}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
{uploadProgress !== undefined && uploadProgress < 1 && (
|
||||||
|
<View style={styles.attachmentProgressOverlay}>
|
||||||
|
<Text style={styles.attachmentProgressText}>
|
||||||
|
{Math.round(uploadProgress * 100)}%
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{uploadError && (
|
||||||
|
<View style={styles.attachmentErrorOverlay}>
|
||||||
|
<Text style={styles.attachmentErrorText}>上传失败</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'video') {
|
||||||
|
// 视频附件的实现
|
||||||
|
return (
|
||||||
|
<View key={attachment.id} style={styles.attachmentContainer}>
|
||||||
|
<TouchableOpacity style={styles.videoAttachment}>
|
||||||
|
<View style={styles.videoPlaceholder}>
|
||||||
|
<Ionicons name="play-circle" size={48} color="rgba(255,255,255,0.9)" />
|
||||||
|
<Text style={styles.videoFilename}>{filename || '视频文件'}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'file') {
|
||||||
|
// 文件附件的实现
|
||||||
|
return (
|
||||||
|
<View key={attachment.id} style={styles.attachmentContainer}>
|
||||||
|
<TouchableOpacity style={styles.fileAttachment}>
|
||||||
|
<Ionicons name="document-outline" size={24} color="#687076" />
|
||||||
|
<Text style={styles.fileFilename} numberOfLines={1}>
|
||||||
|
{filename || 'unknown_file'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染所有附件
|
||||||
|
function renderAttachments(attachments: MessageAttachment[], isUser: boolean) {
|
||||||
|
if (!attachments || attachments.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.attachmentsContainer}>
|
||||||
|
{attachments.map(attachment => renderAttachment(attachment, isUser))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderItem({ item }: { item: ChatMessage }) {
|
function renderItem({ item }: { item: ChatMessage }) {
|
||||||
const isUser = item.role === 'user';
|
const isUser = item.role === 'user';
|
||||||
return (
|
return (
|
||||||
@@ -726,6 +855,7 @@ export default function CoachScreen() {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{renderBubbleContent(item)}
|
{renderBubbleContent(item)}
|
||||||
|
{renderAttachments(item.attachments || [], isUser)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -900,7 +1030,7 @@ export default function CoachScreen() {
|
|||||||
|
|
||||||
// 在对话中插入"确认消息"并发送给教练
|
// 在对话中插入"确认消息"并发送给教练
|
||||||
const textMsg = `#记体重:\n\n${val} kg`;
|
const textMsg = `#记体重:\n\n${val} kg`;
|
||||||
await send(textMsg);
|
await sendStream(textMsg);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[AI_CHAT] Error handling weight submission:', e);
|
console.error('[AI_CHAT] Error handling weight submission:', e);
|
||||||
Alert.alert('保存失败', e?.message || '请稍后重试');
|
Alert.alert('保存失败', e?.message || '请稍后重试');
|
||||||
@@ -992,9 +1122,9 @@ export default function CoachScreen() {
|
|||||||
// 移除饮食选择卡片
|
// 移除饮食选择卡片
|
||||||
setMessages((prev) => prev.filter(msg => msg.id !== currentCardId));
|
setMessages((prev) => prev.filter(msg => msg.id !== currentCardId));
|
||||||
|
|
||||||
// 发送包含图片的饮食记录消息
|
// 发送包含图片的饮食记录消息,图片通过 imageUrls 参数传递
|
||||||
const dietMsg = `#记饮食:\n\n`;
|
const dietMsg = `#记饮食:请分析这张食物照片的营养成分和热量`;
|
||||||
await send(dietMsg);
|
await sendStream(dietMsg, [url]);
|
||||||
} catch (uploadError) {
|
} catch (uploadError) {
|
||||||
console.error('[DIET] 图片上传失败:', uploadError);
|
console.error('[DIET] 图片上传失败:', uploadError);
|
||||||
Alert.alert('上传失败', '图片上传失败,请重试');
|
Alert.alert('上传失败', '图片上传失败,请重试');
|
||||||
@@ -1031,7 +1161,7 @@ export default function CoachScreen() {
|
|||||||
|
|
||||||
// 发送饮食记录消息
|
// 发送饮食记录消息
|
||||||
const dietMsg = `记录了今日饮食:${trimmedText}`;
|
const dietMsg = `记录了今日饮食:${trimmedText}`;
|
||||||
await send(dietMsg);
|
await sendStream(dietMsg);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[DIET] 提交饮食记录失败:', e);
|
console.error('[DIET] 提交饮食记录失败:', e);
|
||||||
Alert.alert('提交失败', e?.message || '提交失败,请重试');
|
Alert.alert('提交失败', e?.message || '提交失败,请重试');
|
||||||
@@ -1278,7 +1408,6 @@ export default function CoachScreen() {
|
|||||||
options={[
|
options={[
|
||||||
{ id: 'camera', title: '拍照', onPress: handleCameraPhoto },
|
{ id: 'camera', title: '拍照', onPress: handleCameraPhoto },
|
||||||
{ id: 'library', title: '从相册选择', onPress: handleLibraryPhoto },
|
{ id: 'library', title: '从相册选择', onPress: handleLibraryPhoto },
|
||||||
{ id: 'cancel', title: '取消', onPress: () => setShowDietPhotoActionSheet(false) }
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -1663,6 +1792,87 @@ const styles = StyleSheet.create({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
},
|
},
|
||||||
|
// 附件相关样式
|
||||||
|
attachmentsContainer: {
|
||||||
|
marginTop: 8,
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
attachmentContainer: {
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
imageAttachment: {
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
attachmentImage: {
|
||||||
|
width: '100%',
|
||||||
|
minHeight: 120,
|
||||||
|
maxHeight: 200,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
attachmentProgressOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
attachmentProgressText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
attachmentErrorOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(255,0,0,0.4)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
attachmentErrorText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
videoAttachment: {
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.1)',
|
||||||
|
},
|
||||||
|
videoPlaceholder: {
|
||||||
|
height: 120,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
},
|
||||||
|
videoFilename: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 12,
|
||||||
|
marginTop: 4,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
fileAttachment: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.06)',
|
||||||
|
borderRadius: 8,
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
fileFilename: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#192126',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const markdownStyles = {
|
const markdownStyles = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "logo.png",
|
"filename" : "未命名项目 (1).jpeg",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 441 KiB |
|
After Width: | Height: | Size: 304 KiB |
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "logo.png",
|
"filename" : "未命名项目 (1).jpeg",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "logo 1.png",
|
"filename" : "未命名项目.jpeg",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "logo 2.png",
|
"filename" : "未命名项目 1.jpeg",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 441 KiB |
|
Before Width: | Height: | Size: 441 KiB |
|
Before Width: | Height: | Size: 441 KiB |
BIN
ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/未命名项目 (1).jpeg
vendored
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/未命名项目 1.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 745 KiB |
BIN
ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/未命名项目.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 745 KiB |
@@ -1,15 +1,17 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "logo.png",
|
"filename" : "未命名项目 (1).jpeg",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "未命名项目.jpeg",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "未命名项目 1.jpeg",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 396 KiB |
BIN
ios/digitalpilates/Images.xcassets/logo.imageset/未命名项目 (1).jpeg
vendored
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
ios/digitalpilates/Images.xcassets/logo.imageset/未命名项目 1.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 745 KiB |
BIN
ios/digitalpilates/Images.xcassets/logo.imageset/未命名项目.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 745 KiB |