import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:new_learn/core/theme/brand_theme.dart'; /// ============================================================ /// 【ThemeDemoPage】品牌换肤 Demo(两级主题模型) /// /// 一、两级模型(模型与配置已解耦,见 core/theme/): /// core/theme/brand_theme.dart ← 模型(BrandTheme/PageTheme/PageLevel) /// + 注册表 brandThemes /// core/theme/brands/xxx_theme.dart← 每品牌一个文件,只有具体配置 /// 一级 = 品牌:seedColor 生成整套 Material 色板(全 App 共享); /// 二级 = 页面细节(PageTheme):按页面层级(home/detail/result) /// 区分背景色 / 头部色 / 强调色 / 专属 banner / 标题文案。 /// 页面只声明"我是哪个层级",细节从品牌对象按层级查出来: /// 换品牌 → 一级色板全换 + 二级细节全换(一套代码) /// /// 二、本页怎么实现切换(不依赖全局 MaterialApp): /// 页面根部用 Theme(data:) 局部覆盖——ThemeData 以全局为基底, /// 局部 Theme 再套一层,页面内组件(AppBar/Button/进度条)都从 /// 局部主题取色。真实项目里把"_brand"换成 Riverpod Provider + /// MaterialApp 的 theme,切换点就移到根层(见注释)。 /// /// 三、资源(图片)怎么做: /// 真实项目 PageTheme.banner 存资源路径(如 assets/img/banner_sky.png), /// 页面 Image.asset(page.banner) 即可跟随换图。本 demo 没有两套图片 /// 资源,用"强调色渐变块"模拟 banner 占位(代码里标注了替换点)。 /// /// 四、高频坑: /// 1. colorSchemeSeed 生成的 primary 不是精确色值(Material 会调和), /// 要求精确用 ColorScheme.fromSeed 后手动覆盖 primary; /// 2. 局部 Theme(data:) 必须包在 Scaffold 外面,AppBar 才能取到; /// 3. PNG 位图不跟主题着色,能矢量(IconData)就别放多份图。 /// ============================================================ class ThemeDemoPage extends StatefulWidget { const ThemeDemoPage({super.key}); @override State createState() => _ThemeDemoPageState(); } class _ThemeDemoPageState extends State { /// 当前品牌(真实项目 → Riverpod Provider + shared_preferences 持久化) BrandTheme _brand = brandThemes.first; @override Widget build(BuildContext context) { // 局部主题覆盖:colorSchemeSeed 换 seed = 页面内所有 Material // 组件自动换色(真实项目这里换成 MaterialApp.theme) return Theme( data: ThemeData(colorSchemeSeed: _brand.seedColor), child: Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, title: Text('品牌换肤 Demo(${_brand.name})'), ), body: SingleChildScrollView( padding: EdgeInsets.all(16.w), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ---------- 品牌切换 ---------- Wrap( spacing: 8, children: [ for (final b in brandThemes) // 注册表:新增品牌自动出现 ChoiceChip( label: Text(b.name), selected: _brand.name == b.name, onSelected: (_) => setState(() => _brand = b), ), ], ), SizedBox(height: 16.h), _buildGlobalCard(), SizedBox(height: 16.h), _buildSectionTitle('二级:页面细节(按层级,随品牌联动)'), SizedBox(height: 8.h), // 三个层级各渲染一个"页面骨架预览" _PagePreview( level: 'home(任务 tab)', page: _brand.pages[PageLevel.home]!, ), _PagePreview( level: 'detail(子详情页)', page: _brand.pages[PageLevel.detail]!, ), _PagePreview( level: 'result(答题结果页)', page: _brand.pages[PageLevel.result]!, ), ], ), ), ), ); } /// 一级:全局色板效果区(Material 组件自动跟随 seed) Widget _buildGlobalCard() { final scheme = Theme.of(context).colorScheme; return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.r), border: Border.all(color: const Color(0xFFEEF1F5)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionTitle('一级:全局色板(seed: ${_brand.seedColor})'), SizedBox(height: 8.h), // seed 色与生成的 primary 对比 Row( children: [ _colorChip('seed', _brand.seedColor), SizedBox(width: 8.w), _colorChip('primary', scheme.primary), ], ), SizedBox(height: 12.h), // Material 组件不用改代码,seed 一变自动换色 FilledButton(onPressed: () {}, child: const Text('跟随主题的按钮')), SizedBox(height: 12.h), LinearProgressIndicator( value: 0.6, backgroundColor: scheme.surfaceContainerHighest, ), ], ), ); } Widget _colorChip(String label, Color color) { return Container( padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(6.r), ), child: Text( label, style: const TextStyle(color: Colors.white, fontSize: 12), ), ); } Widget _buildSectionTitle(String title) { return Text( title, style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, color: const Color(0xFF3B82F6), ), ); } } // ==================== 页面骨架预览(演示二级细节怎么被消费) ==================== class _PagePreview extends StatelessWidget { const _PagePreview({required this.level, required this.page}); /// 本页属于哪个层级(页面只声明层级,细节全从主题查) final String level; final PageTheme page; @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.only(bottom: 12.h), decoration: BoxDecoration( color: page.bg, // ← 二级:背景 borderRadius: BorderRadius.circular(12.r), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 模拟 AppBar 条(真实页面是 Scaffold.appBar) Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h), decoration: BoxDecoration( color: page.headerBg, // ← 二级:头部色 borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)), ), child: Row( children: [ const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 14), SizedBox(width: 6.w), Text( page.title, // ← 二级:标题文案 style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600, ), ), ], ), ), Padding( padding: EdgeInsets.all(12.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // banner 占位:真实项目这里替换成 Image.asset(page.banner!) // (banner 为 null 的层级显示"本层无专属图") Container( width: double.infinity, height: 44.h, decoration: BoxDecoration( gradient: LinearGradient( colors: [page.accent, page.headerBg], ), borderRadius: BorderRadius.circular(8.r), ), alignment: Alignment.center, child: Text( page.banner ?? '本层无专属 banner', style: const TextStyle(color: Colors.white, fontSize: 12), ), ), SizedBox(height: 8.h), Text( '层级:$level\n背景 ${page.bg} · 头部 ${page.headerBg} · 强调 ${page.accent}', style: TextStyle( fontSize: 12.sp, height: 1.5, color: const Color(0xFF666666), ), ), SizedBox(height: 8.h), FilledButton( style: FilledButton.styleFrom( backgroundColor: page.accent, // ← 二级:强调色 ), onPressed: () {}, child: const Text('本页主按钮'), ), ], ), ), ], ), ); } }