在这里插入图片描述

访客管理是小区门禁管理系统中的重要功能,它为住户提供了邀请访客、管理访客记录、审核访客申请的便捷方式,同时确保了小区的安全管控。

在实际项目中,访客管理页面需要解决几个关键问题:

  • 如何支持访客记录的分类展示和筛选
  • 如何处理访客的审核状态和权限管理
  • 如何提供访客邀请码和二维码功能
  • 如何展示访客的访问历史和统计信息

这篇讲访客管理页面的实现,重点是如何让访客管理既安全又便捷。

对应源码

  • lib/pages/home/visitor_page.dart

访客管理页面的设计思路是:分类清晰 + 状态明确 + 操作便捷

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'visitor_detail_page.dart';

首先引入核心依赖包,Flutter基础组件库满足页面布局需求,screenutil用于屏幕适配,GetX框架简化页面跳转,同时引入访客详情页组件支撑后续跳转逻辑。

class VisitorPage extends StatefulWidget {
  const VisitorPage({Key? key}) : super(key: key);

  
  State<VisitorPage> createState() => _VisitorPageState();
}

将访客管理页面定义为有状态组件,因为页面需要维护Tab切换状态、访客列表数据等动态内容,StatefulWidget能更好地处理状态变更。

class _VisitorPageState extends State<VisitorPage> with SingleTickerProviderStateMixin {
  late TabController _tabController;
  List<Map<String, dynamic>> _visitors = [];
  bool _isLoading = false;

页面状态类混入SingleTickerProviderStateMixin,为TabController提供动画所需的Ticker;同时声明核心变量,_tabController控制Tab切换,_visitors存储所有访客数据,_isLoading标记数据加载状态。

基础结构说明

  • 访客管理页面需要维护Tab状态和访客列表,所以用 StatefulWidget
  • 使用 SingleTickerProviderStateMixin 支持 Tab 切换动画。
  • _tabController 控制 Tab 切换。
  • _visitors 存储所有访客记录。
  
  void initState() {
    super.initState();
    _tabController = TabController(length: 2, vsync: this);
    _loadVisitors();
  }

在页面初始化阶段,创建长度为2的TabController(对应待审核/已通过两个标签),并立即调用_loadVisitors方法加载访客数据,保证页面渲染时已有数据支撑。

  
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

页面销毁时释放TabController资源,避免内存泄漏,这是Flutter中使用动画控制器的标准操作,确保资源合理回收。

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('访客管理'),
        centerTitle: true,
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: '待审核'),
            Tab(text: '已通过'),
          ],
        ),
      ),

构建页面主体布局,Scaffold作为基础骨架,AppBar设置页面标题并居中,底部嵌入TabBar,通过_tabController关联标签与内容,两个Tab分别对应待审核和已通过访客分类。

      body: TabBarView(
        controller: _tabController,
        children: [
          _buildVisitorList('pending'),
          _buildVisitorList('approved'),
        ],
      ),

TabBarView与TabBar联动,根据_tabController的选中状态,分别渲染对应状态的访客列表,通过传入不同状态参数复用_buildVisitorList方法,减少代码冗余。

      floatingActionButton: FloatingActionButton(
        onPressed: () => Get.to(() => const VisitorDetailPage(isNew: true)),
        child: const Icon(Icons.add),
      ),
    );
  }

添加悬浮按钮作为新增访客的快捷入口,点击后通过GetX跳转到访客详情页,并传入isNew参数标记为新增访客,简化页面跳转逻辑的同时明确操作意图。

