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.
786 lines
26 KiB
786 lines
26 KiB
import { Ionicons } from '@expo/vector-icons';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { useRouter } from 'expo-router';
|
|
import { useState } from 'react';
|
|
import {
|
|
Alert,
|
|
Modal,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { useSalaryCalculator, useSalaryRecords, useSalaryToolLinks } from '../../src/hooks';
|
|
import { clampSocialBase } from '../../src/api';
|
|
import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
|
|
import { colors, headerGradientShort , spacing } from '../../src/theme';
|
|
import {
|
|
CITIES,
|
|
DEFAULT_SOCIAL_PROFILE,
|
|
SOCIAL_SECURITY_CITY_PROFILES,
|
|
} from '../../src/constants';
|
|
import type { SalaryCalcInput, SalaryCalcRecord, SalaryToolLink } from '../../src/types';
|
|
import { formatCurrency, formatDateTime } from '../../src/utils';
|
|
|
|
/* ===================== 子组件:顶部导航 ===================== */
|
|
|
|
function Header({ onBack, onHistory }: { onBack: () => void; onHistory: () => void }) {
|
|
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={onHistory} hitSlop={8}>
|
|
<Text style={styles.headerAction}>历史记录</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:数字输入行 ===================== */
|
|
|
|
function NumberInput({
|
|
label,
|
|
value,
|
|
suffix,
|
|
onChange,
|
|
placeholder,
|
|
}: {
|
|
label: string;
|
|
value: number;
|
|
suffix: string;
|
|
onChange: (v: number) => void;
|
|
placeholder?: string;
|
|
}) {
|
|
const [text, setText] = useState(String(value));
|
|
return (
|
|
<View style={styles.inputRow}>
|
|
<Text style={styles.inputLabel}>{label}</Text>
|
|
<View style={styles.inputBox}>
|
|
<TextInput
|
|
style={styles.input}
|
|
value={text}
|
|
onChangeText={(t) => {
|
|
setText(t);
|
|
const n = parseFloat(t);
|
|
if (!Number.isNaN(n)) onChange(n);
|
|
}}
|
|
keyboardType="numeric"
|
|
placeholder={placeholder}
|
|
placeholderTextColor={colors.textHint}
|
|
/>
|
|
<Text style={styles.inputSuffix}>{suffix}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function SelectRow({
|
|
label,
|
|
value,
|
|
onPress,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onPress: () => void;
|
|
}) {
|
|
return (
|
|
<Pressable style={styles.inputRow} onPress={onPress}>
|
|
<Text style={styles.inputLabel}>{label}</Text>
|
|
<View style={styles.selectBox}>
|
|
<Text style={styles.selectValue}>{value}</Text>
|
|
<Ionicons name="chevron-forward" size={18} color={colors.textHint} />
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:五险一金明细 ===================== */
|
|
|
|
function InsuranceCard({ result }: { result: import('../../src/types').SalaryCalcResult }) {
|
|
return (
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>五险一金明细</Text>
|
|
<View style={styles.cardSubTitleRow}>
|
|
<Text style={styles.cardSubTitle}>个人缴纳</Text>
|
|
<Text style={styles.cardSubTitle}>缴纳比例</Text>
|
|
</View>
|
|
{result.insuranceItems.map((item) => (
|
|
<View key={item.label} style={styles.insuranceRow}>
|
|
<Text style={styles.insuranceLabel}>{item.label}</Text>
|
|
<Text style={styles.insuranceAmount}>{formatCurrency(item.amount)}</Text>
|
|
<Text style={styles.insuranceRate}>{(item.rate * 100).toFixed(1)}%</Text>
|
|
</View>
|
|
))}
|
|
<View style={styles.divider} />
|
|
<View style={styles.totalRow}>
|
|
<Text style={styles.totalLabel}>合计个人缴纳</Text>
|
|
<Text style={styles.totalAmount}>{formatCurrency(result.monthlySocial)}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:个人所得税明细 ===================== */
|
|
|
|
function TaxCard({ result }: { result: import('../../src/types').SalaryCalcResult }) {
|
|
const taxItems = result.taxItems;
|
|
const monthlyTax = taxItems.find((t) => t.label === '月度个税')?.amount ?? 0;
|
|
const annualTax = taxItems.find((t) => t.label === '年度个税')?.amount ?? 0;
|
|
const taxable = taxItems.find((t) => t.label === '应纳税所得额')?.amount ?? 0;
|
|
const rate = taxItems.find((t) => t.label === '适用税率')?.amount ?? 0;
|
|
|
|
return (
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>个人所得税明细</Text>
|
|
<View style={styles.taxGrid}>
|
|
<View style={styles.taxItem}>
|
|
<Text style={styles.taxItemLabel}>应纳税所得额</Text>
|
|
<Text style={styles.taxItemValue}>{formatCurrency(taxable)}</Text>
|
|
</View>
|
|
<View style={styles.taxItem}>
|
|
<Text style={styles.taxItemLabel}>适用税率</Text>
|
|
<Text style={styles.taxItemValue}>{(rate * 100).toFixed(0)}%</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.divider} />
|
|
<View style={styles.taxRow}>
|
|
<Text style={styles.taxRowLabel}>月度个税</Text>
|
|
<Text style={styles.taxRowValue}>{formatCurrency(monthlyTax)}/月</Text>
|
|
</View>
|
|
<View style={styles.taxRow}>
|
|
<Text style={styles.taxRowLabel}>年度个税</Text>
|
|
<Text style={styles.taxRowValue}>{formatCurrency(annualTax)}/年</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:薪资结构 ===================== */
|
|
|
|
function StructureCard({ result }: { result: import('../../src/types').SalaryCalcResult }) {
|
|
const items = result.structure.filter((s) => s.label !== '税前工资');
|
|
const themeColors: Record<string, string> = {
|
|
primary: colors.primary,
|
|
info: colors.info,
|
|
action: colors.action,
|
|
success: colors.success,
|
|
};
|
|
return (
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>薪资结构</Text>
|
|
<View style={styles.structureChart}>
|
|
<View style={styles.pie}>
|
|
{items.map((item, index) => {
|
|
const start = items.slice(0, index).reduce((sum, i) => sum + i.percent, 0);
|
|
return (
|
|
<View
|
|
key={item.label}
|
|
style={[
|
|
styles.pieSlice,
|
|
{
|
|
backgroundColor: themeColors[item.color] ?? colors.primary,
|
|
transform: [{ rotate: `${(start / 100) * 360}deg` }],
|
|
},
|
|
]}
|
|
/>
|
|
);
|
|
})}
|
|
</View>
|
|
<View style={styles.structureLegend}>
|
|
{items.map((item) => (
|
|
<View key={item.label} style={styles.legendRow}>
|
|
<View
|
|
style={[
|
|
styles.legendDot,
|
|
{ backgroundColor: themeColors[item.color] ?? colors.primary },
|
|
]}
|
|
/>
|
|
<Text style={styles.legendLabel}>{item.label}</Text>
|
|
<Text style={styles.legendValue}>{formatCurrency(item.value / 1000, 0)}K</Text>
|
|
<Text style={styles.legendPercent}>{item.percent}%</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:更多工具 ===================== */
|
|
|
|
function ToolLink({ tool }: { tool: SalaryToolLink }) {
|
|
const router = useRouter();
|
|
// 目标路由由接口层给出;没有 route 的只做展示,不带箭头
|
|
const route = tool.route;
|
|
|
|
const body = (
|
|
<>
|
|
<View style={[styles.toolIcon, { backgroundColor: colors.primaryLight }]}>
|
|
<Ionicons name={tool.icon as keyof typeof Ionicons.glyphMap} size={20} color={colors.primary} />
|
|
</View>
|
|
<View style={styles.toolBody}>
|
|
<Text style={styles.toolTitle}>{tool.title}</Text>
|
|
<Text style={styles.toolDesc}>{tool.desc}</Text>
|
|
</View>
|
|
{route ? <Ionicons name="chevron-forward" size={18} color={colors.textHint} /> : null}
|
|
</>
|
|
);
|
|
|
|
if (!route) {
|
|
return <View style={styles.toolRow}>{body}</View>;
|
|
}
|
|
return (
|
|
<Pressable style={styles.toolRow} onPress={() => router.push(route as never)}>
|
|
{body}
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
/* ===================== 页面 ===================== */
|
|
|
|
export default function SalaryCalculatorScreen() {
|
|
const router = useRouter();
|
|
const { inputs, setInputs, result } = useSalaryCalculator();
|
|
const records = useSalaryRecords();
|
|
const { tools } = useSalaryToolLinks();
|
|
const [cityPickerVisible, setCityPickerVisible] = useState(false);
|
|
const [historyVisible, setHistoryVisible] = useState(false);
|
|
const [saveSheetVisible, setSaveSheetVisible] = useState(false);
|
|
const [saveNote, setSaveNote] = useState('');
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
/** 保存当前计算结果到本地记录 */
|
|
const handleSave = async () => {
|
|
if (saving) return;
|
|
setSaving(true);
|
|
try {
|
|
await records.save(inputs, result, saveNote);
|
|
setSaveSheetVisible(false);
|
|
setSaveNote('');
|
|
Alert.alert('已保存', '当前薪资计算已保存到本地记录');
|
|
} catch {
|
|
Alert.alert('保存失败', '请稍后重试');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
/** 从历史记录载入一笔参数到计算器 */
|
|
const handleLoadRecord = (record: SalaryCalcRecord) => {
|
|
setInputs(record.input);
|
|
setHistoryVisible(false);
|
|
};
|
|
|
|
const cityProfile =
|
|
SOCIAL_SECURITY_CITY_PROFILES[inputs.city] ?? DEFAULT_SOCIAL_PROFILE;
|
|
|
|
const update = <K extends keyof SalaryCalcInput>(key: K, value: SalaryCalcInput[K]) => {
|
|
setInputs({ ...inputs, [key]: value });
|
|
};
|
|
|
|
/** 选城市:切换城市并把社保基数 clamp 到**新城市**的上下限(规则在接口层) */
|
|
const handleSelectCity = (city: string) => {
|
|
setInputs({ ...inputs, city, socialBase: clampSocialBase(city, inputs.socialBase) });
|
|
setCityPickerVisible(false);
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onHistory={() => setHistoryVisible(true)} />
|
|
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
|
|
{/* 输入卡片 */}
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>收入参数</Text>
|
|
<NumberInput
|
|
label="税前月薪"
|
|
value={inputs.monthlyGross}
|
|
suffix="元"
|
|
onChange={(v) => update('monthlyGross', Math.max(0, v))}
|
|
/>
|
|
<SelectRow label="所在城市" value={inputs.city} onPress={() => setCityPickerVisible(true)} />
|
|
<NumberInput
|
|
label="社保基数"
|
|
value={inputs.socialBase}
|
|
suffix="元"
|
|
onChange={(v) => update('socialBase', clampSocialBase(inputs.city, Math.max(0, v)))}
|
|
/>
|
|
<Text style={styles.baseHint}>
|
|
基数范围 ¥{formatCurrency(cityProfile.socialBaseLower, 0)} – ¥
|
|
{formatCurrency(cityProfile.socialBaseUpper, 0)}({inputs.city})
|
|
</Text>
|
|
<NumberInput
|
|
label="公积金比例"
|
|
value={inputs.providentFundRate * 100}
|
|
suffix="%"
|
|
onChange={(v) => update('providentFundRate', Math.max(0, Math.min(100, v)) / 100)}
|
|
/>
|
|
<NumberInput
|
|
label="专项附加扣除"
|
|
value={inputs.specialDeduction}
|
|
suffix="元/月"
|
|
onChange={(v) => update('specialDeduction', Math.max(0, v))}
|
|
/>
|
|
</View>
|
|
|
|
{/* 结果卡片 */}
|
|
<LinearGradient
|
|
colors={headerGradientShort}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={styles.resultCard}
|
|
>
|
|
<Text style={styles.resultLabel}>税后月工资</Text>
|
|
<Text style={styles.resultValue}>{formatCurrency(result.monthlyNet)}</Text>
|
|
<Text style={styles.resultUnit}>/月</Text>
|
|
</LinearGradient>
|
|
|
|
{/* 五险一金 */}
|
|
<InsuranceCard result={result} />
|
|
|
|
{/* 个税 */}
|
|
<TaxCard result={result} />
|
|
|
|
{/* 薪资结构 */}
|
|
<StructureCard result={result} />
|
|
|
|
{/* 更多工具:清单来自接口层,均为项目内真实页面 */}
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>更多工具</Text>
|
|
{tools.map((tool) => (
|
|
<ToolLink key={tool.id} tool={tool} />
|
|
))}
|
|
</View>
|
|
|
|
<Pressable style={styles.saveBtn} onPress={() => setSaveSheetVisible(true)}>
|
|
<Text style={styles.saveBtnText}>保存计算记录</Text>
|
|
</Pressable>
|
|
</ScrollView>
|
|
|
|
<CityPicker
|
|
visible={cityPickerVisible}
|
|
current={inputs.city}
|
|
onClose={() => setCityPickerVisible(false)}
|
|
onSelect={handleSelectCity}
|
|
/>
|
|
|
|
<SaveSheet
|
|
visible={saveSheetVisible}
|
|
note={saveNote}
|
|
onNoteChange={setSaveNote}
|
|
onSave={handleSave}
|
|
onClose={() => setSaveSheetVisible(false)}
|
|
saving={saving}
|
|
/>
|
|
|
|
<HistoryModal
|
|
visible={historyVisible}
|
|
records={records.records}
|
|
loading={records.loading}
|
|
error={records.error}
|
|
onRetry={records.refresh}
|
|
onSelect={handleLoadRecord}
|
|
onDelete={records.remove}
|
|
onClose={() => setHistoryVisible(false)}
|
|
/>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:城市选择底部弹层 ===================== */
|
|
|
|
function CityPicker({
|
|
visible,
|
|
current,
|
|
onClose,
|
|
onSelect,
|
|
}: {
|
|
visible: boolean;
|
|
current: string;
|
|
onClose: () => void;
|
|
onSelect: (city: string) => void;
|
|
}) {
|
|
return (
|
|
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
|
|
<Pressable style={styles.sheetBackdrop} onPress={onClose}>
|
|
<View style={styles.sheet}>
|
|
<View style={styles.sheetHandle} />
|
|
<Text style={styles.sheetTitle}>选择所在城市</Text>
|
|
<ScrollView style={styles.sheetList} showsVerticalScrollIndicator={false}>
|
|
{CITIES.map((city) => {
|
|
const active = city === current;
|
|
return (
|
|
<Pressable
|
|
key={city}
|
|
style={[styles.sheetItem, active && styles.sheetItemActive]}
|
|
onPress={() => onSelect(city)}
|
|
>
|
|
<Text style={[styles.sheetItemText, active && styles.sheetItemTextActive]}>
|
|
{city}
|
|
</Text>
|
|
{active && <Ionicons name="checkmark" size={18} color={colors.primary} />}
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</ScrollView>
|
|
</View>
|
|
</Pressable>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:保存记录底部弹层 ===================== */
|
|
|
|
function SaveSheet({
|
|
visible,
|
|
note,
|
|
onNoteChange,
|
|
onSave,
|
|
onClose,
|
|
saving,
|
|
}: {
|
|
visible: boolean;
|
|
note: string;
|
|
onNoteChange: (v: string) => void;
|
|
onSave: () => void;
|
|
onClose: () => void;
|
|
saving: boolean;
|
|
}) {
|
|
return (
|
|
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
|
|
<Pressable style={styles.sheetBackdrop} onPress={onClose}>
|
|
<View style={styles.sheet}>
|
|
<View style={styles.sheetHandle} />
|
|
<Text style={styles.sheetTitle}>保存计算记录</Text>
|
|
<Text style={styles.saveHint}>备注(选填,便于区分不同方案)</Text>
|
|
<TextInput
|
|
style={styles.saveNoteInput}
|
|
value={note}
|
|
onChangeText={onNoteChange}
|
|
placeholder="如:深圳 offer 对比"
|
|
placeholderTextColor={colors.textHint}
|
|
maxLength={20}
|
|
/>
|
|
<View style={styles.saveActions}>
|
|
<Pressable style={styles.saveCancel} onPress={onClose}>
|
|
<Text style={styles.saveCancelText}>取消</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
style={[styles.saveConfirm, saving && styles.saveConfirmDisabled]}
|
|
onPress={onSave}
|
|
disabled={saving}
|
|
>
|
|
<Text style={styles.saveConfirmText}>{saving ? '保存中…' : '保存'}</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</Pressable>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:历史记录底部弹层 ===================== */
|
|
|
|
function HistoryModal({
|
|
visible,
|
|
records,
|
|
loading,
|
|
error,
|
|
onRetry,
|
|
onSelect,
|
|
onDelete,
|
|
onClose,
|
|
}: {
|
|
visible: boolean;
|
|
records: SalaryCalcRecord[];
|
|
loading: boolean;
|
|
error: Error | null;
|
|
onRetry: () => void;
|
|
onSelect: (record: SalaryCalcRecord) => void;
|
|
onDelete: (id: string) => void;
|
|
onClose: () => void;
|
|
}) {
|
|
return (
|
|
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
|
|
<Pressable style={styles.sheetBackdrop} onPress={onClose}>
|
|
<View style={styles.sheet}>
|
|
<View style={styles.sheetHandle} />
|
|
<Text style={styles.sheetTitle}>计算历史</Text>
|
|
{loading ? (
|
|
<View style={styles.historyBody}>
|
|
<LoadingState text="加载历史记录…" />
|
|
</View>
|
|
) : error ? (
|
|
<View style={styles.historyBody}>
|
|
<ErrorState message="历史记录加载失败" onRetry={onRetry} />
|
|
</View>
|
|
) : records.length === 0 ? (
|
|
<View style={styles.historyBody}>
|
|
<EmptyState icon="time-outline" title="暂无保存的计算记录" subtitle="在下方「保存计算记录」即可留档" />
|
|
</View>
|
|
) : (
|
|
<ScrollView style={styles.historyList} showsVerticalScrollIndicator={false}>
|
|
{records.map((record) => (
|
|
<View key={record.id} style={styles.historyRow}>
|
|
<Pressable style={styles.historyRowMain} onPress={() => onSelect(record)}>
|
|
<Text style={styles.historyTitle} numberOfLines={1}>
|
|
{record.note || `${record.input.city} · 税前${formatCurrency(record.input.monthlyGross, 0)}`}
|
|
</Text>
|
|
<Text style={styles.historySub}>
|
|
到手 {formatCurrency(record.result.monthlyNet)}/月 · {formatDateTime(record.createdAt)}
|
|
</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
style={styles.historyDelete}
|
|
onPress={() =>
|
|
Alert.alert('删除记录', '确定删除这条计算记录?', [
|
|
{ text: '取消', style: 'cancel' },
|
|
{ text: '删除', style: 'destructive', onPress: () => onDelete(record.id) },
|
|
])
|
|
}
|
|
hitSlop={8}
|
|
>
|
|
<Ionicons name="trash-outline" size={20} color={colors.danger} />
|
|
</Pressable>
|
|
</View>
|
|
))}
|
|
</ScrollView>
|
|
)}
|
|
</View>
|
|
</Pressable>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
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: 60, justifyContent: 'center' },
|
|
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
|
|
headerAction: { fontSize: 14, color: colors.primary, textAlign: 'right' },
|
|
body: { padding: spacing.card, paddingBottom: 32 },
|
|
card: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: spacing.card,
|
|
padding: spacing.card,
|
|
marginBottom: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
cardTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 14 },
|
|
inputRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingVertical: 12,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
inputLabel: { fontSize: 14, color: colors.textPrimary },
|
|
inputBox: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: colors.pageBg,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: spacing.card,
|
|
},
|
|
input: {
|
|
minWidth: 80,
|
|
fontSize: 15,
|
|
fontWeight: '700',
|
|
color: colors.textPrimary,
|
|
textAlign: 'right',
|
|
},
|
|
inputSuffix: { fontSize: 13, color: colors.textSecondary, marginLeft: 4 },
|
|
selectBox: { flexDirection: 'row', alignItems: 'center' },
|
|
selectValue: { fontSize: 15, fontWeight: '600', color: colors.textPrimary, marginRight: 4 },
|
|
resultCard: {
|
|
borderRadius: spacing.card,
|
|
padding: spacing.card,
|
|
alignItems: 'center',
|
|
marginBottom: 8,
|
|
},
|
|
resultLabel: { fontSize: 14, color: colors.textInverse, opacity: 0.9 },
|
|
resultValue: { fontSize: 42, fontWeight: '800', color: colors.textInverse, marginTop: 8 },
|
|
resultUnit: { fontSize: 14, color: colors.textInverse, opacity: 0.9, marginTop: 2 },
|
|
cardSubTitleRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginBottom: 10,
|
|
},
|
|
cardSubTitle: { fontSize: 12, color: colors.textHint },
|
|
insuranceRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: 10,
|
|
},
|
|
insuranceLabel: { flex: 1, fontSize: 14, color: colors.textPrimary },
|
|
insuranceAmount: { width: 80, fontSize: 14, fontWeight: '700', color: colors.textPrimary, textAlign: 'right' },
|
|
insuranceRate: { width: 60, fontSize: 13, color: colors.textSecondary, textAlign: 'right' },
|
|
divider: { height: 1, backgroundColor: colors.borderLight, marginVertical: 10 },
|
|
totalRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
|
totalLabel: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
|
|
totalAmount: { fontSize: 18, fontWeight: '800', color: colors.primary },
|
|
taxGrid: { flexDirection: 'row', gap: 12, marginBottom: 10 },
|
|
taxItem: {
|
|
flex: 1,
|
|
backgroundColor: colors.pageBg,
|
|
borderRadius: spacing.card,
|
|
padding: spacing.card,
|
|
},
|
|
taxItemLabel: { fontSize: 12, color: colors.textSecondary, marginBottom: 6 },
|
|
taxItemValue: { fontSize: 18, fontWeight: '800', color: colors.textPrimary },
|
|
taxRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
paddingVertical: spacing.card,
|
|
},
|
|
taxRowLabel: { fontSize: 14, color: colors.textPrimary },
|
|
taxRowValue: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
|
|
structureChart: { flexDirection: 'row', alignItems: 'center' },
|
|
pie: {
|
|
width: 100,
|
|
height: 100,
|
|
borderRadius: 50,
|
|
backgroundColor: colors.borderLight,
|
|
overflow: 'hidden',
|
|
},
|
|
pieSlice: {
|
|
position: 'absolute',
|
|
width: 100,
|
|
height: 100,
|
|
borderRadius: 50,
|
|
},
|
|
structureLegend: { flex: 1, marginLeft: 20 },
|
|
legendRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 10 },
|
|
legendDot: { width: 10, height: 10, borderRadius: 5 },
|
|
legendLabel: { flex: 1, fontSize: 13, color: colors.textPrimary, marginLeft: 8 },
|
|
legendValue: { fontSize: 13, fontWeight: '700', color: colors.textPrimary, marginRight: 8 },
|
|
legendPercent: { width: 40, fontSize: 12, color: colors.textSecondary, textAlign: 'right' },
|
|
toolRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: 12,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
toolIcon: {
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 18,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
toolBody: { flex: 1, marginLeft: 12 },
|
|
toolTitle: { fontSize: 14, fontWeight: '700', color: colors.textPrimary },
|
|
toolDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2 },
|
|
saveBtn: {
|
|
backgroundColor: colors.primaryBtn,
|
|
borderRadius: 24,
|
|
paddingVertical: 14,
|
|
alignItems: 'center',
|
|
marginTop: 8,
|
|
},
|
|
saveBtnText: { fontSize: 16, fontWeight: '700', color: colors.white },
|
|
baseHint: {
|
|
fontSize: 12,
|
|
color: colors.textHint,
|
|
marginTop: 6,
|
|
paddingHorizontal: 2,
|
|
},
|
|
sheetBackdrop: {
|
|
flex: 1,
|
|
backgroundColor: colors.scrim,
|
|
justifyContent: 'flex-end',
|
|
},
|
|
sheet: {
|
|
backgroundColor: colors.white,
|
|
borderTopLeftRadius: 16,
|
|
borderTopRightRadius: 16,
|
|
paddingTop: 10,
|
|
paddingBottom: 24,
|
|
maxHeight: '72%',
|
|
},
|
|
sheetHandle: {
|
|
width: 36,
|
|
height: 4,
|
|
borderRadius: 2,
|
|
backgroundColor: colors.borderLight,
|
|
alignSelf: 'center',
|
|
marginBottom: 10,
|
|
},
|
|
sheetTitle: {
|
|
fontSize: 16,
|
|
fontWeight: '700',
|
|
color: colors.textPrimary,
|
|
textAlign: 'center',
|
|
marginBottom: 12,
|
|
},
|
|
sheetList: { maxHeight: 360 },
|
|
sheetItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingVertical: 14,
|
|
paddingHorizontal: 20,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
sheetItemActive: { backgroundColor: colors.primaryLight },
|
|
sheetItemText: { fontSize: 15, color: colors.textPrimary },
|
|
sheetItemTextActive: { color: colors.primary, fontWeight: '700' },
|
|
saveHint: { fontSize: 12, color: colors.textHint, marginBottom: 8 },
|
|
saveNoteInput: {
|
|
backgroundColor: colors.pageBg,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 10,
|
|
fontSize: 15,
|
|
color: colors.textPrimary,
|
|
marginBottom: 16,
|
|
},
|
|
saveActions: { flexDirection: 'row', gap: 12 },
|
|
saveCancel: {
|
|
flex: 1,
|
|
backgroundColor: colors.pageBg,
|
|
borderRadius: 24,
|
|
paddingVertical: 13,
|
|
alignItems: 'center',
|
|
},
|
|
saveCancelText: { fontSize: 16, fontWeight: '700', color: colors.textSecondary },
|
|
saveConfirm: {
|
|
flex: 1,
|
|
backgroundColor: colors.primaryBtn,
|
|
borderRadius: 24,
|
|
paddingVertical: 13,
|
|
alignItems: 'center',
|
|
},
|
|
saveConfirmDisabled: { opacity: 0.6 },
|
|
saveConfirmText: { fontSize: 16, fontWeight: '700', color: colors.white },
|
|
historyBody: { height: 300, justifyContent: 'center' },
|
|
historyList: { maxHeight: 420 },
|
|
historyRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: 14,
|
|
paddingHorizontal: 4,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
historyRowMain: { flex: 1, marginRight: 8 },
|
|
historyTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
|
|
historySub: { fontSize: 12, color: colors.textSecondary, marginTop: 4 },
|
|
historyDelete: { padding: 6 },
|
|
});
|
|
|