Ctrl K
ComunidadGuías…

src/components/LoginModal.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import React, { useEffect, useState } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { COLORS, FONT_SIZE, RADIUS, SPACING } from '../constants';
import Modal from './Modal';
import { hapticError, hapticLight } from '../utils/haptics';

interface Props {
  visible: boolean;
  baseUrl: string; // 已填的服务器地址(注册成功后写设置)
  onClose: () => void;
  onAuthed: (
    token: string,
    user: { id: number; displayName: string; avatarEmoji: string; familyId: number | null },
  ) => void;
  onError: (msg: string) => void;
}

// 登录 / 注册弹窗(注册成功自动登录)
export default function LoginModal({ visible, baseUrl, onClose, onAuthed, onError }: Props) {
  const [mode, setMode] = useState<'login' | 'register'>('login');
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [confirm, setConfirm] = useState('');
  const [displayName, setDisplayName] = useState('');
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    if (visible) {
      setUsername('');
      setPassword('');
      setConfirm('');
      setDisplayName('');
      setBusy(false);
    }
  }, [visible]);

  const submit = async () => {
    if (!username.trim() || !password) {
      hapticError();
      onError('请输入用户名和密码');
      return;
    }
    if (mode === 'register') {
      if (password.length < 6) {
        hapticError();
        onError('密码至少 6 位');
        return;
      }
      if (password !== confirm) {
        hapticError();
        onError('两次输入的密码不一致');
        return;
      }
    }
    setBusy(true);
    try {
      const { apiRegister, apiLogin } = await import('../sync/apiClient');
      const res =
        mode === 'register'
          ? await apiRegister(baseUrl, username.trim(), password, displayName.trim() || undefined)
          : await apiLogin(baseUrl, username.trim(), password);
      hapticLight();
      onAuthed(res.token, res.user);
    } catch (e) {
      hapticError();
      onError(e instanceof Error ? e.message : '登录失败');
    } finally {
      setBusy(false);
    }
  };

  return (
    <Modal
      visible={visible}
      title={mode === 'login' ? '登录' : '注册'}
      saveLabel={mode === 'login' ? '登录' : '注册并登录'}
      onSave={submit}
      saveDisabled={busy}
      onClose={onClose}
    >
      <View style={styles.formGroup}>
        <Text style={styles.fieldLabel}>用户名(中英文/数字/下划线)</Text>
        <TextInput
          style={styles.input}
          placeholder="如 dad / 妈妈"
          placeholderTextColor={COLORS.textTertiary}
          value={username}
          onChangeText={setUsername}
          maxLength={20}
          autoCapitalize="none"
        />
      </View>
      {mode === 'register' ? (
        <View style={styles.formGroup}>
          <Text style={styles.fieldLabel}>昵称(家庭成员列表中显示)</Text>
          <TextInput
            style={styles.input}
            placeholder="如 爸爸"
            placeholderTextColor={COLORS.textTertiary}
            value={displayName}
            onChangeText={setDisplayName}
            maxLength={12}
          />
        </View>
      ) : null}
      <View style={styles.formGroup}>
        <Text style={styles.fieldLabel}>密码</Text>
        <TextInput
          style={styles.input}
          placeholder="至少 6 位"
          placeholderTextColor={COLORS.textTertiary}
          value={password}
          onChangeText={setPassword}
          maxLength={64}
          secureTextEntry
        />
      </View>
      {mode === 'register' ? (
        <View style={styles.formGroup}>
          <Text style={styles.fieldLabel}>确认密码</Text>
          <TextInput
            style={styles.input}
            placeholder="再输入一次"
            placeholderTextColor={COLORS.textTertiary}
            value={confirm}
            onChangeText={setConfirm}
            maxLength={64}
            secureTextEntry
          />
        </View>
      ) : null}
      <Pressable
        style={styles.switchRow}
        onPress={() => setMode(mode === 'login' ? 'register' : 'login')}
        accessibilityRole="button"
      >
        <Text style={styles.switchText}>
          {mode === 'login' ? '没有账号?注册一个' : '已有账号?直接登录'}
        </Text>
      </Pressable>
    </Modal>
  );
}

const styles = StyleSheet.create({
  formGroup: {
    marginBottom: SPACING.md,
  },
  fieldLabel: {
    fontSize: FONT_SIZE.xs,
    color: COLORS.textTertiary,
    fontWeight: '600',
    marginBottom: 6,
  },
  input: {
    backgroundColor: COLORS.bgAlt,
    borderRadius: RADIUS.sm,
    paddingHorizontal: SPACING.md,
    paddingVertical: 10,
    fontSize: FONT_SIZE.md,
    color: COLORS.text,
  },
  switchRow: {
    alignItems: 'center',
    paddingVertical: SPACING.md,
  },
  switchText: {
    fontSize: FONT_SIZE.sm,
    color: COLORS.accentDark,
    fontWeight: '600',
  },
});