import 'package:ducafe_ui_core/ducafe_ui_core.dart'; import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; /// 设置页通用分组,统一标题层级、边框和圆角。 class SettingGroup extends StatelessWidget { /// 创建设置分组;[dense] 仅用于需要提高信息密度的页面。 const SettingGroup({ super.key, required this.title, required this.child, this.dense = false, }); /// 分组标题。 final String title; /// 分组卡片中的实际设置内容。 final Widget child; /// 是否使用紧凑标题层级;只压缩排版,不改变分组内容的触控尺寸。 final bool dense; /// 构建标题和白色分组卡片。 @override Widget build(BuildContext context) { final theme = FTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: (dense ? theme.typography.base : theme.typography.lg).copyWith( color: XfjDesignTokens.ink, fontWeight: FontWeight.w800, ), ), SizedBox(height: (dense ? 8 : 12).h), Container( clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: XfjDesignTokens.surface, border: Border.all(color: XfjDesignTokens.line), borderRadius: BorderRadius.circular(XfjDesignTokens.cardRadius), ), child: child, ), ], ); } } class SettingSegmentedSelector extends StatelessWidget { const SettingSegmentedSelector({ super.key, required this.label, required this.value, required this.options, required this.onChanged, this.showDivider = true, }); final String label; final T value; final Map options; final ValueChanged onChanged; final bool showDivider; @override Widget build(BuildContext context) { final theme = FTheme.of(context); final optionStyle = theme.typography.base.copyWith( color: XfjDesignTokens.muted, fontWeight: FontWeight.w600, ); final entries = options.entries.toList(growable: false); return [ Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.typography.base.copyWith( color: XfjDesignTokens.ink, fontWeight: FontWeight.w600, ), ).width(230.w), LayoutBuilder( builder: (context, constraints) { if (entries.isEmpty) return const SizedBox.shrink(); final gap = 4.w; final regularHorizontalPadding = 20.w * XfjDesignTokens.buttonSizeScale; final preferredWidth = entries.fold(8.w, (width, entry) { final painter = TextPainter( text: TextSpan(text: entry.value, style: optionStyle), maxLines: 1, textDirection: Directionality.of(context), textScaler: MediaQuery.textScalerOf(context), )..layout(); final optionWidth = (painter.width + regularHorizontalPadding * 2) .clamp(112.w, double.infinity); return width + optionWidth; }) + gap * (entries.length - 1); final fillAvailableWidth = constraints.maxWidth.isFinite && preferredWidth > constraints.maxWidth; // 支付方式较多时等分可用宽度,保留 48dp 点击高度并让长文案省略, // 避免固定最小宽度在页面设置的窄内容区产生横向溢出。 final optionWidgets = entries .map( (entry) => _segmentedOption( entry: entry, active: value == entry.key, style: optionStyle, fillAvailableWidth: fillAvailableWidth, ), ) .toList(growable: false); return optionWidgets .toRow( mainAxisSize: fillAvailableWidth ? MainAxisSize.max : MainAxisSize.min, separator: SizedBox(width: gap), ) .padding(all: 4.w) .decorated( color: const Color(0xFFF0F2F5), borderRadius: BorderRadius.circular( XfjDesignTokens.controlRadius, ), ) .alignRight(); }, ).expanded(), ] .toRowSpace(space: 16.w) .padding(horizontal: 20.w, vertical: 12.h) .constrained(minHeight: 72.h) .decorated( border: showDivider ? const Border(bottom: BorderSide(color: XfjDesignTokens.line)) : null, ); } Widget _segmentedOption({ required MapEntry entry, required bool active, required TextStyle style, required bool fillAvailableWidth, }) { final horizontalPadding = fillAvailableWidth ? 8.w : 20.w * XfjDesignTokens.buttonSizeScale; final option = InkWell( onTap: () => onChanged(entry.key), borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius), child: Text( entry.value, maxLines: 1, overflow: TextOverflow.ellipsis, style: style.copyWith( color: active ? XfjDesignTokens.primary : XfjDesignTokens.muted, ), ) .center() .padding( horizontal: horizontalPadding, vertical: 6.h * XfjDesignTokens.buttonSizeScale, ) .constrained( minWidth: fillAvailableWidth ? 0 : 112.w, minHeight: 48.h, ) .decorated( color: active ? Colors.white : Colors.transparent, borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius), boxShadow: active ? XfjDesignTokens.surfaceShadow : null, ), ); return fillAvailableWidth ? option.expanded() : option; } } class SettingInfoRow extends StatelessWidget { const SettingInfoRow({ super.key, required this.icon, required this.label, this.value, this.actionLabel, this.onTap, this.showDivider = true, this.trailing, this.subtitle, }); final SvgAsset icon; final String label; final String? value; final String? actionLabel; final VoidCallback? onTap; final bool showDivider; final Widget? trailing; final String? subtitle; @override Widget build(BuildContext context) { final theme = FTheme.of(context); final hasAction = actionLabel != null; return InkWell( onTap: onTap, child: Container( constraints: BoxConstraints(minHeight: subtitle == null ? 68.h : 80.h), padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), decoration: BoxDecoration( border: showDivider ? const Border(bottom: BorderSide(color: XfjDesignTokens.line)) : null, ), child: Row( children: [ Container( width: 40.w, height: 40.w, alignment: Alignment.center, decoration: BoxDecoration( color: XfjDesignTokens.control, borderRadius: BorderRadius.circular( XfjDesignTokens.controlRadius, ), ), child: FIcon(icon, color: XfjDesignTokens.muted, size: 22.sp), ), SizedBox(width: 16.w), SizedBox( width: subtitle == null ? 230.w : 520.w, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.typography.base.copyWith( color: XfjDesignTokens.ink, fontWeight: FontWeight.w600, ), ), if (subtitle != null) ...[ SizedBox(height: 4.h), Text( subtitle!, maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.typography.sm.copyWith( color: XfjDesignTokens.muted, ), ), ], ], ), ), SizedBox(width: 16.w), Expanded( child: value == null ? const SizedBox.shrink() : Text( value!, maxLines: subtitle == null ? 1 : 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.right, style: theme.typography.base.copyWith( color: XfjDesignTokens.muted, fontFeatures: const [FontFeature.tabularFigures()], ), ), ), if (trailing != null) ...[SizedBox(width: 16.w), trailing!], if (hasAction) ...[ SizedBox(width: 16.w), SizedBox( width: 148.w, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Flexible( child: Text( actionLabel!, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.typography.base.copyWith( color: XfjDesignTokens.primary, fontWeight: FontWeight.w600, ), ), ), SizedBox(width: 6.w), FIcon( actionLabel == '复制' ? FAssets.icons.copy : FAssets.icons.chevronRight, color: XfjDesignTokens.primary, size: 18.sp, ), ], ), ), ], ], ), ), ); } } /// 设置页通用提示条,用图标、文字和语义颜色反馈说明或状态。 class SettingNotice extends StatelessWidget { /// 创建提示条;[dense] 适用于紧凑表单,但不会删减提示内容。 const SettingNotice({ super.key, required this.text, this.color = XfjDesignTokens.muted, this.background = XfjDesignTokens.control, this.borderColor, this.icon, this.dense = false, }); /// 提示正文。 final String text; /// 提示正文和图标的前景色。 final Color color; /// 提示条背景色。 final Color background; /// 可选边框颜色,为空时不绘制边框。 final Color? borderColor; /// 可选语义图标。 final SvgAsset? icon; /// 是否使用紧凑提示样式;保留完整文案和语义,仅降低字号与内边距。 final bool dense; /// 构建可随宽度换行的提示内容。 @override Widget build(BuildContext context) { final theme = FTheme.of(context); return Container( width: double.infinity, padding: EdgeInsets.symmetric( horizontal: (dense ? 12 : 16).w, vertical: (dense ? 10 : 16).h, ), decoration: BoxDecoration( color: background, border: borderColor == null ? null : Border.all(color: borderColor!), borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius), ), child: Row( children: [ if (icon != null) ...[ FIcon(icon!, color: color, size: (dense ? 18 : 20).sp), SizedBox(width: (dense ? 8 : 10).w), ], Expanded( child: Text( text, style: (dense ? theme.typography.xs : theme.typography.sm) .copyWith(color: color), ), ), ], ), ); } } class SettingEmpty extends StatelessWidget { const SettingEmpty({super.key, required this.message, this.icon}); final String message; final SvgAsset? icon; @override Widget build(BuildContext context) { final theme = FTheme.of(context); return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ FIcon( icon ?? FAssets.icons.inbox, color: XfjDesignTokens.muted, size: 48.sp, ), SizedBox(height: 12.h), Text( message, style: theme.typography.base.copyWith( color: XfjDesignTokens.muted, ), ), ], ), ); } } /// 设置页通用操作按钮,提供主要、普通和危险三种层级。 class SettingActionButton extends StatelessWidget { /// 创建具有真实禁用态和最小触控高度的操作按钮。 const SettingActionButton({ super.key, required this.label, required this.onTap, this.icon, this.primary = false, this.danger = false, this.enabled = true, this.dense = false, }); /// 按钮文字。 final String label; /// 按钮处于可用状态时的点击回调。 final VoidCallback onTap; /// 可选前置图标。 final SvgAsset? icon; /// 是否按页面唯一主要操作展示。 final bool primary; /// 是否按危险操作展示。 final bool danger; /// 是否允许点击;禁用时同时移除回调并降低视觉强调。 final bool enabled; /// 是否使用紧凑视觉样式;点击高度仍固定不小于 48dp。 final bool dense; /// 构建保持至少 48dp 点击高度的操作按钮。 @override Widget build(BuildContext context) { final theme = FTheme.of(context); final foreground = danger ? XfjDesignTokens.danger : primary ? Colors.white : XfjDesignTokens.primary; final background = primary ? XfjDesignTokens.primary : danger ? const Color(0xFFFFF3F2) : XfjDesignTokens.surface; final sizeScale = XfjDesignTokens.buttonSizeScale; return Opacity( opacity: enabled ? 1 : 0.45, child: InkWell( onTap: enabled ? onTap : null, borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius), child: Container( constraints: BoxConstraints(minHeight: 48.h), padding: EdgeInsets.symmetric( horizontal: (dense ? 14 : 18).w * sizeScale, vertical: (dense ? 8 : 10).h * sizeScale, ), decoration: BoxDecoration( color: background, border: Border.all( color: primary ? XfjDesignTokens.primary : danger ? XfjDesignTokens.danger.withValues(alpha: 0.35) : XfjDesignTokens.line, ), borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius), ), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ if (icon != null) ...[ FIcon( icon!, color: foreground, size: (dense ? 18 : 20).sp, ), SizedBox(width: (dense ? 6 : 8).w), ], Text( label, style: (dense ? theme.typography.sm : theme.typography.base) .copyWith( color: foreground, fontWeight: FontWeight.w600, ), ), ], ), ), ), ); } }