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.
209 lines
6.6 KiB
209 lines
6.6 KiB
/**
|
|
* 简历编辑页共用件(基本信息 / 教育背景 / 工作经历 / 项目经历四页同一套骨架)。
|
|
*
|
|
* 抽出来的理由:四页的头部、表单行、四态、页脚完全一致,逐页复制会让
|
|
* 「错误提示样式」「禁用态按钮」这类细节在四处各自漂移。
|
|
*/
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import type { ReactNode } from 'react';
|
|
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
|
import { colors } from '../../src/theme';
|
|
|
|
/** 顶部导航:返回 + 居中标题(右侧留同宽占位,保证标题居中) */
|
|
export function EditHeader({ title, onBack }: { title: string; onBack: () => 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}>{title}</Text>
|
|
<View style={styles.headerBtn} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 表单行。`error` 只在**填了但填错**时传入 —— 空值是「未采集」不是「填错了」,
|
|
* 一进页面就飘红字会让人以为自己做错了什么。
|
|
*/
|
|
export function Field({
|
|
label,
|
|
value,
|
|
onChangeText,
|
|
placeholder,
|
|
keyboardType,
|
|
maxLength,
|
|
error,
|
|
hint,
|
|
multiline,
|
|
autoFocus,
|
|
required,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onChangeText: (v: string) => void;
|
|
placeholder: string;
|
|
keyboardType?: 'default' | 'phone-pad' | 'email-address' | 'number-pad';
|
|
maxLength?: number;
|
|
error?: string;
|
|
hint?: string;
|
|
multiline?: boolean;
|
|
autoFocus?: boolean;
|
|
/** 必填项在 label 后打一个红色星号(只做视觉标注,拦截规则在页面里) */
|
|
required?: boolean;
|
|
}) {
|
|
const hasError = error !== undefined && error !== '';
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.label}>
|
|
{label}
|
|
{required === true && <Text style={styles.requiredMark}> *</Text>}
|
|
</Text>
|
|
<TextInput
|
|
style={[styles.input, multiline === true && styles.inputMultiline, hasError && styles.inputError]}
|
|
value={value}
|
|
onChangeText={onChangeText}
|
|
placeholder={placeholder}
|
|
placeholderTextColor={colors.textHint}
|
|
keyboardType={keyboardType ?? 'default'}
|
|
maxLength={maxLength}
|
|
multiline={multiline}
|
|
autoFocus={autoFocus}
|
|
autoCapitalize="none"
|
|
/>
|
|
{hasError && <Text style={styles.fieldError}>{error}</Text>}
|
|
{!hasError && hint !== undefined && hint !== '' && <Text style={styles.hint}>{hint}</Text>}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/** 只读说明行(放在卡片或页脚里,字号小于正文) */
|
|
export function FormNote({ text }: { text: string }) {
|
|
return <Text style={styles.note}>{text}</Text>;
|
|
}
|
|
|
|
/** 白色圆角卡片容器(与简历首页的卡片同款) */
|
|
export function SectionCard({ title, right, children }: { title: string; right?: ReactNode; children: ReactNode }) {
|
|
return (
|
|
<View style={styles.card}>
|
|
<View style={styles.cardHeader}>
|
|
<Text style={styles.cardTitle}>{title}</Text>
|
|
{right}
|
|
</View>
|
|
{children}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/** 页脚容器:上方一条浅描边,主按钮靠它压底 */
|
|
export function PageFooter({ children }: { children: ReactNode }) {
|
|
return <View style={styles.footer}>{children}</View>;
|
|
}
|
|
|
|
/** 居中状态块(加载中 / 加载失败 / 空列表都走这一个) */
|
|
export function CenterState({
|
|
icon,
|
|
text,
|
|
actionLabel,
|
|
onAction,
|
|
}: {
|
|
icon?: keyof typeof Ionicons.glyphMap;
|
|
text: string;
|
|
actionLabel?: string;
|
|
onAction?: () => void;
|
|
}) {
|
|
return (
|
|
<View style={styles.center}>
|
|
{icon !== undefined && <Ionicons name={icon} size={40} color={colors.textHint} />}
|
|
<Text style={styles.centerText}>{text}</Text>
|
|
{actionLabel !== undefined && onAction !== undefined && (
|
|
<Pressable style={styles.centerBtn} onPress={onAction} hitSlop={6}>
|
|
<Text style={styles.centerBtnText}>{actionLabel}</Text>
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/** 列表行右侧的图标按钮(编辑 / 删除) */
|
|
export function IconAction({
|
|
icon,
|
|
onPress,
|
|
color,
|
|
label,
|
|
}: {
|
|
icon: keyof typeof Ionicons.glyphMap;
|
|
onPress: () => void;
|
|
color?: string;
|
|
/** 无障碍标签:图标按钮对读屏用户是没有名字的,必须补一个 */
|
|
label: string;
|
|
}) {
|
|
return (
|
|
<Pressable onPress={onPress} hitSlop={8} style={styles.iconBtn} accessibilityLabel={label}>
|
|
<Ionicons name={icon} size={18} color={color ?? colors.textHint} />
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 12,
|
|
backgroundColor: colors.white,
|
|
},
|
|
headerBtn: { width: 32, height: 32, alignItems: 'center', justifyContent: 'center' },
|
|
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
|
|
section: { marginBottom: 18 },
|
|
label: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 10 },
|
|
requiredMark: { color: colors.danger },
|
|
input: {
|
|
height: 50,
|
|
borderRadius: 12,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.white,
|
|
paddingHorizontal: 14,
|
|
fontSize: 16,
|
|
color: colors.textPrimary,
|
|
},
|
|
inputMultiline: { height: 120, paddingTop: 12, textAlignVertical: 'top' },
|
|
inputError: { borderColor: colors.dangerBorder },
|
|
fieldError: { fontSize: 12, color: colors.dangerText, marginTop: 6 },
|
|
hint: { fontSize: 12, color: colors.textHint, marginTop: 6 },
|
|
note: { fontSize: 12, color: colors.textHint, lineHeight: 18 },
|
|
card: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: 8,
|
|
padding: 16,
|
|
marginBottom: 12,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
cardHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: 10,
|
|
},
|
|
cardTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary },
|
|
footer: {
|
|
paddingHorizontal: 20,
|
|
paddingVertical: 14,
|
|
backgroundColor: colors.white,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12, paddingVertical: 40 },
|
|
centerText: { fontSize: 14, color: colors.textSecondary },
|
|
centerBtn: {
|
|
paddingHorizontal: 18,
|
|
paddingVertical: 8,
|
|
borderRadius: 16,
|
|
backgroundColor: colors.primaryLight,
|
|
},
|
|
centerBtnText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
|
|
iconBtn: { width: 32, height: 32, alignItems: 'center', justifyContent: 'center' },
|
|
});
|
|
|