diff --git a/client/lib/pangolin_theme.dart b/client/lib/pangolin_theme.dart index 9d78795..74b7412 100644 --- a/client/lib/pangolin_theme.dart +++ b/client/lib/pangolin_theme.dart @@ -33,10 +33,12 @@ export 'pangolin_tokens.gen.dart'; /// (登录/注册的邮箱·密码·验证码、节点页搜索框聚焦时左右各缺一截即此因)。 /// 聚焦指示交给外层容器自己负责(如 [Border.all(accent)])。 /// -/// 注意:只压边框、不动 `filled`/`fillColor`——未聚焦态外观与改前完全一致(不动 golden), -/// 漏出的橙框只来自 `focusedBorder`(未聚焦态早已解析为无边框)。 +/// `filled: false`:同时关掉主题的 `filled:true / fillColor:bg`。否则内层 TextField +/// 会用 `bg` 自铺一层底,与外层容器底色(如搜索框 `bgSubtle`)不一致——图标与左右 +/// 内边距露出容器色、中间是 `bg`,出现两端一色中间另一色的接缝。底色统一交给外层容器。 const InputDecoration kBareInput = InputDecoration( isCollapsed: true, + filled: false, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, diff --git a/client/lib/screens/nodes_page.dart b/client/lib/screens/nodes_page.dart index 8df6459..f922a0c 100644 --- a/client/lib/screens/nodes_page.dart +++ b/client/lib/screens/nodes_page.dart @@ -10,6 +10,7 @@ import '../state/connection_provider.dart'; import '../state/nodes_provider.dart'; import '../widgets/app_top_bar.dart'; import '../widgets/country_code.dart'; +import '../widgets/pangolin_field.dart'; import '../widgets/pangolin_icons.dart'; import '../widgets/server_tile.dart'; import '../widgets/smart_select_card.dart'; @@ -195,29 +196,19 @@ class _SearchBox extends StatelessWidget { @override Widget build(BuildContext context) { final c = context.pangolin; - return Container( - decoration: BoxDecoration(color: c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.md)), - padding: const EdgeInsets.symmetric(horizontal: 14), - child: Row(children: [ - Icon(PangolinIcons.search, size: 18, color: c.fg3), - const SizedBox(width: 10), - Expanded( - child: TextField( - onChanged: onChanged, - style: PangolinText.sm.copyWith(color: c.fg1, fontSize: 15), - // kBareInput 关掉主题 inputDecorationTheme 的边框/填充泄漏(否则聚焦时 - // 橙色 OutlineInputBorder 会画到内层 TextField 上、与搜索框圆角错位/被裁)。 - // filled:false 关掉主题的 fillColor=bg 填充——否则内层 TextField 用 bg 铺底, - // 与外层 bgSubtle 容器两色不一致(图标/左右内边距露出 bgSubtle、中间是 bg)。 - decoration: kBareInput.copyWith( - filled: false, - contentPadding: const EdgeInsets.symmetric(vertical: 12), - hintText: hint, - hintStyle: PangolinText.sm.copyWith(color: c.fg3, fontSize: 15), - ), - ), + // 搜索框:只有底色无描边的统一输入框(PangolinFieldBox bordered:false)。 + return PangolinFieldBox( + fill: c.bgSubtle, + bordered: false, + leading: PangolinIcons.search, + child: TextField( + onChanged: onChanged, + style: PangolinText.sm.copyWith(color: c.fg1, fontSize: 15), + decoration: bareInputDecoration( + hintText: hint, + hintStyle: PangolinText.sm.copyWith(color: c.fg3, fontSize: 15), ), - ]), + ), ); } } diff --git a/client/lib/widgets/account_screens.dart b/client/lib/widgets/account_screens.dart index acfb74a..9ac2a0e 100644 --- a/client/lib/widgets/account_screens.dart +++ b/client/lib/widgets/account_screens.dart @@ -10,6 +10,7 @@ import '../models/device.dart'; import '../pangolin_theme.dart'; import '../services/device_identity.dart'; import 'adaptive_menu.dart'; +import 'pangolin_field.dart'; import '../services/auth_api.dart'; import '../state/account_providers.dart'; import 'pangolin_button.dart'; @@ -384,15 +385,13 @@ class _RedeemScreenState extends ConsumerState { ]) else Row(children: [ - Expanded(child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14), - decoration: BoxDecoration(color: c.bg, borderRadius: BorderRadius.circular(PangolinRadius.md), border: Border.all(color: c.borderStrong, width: 1.5)), + Expanded(child: PangolinFieldBox( + fill: c.bg, child: TextField( controller: _code, textCapitalization: TextCapitalization.characters, style: PangolinText.mono.copyWith(color: c.fg1, letterSpacing: 1.5), - // kBareInput 关掉主题边框/填充泄漏(聚焦时橙框错位/被裁),聚焦态交给外层 Border。 - decoration: kBareInput.copyWith(contentPadding: const EdgeInsets.symmetric(vertical: 13), hintText: t.redeemPh, hintStyle: PangolinText.sm.copyWith(color: c.fg3)), + decoration: bareInputDecoration(contentPadding: const EdgeInsets.symmetric(vertical: 13), hintText: t.redeemPh, hintStyle: PangolinText.sm.copyWith(color: c.fg3)), onChanged: (_) => setState(() {}), ), )), diff --git a/client/lib/widgets/auth_screen.dart b/client/lib/widgets/auth_screen.dart index 768216c..c609c49 100644 --- a/client/lib/widgets/auth_screen.dart +++ b/client/lib/widgets/auth_screen.dart @@ -13,6 +13,7 @@ import '../services/auth_api.dart'; import '../services/device_identity.dart'; import '../state/auth_provider.dart'; import 'pangolin_button.dart'; +import 'pangolin_field.dart'; import 'pangolin_icons.dart'; import 'pangolin_logo.dart'; @@ -342,29 +343,13 @@ class _AuthScreenState extends ConsumerState ), ); - // 输入字段:标签 + 图标 + 聚焦态(accent 边 + 光环)。 - Widget _field(PangolinScheme c, {required IconData icon, required String label, required bool focused, required Widget child}) { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: PangolinText.caption.copyWith(color: c.fg2, fontWeight: FontWeight.w600)), - const SizedBox(height: 7), - AnimatedContainer( - duration: const Duration(milliseconds: 160), - padding: const EdgeInsets.symmetric(horizontal: 14), - decoration: BoxDecoration( - color: c.surface, - borderRadius: BorderRadius.circular(PangolinRadius.md), - border: Border.all(color: focused ? c.accent : c.borderStrong, width: 1.5), - boxShadow: focused ? [BoxShadow(color: c.ring, spreadRadius: 3)] : null, - ), - child: Row(children: [Icon(icon, size: 18, color: c.fg3), const SizedBox(width: 10), Expanded(child: child)]), - ), - ]); - } + // 输入字段:标签 + 图标 + 聚焦态(accent 边 + 光环)。统一走 PangolinFieldBox。 + Widget _field(PangolinScheme c, {required IconData icon, required String label, required bool focused, required Widget child}) => + PangolinFieldBox(label: label, leading: icon, focused: focused, child: child); - // kBareInput 关掉主题 inputDecorationTheme 的边框/填充泄漏(否则聚焦时橙色 - // OutlineInputBorder 会画到内层 TextField 上、与外层容器错位/被裁)。 + // 内层裸装饰(无边框无填充,底色交给 PangolinFieldBox)。 InputDecoration get _bare => - kBareInput.copyWith(contentPadding: const EdgeInsets.symmetric(vertical: 13)); + bareInputDecoration(contentPadding: const EdgeInsets.symmetric(vertical: 13)); // 密码输入 + 显示/隐藏切换(眼睛按钮)。 Widget _pwInput(PangolinScheme c, String hint, {VoidCallback? onSubmit}) => Row(children: [ diff --git a/client/lib/widgets/pangolin_field.dart b/client/lib/widgets/pangolin_field.dart new file mode 100644 index 0000000..59ca716 --- /dev/null +++ b/client/lib/widgets/pangolin_field.dart @@ -0,0 +1,88 @@ +// pangolin_field.dart — 统一输入框容器组件。 +// +// 圆角容器(底色 + 可选描边/聚焦光环 + 可选上方标签 + 前置图标 + 后置 widget), +// 内层放一个用 [bareInputDecoration] 的 TextField。底色由本容器统一提供,内层不再 +// 自铺填充 → 杜绝「容器底色 ≠ TextField filled 底色」的两端一色中间另一色接缝。 +// 聚焦态(accent 边 + 光环)也由容器负责。搜索框/兑换码/登录注册输入框共用此组件。 +import 'package:flutter/material.dart'; + +import '../pangolin_theme.dart'; + +/// 内层 TextField 用的「裸」装饰:无边框、无填充。底色/边框/聚焦都交给 [PangolinFieldBox]。 +InputDecoration bareInputDecoration({ + EdgeInsetsGeometry contentPadding = const EdgeInsets.symmetric(vertical: 12), + String? hintText, + TextStyle? hintStyle, + String? counterText, +}) => + kBareInput.copyWith( + contentPadding: contentPadding, + hintText: hintText, + hintStyle: hintStyle, + counterText: counterText, + ); + +class PangolinFieldBox extends StatelessWidget { + const PangolinFieldBox({ + super.key, + required this.child, + this.fill, + this.label, + this.leading, + this.trailing, + this.focused = false, + this.bordered = true, + this.horizontalPadding = 14, + }); + + /// 内层输入(用 [bareInputDecoration] 构造装饰)。 + final Widget child; + + /// 容器底色;默认 [PangolinScheme.surface]。搜索框传 bgSubtle、兑换码传 bg。 + final Color? fill; + + /// 上方标签(可空)。登录/注册字段用。 + final String? label; + + /// 前置图标(可空)。 + final IconData? leading; + + /// 后置 widget(可空):如密码显示切换、发送验证码。 + final Widget? trailing; + + /// 聚焦态:accent 边 + 光环(仅 [bordered] 时生效)。 + final bool focused; + + /// 是否描边。搜索框传 false(只有底色,无边)。 + final bool bordered; + + final double horizontalPadding; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final box = AnimatedContainer( + duration: const Duration(milliseconds: 160), + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + decoration: BoxDecoration( + color: fill ?? c.surface, + borderRadius: BorderRadius.circular(PangolinRadius.md), + border: bordered + ? Border.all(color: focused ? c.accent : c.borderStrong, width: 1.5) + : null, + boxShadow: (bordered && focused) ? [BoxShadow(color: c.ring, spreadRadius: 3)] : null, + ), + child: Row(children: [ + if (leading != null) ...[Icon(leading, size: 18, color: c.fg3), const SizedBox(width: 10)], + Expanded(child: child), + if (trailing != null) trailing!, + ]), + ); + if (label == null) return box; + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(label!, style: PangolinText.caption.copyWith(color: c.fg2, fontWeight: FontWeight.w600)), + const SizedBox(height: 7), + box, + ]); + } +} diff --git a/client/test/golden/goldens/auth_login_dark.png b/client/test/golden/goldens/auth_login_dark.png index 0eabcf5..7371672 100644 Binary files a/client/test/golden/goldens/auth_login_dark.png and b/client/test/golden/goldens/auth_login_dark.png differ diff --git a/client/test/golden/goldens/auth_login_light.png b/client/test/golden/goldens/auth_login_light.png index c2c6c8d..484fe64 100644 Binary files a/client/test/golden/goldens/auth_login_light.png and b/client/test/golden/goldens/auth_login_light.png differ diff --git a/client/test/golden/goldens/desktop_redeem.png b/client/test/golden/goldens/desktop_redeem.png index 662e8cc..de35588 100644 Binary files a/client/test/golden/goldens/desktop_redeem.png and b/client/test/golden/goldens/desktop_redeem.png differ