优化用户端首页服务归属展示

This commit is contained in:
czl231
2026-09-02 21:59:24 +08:00
parent dd250d80a0
commit 2033a9407e
11 changed files with 321 additions and 37 deletions

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -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<HomePage> createState() => _HomePageState();
}
/// 管理首页远端数据加载与下拉刷新状态。
class _HomePageState extends State<HomePage> {
late Future<(List<ClientRecord>, Map<String, Object?>?)> _future;
@@ -22,9 +27,11 @@ class _HomePageState extends State<HomePage> {
_future = _load();
}
/// 并行语义上聚合公告和当前服务归属数据。
Future<(List<ClientRecord>, Map<String, Object?>?)> _load() async =>
(await widget.repository.contents(), await widget.repository.serviceRelation());
/// 重新加载首页数据并等待刷新完成。
Future<void> _refresh() async {
setState(() => _future = _load());
await _future;
@@ -59,43 +66,7 @@ class _HomePageState extends State<HomePage> {
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(

View File

@@ -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<String, Object?>? 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,
),
),
),
],
);
}

View File

@@ -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<String, Object?>? 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<Text>(find.text(longName));
expect(nameText.maxLines, 2);
expect(nameText.overflow, TextOverflow.ellipsis);
});
}

View File

@@ -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<List<ClientRecord>> contents() async => List.generate(
6,
(index) => ClientRecord(
identity: 'notice-${9 - index}',
title: '模拟公告 ${9 - index}',
subtitle: 'notice',
raw: const {},
),
);
@override
Future<Map<String, Object?>> serviceRelation() async => const {
'gas_name': '和气城南气站',
'delivery_name': '安顺路配送点',
};
}