页面布局设计

  • AppBar 底部使用 TabBar 提供两个标签页。
  • 使用 TabBarView 对应 TabBar 的内容。
  • 浮动按钮提供快速添加访客的入口。
  • Tab 切换使用 SingleTickerProviderStateMixin 确保动画流畅。
  Widget _buildVisitorList(String status) {
    final filteredVisitors = _visitors.where((v) => v['status'] == status).toList();

根据传入的状态参数筛选访客列表,将_visitors中对应status的访客数据过滤出来,实现不同标签页展示不同状态访客的核心逻辑。

    if (_isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

判断数据加载状态,若处于加载中,在列表区域显示圆形加载指示器,给用户明确的加载反馈,提升交互体验。

    if (filteredVisitors.isEmpty) {
      return _buildEmptyState(status);
    }

若筛选后的访客列表为空,调用_buildEmptyState方法渲染空状态页面,针对不同状态展示差异化的空状态提示,让用户清晰知晓当前列表状态。

    return RefreshIndicator(
      onRefresh: _refreshVisitors,
      child: ListView.builder(
        padding: EdgeInsets.all(16.w),
        itemCount: filteredVisitors.length,
        itemBuilder: (context, index) {
          return _buildVisitorItem(filteredVisitors[index]);
        },
      ),
    );
  }

为列表添加下拉刷新功能,RefreshIndicator包裹ListView.builder,刷新时触发_refreshVisitors方法重新加载数据;ListView.builder按需构建列表项,通过_buildVisitorItem渲染单个访客条目,提升大数据列表的性能。

访客列表组件

  • 根据状态筛选访客列表。
  • 加载状态显示进度器。
  • 空状态显示相应的提示信息。
  • 使用 RefreshIndicator 支持下拉刷新。
  Widget _buildEmptyState(String status) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            status == 'pending' ? Icons.pending_actions : Icons.check_circle,
            size: 80.sp,
            color: Colors.grey[300],
          ),

空状态页面居中布局,根据状态选择不同图标:待审核状态用pending_actions图标,已通过状态用check_circle图标,图标尺寸适配屏幕,颜色选用浅灰色,视觉上更柔和。

          SizedBox(height: 16.h),
          Text(
            status == 'pending' ? '暂无待审核访客' : '暂无已通过访客',
            style: TextStyle(
              fontSize: 16.sp,
              color: Colors.grey[600],
              fontWeight: FontWeight.w500,
            ),
          ),

添加间距后展示核心提示文字,不同状态对应不同文案,字体大小和颜色经过适配,加粗的字体让提示更醒目。

          SizedBox(height: 8.h),
          Text(
            status == 'pending' 
                ? '点击右下角邀请访客'
                : '已通过的访客将显示在这里',
            style: TextStyle(
              fontSize: 12.sp,
              color: Colors.grey[500],
            ),
          ),
        ],
      ),
    );
  }

补充辅助提示文字,待审核状态引导用户点击悬浮按钮添加访客,已通过状态说明数据展示规则,字体尺寸更小,颜色更浅,形成视觉层级,提升用户理解度。

空状态设计

  • 根据不同状态显示不同的图标和文字。
  • 提供明确的操作指引。
  • 布局居中,视觉上更平衡。
  • 图标选择符合状态特征。
  Widget _buildVisitorItem(Map<String, dynamic> visitor) {
    return Container(
      margin: EdgeInsets.only(bottom: 12.h),
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
        border: Border.all(color: Colors.grey[300]!),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.1),
            blurRadius: 4.r,
            offset: Offset(0, 2.h),
          ),
        ],
      ),

单个访客条目使用容器包裹,设置底部间距避免条目拥挤,内边距保证内容不贴边;白色背景搭配圆角边框和轻微阴影,提升卡片质感,边框选用浅灰色,视觉上更清爽。

      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 访客信息头部
          Row(
            children: [
              // 头像
              Container(
                width: 40.w,
                height: 40.w,
                decoration: BoxDecoration(
                  color: Colors.blue[50],
                  shape: BoxShape.circle,
                ),

访客条目内部采用列布局,头部为行布局展示核心信息;首先构建圆形头像容器,尺寸适配屏幕,浅蓝色背景搭配圆形造型,符合移动端UI设计习惯。

                child: Icon(
                  Icons.person,
                  color: Colors.blue,
                  size: 20.sp,
                ),
              ),
              SizedBox(width: 12.w),

头像容器内嵌入person图标,蓝色图标与浅蓝背景形成层次感,添加右侧间距,分隔头像与访客信息区域,保证布局呼吸感。

              // 访客信息
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      visitor['name'],
                      style: TextStyle(
                        fontSize: 16.sp,
                        fontWeight: FontWeight.bold,
                      ),
                    ),

访客信息区域占满剩余空间,列布局展示姓名和电话;姓名使用加粗字体,尺寸更大,突出核心信息,符合用户视觉优先级习惯。

                    SizedBox(height: 4.h),
                    Text(
                      visitor['phone'],
                      style: TextStyle(
                        fontSize: 12.sp,
                        color: Colors.grey[600],
                      ),
                    ),
                  ],
                ),
              ),

