Files
biz_xfj_cloud/lib/sub_screen/page/customer_welcome.dart
T
dzc 6ef0b6320f feat:日志查看
fix:真机bug
2026-07-27 13:28:35 +08:00

660 lines
19 KiB
Dart

import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:pj_supermarket_cloud/common/style/xfj_design_tokens.dart';
/// 顾客副屏待机欢迎页,真实副屏与主屏全屏预览共用同一份布局和时钟生命周期。
class CustomerWelcomePage extends StatefulWidget {
const CustomerWelcomePage({
super.key,
this.previewMode = false,
this.onExit,
this.now,
this.clockTick = const Duration(minutes: 1),
});
final bool previewMode;
final VoidCallback? onExit;
final DateTime Function()? now;
final Duration clockTick;
@override
State<CustomerWelcomePage> createState() => _CustomerWelcomePageState();
}
class _CustomerWelcomePageState extends State<CustomerWelcomePage> {
late DateTime _currentTime;
Timer? _clockTimer;
@override
void initState() {
super.initState();
_currentTime = _readNow();
_startClock();
}
@override
void didUpdateWidget(CustomerWelcomePage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.clockTick != widget.clockTick ||
oldWidget.now != widget.now) {
_currentTime = _readNow();
_startClock();
}
}
DateTime _readNow() => widget.now?.call() ?? DateTime.now();
void _startClock() {
_clockTimer?.cancel();
final tick = widget.clockTick > Duration.zero
? widget.clockTick
: const Duration(minutes: 1);
_clockTimer = Timer.periodic(tick, (_) {
if (mounted) setState(() => _currentTime = _readNow());
});
}
@override
void dispose() {
_clockTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: XfjDesignTokens.canvas,
body: Stack(
children: [
Positioned.fill(
child: _CustomerWelcomeContent(currentTime: _currentTime),
),
if (widget.previewMode)
Positioned(
top: 16,
right: 16,
child: SafeArea(
child: _PreviewExitButton(onTap: widget.onExit),
),
),
],
),
);
}
}
class _CustomerWelcomeContent extends StatelessWidget {
const _CustomerWelcomeContent({required this.currentTime});
final DateTime currentTime;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final scale = math
.min(constraints.maxWidth / 1920, constraints.maxHeight / 1080)
.clamp(0.55, 1.12)
.toDouble();
return ColoredBox(
key: const ValueKey('customer-welcome-page'),
color: XfjDesignTokens.canvas,
child: SafeArea(
child: Column(
children: [
_WelcomeHeader(currentTime: currentTime, scale: scale),
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(
24 * scale,
0,
24 * scale,
24 * scale,
),
child: _WelcomeHero(scale: scale),
),
),
],
),
),
);
},
);
}
}
class _WelcomeHeader extends StatelessWidget {
const _WelcomeHeader({required this.currentTime, required this.scale});
final DateTime currentTime;
final double scale;
@override
Widget build(BuildContext context) {
final hour = currentTime.hour.toString().padLeft(2, '0');
final minute = currentTime.minute.toString().padLeft(2, '0');
return SizedBox(
height: 80 * scale,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 32 * scale),
child: Row(
children: [
Text(
'ZAX',
style: TextStyle(
color: const Color(0xFF0865E8),
fontSize: 36 * scale,
fontWeight: FontWeight.w900,
fontStyle: FontStyle.italic,
letterSpacing: -2 * scale,
),
),
SizedBox(width: 20 * scale),
Container(
width: 1,
height: 36 * scale,
color: XfjDesignTokens.line,
),
SizedBox(width: 20 * scale),
Text(
'欢迎光临',
style: TextStyle(
color: XfjDesignTokens.ink,
fontSize: 24 * scale,
fontWeight: FontWeight.w800,
letterSpacing: 1.2 * scale,
),
),
SizedBox(width: 28 * scale),
Container(
padding: EdgeInsets.symmetric(
horizontal: 16 * scale,
vertical: 9 * scale,
),
decoration: BoxDecoration(
color: XfjDesignTokens.primarySoft,
border: Border.all(
color: XfjDesignTokens.primary.withValues(alpha: 0.2),
),
borderRadius: BorderRadius.circular(12 * scale),
),
child: Text(
'智慧收银',
style: TextStyle(
color: XfjDesignTokens.primary,
fontSize: 16 * scale,
fontWeight: FontWeight.w700,
),
),
),
const Spacer(),
_ConnectionStatus(scale: scale),
SizedBox(width: 20 * scale),
Container(
width: 1,
height: 28 * scale,
color: XfjDesignTokens.line,
),
SizedBox(width: 20 * scale),
Text(
'$hour:$minute',
key: const ValueKey('customer-welcome-clock'),
style: TextStyle(
color: XfjDesignTokens.ink,
fontSize: 20 * scale,
fontWeight: FontWeight.w700,
fontFeatures: const [FontFeature.tabularFigures()],
),
),
],
),
),
);
}
}
class _ConnectionStatus extends StatelessWidget {
const _ConnectionStatus({required this.scale});
final double scale;
@override
Widget build(BuildContext context) {
return Semantics(
label: '设备已连接',
child: Row(
children: [
Container(
width: 10 * scale,
height: 10 * scale,
decoration: BoxDecoration(
color: XfjDesignTokens.success,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: XfjDesignTokens.success.withValues(alpha: 0.12),
spreadRadius: 5 * scale,
),
],
),
),
SizedBox(width: 10 * scale),
Text(
'设备已连接',
style: TextStyle(
color: XfjDesignTokens.success,
fontSize: 18 * scale,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
class _WelcomeHero extends StatelessWidget {
const _WelcomeHero({required this.scale});
final double scale;
@override
Widget build(BuildContext context) {
final radius = BorderRadius.circular(24 * scale);
return DecoratedBox(
decoration: BoxDecoration(
color: XfjDesignTokens.surface,
border: Border.all(color: XfjDesignTokens.line),
borderRadius: radius,
boxShadow: const [
BoxShadow(
color: Color(0x14111827),
blurRadius: 56,
offset: Offset(0, 18),
),
],
),
child: ClipRRect(
borderRadius: radius,
child: Stack(
fit: StackFit.expand,
children: [
_WelcomeAssetImage(
key: const ValueKey('customer-welcome-hero'),
asset:
'assets/images/customer_welcome/abstract-welcome-hero.webp',
semanticsLabel: '蓝色、薄荷绿与暖黄色卡通伙伴挥手欢迎',
fit: BoxFit.cover,
),
Positioned(
left: 32 * scale,
top: 32 * scale,
child: _ReadyCard(scale: scale),
),
Positioned(
right: 32 * scale,
top: 32 * scale,
child: _SyncBadge(scale: scale),
),
Positioned(
left: 32 * scale,
bottom: 32 * scale,
child: Row(
children: [
_WelcomeInfoCard(
imageKey: const ValueKey('customer-welcome-flow'),
asset: 'assets/images/customer_welcome/abstract-flow.webp',
semanticsLabel: '卡通伙伴与空白卡片组成的轻量信息流插图',
icon: Icons.auto_awesome_rounded,
title: '内容实时更新',
detail: '新的信息会自动显示',
scale: scale,
),
SizedBox(width: 16 * scale),
_WelcomeInfoCard(
imageKey: const ValueKey('customer-welcome-care'),
asset: 'assets/images/customer_welcome/abstract-care.webp',
semanticsLabel: '卡通伙伴拥抱爱心盾牌的安心守护插图',
icon: Icons.shield_outlined,
title: '安心使用',
detail: '个人信息会妥善保护',
scale: scale,
),
],
),
),
],
),
),
);
}
}
class _ReadyCard extends StatelessWidget {
const _ReadyCard({required this.scale});
final double scale;
@override
Widget build(BuildContext context) {
return Container(
width: 504 * scale,
padding: EdgeInsets.all(24 * scale),
decoration: _floatingDecoration(radius: 20 * scale),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check_circle_rounded,
color: XfjDesignTokens.success,
size: 19 * scale,
),
SizedBox(width: 8 * scale),
Text(
'终端服务已就绪',
style: TextStyle(
color: XfjDesignTokens.success,
fontSize: 14 * scale,
fontWeight: FontWeight.w700,
),
),
],
),
SizedBox(height: 16 * scale),
Text(
'欢迎使用',
style: TextStyle(
color: XfjDesignTokens.ink,
fontSize: 48 * scale,
height: 1.05,
fontWeight: FontWeight.w900,
letterSpacing: -1.7 * scale,
),
),
SizedBox(height: 12 * scale),
Text(
'请稍候,接下来的内容会在这里清晰展示。',
style: TextStyle(
color: XfjDesignTokens.muted,
fontSize: 18 * scale,
height: 1.55,
),
),
SizedBox(height: 20 * scale),
Container(
padding: EdgeInsets.symmetric(
horizontal: 16 * scale,
vertical: 12 * scale,
),
decoration: BoxDecoration(
color: XfjDesignTokens.primarySoft,
borderRadius: BorderRadius.circular(12 * scale),
),
child: Row(
children: [
Container(
width: 10 * scale,
height: 10 * scale,
decoration: const BoxDecoration(
color: XfjDesignTokens.primary,
shape: BoxShape.circle,
),
),
SizedBox(width: 12 * scale),
Text(
'正在等待服务开始',
style: TextStyle(
color: XfjDesignTokens.primary,
fontSize: 14 * scale,
fontWeight: FontWeight.w700,
),
),
],
),
),
],
),
);
}
}
class _SyncBadge extends StatelessWidget {
const _SyncBadge({required this.scale});
final double scale;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(
horizontal: 16 * scale,
vertical: 10 * scale,
),
decoration: _floatingDecoration(
radius: 24 * scale,
shadow: const BoxShadow(
color: Color(0x0F111827),
blurRadius: 8,
offset: Offset(0, 2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.wifi_rounded,
color: XfjDesignTokens.success,
size: 19 * scale,
),
SizedBox(width: 8 * scale),
Text(
'内容将自动同步',
style: TextStyle(
color: XfjDesignTokens.success,
fontSize: 14 * scale,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
class _WelcomeInfoCard extends StatelessWidget {
const _WelcomeInfoCard({
required this.imageKey,
required this.asset,
required this.semanticsLabel,
required this.icon,
required this.title,
required this.detail,
required this.scale,
});
final Key imageKey;
final String asset;
final String semanticsLabel;
final IconData icon;
final String title;
final String detail;
final double scale;
@override
Widget build(BuildContext context) {
final radius = BorderRadius.circular(16 * scale);
return Container(
width: 248 * scale,
decoration: _floatingDecoration(radius: 16 * scale),
child: ClipRRect(
borderRadius: radius,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: double.infinity,
height: 120 * scale,
child: _WelcomeAssetImage(
key: imageKey,
asset: asset,
semanticsLabel: semanticsLabel,
fit: BoxFit.cover,
),
),
Container(
color: XfjDesignTokens.surface,
padding: EdgeInsets.all(14 * scale),
child: Row(
children: [
Container(
width: 36 * scale,
height: 36 * scale,
alignment: Alignment.center,
decoration: BoxDecoration(
color: XfjDesignTokens.primarySoft,
borderRadius: BorderRadius.circular(10 * scale),
),
child: Icon(
icon,
color: XfjDesignTokens.primary,
size: 20 * scale,
),
),
SizedBox(width: 12 * scale),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: XfjDesignTokens.ink,
fontSize: 14 * scale,
fontWeight: FontWeight.w800,
),
),
SizedBox(height: 4 * scale),
Text(
detail,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: XfjDesignTokens.muted,
fontSize: 12 * scale,
),
),
],
),
),
],
),
),
],
),
),
);
}
}
class _WelcomeAssetImage extends StatelessWidget {
const _WelcomeAssetImage({
super.key,
required this.asset,
required this.semanticsLabel,
required this.fit,
});
final String asset;
final String semanticsLabel;
final BoxFit fit;
@override
Widget build(BuildContext context) {
return Image.asset(
asset,
fit: fit,
semanticLabel: semanticsLabel,
errorBuilder: (context, error, stackTrace) => ColoredBox(
color: XfjDesignTokens.primarySoft,
child: Center(
child: Icon(
Icons.image_not_supported_outlined,
color: XfjDesignTokens.primary,
size: 40,
semanticLabel: '欢迎插图暂不可用',
),
),
),
);
}
}
class _PreviewExitButton extends StatelessWidget {
const _PreviewExitButton({required this.onTap});
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
label: '退出欢迎副屏预览',
child: Material(
color: const Color(0xD9111827),
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: const SizedBox(
width: 118,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.close_fullscreen_rounded,
color: Colors.white,
size: 20,
),
SizedBox(width: 8),
Text(
'退出预览',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
),
);
}
}
BoxDecoration _floatingDecoration({
required double radius,
BoxShadow shadow = const BoxShadow(
color: Color(0x1F111827),
blurRadius: 48,
offset: Offset(0, 18),
),
}) {
return BoxDecoration(
color: const Color(0xF0FFFFFF),
border: Border.all(color: const Color(0xE6FFFFFF)),
borderRadius: BorderRadius.circular(radius),
boxShadow: [shadow],
);
}