You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中处理Supabase查询结果的方案(含登录校验场景)

Flutter自定义Supabase查询校验实现(受访者登录)

完整代码示例

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

void main() async {
  // 初始化Supabase(替换为你的项目URL和匿名密钥)
  await Supabase.initialize(
    url: '你的Supabase项目URL',
    anonKey: '你的Supabase匿名密钥',
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '受访者登录',
      home: LoginPage(),
    );
  }
}

// 受访者模型类,映射数据库表字段
class Respondent {
  final int id;
  final String householdNumber;
  final String pinPassword;
  // 根据你的表结构添加其他字段(如姓名、联系方式等)

  Respondent({
    required this.id,
    required this.householdNumber,
    required this.pinPassword,
    // 其他字段初始化
  });

  // 将Supabase返回的Map数据转换为Respondent对象
  factory Respondent.fromMap(Map<String, dynamic> map) {
    return Respondent(
      id: map['id'] as int,
      householdNumber: map['household_number'] as String,
      pinPassword: map['pin_password'] as String,
      // 其他字段映射
    );
  }
}

class LoginPage extends StatefulWidget {
  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _householdNumberController = TextEditingController();
  final _pinPasswordController = TextEditingController();
  final _supabase = Supabase.instance.client;
  bool _isLoading = false;

  Future<void> _handleLogin() async {
    setState(() => _isLoading = true);
    final householdNumber = _householdNumberController.text.trim();
    final pinPassword = _pinPasswordController.text.trim();

    // 输入合法性校验
    if (householdNumber.isEmpty || pinPassword.isEmpty) {
      _showErrorDialog('请填写完整的家庭编号和PIN码');
      setState(() => _isLoading = false);
      return;
    }

    try {
      // 查询匹配的受访者,maybeSingle()自动处理结果数量
      final response = await _supabase
          .from('respondents')
          .select()
          .eq('household_number', householdNumber)
          .eq('pin_password', pinPassword)
          .maybeSingle();

      if (response != null) {
        // 将查询结果转换为强类型对象
        final respondent = Respondent.fromMap(response);
        // 跳转至问卷页面并传递受访者信息
        Navigator.pushReplacement(
          context,
          MaterialPageRoute(
            builder: (context) => SurveyForm(respondent: respondent),
          ),
        );
      } else {
        _showErrorDialog('家庭编号或PIN码不正确');
      }
    } on PostgrestException catch (e) {
      // 捕获多条匹配结果的异常(错误码PGRST116)
      if (e.code == 'PGRST116') {
        _showErrorDialog('发现多个匹配的受访者,请联系管理员');
      } else {
        _showErrorDialog('登录失败:${e.message}');
      }
    } catch (e) {
      // 处理网络异常、数据库连接失败等通用错误
      _showErrorDialog('网络异常,请稍后重试');
    } finally {
      setState(() => _isLoading = false);
    }
  }

  // 错误提示弹窗
  void _showErrorDialog(String message) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('登录失败'),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('确定'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('受访者登录')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _householdNumberController,
              decoration: const InputDecoration(
                labelText: '家庭编号',
                border: OutlineInputBorder(),
              ),
              enabled: !_isLoading,
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _pinPasswordController,
              decoration: const InputDecoration(
                labelText: 'PIN码',
                border: OutlineInputBorder(),
              ),
              obscureText: true,
              enabled: !_isLoading,
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: _isLoading ? null : _handleLogin,
              child: _isLoading
                  ? const CircularProgressIndicator(color: Colors.white)
                  : const Text('登录'),
            ),
          ],
        ),
      ),
    );
  }
}

class SurveyForm extends StatelessWidget {
  final Respondent respondent;

  const SurveyForm({super.key, required this.respondent});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('调查问卷')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('欢迎,家庭编号:${respondent.householdNumber}'),
            // 此处添加调查问卷表单组件
          ],
        ),
      ),
    );
  }
}

核心逻辑说明

1. 模型类设计

定义Respondent类实现数据库字段与代码对象的映射,通过fromMap方法将Supabase返回的原始Map数据转换为强类型对象,避免直接操作松散的Map结构,提升代码可读性和可维护性。

2. 查询与结果处理

使用maybeSingle()替代弃用的execute():

  • 返回0条结果:返回null,触发"账号或密码错误"提示;
  • 返回1条结果:返回该条数据的Map对象,转换为Respondent后跳转至问卷页面;
  • 返回多条结果:抛出PostgrestException(错误码PGRST116),触发"多个匹配结果"提示。

3. 错误处理分层

  • 输入校验:提前拦截空输入,避免无效数据库查询;
  • 业务异常捕获:处理多条匹配、查询失败等业务相关错误;
  • 通用异常捕获:处理网络中断、数据库连接失败等系统级错误;
  • 统一弹窗提示:给用户清晰的错误反馈,避免无意义的报错信息。

4. 页面状态管理

添加_isLoading状态控制按钮可用性和加载提示,避免用户重复点击,提升交互体验。

内容的提问来源于stack exchange,提问作者Sheenergizer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 14:32:04