电话信息字体更小、颜色更浅,与姓名形成视觉区分,少量间距保证两行文字不拥挤,提升可读性。

              // 状态标签
              Container(
                padding: EdgeInsets.symmetric(
                  horizontal: 8.w,
                  vertical: 4.h,
                ),
                decoration: BoxDecoration(
                  color: _getStatusColor(visitor['status']),
                  borderRadius: BorderRadius.circular(8.r),
                ),

状态标签容器设置对称内边距,圆角设计让标签更柔和,背景色通过_getStatusColor方法根据访客状态动态获取,实现状态可视化。

                child: Text(
                  _getStatusText(visitor['status']),
                  style: TextStyle(
                    fontSize: 10.sp,
                    color: Colors.white,
                  ),
                ),
              ),
            ],
          ),
          SizedBox(height: 12.h),

状态标签文字通过_getStatusText方法获取对应中文,白色文字搭配彩色背景,对比鲜明;头部区域结束后添加底部间距,分隔头部与访问信息区域。

          // 访问信息
          Container(
            width: double.infinity,
            padding: EdgeInsets.all(12.w),
            decoration: BoxDecoration(
              color: Colors.grey[50],
              borderRadius: BorderRadius.circular(8.r),
            ),

访问信息区域设置通栏宽度,浅灰色背景区分于主体白色,内边距和圆角保证内容展示舒适,提升信息区域的辨识度。

            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '来访事由:${visitor['purpose']}',
                  style: TextStyle(
                    fontSize: 12.sp,
                    color: Colors.grey[700],
                  ),
                ),

来访事由作为访问信息的第一项,字体尺寸适配,深灰色文字保证可读性,同时不抢夺核心信息的视觉焦点。

                SizedBox(height: 4.h),
                Text(
                  '访问日期:${visitor['visitDate']}',
                  style: TextStyle(
                    fontSize: 12.sp,
                    color: Colors.grey[700],
                  ),
                ),

添加小间距后展示访问日期,与来访事由样式保持一致,保证信息展示的统一性,让用户阅读更流畅。

                SizedBox(height: 4.h),
                Text(
                  '邀请码:${visitor['accessCode']}',
                  style: TextStyle(
                    fontSize: 12.sp,
                    color: Colors.grey[700],
                  ),
                ),
              ],
            ),
          ),
          SizedBox(height: 12.h),

邀请码是访客通行的核心凭证,放置在访问信息区域最后一项,样式与前两项统一,区域结束后添加底部间距,分隔访问信息与操作按钮。

          // 操作按钮
          if (visitor['status'] == 'pending')
            Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                TextButton(
                  onPressed: () => _rejectVisitor(visitor),
                  child: const Text('拒绝'),
                ),

针对待审核状态的访客,展示操作按钮行,按钮右对齐符合移动端操作习惯;首先展示拒绝按钮,点击触发_rejectVisitor方法处理拒绝逻辑。

                SizedBox(width: 8.w),
                ElevatedButton(
                  onPressed: () => _approveVisitor(visitor),
                  child: const Text('通过'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.green,
                    foregroundColor: Colors.white,
                  ),
                ),
              ],
            )

拒绝按钮右侧添加小间距,再展示通过按钮,绿色背景搭配白色文字,突出通过操作的视觉优先级,点击触发_approveVisitor方法处理通过逻辑。

          else
            Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                TextButton(
                  onPressed: () => _viewQrCode(visitor),
                  child: const Text('查看二维码'),
                ),

非待审核状态(已通过/已拒绝)展示查看类按钮,首先是查看二维码按钮,点击触发_viewQrCode方法弹出二维码对话框,满足访客通行凭证查看需求。

                SizedBox(width: 8.w),
                TextButton(
                  onPressed: () => _viewVisitorDetail(visitor),
                  child: const Text('查看详情'),
                ),
              ],
            ),
        ],
      ),
    );
  }

查看二维码按钮右侧添加间距,再展示查看详情按钮,点击触发_viewVisitorDetail方法跳转到详情页,满足用户查看完整访客信息的需求。

