已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
// 功能:首页设备能力未接入时保留设计结构,禁止展示虚构遥测;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 仅作为未开放状态;接入真实设备接口后替换,不构造模拟在线或安全结论。
|
||||
class DeviceStatusPlaceholder extends StatelessWidget {
|
||||
const DeviceStatusPlaceholder({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.settings_input_component,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('设备服务暂未开放', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
SizedBox(height: 2),
|
||||
Text('设备状态与遥测暂不可查询', style: TextStyle(fontSize: 12, color: Color(0xFF626977))),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Divider(),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final columns =
|
||||
constraints.maxWidth < 330 || MediaQuery.textScalerOf(context).scale(12) > 14 ? 2 : 4;
|
||||
final width = constraints.maxWidth / columns;
|
||||
return Wrap(
|
||||
children: [
|
||||
for (final label in ['环境压力', '温度', '可燃气体', '最后更新时间'])
|
||||
SizedBox(
|
||||
width: width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF727987)),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text('暂不可查询', style: TextStyle(fontSize: 11)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1,105 +1,331 @@
|
||||
// 功能描述:展示用户端首页的安全内容、服务归属与公告列表。
|
||||
// 版本:1.1.0
|
||||
// 功能描述:首页服务归属、内容和保留入口;未开放设备不展示虚构遥测。
|
||||
// 版本:2.0.0
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../data/repositories/client_repository.dart';
|
||||
import '../../../data/services/api_client.dart';
|
||||
import '../../../domain/models/client_models.dart';
|
||||
import '../../../data/repositories/primary_repository.dart';
|
||||
import '../../../domain/models/primary_models.dart';
|
||||
import '../../core/async_content.dart';
|
||||
import '../../core/feature_entry.dart';
|
||||
import '../../core/widgets.dart';
|
||||
import 'service_relation_card.dart';
|
||||
import 'device_status_placeholder.dart';
|
||||
|
||||
/// 用户端首页。
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({required this.repository, super.key});
|
||||
|
||||
/// 首页;游客可浏览内容,个人归属仅登录后读取。
|
||||
class HomePage extends StatelessWidget {
|
||||
const HomePage({required this.repository, this.authenticated = true, super.key});
|
||||
final ClientRepository repository;
|
||||
final bool authenticated;
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
/// 管理首页远端数据加载与下拉刷新状态。
|
||||
class _HomePageState extends State<HomePage> {
|
||||
late Future<(List<ClientRecord>, Map<String, Object?>?)> _future;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_future = _load();
|
||||
/// 并行获取独立数据;所有异常由公共异步容器处理。
|
||||
Future<(List<PublishedContent>, ServiceRelation?)> _load() async {
|
||||
final source = PrimaryRepository(repository);
|
||||
final results = await Future.wait<Object?>([
|
||||
source.contents(),
|
||||
authenticated ? source.relation() : Future.value(null),
|
||||
]);
|
||||
return (results[0] as List<PublishedContent>, results[1] as ServiceRelation?);
|
||||
}
|
||||
|
||||
/// 并行语义上聚合公告和当前服务归属数据。
|
||||
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;
|
||||
/// 登录入口保留当前目标页,避免认证后丢失用户意图。
|
||||
void _openProtected(BuildContext context, String location) {
|
||||
if (authenticated) {
|
||||
context.push(location);
|
||||
return;
|
||||
}
|
||||
context.push('/login?redirect=${Uri.encodeComponent(location)}');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: SafeArea(
|
||||
child: FutureBuilder<(List<ClientRecord>, Map<String, Object?>?)>(
|
||||
future: _future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
if (snapshot.error is SessionExpiredException) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return EmptyState(
|
||||
title: '首页加载失败',
|
||||
description: snapshot.error.toString(),
|
||||
onRetry: _refresh,
|
||||
);
|
||||
}
|
||||
final (contents, relation) = snapshot.data ?? (const <ClientRecord>[], null);
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refresh,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
children: [
|
||||
const PageIntro(
|
||||
eyebrow: '安全生活',
|
||||
title: '今天也要安心用气',
|
||||
description: '设备控制能力尚未开放,本页只展示真实服务与安全内容。',
|
||||
),
|
||||
AppGutter(
|
||||
child: ServiceRelationCard(relation: relation),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const AppGutter(
|
||||
child: SectionHeader(title: '安全公告', description: '来自平台的最新安全提醒与服务信息'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (contents.isEmpty)
|
||||
const AppGutter(
|
||||
child: SurfaceSection(child: Text('暂无已发布内容')),
|
||||
)
|
||||
else
|
||||
AppGutter(
|
||||
child: SurfaceSection(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (var index = 0; index < contents.take(6).length; index++) ...[
|
||||
RecordCard(record: contents[index]),
|
||||
if (index < contents.take(6).length - 1)
|
||||
const Divider(indent: 16, endIndent: 16),
|
||||
],
|
||||
],
|
||||
child: AsyncContent<(List<PublishedContent>, ServiceRelation?)>(
|
||||
key: ValueKey(authenticated),
|
||||
load: _load,
|
||||
builder: (context, data) => ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(18, 16, 18, 20),
|
||||
children: [
|
||||
Text(
|
||||
'安全生活',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('今天也要安心用气', style: Theme.of(context).textTheme.headlineMedium),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'设备控制能力尚未开放,本页只展示真实服务与安全内容。',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (authenticated)
|
||||
ServiceRelationCard.typed(data.$2)
|
||||
else
|
||||
ServiceRelationCard(
|
||||
relation: null,
|
||||
emptyMessage: '登录后查看所属气站与配送点',
|
||||
onTap: () => _openProtected(context, '/home'),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SurfaceSection(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'我的智能设备',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => _openProtected(context, '/devices'),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4, vertical: 5),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('查看详情', style: TextStyle(color: Color(0xFF2563EB))),
|
||||
SizedBox(width: 2),
|
||||
Icon(Icons.chevron_right, size: 18, color: Color(0xFF2563EB)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const DeviceStatusPlaceholder(),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SurfaceSection(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: FeatureEntry(
|
||||
icon: Icons.qr_code_scanner,
|
||||
label: '扫码添加',
|
||||
compact: true,
|
||||
showStatus: false,
|
||||
),
|
||||
),
|
||||
const VerticalDivider(indent: 10, endIndent: 10),
|
||||
const Expanded(
|
||||
child: FeatureEntry(
|
||||
icon: Icons.bluetooth,
|
||||
label: '蓝牙连接',
|
||||
compact: true,
|
||||
showStatus: false,
|
||||
),
|
||||
),
|
||||
const VerticalDivider(indent: 10, endIndent: 10),
|
||||
Expanded(
|
||||
child: FeatureEntry(
|
||||
icon: Icons.grid_view_outlined,
|
||||
label: '分组控制',
|
||||
compact: true,
|
||||
showStatus: false,
|
||||
onTap: () => _openProtected(context, '/device-groups'),
|
||||
),
|
||||
),
|
||||
const VerticalDivider(indent: 10, endIndent: 10),
|
||||
Expanded(
|
||||
child: FeatureEntry(
|
||||
icon: Icons.build_outlined,
|
||||
label: '一键报修',
|
||||
compact: true,
|
||||
showStatus: false,
|
||||
onTap: () => _openProtected(context, '/repair'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SurfaceSection(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
_entry(
|
||||
context,
|
||||
Icons.shield_outlined,
|
||||
'安全记录 / 最近告警',
|
||||
'查看设备状态与告警记录',
|
||||
),
|
||||
const Divider(indent: 12, endIndent: 12),
|
||||
_entry(
|
||||
context,
|
||||
Icons.propane_tank_outlined,
|
||||
'气瓶下单',
|
||||
'选择本人合同气瓶并预约配送',
|
||||
onTap: () => _openProtected(context, '/gas/order'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'安全公告',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'来自平台的最新安全提醒与服务信息',
|
||||
style: TextStyle(fontSize: 11, color: Color(0xFF626977)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => context.push('/contents'),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('查看全部', style: TextStyle(color: Color(0xFF2563EB))),
|
||||
SizedBox(width: 2),
|
||||
Icon(Icons.chevron_right, size: 18, color: Color(0xFF2563EB)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
const SizedBox(height: 4),
|
||||
if (data.$1.where((c) => c.type == 'notice').isEmpty)
|
||||
const SurfaceSection(child: Text('暂无已发布内容')),
|
||||
if (data.$1.where((c) => c.type == 'notice').isNotEmpty)
|
||||
SurfaceSection(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (final (index, content)
|
||||
in data.$1.where((c) => c.type == 'notice').take(2).indexed) ...[
|
||||
_AnnouncementRow(
|
||||
content: content,
|
||||
onTap: () => context.push(
|
||||
'/contents/${Uri.encodeComponent(content.identity)}',
|
||||
),
|
||||
),
|
||||
if (index == 0 && data.$1.where((c) => c.type == 'notice').length > 1)
|
||||
const Divider(indent: 12, endIndent: 12),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// 保留安全与气瓶入口,点击解释当前可用性。
|
||||
Widget _entry(
|
||||
BuildContext context,
|
||||
IconData icon,
|
||||
String text,
|
||||
String description, {
|
||||
VoidCallback? onTap,
|
||||
}) => InkWell(
|
||||
onTap: onTap ?? () => showUnavailableFeature(context, text),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, size: 21, color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(text, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
Text(
|
||||
description,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF626977)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right, color: Color(0xFF626977)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 公告行只显示服务端真实标题、摘要与日期,缺失字段保持空白。
|
||||
class _AnnouncementRow extends StatelessWidget {
|
||||
const _AnnouncementRow({required this.content, required this.onTap});
|
||||
final PublishedContent content;
|
||||
final VoidCallback onTap;
|
||||
|
||||
String get _date {
|
||||
final value = content.publishedAt?.toLocal();
|
||||
return value == null ? '' : '${value.year}/${value.month}/${value.day}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
content.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
content.body,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF626977)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_date.isNotEmpty) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(_date, style: const TextStyle(fontSize: 11, color: Color(0xFF777F8D))),
|
||||
],
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.chevron_right, size: 20, color: Color(0xFF626977)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
181
apps/user_app/lib/ui/features/home/safety_contents_page.dart
Normal file
181
apps/user_app/lib/ui/features/home/safety_contents_page.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
// 功能:安全内容中心的真实公告搜索、分类入口与正文阅读;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../data/repositories/client_repository.dart';
|
||||
import '../../../data/repositories/primary_repository.dart';
|
||||
import '../../../data/services/api_client.dart';
|
||||
import '../../../domain/models/primary_models.dart';
|
||||
import '../../core/async_content.dart';
|
||||
import '../../core/feature_entry.dart';
|
||||
|
||||
/// 图文内容使用后台发布接口;安全视频尚缺媒体能力,保留入口但不伪造播放。
|
||||
class SafetyContentsPage extends StatefulWidget {
|
||||
const SafetyContentsPage({required this.repository, this.identity, super.key});
|
||||
final ClientRepository repository;
|
||||
final String? identity;
|
||||
@override
|
||||
State<SafetyContentsPage> createState() => _SafetyContentsPageState();
|
||||
}
|
||||
|
||||
class _SafetyContentsPageState extends State<SafetyContentsPage> {
|
||||
String _query = '';
|
||||
int _selected = 0;
|
||||
final _searchFocus = FocusNode();
|
||||
|
||||
/// 列表保留兼容接口,正文不再重复下载全部文章。
|
||||
Future<List<PublishedContent>> _load() async {
|
||||
if (widget.identity == null) return PrimaryRepository(widget.repository).contents();
|
||||
try {
|
||||
final item = await widget.repository.safetyContent(widget.identity!);
|
||||
return [
|
||||
PublishedContent(
|
||||
identity: item.identity,
|
||||
title: item.title,
|
||||
body: item.raw['body'] as String? ?? '',
|
||||
type: item.raw['content_type'] as String? ?? '',
|
||||
version: item.raw['version_no'] as int? ?? 1,
|
||||
),
|
||||
];
|
||||
} on ApiException catch (error) {
|
||||
if (error.code == 1112) return [];
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.identity == null ? '安全内容' : '内容详情'),
|
||||
leading: IconButton(
|
||||
tooltip: '返回',
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.canPop() ? context.pop() : context.go('/home'),
|
||||
),
|
||||
actions: [
|
||||
if (widget.identity == null)
|
||||
IconButton(
|
||||
tooltip: '搜索公告',
|
||||
onPressed: _searchFocus.requestFocus,
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: AsyncContent<List<PublishedContent>>(
|
||||
key: ValueKey(widget.identity),
|
||||
load: _load,
|
||||
builder: (context, contents) {
|
||||
final notices = contents
|
||||
.where((item) => ['notice', 'safety_article', 'law'].contains(item.type))
|
||||
.toList();
|
||||
if (widget.identity != null) {
|
||||
final matches = notices.where((item) => item.identity == widget.identity);
|
||||
if (matches.isEmpty) return const Center(child: Text('内容不存在或已下架'));
|
||||
final item = matches.first;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
SelectableText(
|
||||
item.title,
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('平台发布', style: TextStyle(color: Color(0xFF727987))),
|
||||
const Divider(height: 32),
|
||||
SelectableText(
|
||||
item.body.isEmpty ? '暂无正文' : item.body,
|
||||
style: const TextStyle(fontSize: 16, height: 1.7),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
final type = {1: 'safety_article', 3: 'law', 4: 'notice'}[_selected];
|
||||
final shown = notices
|
||||
.where(
|
||||
(item) =>
|
||||
(type == null || item.type == type) &&
|
||||
item.title.toLowerCase().contains(_query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
for (final (index, label) in ['全部', '安全宣传', '安全视频', '法律法规', '平台公告'].indexed)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (index == 2) {
|
||||
showUnavailableFeature(context, label);
|
||||
return;
|
||||
}
|
||||
setState(() => _selected = index);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: _selected == index
|
||||
? const Color(0xFF0064FF)
|
||||
: const Color(0xFF616979),
|
||||
),
|
||||
),
|
||||
if (index == 2)
|
||||
const Text(
|
||||
'暂未开放',
|
||||
style: TextStyle(fontSize: 10, color: Color(0xFF727987)),
|
||||
),
|
||||
if (_selected == index)
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
child: Divider(color: Color(0xFF0064FF), thickness: 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
focusNode: _searchFocus,
|
||||
onChanged: (value) => setState(() => _query = value.trim()),
|
||||
decoration: InputDecoration(
|
||||
hintText: _selected == 4 ? '搜索公告标题' : '搜索内容标题',
|
||||
suffixIcon: const Icon(Icons.search),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (shown.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(28),
|
||||
child: Center(child: Text('暂无符合条件的内容')),
|
||||
),
|
||||
for (final item in shown) ...[
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||
leading: const Icon(Icons.campaign_outlined, size: 32, color: Color(0xFF0064FF)),
|
||||
title: Text(
|
||||
item.title,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: const Padding(padding: EdgeInsets.only(top: 8), child: Text('平台发布')),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => context.push('/contents/${Uri.encodeComponent(item.identity)}'),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2,13 +2,28 @@
|
||||
// 版本:1.0.0
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:heqi_design_system/heqi_design_system.dart';
|
||||
import '../../../domain/models/primary_models.dart';
|
||||
|
||||
/// 服务归属信息卡片,负责清晰区分气站主体与配送履约网点。
|
||||
class ServiceRelationCard extends StatelessWidget {
|
||||
const ServiceRelationCard({required this.relation, super.key});
|
||||
const ServiceRelationCard({
|
||||
required this.relation,
|
||||
this.emptyMessage = '尚未建立服务关系',
|
||||
this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// 以强类型归属创建卡片,保留旧构造函数供未迁移页面使用。
|
||||
factory ServiceRelationCard.typed(ServiceRelation? value) => ServiceRelationCard(
|
||||
relation: value == null
|
||||
? null
|
||||
: {'gas_name': value.gasName, 'delivery_name': value.deliveryName},
|
||||
);
|
||||
|
||||
/// 服务端返回的当前有效服务关系;为空表示尚未建立归属。
|
||||
final Map<String, Object?>? relation;
|
||||
final String emptyMessage;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// 提取并清理指定关系字段,空字符串按无值处理。
|
||||
String? _relationValue(String key) {
|
||||
@@ -23,53 +38,60 @@ class ServiceRelationCard extends StatelessWidget {
|
||||
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: '服务归属',
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(HeqiSpacing.x3),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(HeqiRadius.extraLarge),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.store_mall_directory_outlined,
|
||||
size: 30,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
semanticLabel: '服务归属',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: HeqiSpacing.x3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('当前服务归属', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: HeqiSpacing.x1),
|
||||
if (!hasRelation)
|
||||
Text(
|
||||
emptyMessage,
|
||||
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.x1),
|
||||
child: Divider(height: 1),
|
||||
),
|
||||
_RelationLine(
|
||||
label: deliveryName == null ? '服务方式' : '服务配送点',
|
||||
value: deliveryName ?? '气站直接服务',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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 ?? '气站直接服务',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user