feat(client): 设置页新增外观主题选择器(A/B/C 实时切换)

系统参数 Tab 顶部加「外观主题」卡片:三枚带微缩预览的主题色板
(经典蓝/琥珀/酒窖),点选即调用 themeController.set 实时换色并持久化。
表格列头 F0F4FF → thBg 一并清理。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
This commit is contained in:
wangjia
2026-06-25 06:21:35 +08:00
parent dfe9796ae8
commit 648c00434c
@@ -13,6 +13,9 @@ import '../../core/config/app_info.dart';
import '../../core/config/license_copy.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/context_tokens.dart';
import '../../core/theme/app_tokens.dart';
import '../../core/theme/app_tokens.g.dart';
import '../../core/theme/theme_controller.dart';
import '../../models/number_rule.dart';
import '../../models/user.dart';
import '../../providers/license_provider.dart';
@@ -228,7 +231,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
data: (users) => SingleChildScrollView(
child: DataTable(
headingRowColor: WidgetStateProperty.all(const Color(0xFFF0F4FF)),
headingRowColor: WidgetStateProperty.all(context.tokens.thBg),
columns: const [
DataColumn(label: Text('姓名')),
DataColumn(label: Text('用户名')),
@@ -600,7 +603,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
Card(
child: DataTable(
headingRowColor:
WidgetStateProperty.all(const Color(0xFFF0F4FF)),
WidgetStateProperty.all(context.tokens.thBg),
columns: const [
DataColumn(label: Text('单据类型')),
DataColumn(label: Text('前缀')),
@@ -725,6 +728,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const Text('系统参数',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(height: 16),
const _ThemePickerCard(),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(20),
@@ -1184,6 +1189,163 @@ class _ParamRow extends StatelessWidget {
}
// ── 外观主题选择 ──────────────────────────────────────────
class _ThemePickerCard extends ConsumerWidget {
const _ThemePickerCard();
// (key, 名称, 说明)
static const List<(String, String, String)> _themes = [
('a', '经典蓝', '浅色 · 默认'),
('b', '琥珀', '深色'),
('c', '酒窖', '暖色'),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final current = ref.watch(themeControllerProvider);
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('外观主题',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: context.tokens.primaryDark)),
const SizedBox(height: 4),
Text('切换配色方案,立即生效并记住选择',
style: TextStyle(fontSize: 13, color: context.tokens.muted)),
const SizedBox(height: 16),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
for (final (key, name, desc) in _themes)
_ThemeSwatch(
preview: appTokensOf(key),
name: name,
desc: desc,
selected: current == key,
onTap: () =>
ref.read(themeControllerProvider.notifier).set(key),
),
],
),
],
),
),
);
}
}
class _ThemeSwatch extends StatelessWidget {
final AppTokens preview;
final String name;
final String desc;
final bool selected;
final VoidCallback onTap;
const _ThemeSwatch({
required this.preview,
required this.name,
required this.desc,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(10),
onTap: onTap,
child: Container(
width: 150,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: context.tokens.surface,
border: Border.all(
color: selected ? context.tokens.primary : context.tokens.border,
width: selected ? 2 : 1,
),
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 微缩预览:用该主题真实 token 渲染一小块 chrome
Container(
height: 46,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: preview.page,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: preview.border),
),
child: Row(
children: [
Container(
width: 22,
height: 34,
decoration: BoxDecoration(
color: preview.surface,
borderRadius: BorderRadius.circular(3),
border: Border.all(color: preview.border),
),
),
const SizedBox(width: 6),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 36,
height: 6,
decoration: BoxDecoration(
color: preview.primary,
borderRadius: BorderRadius.circular(3))),
const SizedBox(height: 5),
Container(
width: 24,
height: 5,
decoration: BoxDecoration(
color: preview.muted,
borderRadius: BorderRadius.circular(3))),
],
),
),
],
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name,
style: const TextStyle(
fontSize: 13, fontWeight: FontWeight.w600)),
Text(desc,
style: TextStyle(
fontSize: 11, color: context.tokens.muted)),
],
),
),
if (selected)
Icon(Icons.check_circle,
size: 18, color: context.tokens.primary),
],
),
],
),
),
);
}
}
// ── 门店 Logo 预览 ────────────────────────────────────────
class _ShopLogoPreview extends StatelessWidget {
final String logoUrl;