访客条目设计

  • 显示访客头像、姓名、电话、状态等信息。
  • 访问信息区域显示来访事由、访问日期、邀请码。
  • 待审核状态显示拒绝和通过按钮。
  • 已通过状态显示查看二维码和详情按钮。
  Color _getStatusColor(String status) {
    switch (status) {
      case 'pending':
        return Colors.orange;
      case 'approved':
        return Colors.green;
      case 'rejected':
        return Colors.red;
      default:
        return Colors.grey;
    }
  }

getStatusColor方法根据访客状态返回对应颜色:待审核(橙色)、已通过(绿色)、已拒绝(红色),默认灰色,通过色彩直观区分访客状态,符合用户认知习惯。

  String _getStatusText(String status) {
    switch (status) {
      case 'pending':
        return '待审核';
      case 'approved':
        return '已通过';
      case 'rejected':
        return '已拒绝';
      default:
        return '未知';
    }
  }

getStatusText方法将状态标识转换为中文文本,与getStatusColor配合使用,让状态标签既有色差又有文字说明,信息传达更清晰。

状态处理方法

  • _getStatusColor 获取状态对应的颜色。
  • _getStatusText 获取状态的中文文本。
  • 待审核(橙色)、已通过(绿色)、已拒绝(红色)。
  Future<void> _loadVisitors() async {
    setState(() {
      _isLoading = true;
    });

loadVisitors方法用于加载访客数据,首先更新_isLoading为true,触发UI刷新显示加载状态,保证用户感知数据加载过程。

    // 模拟加载访客数据
    await Future.delayed(const Duration(seconds: 1));

通过Future.delayed模拟异步请求,延迟1秒模拟网络请求耗时,让加载状态有足够时间展示,符合真实项目的异步数据加载逻辑。

    final mockVisitors = [
      {
        'id': '1',
        'name': '张三',
        'phone': '13800138000',
        'purpose': '朋友访问',
        'visitDate': '2024-01-20',
        'accessCode': 'ABC123',
        'status': 'pending',
        'createTime': '2024-01-18 14:30',
      },

构造模拟访客数据列表,第一条为待审核状态的访客,包含id、姓名、电话、来访事由等完整字段,覆盖访客管理所需的核心信息。

      {
        'id': '2',
        'name': '李四',
        'phone': '13900139000',
        'purpose': '快递员',
        'visitDate': '2024-01-19',
        'accessCode': 'DEF456',
        'status': 'approved',
        'createTime': '2024-01-17 10:15',
      },

第二条为已通过状态的访客,字段与第一条保持一致,不同的状态和内容模拟真实场景中的不同访客数据。

      {
        'id': '3',
        'name': '王五',
        'phone': '13700137000',
        'purpose': '家政服务',
        'visitDate': '2024-01-18',
        'accessCode': 'GHI789',
        'status': 'approved',
        'createTime': '2024-01-16 16:45',
      },

第三条同样为已通过状态,丰富已通过访客的模拟数据,让列表展示更贴近真实场景。

      {
        'id': '4',
        'name': '赵六',
        'phone': '13600136000',
        'purpose': '装修工人',
        'visitDate': '2024-01-21',
        'accessCode': 'JKL012',
        'status': 'rejected',
        'createTime': '2024-01-15 09:20',
      },
    ];

第四条为已拒绝状态的访客,补充不同状态的模拟数据,覆盖访客管理的全状态场景。

    setState(() {
      _visitors = mockVisitors;
      _isLoading = false;
    });
  }

数据加载完成后,更新_visitors为模拟数据,同时将_isLoading置为false,触发UI刷新展示访客列表,完成数据加载的核心逻辑。

数据加载逻辑

  • 模拟异步加载访客数据。
  • 包含完整的访客信息:姓名、电话、来访事由、访问日期、邀请码、状态等。
  • 不同状态的访客:待审核、已通过、已拒绝。
  • 加载完成后更新UI状态。
  Future<void> _refreshVisitors() async {
    await _loadVisitors();
  }

