553 行
15 KiB
TypeScript
553 行
15 KiB
TypeScript
|
|
import React, {startTransition, useEffect, useRef, useState} from 'react';
|
|||
|
|
import {
|
|||
|
|
Alert,
|
|||
|
|
Pressable,
|
|||
|
|
ScrollView,
|
|||
|
|
StatusBar,
|
|||
|
|
StyleSheet,
|
|||
|
|
Text,
|
|||
|
|
TextInput,
|
|||
|
|
useColorScheme,
|
|||
|
|
View,
|
|||
|
|
} from 'react-native';
|
|||
|
|
import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context';
|
|||
|
|
import {ImSDK, type ImEventListener, type ImMessage, UpdateSDK, XuqmSDK} from '@xuqm/rn-sdk';
|
|||
|
|
|
|||
|
|
const APP_ID = 'ak_demo_chat'
|
|||
|
|
const MODULE_ID = 'chat-home'
|
|||
|
|
const CURRENT_APP_VERSION_CODE = 1
|
|||
|
|
const CURRENT_RN_VERSION = '1.0.0'
|
|||
|
|
const API_BASE_URL = 'https://sentry.xuqinmin.com'
|
|||
|
|
const IM_WS_URL = 'wss://sentry.xuqinmin.com/ws/im'
|
|||
|
|
|
|||
|
|
type DemoUser = {
|
|||
|
|
id: string
|
|||
|
|
nickname: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const DEMO_USERS: DemoUser[] = [
|
|||
|
|
{id: 'demo_alice', nickname: 'Alice'},
|
|||
|
|
{id: 'demo_bob', nickname: 'Bob'},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
function App() {
|
|||
|
|
const isDarkMode = useColorScheme() === 'dark'
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<SafeAreaProvider>
|
|||
|
|
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
|||
|
|
<SafeAreaView style={styles.safeArea}>
|
|||
|
|
<DemoConsole />
|
|||
|
|
</SafeAreaView>
|
|||
|
|
</SafeAreaProvider>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function DemoConsole() {
|
|||
|
|
const [currentUser, setCurrentUser] = useState<DemoUser>(DEMO_USERS[0])
|
|||
|
|
const [connected, setConnected] = useState(false)
|
|||
|
|
const [messages, setMessages] = useState<ImMessage[]>([])
|
|||
|
|
const [draft, setDraft] = useState('你好,我是 RN Demo。')
|
|||
|
|
const [activityLog, setActivityLog] = useState<string[]>([])
|
|||
|
|
const [appUpdateResult, setAppUpdateResult] = useState('点击“检查 App 更新”查看线上版本结果')
|
|||
|
|
const [rnUpdateResult, setRnUpdateResult] = useState('点击“检查插件更新”查看热更新结果')
|
|||
|
|
const [cachedBundleSummary, setCachedBundleSummary] = useState('暂无本地缓存')
|
|||
|
|
const activeUserRef = useRef(currentUser)
|
|||
|
|
const peerUser = DEMO_USERS.find(user => user.id !== currentUser.id) ?? DEMO_USERS[1]
|
|||
|
|
|
|||
|
|
const appendLog = (message: string) => {
|
|||
|
|
startTransition(() => {
|
|||
|
|
setActivityLog(prev => [message, ...prev].slice(0, 12))
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mergeMessage = (message: ImMessage) => {
|
|||
|
|
startTransition(() => {
|
|||
|
|
setMessages(prev => {
|
|||
|
|
const exists = prev.some(item => item.id === message.id)
|
|||
|
|
if (exists) {
|
|||
|
|
return prev.map(item => (item.id === message.id ? message : item))
|
|||
|
|
}
|
|||
|
|
return [...prev, message].sort((a, b) => a.createdAt.localeCompare(b.createdAt))
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const connectUser = async (user: DemoUser) => {
|
|||
|
|
try {
|
|||
|
|
ImSDK.disconnect()
|
|||
|
|
setConnected(false)
|
|||
|
|
setMessages([])
|
|||
|
|
await ImSDK.login(user.id, user.nickname)
|
|||
|
|
appendLog(`已登录 IM:${user.nickname}`)
|
|||
|
|
const history = await ImSDK.fetchHistory(peerUser.id, 0, 50)
|
|||
|
|
history
|
|||
|
|
.slice()
|
|||
|
|
.reverse()
|
|||
|
|
.forEach(item => mergeMessage(item))
|
|||
|
|
setConnected(ImSDK.isConnected())
|
|||
|
|
} catch (error) {
|
|||
|
|
const message = error instanceof Error ? error.message : 'IM 登录失败'
|
|||
|
|
appendLog(`连接失败:${message}`)
|
|||
|
|
Alert.alert('IM 登录失败', message)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
XuqmSDK.init({
|
|||
|
|
appId: APP_ID,
|
|||
|
|
appKey: APP_ID,
|
|||
|
|
appSecret: 'demo-secret-not-used-by-current-services',
|
|||
|
|
apiBaseUrl: API_BASE_URL,
|
|||
|
|
imWsUrl: IM_WS_URL,
|
|||
|
|
debug: true,
|
|||
|
|
})
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
activeUserRef.current = currentUser
|
|||
|
|
}, [currentUser, peerUser.id])
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const listener: ImEventListener = {
|
|||
|
|
onConnected() {
|
|||
|
|
setConnected(true)
|
|||
|
|
appendLog(`WebSocket 已连接:${activeUserRef.current.nickname}`)
|
|||
|
|
},
|
|||
|
|
onDisconnected(reason) {
|
|||
|
|
setConnected(false)
|
|||
|
|
appendLog(`WebSocket 断开:${reason || 'unknown'}`)
|
|||
|
|
},
|
|||
|
|
onMessage(message) {
|
|||
|
|
mergeMessage(message)
|
|||
|
|
},
|
|||
|
|
onGroupMessage(message) {
|
|||
|
|
mergeMessage(message)
|
|||
|
|
},
|
|||
|
|
onError(error) {
|
|||
|
|
appendLog(`IM 错误:${error}`)
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ImSDK.addListener(listener)
|
|||
|
|
return () => {
|
|||
|
|
ImSDK.removeListener(listener)
|
|||
|
|
ImSDK.disconnect()
|
|||
|
|
}
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
async function run() {
|
|||
|
|
try {
|
|||
|
|
ImSDK.disconnect()
|
|||
|
|
setConnected(false)
|
|||
|
|
setMessages([])
|
|||
|
|
await ImSDK.login(currentUser.id, currentUser.nickname)
|
|||
|
|
appendLog(`已登录 IM:${currentUser.nickname}`)
|
|||
|
|
const history = await ImSDK.fetchHistory(peerUser.id, 0, 50)
|
|||
|
|
history
|
|||
|
|
.slice()
|
|||
|
|
.reverse()
|
|||
|
|
.forEach(item => mergeMessage(item))
|
|||
|
|
setConnected(ImSDK.isConnected())
|
|||
|
|
} catch (error) {
|
|||
|
|
const message = error instanceof Error ? error.message : 'IM 登录失败'
|
|||
|
|
appendLog(`连接失败:${message}`)
|
|||
|
|
Alert.alert('IM 登录失败', message)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
run()
|
|||
|
|
}, [currentUser, peerUser.id])
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
UpdateSDK.getCachedRnBundle(MODULE_ID).then(bundle => {
|
|||
|
|
if (!bundle) return
|
|||
|
|
setCachedBundleSummary(`已缓存 ${bundle.version},md5=${bundle.md5}`)
|
|||
|
|
})
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
const handleSwitchUser = (user: DemoUser) => {
|
|||
|
|
setCurrentUser(user)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleSend = async () => {
|
|||
|
|
const content = draft.trim()
|
|||
|
|
if (!content) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const sent = await ImSDK.sendMessage(peerUser.id, 'SINGLE', 'TEXT', content)
|
|||
|
|
mergeMessage(sent)
|
|||
|
|
setDraft('')
|
|||
|
|
appendLog(`已发送给 ${peerUser.nickname}`)
|
|||
|
|
} catch (error) {
|
|||
|
|
const message = error instanceof Error ? error.message : '消息发送失败'
|
|||
|
|
appendLog(`发送失败:${message}`)
|
|||
|
|
Alert.alert('发送失败', message)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleReloadHistory = async () => {
|
|||
|
|
try {
|
|||
|
|
const history = await ImSDK.fetchHistory(peerUser.id, 0, 50)
|
|||
|
|
setMessages(history.slice().reverse())
|
|||
|
|
appendLog(`历史消息已刷新,共 ${history.length} 条`)
|
|||
|
|
} catch (error) {
|
|||
|
|
const message = error instanceof Error ? error.message : '历史消息加载失败'
|
|||
|
|
appendLog(`历史拉取失败:${message}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleCheckAppUpdate = async () => {
|
|||
|
|
try {
|
|||
|
|
const result = await UpdateSDK.checkAppUpdate(CURRENT_APP_VERSION_CODE)
|
|||
|
|
if (!result.needsUpdate) {
|
|||
|
|
setAppUpdateResult('当前已是最新 App 版本')
|
|||
|
|
appendLog('App 更新检查:当前最新')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
setAppUpdateResult(
|
|||
|
|
`发现版本 ${result.versionName}(code=${result.versionCode})\nforce=${String(result.forceUpdate)}\n下载地址:${result.downloadUrl || '未提供'}`,
|
|||
|
|
)
|
|||
|
|
appendLog(`发现 App 新版本:${result.versionName}`)
|
|||
|
|
} catch (error) {
|
|||
|
|
const message = error instanceof Error ? error.message : 'App 更新检查失败'
|
|||
|
|
setAppUpdateResult(message)
|
|||
|
|
appendLog(`App 更新失败:${message}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleCheckRnUpdate = async () => {
|
|||
|
|
try {
|
|||
|
|
const result = await UpdateSDK.checkRnUpdate(MODULE_ID, CURRENT_RN_VERSION)
|
|||
|
|
if (!result.needsUpdate) {
|
|||
|
|
setRnUpdateResult('当前已是最新插件版本')
|
|||
|
|
appendLog('插件更新检查:当前最新')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const source = await UpdateSDK.downloadRnBundle(result.downloadUrl)
|
|||
|
|
const cached = await UpdateSDK.cacheRnBundle(MODULE_ID, result.latestVersion, result.md5, source)
|
|||
|
|
setRnUpdateResult(
|
|||
|
|
`发现插件版本 ${result.latestVersion}\nmd5=${result.md5}\n说明:${result.note || '无'}\n已下载源码长度:${source.length}`,
|
|||
|
|
)
|
|||
|
|
setCachedBundleSummary(`已缓存 ${cached.version},下载于 ${cached.downloadedAt}`)
|
|||
|
|
appendLog(`插件更新已缓存:${cached.version}`)
|
|||
|
|
} catch (error) {
|
|||
|
|
const message = error instanceof Error ? error.message : '插件更新检查失败'
|
|||
|
|
setRnUpdateResult(message)
|
|||
|
|
appendLog(`插件更新失败:${message}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|||
|
|
<Text style={styles.heroEyebrow}>XuqmGroup RN SDK Demo</Text>
|
|||
|
|
<Text style={styles.heroTitle}>聊天、App 更新、插件热更新一屏演示</Text>
|
|||
|
|
<Text style={styles.heroSubtitle}>
|
|||
|
|
这个项目直接连接 {API_BASE_URL},默认使用两个演示用户互聊。
|
|||
|
|
</Text>
|
|||
|
|
|
|||
|
|
<SectionCard title="1. 当前会话">
|
|||
|
|
<View style={styles.userSwitcher}>
|
|||
|
|
{DEMO_USERS.map(user => {
|
|||
|
|
const active = user.id === currentUser.id
|
|||
|
|
return (
|
|||
|
|
<Pressable
|
|||
|
|
key={user.id}
|
|||
|
|
onPress={() => {
|
|||
|
|
handleSwitchUser(user)
|
|||
|
|
}}
|
|||
|
|
style={[styles.userChip, active && styles.userChipActive]}>
|
|||
|
|
<Text style={[styles.userChipText, active && styles.userChipTextActive]}>
|
|||
|
|
{user.nickname}
|
|||
|
|
</Text>
|
|||
|
|
</Pressable>
|
|||
|
|
)
|
|||
|
|
})}
|
|||
|
|
</View>
|
|||
|
|
<Text style={styles.metaText}>当前用户:{currentUser.nickname}({currentUser.id})</Text>
|
|||
|
|
<Text style={styles.metaText}>聊天对象:{peerUser.nickname}({peerUser.id})</Text>
|
|||
|
|
<Text style={styles.metaText}>连接状态:{connected ? '已连接' : '连接中 / 未连接'}</Text>
|
|||
|
|
<View style={styles.inlineActions}>
|
|||
|
|
<PrimaryButton title="重新拉历史" onPress={() => {
|
|||
|
|
handleReloadHistory()
|
|||
|
|
}} />
|
|||
|
|
<GhostButton title="重新登录" onPress={() => {
|
|||
|
|
connectUser(currentUser)
|
|||
|
|
}} />
|
|||
|
|
</View>
|
|||
|
|
</SectionCard>
|
|||
|
|
|
|||
|
|
<SectionCard title="2. 聊天演示">
|
|||
|
|
<View style={styles.chatPanel}>
|
|||
|
|
{messages.length === 0 ? (
|
|||
|
|
<Text style={styles.emptyText}>还没有消息,发一条试试看。</Text>
|
|||
|
|
) : (
|
|||
|
|
messages.map(message => {
|
|||
|
|
const mine = message.fromUserId === currentUser.id
|
|||
|
|
return (
|
|||
|
|
<View
|
|||
|
|
key={message.id}
|
|||
|
|
style={[styles.bubbleRow, mine ? styles.bubbleRowMine : styles.bubbleRowPeer]}>
|
|||
|
|
<View style={[styles.bubble, mine ? styles.bubbleMine : styles.bubblePeer]}>
|
|||
|
|
<Text style={styles.bubbleMeta}>
|
|||
|
|
{mine ? '我' : peerUser.nickname} · {message.msgType}
|
|||
|
|
</Text>
|
|||
|
|
<Text style={styles.bubbleText}>{message.content}</Text>
|
|||
|
|
</View>
|
|||
|
|
</View>
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
)}
|
|||
|
|
</View>
|
|||
|
|
<TextInput
|
|||
|
|
value={draft}
|
|||
|
|
onChangeText={setDraft}
|
|||
|
|
placeholder="输入一条消息"
|
|||
|
|
placeholderTextColor="#7c7f85"
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
<PrimaryButton title="发送消息" onPress={() => {
|
|||
|
|
handleSend()
|
|||
|
|
}} />
|
|||
|
|
</SectionCard>
|
|||
|
|
|
|||
|
|
<SectionCard title="3. App 更新演示">
|
|||
|
|
<Text style={styles.paragraph}>
|
|||
|
|
当前本地版本号写死为 {CURRENT_APP_VERSION_CODE},方便你在服务端发布更高版本后立即观察效果。
|
|||
|
|
</Text>
|
|||
|
|
<PrimaryButton title="检查 App 更新" onPress={() => {
|
|||
|
|
handleCheckAppUpdate()
|
|||
|
|
}} />
|
|||
|
|
<Text style={styles.resultText}>{appUpdateResult}</Text>
|
|||
|
|
</SectionCard>
|
|||
|
|
|
|||
|
|
<SectionCard title="4. 插件热更新演示">
|
|||
|
|
<Text style={styles.paragraph}>
|
|||
|
|
当前模块:{MODULE_ID},本地插件版本:{CURRENT_RN_VERSION}。
|
|||
|
|
</Text>
|
|||
|
|
<PrimaryButton title="检查插件更新并缓存" onPress={() => {
|
|||
|
|
handleCheckRnUpdate()
|
|||
|
|
}} />
|
|||
|
|
<Text style={styles.resultText}>{rnUpdateResult}</Text>
|
|||
|
|
<Text style={styles.cacheText}>{cachedBundleSummary}</Text>
|
|||
|
|
</SectionCard>
|
|||
|
|
|
|||
|
|
<SectionCard title="5. 活动日志">
|
|||
|
|
{activityLog.length === 0 ? (
|
|||
|
|
<Text style={styles.emptyText}>暂无日志</Text>
|
|||
|
|
) : (
|
|||
|
|
activityLog.map(item => (
|
|||
|
|
<Text key={item} style={styles.logLine}>
|
|||
|
|
{item}
|
|||
|
|
</Text>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</SectionCard>
|
|||
|
|
</ScrollView>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function SectionCard({title, children}: {title: string; children: React.ReactNode}) {
|
|||
|
|
return (
|
|||
|
|
<View style={styles.card}>
|
|||
|
|
<Text style={styles.cardTitle}>{title}</Text>
|
|||
|
|
{children}
|
|||
|
|
</View>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function PrimaryButton({title, onPress}: {title: string; onPress: () => void}) {
|
|||
|
|
return (
|
|||
|
|
<Pressable onPress={onPress} style={styles.primaryButton}>
|
|||
|
|
<Text style={styles.primaryButtonText}>{title}</Text>
|
|||
|
|
</Pressable>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function GhostButton({title, onPress}: {title: string; onPress: () => void}) {
|
|||
|
|
return (
|
|||
|
|
<Pressable onPress={onPress} style={styles.ghostButton}>
|
|||
|
|
<Text style={styles.ghostButtonText}>{title}</Text>
|
|||
|
|
</Pressable>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const styles = StyleSheet.create({
|
|||
|
|
safeArea: {
|
|||
|
|
flex: 1,
|
|||
|
|
backgroundColor: '#f5efe3',
|
|||
|
|
},
|
|||
|
|
container: {
|
|||
|
|
flex: 1,
|
|||
|
|
backgroundColor: '#f5efe3',
|
|||
|
|
},
|
|||
|
|
content: {
|
|||
|
|
padding: 18,
|
|||
|
|
gap: 14,
|
|||
|
|
},
|
|||
|
|
heroEyebrow: {
|
|||
|
|
fontSize: 13,
|
|||
|
|
fontWeight: '700',
|
|||
|
|
color: '#915f34',
|
|||
|
|
textTransform: 'uppercase',
|
|||
|
|
letterSpacing: 1.1,
|
|||
|
|
},
|
|||
|
|
heroTitle: {
|
|||
|
|
fontSize: 30,
|
|||
|
|
lineHeight: 36,
|
|||
|
|
fontWeight: '800',
|
|||
|
|
color: '#1f2933',
|
|||
|
|
},
|
|||
|
|
heroSubtitle: {
|
|||
|
|
fontSize: 15,
|
|||
|
|
lineHeight: 22,
|
|||
|
|
color: '#4b5563',
|
|||
|
|
},
|
|||
|
|
card: {
|
|||
|
|
borderRadius: 24,
|
|||
|
|
padding: 16,
|
|||
|
|
backgroundColor: '#fffaf2',
|
|||
|
|
borderWidth: 1,
|
|||
|
|
borderColor: '#ead7b7',
|
|||
|
|
gap: 12,
|
|||
|
|
},
|
|||
|
|
cardTitle: {
|
|||
|
|
fontSize: 19,
|
|||
|
|
fontWeight: '800',
|
|||
|
|
color: '#1f2933',
|
|||
|
|
},
|
|||
|
|
userSwitcher: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
gap: 10,
|
|||
|
|
},
|
|||
|
|
userChip: {
|
|||
|
|
paddingHorizontal: 14,
|
|||
|
|
paddingVertical: 10,
|
|||
|
|
borderRadius: 999,
|
|||
|
|
backgroundColor: '#efe2c5',
|
|||
|
|
},
|
|||
|
|
userChipActive: {
|
|||
|
|
backgroundColor: '#1f2933',
|
|||
|
|
},
|
|||
|
|
userChipText: {
|
|||
|
|
color: '#4b5563',
|
|||
|
|
fontWeight: '700',
|
|||
|
|
},
|
|||
|
|
userChipTextActive: {
|
|||
|
|
color: '#fffdf8',
|
|||
|
|
},
|
|||
|
|
metaText: {
|
|||
|
|
fontSize: 14,
|
|||
|
|
color: '#4b5563',
|
|||
|
|
},
|
|||
|
|
inlineActions: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
gap: 10,
|
|||
|
|
},
|
|||
|
|
chatPanel: {
|
|||
|
|
maxHeight: 340,
|
|||
|
|
borderRadius: 18,
|
|||
|
|
backgroundColor: '#f4ecdf',
|
|||
|
|
padding: 12,
|
|||
|
|
gap: 8,
|
|||
|
|
},
|
|||
|
|
bubbleRow: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
},
|
|||
|
|
bubbleRowMine: {
|
|||
|
|
justifyContent: 'flex-end',
|
|||
|
|
},
|
|||
|
|
bubbleRowPeer: {
|
|||
|
|
justifyContent: 'flex-start',
|
|||
|
|
},
|
|||
|
|
bubble: {
|
|||
|
|
maxWidth: '85%',
|
|||
|
|
borderRadius: 18,
|
|||
|
|
paddingHorizontal: 12,
|
|||
|
|
paddingVertical: 10,
|
|||
|
|
gap: 4,
|
|||
|
|
},
|
|||
|
|
bubbleMine: {
|
|||
|
|
backgroundColor: '#cae7d8',
|
|||
|
|
},
|
|||
|
|
bubblePeer: {
|
|||
|
|
backgroundColor: '#ffffff',
|
|||
|
|
borderWidth: 1,
|
|||
|
|
borderColor: '#ddd3c2',
|
|||
|
|
},
|
|||
|
|
bubbleMeta: {
|
|||
|
|
fontSize: 11,
|
|||
|
|
color: '#6b7280',
|
|||
|
|
fontWeight: '700',
|
|||
|
|
},
|
|||
|
|
bubbleText: {
|
|||
|
|
fontSize: 15,
|
|||
|
|
lineHeight: 21,
|
|||
|
|
color: '#111827',
|
|||
|
|
},
|
|||
|
|
input: {
|
|||
|
|
minHeight: 48,
|
|||
|
|
borderRadius: 16,
|
|||
|
|
borderWidth: 1,
|
|||
|
|
borderColor: '#dac8a8',
|
|||
|
|
backgroundColor: '#fff',
|
|||
|
|
paddingHorizontal: 14,
|
|||
|
|
color: '#111827',
|
|||
|
|
},
|
|||
|
|
paragraph: {
|
|||
|
|
fontSize: 14,
|
|||
|
|
lineHeight: 21,
|
|||
|
|
color: '#4b5563',
|
|||
|
|
},
|
|||
|
|
primaryButton: {
|
|||
|
|
minHeight: 46,
|
|||
|
|
borderRadius: 16,
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
backgroundColor: '#1f2933',
|
|||
|
|
paddingHorizontal: 16,
|
|||
|
|
},
|
|||
|
|
primaryButtonText: {
|
|||
|
|
color: '#fffdf8',
|
|||
|
|
fontSize: 15,
|
|||
|
|
fontWeight: '800',
|
|||
|
|
},
|
|||
|
|
ghostButton: {
|
|||
|
|
minHeight: 46,
|
|||
|
|
borderRadius: 16,
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
backgroundColor: '#efe2c5',
|
|||
|
|
paddingHorizontal: 16,
|
|||
|
|
},
|
|||
|
|
ghostButtonText: {
|
|||
|
|
color: '#4b5563',
|
|||
|
|
fontSize: 15,
|
|||
|
|
fontWeight: '800',
|
|||
|
|
},
|
|||
|
|
resultText: {
|
|||
|
|
fontSize: 14,
|
|||
|
|
lineHeight: 21,
|
|||
|
|
color: '#1f2933',
|
|||
|
|
},
|
|||
|
|
cacheText: {
|
|||
|
|
fontSize: 13,
|
|||
|
|
color: '#6b7280',
|
|||
|
|
},
|
|||
|
|
emptyText: {
|
|||
|
|
fontSize: 14,
|
|||
|
|
color: '#7c7f85',
|
|||
|
|
},
|
|||
|
|
logLine: {
|
|||
|
|
fontSize: 13,
|
|||
|
|
lineHeight: 20,
|
|||
|
|
color: '#374151',
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export default App
|