import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import React, { useState } from 'react'; import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Switch, Text, TextInput, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useOperationRules } from '../src/hooks'; import {colors, spacing} from '../src/theme'; import type { MatchMode, OperationRules } from '../src/types'; /* ===================== 子组件:顶部导航 ===================== */ function Header({ onBack }: { onBack: () => void }) { return ( 运营规则配置 ); } /* ===================== 子组件:匹配模式卡片 ===================== */ function MatchModeCard({ options, value, onChange, }: { options: OperationRules['matchModeOptions']; value: MatchMode; onChange: (mode: MatchMode) => void; }) { return ( {options.map((opt) => { const selected = opt.id === value; return ( onChange(opt.id)} > {opt.name} {opt.recommended && ( 推荐 )} {opt.desc} {selected && } ); })} ); } /* ===================== 子组件:阈值滑块 ===================== */ function ThresholdRow({ label, value, suffix, onChange, }: { label: string; value: number; suffix: string; onChange: (val: number) => void; }) { const trackRef = React.useRef(null); const handlePress = (evt: { nativeEvent: { locationX: number } }) => { trackRef.current?.measure((_x, _y, width) => { const ratio = Math.max(0, Math.min(1, evt.nativeEvent.locationX / width)); onChange(Math.round(ratio * 100)); }); }; return ( {label} {value} {suffix} 0{suffix} 50{suffix} 100{suffix} ); } /* ===================== 子组件:开关行 ===================== */ function ToggleRow({ label, desc, value, onChange, }: { label: string; desc?: string; value: boolean; onChange: (val: boolean) => void; }) { return ( {label} {desc ? {desc} : null} ); } /* ===================== 页面 ===================== */ export default function OperationRulesScreen() { const router = useRouter(); const { rules, loading, error, refresh, saving, updateRules } = useOperationRules(); const [localLimit, setLocalLimit] = useState(''); if (loading || !rules) { return (
router.back()} /> ); } if (error) { return (
router.back()} /> 加载失败 重试 ); } const handleModeChange = (mode: MatchMode) => { updateRules((prev) => ({ ...prev, matchMode: mode })); }; const handleThresholdChange = (field: 'matchThreshold' | 'visibilityThreshold', val: number) => { updateRules((prev) => ({ ...prev, [field]: val })); }; const handleFilterToggle = (id: string, enabled: boolean) => { updateRules((prev) => ({ ...prev, autoFilters: prev.autoFilters.map((f) => (f.id === id ? { ...f, enabled } : f)), })); }; const handleLimitSubmit = () => { const num = parseInt(localLimit, 10); if (!Number.isNaN(num) && num > 0) { updateRules((prev) => ({ ...prev, dailyRecommendLimit: num })); setLocalLimit(''); } }; return (
router.back()} /> {/* 匹配模式 */} {/* 双向匹配配置 */} handleThresholdChange('matchThreshold', val)} /> handleThresholdChange('visibilityThreshold', val)} /> 每日推荐数量上限 人/天 {/* 自动筛选规则 */} {rules.autoFilters.map((filter, idx) => { const isLast = idx === rules.autoFilters.length - 1; return ( {filter.name} {filter.description} handleFilterToggle(filter.id, v)} trackColor={{ false: colors.border, true: colors.primaryLight }} thumbColor={filter.enabled ? colors.primary : colors.white} ios_backgroundColor={colors.border} /> ); })} {/* 推荐规则 */} updateRules((prev) => ({ ...prev, urgentPriority: v }))} /> updateRules((prev) => ({ ...prev, campusPriority: v }))} /> {/* 通知设置 */} updateRules((prev) => ({ ...prev, notifyNewDelivery: v }))} /> {saving && ( 保存中… )} ); } function SectionTitle({ title }: { title: string }) { return ( {title} ); } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: colors.pageBg }, header: { height: 44, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 8, backgroundColor: colors.white, }, headerBtn: { width: 36, height: 36, alignItems: 'center', justifyContent: 'center' }, headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary }, scroll: { flex: 1 }, center: { flex: 1, alignItems: 'center', justifyContent: 'center' }, tip: { fontSize: 14, color: colors.textSecondary }, retryBtn: { marginTop: 12, paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight, }, retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText }, sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 20, marginBottom: 10, paddingHorizontal: 12, }, sectionBar: { width: 4, height: 16, borderRadius: 2, backgroundColor: colors.primary, marginRight: 8 }, sectionTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary }, card: { marginHorizontal: 8, backgroundColor: colors.white, borderRadius: 8, paddingHorizontal: 8, paddingVertical: 8, }, modeRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.borderLight, }, modeRowSelected: { backgroundColor: colors.primaryLight }, modeTextWrap: { flex: 1, marginRight: 12 }, modeTitleRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 4 }, modeName: { fontSize: 15, fontWeight: '600', color: colors.textPrimary }, modeNameSelected: { color: colors.primaryText }, recommendedTag: { marginLeft: 8, backgroundColor: colors.successLight, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 8, }, recommendedText: { fontSize: 10, fontWeight: '600', color: colors.successText }, modeDesc: { fontSize: 12, color: colors.textSecondary, lineHeight: 18 }, radio: { width: 22, height: 22, borderRadius: 11, borderWidth: 2, borderColor: colors.borderStrong, alignItems: 'center', justifyContent: 'center', }, radioSelected: { backgroundColor: colors.primary, borderColor: colors.primary }, thresholdRow: { paddingVertical: 14 }, thresholdHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, thresholdLabel: { fontSize: 14, color: colors.textPrimary }, thresholdValue: { fontSize: 15, fontWeight: '700', color: colors.primary }, sliderTrack: { height: 6, borderRadius: 3, backgroundColor: colors.border, marginTop: 12, overflow: 'hidden', }, sliderFill: { position: 'absolute', left: 0, top: 0, bottom: 0, backgroundColor: colors.primary, borderRadius: 3, }, sliderThumb: { position: 'absolute', top: -5, marginLeft: -8, width: 16, height: 16, borderRadius: 8, backgroundColor: colors.white, borderWidth: 2, borderColor: colors.primary, }, sliderLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8, }, sliderLabel: { fontSize: 11, color: colors.textHint }, divider: { height: StyleSheet.hairlineWidth, backgroundColor: colors.borderLight }, limitRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 14, }, limitLabel: { fontSize: 14, color: colors.textPrimary }, limitInputWrap: { flexDirection: 'row', alignItems: 'center' }, limitInput: { width: 56, height: 32, borderWidth: 1, borderColor: colors.border, borderRadius: 8, textAlign: 'center', fontSize: 14, color: colors.textPrimary, padding: 0, }, limitUnit: { marginLeft: 6, fontSize: 13, color: colors.textSecondary }, filterRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 14, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.borderLight, }, filterRowLast: { borderBottomWidth: 0 }, filterText: { flex: 1, marginRight: 12 }, filterName: { fontSize: 14, fontWeight: '500', color: colors.textPrimary }, filterDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2 }, toggleRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 14, }, toggleText: { flex: 1, marginRight: 12 }, toggleLabel: { fontSize: 14, fontWeight: '500', color: colors.textPrimary }, toggleDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2 }, bottomPad: { height: 24 }, savingToast: { position: 'absolute', bottom: 24, alignSelf: 'center', flexDirection: 'row', alignItems: 'center', backgroundColor: colors.textPrimary, paddingHorizontal: spacing.framework, paddingVertical: 8, borderRadius: 18, }, savingText: { marginLeft: 8, fontSize: 13, color: colors.textInverse }, });