refreshVisitors方法作为下拉刷新的回调,直接复用loadVisitors方法重新加载数据,保证刷新逻辑与初始加载逻辑一致,减少代码冗余。

  void _approveVisitor(Map<String, dynamic> visitor) async {
    setState(() {
      final index = _visitors.indexWhere((v) => v['id'] == visitor['id']);
      if (index != -1) {
        _visitors[index]['status'] = 'approved';
      }
    });

approveVisitor方法处理访客审核通过逻辑,首先通过id找到对应访客在列表中的索引,若找到则更新其状态为approved,通过setState触发UI刷新,实时展示状态变更。

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('访客审核通过'),
        backgroundColor: Colors.green,
      ),
    );
  }

状态更新后弹出SnackBar提示,绿色背景与通过状态呼应,给用户明确的操作反馈,提升交互体验。

  void _rejectVisitor(Map<String, dynamic> visitor) async {
    setState(() {
      final index = _visitors.indexWhere((v) => v['id'] == visitor['id']);
      if (index != -1) {
        _visitors[index]['status'] = 'rejected';
      }
    });

rejectVisitor方法处理访客审核拒绝逻辑,与通过逻辑类似,找到对应访客后更新状态为rejected,触发UI刷新展示最新状态。

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('访客审核拒绝'),
        backgroundColor: Colors.orange,
      ),
    );
  }

弹出橙色背景的SnackBar提示拒绝操作成功,色彩与拒绝状态的视觉特征匹配,让用户快速感知操作结果。

  void _viewQrCode(Map<String, dynamic> visitor) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('访客二维码'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              '访客姓名:${visitor['name']}',
              style: TextStyle(fontSize: 14.sp),
            ),

viewQrCode方法弹出对话框展示访客二维码相关信息,对话框标题明确,内容区域采用列布局且设置最小主轴尺寸,避免对话框过高;首先展示访客姓名,字体尺寸适配,清晰展示核心信息。

            SizedBox(height: 8.h),
            Text(
              '邀请码:${visitor['accessCode']}',
              style: TextStyle(
                fontSize: 16.sp,
                fontWeight: FontWeight.bold,
                color: Colors.blue,
              ),
            ),

添加间距后展示邀请码,加粗且放大字体,蓝色文字突出核心凭证信息,让用户快速识别邀请码。

            SizedBox(height: 16.h),
            Container(
              width: 150.w,
              height: 150.w,
              decoration: BoxDecoration(
                border: Border.all(color: Colors.grey[300]!),
                borderRadius: BorderRadius.circular(8.r),
              ),

构建二维码展示容器,正方形尺寸适配屏幕,浅灰色边框搭配圆角,模拟真实二维码的展示样式,提升视觉还原度。

              child: Center(
                child: Text(
                  '二维码\n${visitor['accessCode']}',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 12.sp,
                    color: Colors.grey[600],
                  ),
                ),
              ),
            ),
          ],
        ),

容器内居中展示占位文本,标注二维码和邀请码,模拟真实二维码的位置,后续可替换为真实的二维码生成组件,保证代码的可拓展性。

        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('关闭'),
          ),
        ],
      ),
    );
  }

对话框添加关闭按钮,点击关闭对话框,符合移动端弹窗的交互逻辑,保证用户操作的闭环。

  void _viewVisitorDetail(Map<String, dynamic> visitor) {
    Get.to(() => VisitorDetailPage(visitor: visitor));
  }
}

viewVisitorDetail方法通过GetX跳转到访客详情页,并传入当前访客数据,让详情页能够展示完整的访客信息,完成访客信息查看的核心逻辑。

交互功能实现

  • _approveVisitor 审核通过访客申请。
  • _rejectVisitor 审核拒绝访客申请。
  • _viewQrCode 显示访客二维码对话框。
  • _viewVisitorDetail 跳转到访客详情页面。
  • 所有操作都有相应的用户反馈。

用户体验设计

访客管理页面的用户体验重点:

  1. Tab分类:待审核和已通过访客分开管理
  2. 状态可视化:用颜色和标签区分访客状态
  3. 操作便捷:审核操作和查看功能易于访问
  4. 信息完整:显示访客的所有关键信息

这些设计让访客管理变得简单直观。

安全性考虑

访客管理的安全要点:

  1. 审核机制:所有访客需要审核才能生效
  2. 邀请码安全:随机生成唯一的访问码
  3. 权限控制:访客权限严格控制和记录
  4. 访问记录:完整的访客访问历史记录

这些安全措施确保访客管理的安全性。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