refactor(client): 抽公用 PangolinFieldBox 统一输入框 + 根治填充两色接缝
根因:主题 inputDecorationTheme 设了 filled:true/fillColor:bg,自定义容器里嵌 TextField 时内层自铺 bg 底,与容器底色(搜索框 bgSubtle / 登录 surface)不一致 → 两端一色中间另一色接缝(节点搜索框最明显,登录/兑换框也有隐患)。 - kBareInput 加 filled:false:内层 TextField 不再自铺底,底色统一交给外层容器。 - 新增 widgets/pangolin_field.dart:PangolinFieldBox(圆角容器 + 底色 + 可选描边/ 聚焦光环 + 标签 + 前置图标 + 后置 widget)+ bareInputDecoration() 助手。 - 三处输入框统一改用:节点搜索框、登录/注册字段(_field)、兑换码输入框。 - 重生成 auth_login(dark/light)+ desktop_redeem golden(均人工核对外观正确)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RedeemScreen> {
|
||||
])
|
||||
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(() {}),
|
||||
),
|
||||
)),
|
||||
|
||||
@@ -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<AuthScreen>
|
||||
),
|
||||
);
|
||||
|
||||
// 输入字段:标签 + 图标 + 聚焦态(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: [
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Reference in New Issue
Block a user