优化用户端首页服务归属展示
This commit is contained in:
25
apps/user_app/design-qa.md
Normal file
25
apps/user_app/design-qa.md
Normal 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
|
||||
BIN
apps/user_app/design-qa/对照_全页.png
Normal file
BIN
apps/user_app/design-qa/对照_全页.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 440 KiB |
BIN
apps/user_app/design-qa/对照_服务归属卡片.png
Normal file
BIN
apps/user_app/design-qa/对照_服务归属卡片.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
BIN
apps/user_app/design-qa/用户端首页_服务归属实现.png
Normal file
BIN
apps/user_app/design-qa/用户端首页_服务归属实现.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -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(
|
||||
|
||||
112
apps/user_app/lib/ui/features/home/service_relation_card.dart
Normal file
112
apps/user_app/lib/ui/features/home/service_relation_card.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
66
apps/user_app/test/ui/service_relation_card_test.dart
Normal file
66
apps/user_app/test/ui/service_relation_card_test.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
62
apps/user_app/tool/home_visual_preview.dart
Normal file
62
apps/user_app/tool/home_visual_preview.dart
Normal 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': '安顺路配送点',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user