You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
765 lines
24 KiB
765 lines
24 KiB
import { Ionicons } from '@expo/vector-icons';
|
|
import { useRouter } from 'expo-router';
|
|
import { useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
Modal,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { IndustrySkillWordCloud } from '../../components/IndustrySkillWordCloud';
|
|
import { useSkillCloud, useSkillLearningPath } from '../../src/hooks';
|
|
import { colors } from '../../src/theme';
|
|
import type { SkillLearningPath, SkillWord, TopSkill } from '../../src/types';
|
|
|
|
/** 技能分组 → 中文标签(用于选中详情) */
|
|
const GROUP_LABEL: Record<SkillWord['group'], string> = {
|
|
tech: '技术技能',
|
|
soft: '软技能',
|
|
domain: '领域能力',
|
|
tool: '工具',
|
|
};
|
|
|
|
/* ===================== 子组件:顶部导航 ===================== */
|
|
|
|
function Header({
|
|
onBack,
|
|
onToggleSearch,
|
|
searching,
|
|
}: {
|
|
onBack: () => void;
|
|
onToggleSearch: () => void;
|
|
searching: boolean;
|
|
}) {
|
|
return (
|
|
<View style={styles.header}>
|
|
<Pressable style={styles.headerBtn} onPress={onBack} hitSlop={8}>
|
|
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
|
|
</Pressable>
|
|
<Text style={styles.headerTitle}>行业技能词云</Text>
|
|
<Pressable style={styles.headerBtn} onPress={onToggleSearch} hitSlop={8}>
|
|
<Ionicons
|
|
name={searching ? 'close' : 'search-outline'}
|
|
size={22}
|
|
color={colors.textPrimary}
|
|
/>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:搜索栏 ===================== */
|
|
|
|
function SearchBar({
|
|
value,
|
|
onChange,
|
|
onClear,
|
|
}: {
|
|
value: string;
|
|
onChange: (text: string) => void;
|
|
onClear: () => void;
|
|
}) {
|
|
return (
|
|
<View style={styles.searchBar}>
|
|
<Ionicons name="search-outline" size={16} color={colors.textHint} />
|
|
<TextInput
|
|
style={styles.searchInput}
|
|
value={value}
|
|
onChangeText={onChange}
|
|
placeholder="搜索技能,如 React"
|
|
placeholderTextColor={colors.textHint}
|
|
autoFocus
|
|
returnKeyType="search"
|
|
/>
|
|
{value !== '' && (
|
|
<Pressable onPress={onClear} hitSlop={8}>
|
|
<Ionicons name="close-circle" size={16} color={colors.textHint} />
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:行业标签 ===================== */
|
|
|
|
function IndustryTabs({
|
|
industries,
|
|
active,
|
|
onSelect,
|
|
}: {
|
|
industries: string[];
|
|
active: string;
|
|
onSelect: (industry: string) => void;
|
|
}) {
|
|
return (
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
style={styles.tabsWrap}
|
|
contentContainerStyle={styles.tabs}
|
|
>
|
|
{industries.map((item) => {
|
|
const isActive = item === active;
|
|
return (
|
|
<Pressable
|
|
key={item}
|
|
style={[styles.tab, isActive && styles.tabActive]}
|
|
onPress={() => onSelect(item)}
|
|
hitSlop={4}
|
|
>
|
|
<Text style={[styles.tabText, isActive && styles.tabTextActive]}>{item}</Text>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:筛选条件 ===================== */
|
|
|
|
function FilterBar({
|
|
items,
|
|
}: {
|
|
items: { label: string; value: string }[];
|
|
}) {
|
|
return (
|
|
<View style={styles.filterBar}>
|
|
{items.map((item) => (
|
|
<View key={item.label} style={styles.filterItem}>
|
|
<Text style={styles.filterValue}>{item.value}</Text>
|
|
<Ionicons name="chevron-down" size={14} color={colors.textHint} />
|
|
</View>
|
|
))}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:技能分类 ===================== */
|
|
|
|
function CategoryTabs({
|
|
categories,
|
|
activeId,
|
|
onSelect,
|
|
}: {
|
|
categories: { id: string; label: string }[];
|
|
activeId: string;
|
|
onSelect: (id: string) => void;
|
|
}) {
|
|
return (
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerStyle={styles.categoryTabs}
|
|
>
|
|
{categories.map((c) => {
|
|
const isActive = c.id === activeId;
|
|
return (
|
|
<Pressable
|
|
key={c.id}
|
|
style={[styles.categoryTab, isActive && styles.categoryTabActive]}
|
|
onPress={() => onSelect(c.id)}
|
|
hitSlop={4}
|
|
>
|
|
<Text style={[styles.categoryText, isActive && styles.categoryTextActive]}>
|
|
{c.label}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:热门技能 TOP10 ===================== */
|
|
|
|
function TopSkillList({ skills }: { skills: TopSkill[] }) {
|
|
return (
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>热门技能 TOP 10</Text>
|
|
{skills.map((s) => (
|
|
<View key={s.id} style={styles.skillRow}>
|
|
<View style={styles.skillRank}>
|
|
<Text style={styles.skillRankText}>{s.rank}</Text>
|
|
</View>
|
|
<Text style={styles.skillName}>{s.name}</Text>
|
|
<View style={styles.skillTrack}>
|
|
<View style={[styles.skillFill, { width: `${s.demand}%` }]} />
|
|
</View>
|
|
<Text style={styles.skillDemand}>{s.demand}</Text>
|
|
<Text
|
|
style={[styles.skillChange, s.change >= 0 ? styles.skillUp : styles.skillDown]}
|
|
>
|
|
{s.change >= 0 ? '+' : ''}
|
|
{s.change}%
|
|
</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:技能匹配度 ===================== */
|
|
|
|
function MatchCard({
|
|
percent,
|
|
onGenerate,
|
|
generating,
|
|
}: {
|
|
percent: number;
|
|
onGenerate: () => void;
|
|
generating: boolean;
|
|
}) {
|
|
return (
|
|
<View style={styles.matchCard}>
|
|
<View style={styles.matchHeader}>
|
|
<Text style={styles.matchTitle}>你的技能匹配度</Text>
|
|
<Text style={styles.matchPercent}>{percent}%</Text>
|
|
</View>
|
|
<View style={styles.matchTrack}>
|
|
<View style={[styles.matchFill, { width: `${percent}%` }]} />
|
|
</View>
|
|
<Pressable
|
|
style={[styles.matchBtn, generating && styles.matchBtnDisabled]}
|
|
onPress={onGenerate}
|
|
disabled={generating}
|
|
>
|
|
{generating ? (
|
|
<ActivityIndicator size="small" color={colors.white} />
|
|
) : (
|
|
<Ionicons name="sparkles" size={16} color={colors.white} />
|
|
)}
|
|
<Text style={styles.matchBtnText}>
|
|
{generating ? '正在生成…' : '生成技能学习路径'}
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:学习路径弹窗 ===================== */
|
|
|
|
function LearningPathModal({
|
|
visible,
|
|
path,
|
|
loading,
|
|
error,
|
|
onRetry,
|
|
onClose,
|
|
}: {
|
|
visible: boolean;
|
|
path: SkillLearningPath | null;
|
|
loading: boolean;
|
|
error: Error | null;
|
|
onRetry: () => void;
|
|
onClose: () => void;
|
|
}) {
|
|
return (
|
|
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
|
|
<View style={styles.modalMask}>
|
|
<View style={styles.modalSheet}>
|
|
<View style={styles.modalHeader}>
|
|
<Text style={styles.modalTitle}>技能学习路径</Text>
|
|
<Pressable onPress={onClose} hitSlop={8}>
|
|
<Ionicons name="close" size={22} color={colors.textSecondary} />
|
|
</Pressable>
|
|
</View>
|
|
|
|
{loading ? (
|
|
<View style={styles.modalCenter}>
|
|
<ActivityIndicator size="large" color={colors.primary} />
|
|
<Text style={styles.tip}>正在按行业热度排优先级…</Text>
|
|
</View>
|
|
) : error ? (
|
|
<View style={styles.modalCenter}>
|
|
<Text style={styles.tip}>{error.message}</Text>
|
|
<Pressable style={styles.retryBtn} onPress={onRetry} hitSlop={6}>
|
|
<Text style={styles.retryText}>点击重试</Text>
|
|
</Pressable>
|
|
</View>
|
|
) : path ? (
|
|
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.modalBody}>
|
|
<View style={styles.pathSummary}>
|
|
<Text style={styles.pathSummaryText}>
|
|
{path.industry} · {path.position}
|
|
</Text>
|
|
<Text style={styles.pathSummaryGap}>
|
|
当前匹配度 {path.matchPercent}% → 补齐后约 {path.targetPercent}%
|
|
</Text>
|
|
<Text style={styles.pathSummaryHint}>
|
|
按行业热度权重从高到低排,先补权重最高的 {path.items.length} 项
|
|
</Text>
|
|
</View>
|
|
|
|
{path.items.map((item) => (
|
|
<View key={item.id} style={styles.pathItem}>
|
|
<View style={styles.pathPriority}>
|
|
<Text style={styles.pathPriorityText}>{item.priority}</Text>
|
|
</View>
|
|
<View style={styles.pathItemBody}>
|
|
<View style={styles.pathItemHead}>
|
|
<Text style={styles.pathItemName}>{item.name}</Text>
|
|
<View style={styles.groupBadge}>
|
|
<Text style={styles.groupText}>{GROUP_LABEL[item.group]}</Text>
|
|
</View>
|
|
</View>
|
|
<Text style={styles.pathItemReason}>{item.reason}</Text>
|
|
<View style={styles.weightRow}>
|
|
<View style={styles.weightTrack}>
|
|
<View style={[styles.weightFill, { width: `${item.weight}%` }]} />
|
|
</View>
|
|
<Text style={styles.weightNum}>{item.weight}</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
))}
|
|
|
|
<Text style={styles.updated}>{path.generatedAt}</Text>
|
|
</ScrollView>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:选中技能详情 ===================== */
|
|
|
|
function SelectedDetail({ word }: { word: SkillWord }) {
|
|
return (
|
|
<View style={styles.detailCard}>
|
|
<View style={styles.detailHead}>
|
|
<Text style={styles.detailWord}>{word.text}</Text>
|
|
<View style={styles.groupBadge}>
|
|
<Text style={styles.groupText}>{GROUP_LABEL[word.group]}</Text>
|
|
</View>
|
|
</View>
|
|
<Text style={styles.detailDesc}>行业热度权重 {word.weight} / 100,越靠中心越核心</Text>
|
|
<View style={styles.weightRow}>
|
|
<View style={styles.weightTrack}>
|
|
<View style={[styles.weightFill, { width: `${word.weight}%` }]} />
|
|
</View>
|
|
<Text style={styles.weightNum}>{word.weight}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:四态 ===================== */
|
|
|
|
function Loading() {
|
|
return (
|
|
<View style={styles.center}>
|
|
<ActivityIndicator size="large" color={colors.primary} />
|
|
<Text style={styles.tip}>正在生成词云…</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function ErrorState({ onRetry }: { onRetry: () => void }) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<Ionicons name="cloud-offline-outline" size={40} color={colors.textHint} />
|
|
<Text style={styles.tip}>词云加载失败</Text>
|
|
<Pressable style={styles.retryBtn} onPress={onRetry} hitSlop={6}>
|
|
<Text style={styles.retryText}>点击重试</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function Empty() {
|
|
return (
|
|
<View style={styles.center}>
|
|
<Ionicons name="search-outline" size={40} color={colors.textHint} />
|
|
<Text style={styles.tip}>该行业暂无技能词数据</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 页面 ===================== */
|
|
|
|
export default function SkillCloudScreen() {
|
|
const router = useRouter();
|
|
const {
|
|
industry,
|
|
setIndustry,
|
|
cloud,
|
|
keyword,
|
|
setKeyword,
|
|
words,
|
|
isEmpty,
|
|
loading,
|
|
error,
|
|
refresh,
|
|
} = useSkillCloud();
|
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
const [activeCategoryId, setActiveCategoryId] = useState<string>('all');
|
|
const [searching, setSearching] = useState(false);
|
|
const [pathVisible, setPathVisible] = useState(false);
|
|
const { path, loading: generating, error: pathError, generate, reset } = useSkillLearningPath();
|
|
|
|
const selectedWord = words.find((w) => w.id === selectedId) ?? null;
|
|
|
|
const handleGenerate = async () => {
|
|
// 先开弹窗再请求,弹窗自己有 loading / 失败重试三态,免得点完按钮没反应
|
|
setPathVisible(true);
|
|
await generate(industry);
|
|
};
|
|
|
|
const closePathModal = () => {
|
|
setPathVisible(false);
|
|
reset();
|
|
};
|
|
|
|
const toggleSearch = () => {
|
|
if (searching) setKeyword('');
|
|
setSearching((prev) => !prev);
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onToggleSearch={toggleSearch} searching={searching} />
|
|
|
|
{loading ? (
|
|
<Loading />
|
|
) : error ? (
|
|
<ErrorState onRetry={refresh} />
|
|
) : !cloud || cloud.words.length === 0 ? (
|
|
<Empty />
|
|
) : (
|
|
<>
|
|
{searching && (
|
|
<SearchBar value={keyword} onChange={setKeyword} onClear={() => setKeyword('')} />
|
|
)}
|
|
<IndustryTabs
|
|
industries={cloud.filters.industries}
|
|
active={industry}
|
|
onSelect={(i) => {
|
|
setIndustry(i);
|
|
setSelectedId(null);
|
|
}}
|
|
/>
|
|
<FilterBar
|
|
items={[
|
|
{ label: '岗位', value: cloud.position },
|
|
{ label: '城市', value: cloud.city },
|
|
{ label: '经验', value: cloud.experience },
|
|
]}
|
|
/>
|
|
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
|
|
<Text style={styles.sectionTitle}>
|
|
{cloud.industry} · {keyword.trim() === '' ? '高频技能' : `含「${keyword.trim()}」的技能`}
|
|
</Text>
|
|
|
|
{isEmpty ? (
|
|
<View style={styles.searchEmpty}>
|
|
<Ionicons name="search-outline" size={32} color={colors.textHint} />
|
|
<Text style={styles.tip}>没有匹配「{keyword.trim()}」的技能</Text>
|
|
<Pressable style={styles.retryBtn} onPress={() => setKeyword('')} hitSlop={6}>
|
|
<Text style={styles.retryText}>清空关键词</Text>
|
|
</Pressable>
|
|
</View>
|
|
) : (
|
|
<View style={styles.cloudCard}>
|
|
<IndustrySkillWordCloud
|
|
words={words}
|
|
selectedId={selectedId}
|
|
onSelect={(w) => setSelectedId(w.id)}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
<CategoryTabs
|
|
categories={cloud.categories}
|
|
activeId={activeCategoryId}
|
|
onSelect={setActiveCategoryId}
|
|
/>
|
|
|
|
{selectedWord ? (
|
|
<SelectedDetail word={selectedWord} />
|
|
) : (
|
|
<Text style={styles.hint}>点击词云中的技能,查看热度详情</Text>
|
|
)}
|
|
|
|
<TopSkillList skills={cloud.topSkills} />
|
|
|
|
<MatchCard
|
|
percent={cloud.matchPercent}
|
|
onGenerate={handleGenerate}
|
|
generating={generating}
|
|
/>
|
|
|
|
<Text style={styles.updated}>{cloud.updatedAt}</Text>
|
|
</ScrollView>
|
|
</>
|
|
)}
|
|
|
|
<LearningPathModal
|
|
visible={pathVisible}
|
|
path={path}
|
|
loading={generating}
|
|
error={pathError}
|
|
onRetry={() => void generate(industry)}
|
|
onClose={closePathModal}
|
|
/>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: colors.pageBg },
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 12,
|
|
backgroundColor: colors.white,
|
|
},
|
|
headerBtn: { width: 40, alignItems: 'center' },
|
|
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
|
|
// flexShrink: 0 是必需的:搜索框出现后父容器变矮,默认 flexShrink:1 会把这条横向
|
|
// ScrollView 压到内容高度以下,胶囊文字被纵向裁掉(点得到、看不见)
|
|
tabsWrap: { backgroundColor: colors.white, maxHeight: 52, flexGrow: 0, flexShrink: 0 },
|
|
tabs: { flexDirection: 'row', paddingHorizontal: 16, gap: 8, paddingVertical: 10 },
|
|
tab: {
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 6,
|
|
borderRadius: 16,
|
|
backgroundColor: colors.pageBg,
|
|
},
|
|
tabActive: { backgroundColor: colors.primary },
|
|
tabText: { fontSize: 13, color: colors.textSecondary, fontWeight: '500' },
|
|
tabTextActive: { color: colors.textInverse, fontWeight: '600' },
|
|
filterBar: {
|
|
flexDirection: 'row',
|
|
backgroundColor: colors.white,
|
|
paddingHorizontal: 16,
|
|
paddingBottom: 12,
|
|
gap: 10,
|
|
},
|
|
filterItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: colors.pageBg,
|
|
borderRadius: 8,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 5,
|
|
},
|
|
filterValue: { fontSize: 12, color: colors.textPrimary, fontWeight: '600' },
|
|
categoryTabs: { flexDirection: 'row', paddingVertical: 12, gap: 8 },
|
|
categoryTab: {
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 6,
|
|
borderRadius: 16,
|
|
backgroundColor: colors.white,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
},
|
|
categoryTabActive: { backgroundColor: colors.primaryLight, borderColor: colors.primary },
|
|
categoryText: { fontSize: 12, color: colors.textSecondary },
|
|
categoryTextActive: { color: colors.primaryText, fontWeight: '600' },
|
|
body: { paddingHorizontal: 8, paddingBottom: 32 },
|
|
sectionTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 10 },
|
|
cloudCard: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: 8,
|
|
paddingVertical: 8,
|
|
paddingHorizontal: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
hint: { marginTop: 14, textAlign: 'center', fontSize: 13, color: colors.textHint },
|
|
updated: { marginTop: 16, textAlign: 'center', fontSize: 12, color: colors.textHint },
|
|
card: {
|
|
marginTop: 8,
|
|
backgroundColor: colors.white,
|
|
borderRadius: 8,
|
|
padding: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
cardTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 12 },
|
|
skillRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: 8,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
skillRank: {
|
|
width: 20,
|
|
height: 20,
|
|
borderRadius: 10,
|
|
backgroundColor: colors.primaryLight,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
skillRankText: { fontSize: 11, fontWeight: '700', color: colors.primaryText },
|
|
skillName: { width: 90, fontSize: 13, color: colors.textPrimary, marginLeft: 10 },
|
|
skillTrack: {
|
|
flex: 1,
|
|
height: 6,
|
|
borderRadius: 3,
|
|
backgroundColor: colors.borderLight,
|
|
overflow: 'hidden',
|
|
},
|
|
skillFill: { height: 6, borderRadius: 3, backgroundColor: colors.primary },
|
|
skillDemand: {
|
|
width: 30,
|
|
fontSize: 12,
|
|
fontWeight: '700',
|
|
color: colors.textPrimary,
|
|
textAlign: 'right',
|
|
},
|
|
skillChange: { width: 44, fontSize: 11, textAlign: 'right' },
|
|
skillUp: { color: colors.successText },
|
|
skillDown: { color: colors.danger },
|
|
matchCard: {
|
|
marginTop: 8,
|
|
backgroundColor: colors.white,
|
|
borderRadius: 8,
|
|
padding: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
matchHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
|
matchTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
|
|
matchPercent: { fontSize: 20, fontWeight: '800', color: colors.primary },
|
|
matchTrack: {
|
|
marginTop: 12,
|
|
height: 10,
|
|
borderRadius: 5,
|
|
backgroundColor: colors.borderLight,
|
|
overflow: 'hidden',
|
|
},
|
|
matchFill: { height: 10, borderRadius: 5, backgroundColor: colors.primary },
|
|
matchBtn: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginTop: 16,
|
|
backgroundColor: colors.primaryBtn,
|
|
borderRadius: 12,
|
|
paddingVertical: 12,
|
|
},
|
|
matchBtnText: { fontSize: 14, fontWeight: '700', color: colors.white, marginLeft: 6 },
|
|
matchBtnDisabled: { backgroundColor: colors.textDisabled },
|
|
searchBar: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
backgroundColor: colors.white,
|
|
paddingHorizontal: 16,
|
|
paddingBottom: 10,
|
|
},
|
|
searchInput: {
|
|
flex: 1,
|
|
backgroundColor: colors.pageBg,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 8,
|
|
fontSize: 14,
|
|
color: colors.textPrimary,
|
|
},
|
|
searchEmpty: {
|
|
alignItems: 'center',
|
|
gap: 10,
|
|
padding: 8,
|
|
backgroundColor: colors.white,
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
modalMask: { flex: 1, backgroundColor: colors.scrim, justifyContent: 'flex-end' },
|
|
modalSheet: {
|
|
backgroundColor: colors.white,
|
|
borderTopLeftRadius: 18,
|
|
borderTopRightRadius: 18,
|
|
maxHeight: '80%',
|
|
paddingBottom: 20,
|
|
},
|
|
modalHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 14,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.borderLight,
|
|
},
|
|
modalTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary },
|
|
modalCenter: { alignItems: 'center', justifyContent: 'center', gap: 12, paddingVertical: 48 },
|
|
modalBody: { paddingHorizontal: 16, paddingBottom: 12 },
|
|
pathSummary: {
|
|
backgroundColor: colors.primaryLight,
|
|
borderRadius: 8,
|
|
padding: 8,
|
|
marginTop: 8,
|
|
marginBottom: 8,
|
|
},
|
|
pathSummaryText: { fontSize: 14, fontWeight: '700', color: colors.primaryText },
|
|
pathSummaryGap: { marginTop: 6, fontSize: 13, color: colors.primaryText },
|
|
pathSummaryHint: { marginTop: 6, fontSize: 12, color: colors.textSecondary },
|
|
pathItem: {
|
|
flexDirection: 'row',
|
|
paddingVertical: 14,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.borderLight,
|
|
},
|
|
pathPriority: {
|
|
width: 24,
|
|
height: 24,
|
|
borderRadius: 12,
|
|
backgroundColor: colors.primary,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
pathPriorityText: { fontSize: 12, fontWeight: '800', color: colors.white },
|
|
pathItemBody: { flex: 1, marginLeft: 12 },
|
|
pathItemHead: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
pathItemName: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
|
|
pathItemReason: { marginTop: 6, fontSize: 12, color: colors.textSecondary, lineHeight: 18 },
|
|
detailCard: {
|
|
marginTop: 8,
|
|
backgroundColor: colors.white,
|
|
borderRadius: 8,
|
|
padding: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.successBorder,
|
|
},
|
|
detailHead: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
detailWord: { fontSize: 18, fontWeight: '700', color: colors.textPrimary },
|
|
groupBadge: {
|
|
backgroundColor: colors.successLight,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 4,
|
|
},
|
|
groupText: { fontSize: 12, color: colors.successText, fontWeight: '600' },
|
|
detailDesc: { marginTop: 8, fontSize: 13, color: colors.textSecondary },
|
|
weightRow: { marginTop: 12, flexDirection: 'row', alignItems: 'center', gap: 10 },
|
|
weightTrack: { flex: 1, height: 8, borderRadius: 4, backgroundColor: colors.borderLight },
|
|
weightFill: { height: 8, borderRadius: 4, backgroundColor: colors.success },
|
|
weightNum: {
|
|
fontSize: 13,
|
|
fontWeight: '700',
|
|
color: colors.successText,
|
|
width: 28,
|
|
textAlign: 'right',
|
|
},
|
|
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
|
|
tip: { fontSize: 14, color: colors.textSecondary },
|
|
retryBtn: {
|
|
paddingHorizontal: 18,
|
|
paddingVertical: 8,
|
|
borderRadius: 16,
|
|
backgroundColor: colors.primaryLight,
|
|
},
|
|
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
|
|
});
|
|
|