diff --git a/apps/user_app/design-qa.md b/apps/user_app/design-qa.md new file mode 100644 index 0000000..28097f6 --- /dev/null +++ b/apps/user_app/design-qa.md @@ -0,0 +1,25 @@ +# 用户端首页服务归属视觉验收 + +## 验收基线 + +- 视觉事实源:`D:\5k\platforms\docs\设计参考\用户端首页_服务归属方案1.png` +- 实现截图:`D:\5k\platforms\apps\user_app\design-qa\用户端首页_服务归属实现.png` +- 验收视口:390 × 844 CSS 像素,设备像素比 1 +- 页面状态:固定示例数据;所属气站“和气城南气站”,服务配送点“安顺路配送点”,公告 6 条 +- 事实源像素尺寸:853 × 1844;按 390 × 844 归一化后与实现截图比较 +- 实现截图像素尺寸:390 × 844;截图尺寸与 CSS 视口一致,无额外密度换算 + +## 对照证据 + +- 全页对照:`D:\5k\platforms\apps\user_app\design-qa\对照_全页.png`,左侧为归一化事实源,右侧为实现 +- 服务归属局部对照:`D:\5k\platforms\apps\user_app\design-qa\对照_服务归属卡片.png`,截取相同视口区域,左侧为事实源,右侧为实现 + +## 比较记录 + +1. 第一轮发现 P1:实现卡片使用 20 像素内边距和 72 像素图标底座,导致标题与两行字段整体比事实源右移约 12 像素,卡片高度也偏高约 7 像素。 +2. 修正为 16 像素内边距和 64 像素图标底座后重新截图;标题、标签、值、分隔线与图标的相对位置已与事实源对齐。 +3. 第二轮未发现 P0、P1 或 P2 视觉问题。页面无横向溢出,底部导航无遮挡,卡片边框、圆角、行距和信息层级一致。 + +## 最终结果 + +passed diff --git a/apps/user_app/design-qa/对照_全页.png b/apps/user_app/design-qa/对照_全页.png new file mode 100644 index 0000000..c651429 Binary files /dev/null and b/apps/user_app/design-qa/对照_全页.png differ diff --git a/apps/user_app/design-qa/对照_服务归属卡片.png b/apps/user_app/design-qa/对照_服务归属卡片.png new file mode 100644 index 0000000..6047a42 Binary files /dev/null and b/apps/user_app/design-qa/对照_服务归属卡片.png differ diff --git a/apps/user_app/design-qa/用户端首页_服务归属实现.png b/apps/user_app/design-qa/用户端首页_服务归属实现.png new file mode 100644 index 0000000..1de25f9 Binary files /dev/null and b/apps/user_app/design-qa/用户端首页_服务归属实现.png differ diff --git a/apps/user_app/lib/ui/features/home/home_page.dart b/apps/user_app/lib/ui/features/home/home_page.dart index afdc0fd..4cb90b7 100644 --- a/apps/user_app/lib/ui/features/home/home_page.dart +++ b/apps/user_app/lib/ui/features/home/home_page.dart @@ -1,9 +1,13 @@ +// 功能描述:展示用户端首页的安全内容、服务归属与公告列表。 +// 版本:1.1.0 import 'package:flutter/material.dart'; import '../../../data/repositories/client_repository.dart'; import '../../../domain/models/client_models.dart'; import '../../core/widgets.dart'; +import 'service_relation_card.dart'; +/// 用户端首页。 class HomePage extends StatefulWidget { const HomePage({required this.repository, super.key}); @@ -13,6 +17,7 @@ class HomePage extends StatefulWidget { State createState() => _HomePageState(); } +/// 管理首页远端数据加载与下拉刷新状态。 class _HomePageState extends State { late Future<(List, Map?)> _future; @@ -22,9 +27,11 @@ class _HomePageState extends State { _future = _load(); } + /// 并行语义上聚合公告和当前服务归属数据。 Future<(List, Map?)> _load() async => (await widget.repository.contents(), await widget.repository.serviceRelation()); + /// 重新加载首页数据并等待刷新完成。 Future _refresh() async { setState(() => _future = _load()); await _future; @@ -59,43 +66,7 @@ class _HomePageState extends State { description: '设备控制能力尚未开放,本页只展示真实服务与安全内容。', ), AppGutter( - child: SurfaceSection( - child: Row( - children: [ - Container( - width: 52, - height: 52, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(16), - ), - child: Icon( - Icons.store_mall_directory_outlined, - color: Theme.of(context).colorScheme.primary, - semanticLabel: '服务归属', - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('当前服务归属', style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 4), - Text( - relation == null - ? '尚未建立服务关系' - : '${relation['gas_name'] ?? ''} ${relation['delivery_name'] ?? ''}', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ], - ), - ), + child: ServiceRelationCard(relation: relation), ), const SizedBox(height: 32), const AppGutter( diff --git a/apps/user_app/lib/ui/features/home/service_relation_card.dart b/apps/user_app/lib/ui/features/home/service_relation_card.dart new file mode 100644 index 0000000..3b432bb --- /dev/null +++ b/apps/user_app/lib/ui/features/home/service_relation_card.dart @@ -0,0 +1,112 @@ +// 功能描述:展示用户当前所属气站与服务配送点,并处理无配送点和无归属状态。 +// 版本:1.0.0 +import 'package:flutter/material.dart'; +import 'package:heqi_design_system/heqi_design_system.dart'; + +/// 服务归属信息卡片,负责清晰区分气站主体与配送履约网点。 +class ServiceRelationCard extends StatelessWidget { + const ServiceRelationCard({required this.relation, super.key}); + + /// 服务端返回的当前有效服务关系;为空表示尚未建立归属。 + final Map? relation; + + /// 提取并清理指定关系字段,空字符串按无值处理。 + String? _relationValue(String key) { + final value = relation?[key]?.toString().trim() ?? ''; + return value.isEmpty ? null : value; + } + + @override + Widget build(BuildContext context) { + final gasName = _relationValue('gas_name'); + final deliveryName = _relationValue('delivery_name'); + final hasRelation = relation != null && gasName != null; + + return HeqiSurfaceSection( + padding: const EdgeInsets.all(HeqiSpacing.x4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(HeqiRadius.extraLarge), + ), + child: Icon( + Icons.store_mall_directory_outlined, + size: HeqiSize.iconLarge, + color: Theme.of(context).colorScheme.primary, + semanticLabel: '服务归属', + ), + ), + const SizedBox(width: HeqiSpacing.x4), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('当前服务归属', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: HeqiSpacing.x3), + if (!hasRelation) + Text( + '尚未建立服务关系', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + else ...[ + _RelationLine(label: '所属气站', value: gasName), + const Padding( + padding: EdgeInsets.symmetric(vertical: HeqiSpacing.x3), + child: Divider(height: 1), + ), + _RelationLine( + label: deliveryName == null ? '服务方式' : '服务配送点', + value: deliveryName ?? '气站直接服务', + ), + ], + ], + ), + ), + ], + ), + ); + } +} + +/// 服务归属中的单行标签和值,名称最多展示两行。 +class _RelationLine extends StatelessWidget { + const _RelationLine({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 76, + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + const SizedBox(width: HeqiSpacing.x2), + Expanded( + child: Text( + value, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ); +} diff --git a/apps/user_app/test/ui/service_relation_card_test.dart b/apps/user_app/test/ui/service_relation_card_test.dart new file mode 100644 index 0000000..dcb78f1 --- /dev/null +++ b/apps/user_app/test/ui/service_relation_card_test.dart @@ -0,0 +1,66 @@ +// 功能描述:验证服务归属卡片的信息层级、空值状态和长名称约束。 +// 版本:1.0.0 +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:user_app/ui/features/home/service_relation_card.dart'; + +/// 验证服务归属卡片的三类核心展示状态。 +void main() { + /// 构建统一测试宿主,提供 Material 主题和稳定宽度。 + Widget testHost(Map? relation) => MaterialApp( + home: Scaffold( + body: SizedBox( + width: 390, + child: Padding( + padding: const EdgeInsets.all(24), + child: ServiceRelationCard(relation: relation), + ), + ), + ), + ); + + testWidgets('气站与配送点使用独立标签展示', (tester) async { + await tester.pumpWidget( + testHost({ + 'gas_name': '和气城南气站', + 'delivery_name': '安顺路配送点', + }), + ); + + expect(find.text('当前服务归属'), findsOneWidget); + expect(find.text('所属气站'), findsOneWidget); + expect(find.text('和气城南气站'), findsOneWidget); + expect(find.text('服务配送点'), findsOneWidget); + expect(find.text('安顺路配送点'), findsOneWidget); + expect(find.text('和气城南气站 安顺路配送点'), findsNothing); + }); + + testWidgets('没有配送点时明确显示气站直接服务', (tester) async { + await tester.pumpWidget(testHost({'gas_name': '和气城南气站'})); + + expect(find.text('服务方式'), findsOneWidget); + expect(find.text('气站直接服务'), findsOneWidget); + expect(find.text('服务配送点'), findsNothing); + }); + + testWidgets('没有服务关系时显示明确空状态', (tester) async { + await tester.pumpWidget(testHost(null)); + + expect(find.text('尚未建立服务关系'), findsOneWidget); + expect(find.text('所属气站'), findsNothing); + }); + + testWidgets('较长组织名称最多显示两行', (tester) async { + const longName = '和气城市综合能源安全服务城南中心气站'; + await tester.pumpWidget( + testHost({ + 'gas_name': longName, + 'delivery_name': '安顺路配送点', + }), + ); + + final nameText = tester.widget(find.text(longName)); + expect(nameText.maxLines, 2); + expect(nameText.overflow, TextOverflow.ellipsis); + }); +} diff --git a/apps/user_app/tool/home_visual_preview.dart b/apps/user_app/tool/home_visual_preview.dart new file mode 100644 index 0000000..10ea9e9 --- /dev/null +++ b/apps/user_app/tool/home_visual_preview.dart @@ -0,0 +1,62 @@ +// 功能描述:使用固定示例数据启动用户端首页,供本地视觉验收使用。 +// 版本:1.0.0 +import 'package:flutter/material.dart'; +import 'package:user_app/data/repositories/client_repository.dart'; +import 'package:user_app/data/services/api_client.dart'; +import 'package:user_app/domain/models/client_models.dart'; +import 'package:user_app/ui/core/app_theme.dart'; +import 'package:user_app/ui/features/home/home_page.dart'; + +/// 启动不依赖账号和网络的首页视觉预览。 +void main() { + runApp(const _HomeVisualPreviewApp()); +} + +/// 首页视觉预览应用。 +class _HomeVisualPreviewApp extends StatelessWidget { + const _HomeVisualPreviewApp(); + + @override + Widget build(BuildContext context) => MaterialApp( + debugShowCheckedModeBanner: false, + theme: AppTheme.light(), + home: Scaffold( + body: HomePage(repository: _VisualClientRepository()), + bottomNavigationBar: NavigationBar( + selectedIndex: 0, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.home_outlined), + selectedIcon: Icon(Icons.home), + label: '首页', + ), + NavigationDestination(icon: Icon(Icons.shopping_bag_outlined), label: '商城'), + NavigationDestination(icon: Icon(Icons.receipt_long_outlined), label: '订单'), + NavigationDestination(icon: Icon(Icons.person_outline), label: '我的'), + ], + ), + ), + ); +} + +/// 提供稳定首页数据,避免视觉预览依赖真实账号和服务接口。 +class _VisualClientRepository extends ClientRepository { + _VisualClientRepository() : super(ApiClient(() => '')); + + @override + Future> contents() async => List.generate( + 6, + (index) => ClientRecord( + identity: 'notice-${9 - index}', + title: '模拟公告 ${9 - index}', + subtitle: 'notice', + raw: const {}, + ), + ); + + @override + Future> serviceRelation() async => const { + 'gas_name': '和气城南气站', + 'delivery_name': '安顺路配送点', + }; +} diff --git a/docs/03-用户端App需求.md b/docs/03-用户端App需求.md index 145bc63..f3b42f8 100644 --- a/docs/03-用户端App需求.md +++ b/docs/03-用户端App需求.md @@ -18,6 +18,7 @@ | --- | --- | --- | | 定位 | 获取当前位置并允许手动选择地址/气站服务区域 | 授权失败有手动入口,不以定位失败阻塞设备使用 | | 内容 | 轮播图、公告、安全宣传、安全视频列表 | 后台上下架后按缓存策略刷新;外链有风险提示 | +| 当前服务归属 | 分开显示“所属气站”和“服务配送点”,气站为主要信息、配送点为次要信息 | 不拼接组织名称;没有配送点时显示“服务方式:气站直接服务”;没有服务关系时显示明确空状态;组织名称最多两行 | | 智能瓶阀列表 | 显示名称、地址、在线状态、开关状态、最近告警、设备数量 | 可按地址/状态筛选;离线状态显著可见 | | 扫码添加 | 扫码或输入设备码,选择安装地址和设备昵称 | 校验设备归属、激活、服务区域和重复绑定 | | 设备接入 | 扫码添加、手动输入、蓝牙连接与设备校验 | 展示设备 ID、名称、安装位置;绑定前需用户确认,蓝牙权限失败有明确引导 | diff --git a/docs/操作日志_用户端首页服务归属展示优化_20260902.md b/docs/操作日志_用户端首页服务归属展示优化_20260902.md new file mode 100644 index 0000000..a9eb864 --- /dev/null +++ b/docs/操作日志_用户端首页服务归属展示优化_20260902.md @@ -0,0 +1,47 @@ +# 用户端首页服务归属展示优化操作日志 + +操作时间:2026-09-02 + +操作类型:修改、扩展 + +影响模块:用户端 Flutter App 首页 + +## 操作前状态 + +首页将接口返回的气站名称与配送点名称直接拼接在“当前服务归属”下方,例如“薛海 1”。用户无法判断两个值分别代表气站还是配送点;无配送点与无服务关系也缺少明确说明。 + +## 具体操作 + +1. 根据确认的视觉方案一新增独立服务归属卡片,沿用现有安全蓝、白色卡片、圆角和间距令牌。 +2. 将气站和配送点拆分为“所属气站”“服务配送点”两行,气站名称使用主要信息字重。 +3. 无配送点时显示“服务方式:气站直接服务”;无服务关系时显示“尚未建立服务关系”。 +4. 组织名称限制为最多两行,超出部分使用省略号,避免破坏移动端布局。 +5. 新增固定数据的本地视觉预览入口,并在 390 × 844 视口完成两轮截图对照。 +6. 新增组件测试,覆盖完整关系、气站直服、无关系和长名称四种情况。 + +## 操作后状态 + +首页能够明确区分服务主体与履约网点,不再展示难以理解的拼接名称。功能仍只读,不新增点击、状态或接口字段,原有公告加载、刷新和底部导航行为保持不变。 + +## 代码变更 + +- `apps/user_app/lib/ui/features/home/home_page.dart`:替换旧的拼接展示,接入独立服务归属卡片。 +- `apps/user_app/lib/ui/features/home/service_relation_card.dart`:新增服务归属组件及空值处理。 +- `apps/user_app/test/ui/service_relation_card_test.dart`:新增四类组件测试。 +- `apps/user_app/tool/home_visual_preview.dart`:新增不依赖账号和网络的视觉验收入口。 +- `apps/user_app/design-qa.md` 与 `apps/user_app/design-qa/`:记录视觉验收基线、截图和对照结果。 +- `docs/03-用户端App需求.md`:补充首页服务归属展示规则。 + +## 验证结果 + +- `flutter analyze`:通过,无问题。 +- `flutter test`:通过,共 8 项测试。 +- `flutter build web --release --dart-define=API_BASE_URL=http://rest.heqiapp.com`:通过,已生成 Web 发布构建。 +- 390 × 844 页面视觉验收:通过;第二轮未发现 P0、P1、P2 问题。 +- 边界案例:已覆盖无配送点、无服务关系、长组织名称最多两行。 + +## 风险评估 + +- 影响范围仅限用户端首页展示层,不修改 API、数据模型、登录态或服务关系业务规则。 +- 组织名称超过两行时会省略尾部内容,这是为保证移动端布局稳定的预期行为。 +- 视觉预览使用固定示例数据,仅用于开发验收,不会进入生产路由或伪造业务状态。 diff --git a/docs/设计参考/用户端首页_服务归属方案1.png b/docs/设计参考/用户端首页_服务归属方案1.png new file mode 100644 index 0000000..49fbe74 Binary files /dev/null and b/docs/设计参考/用户端首页_服务归属方案1.png differ