Files
2026-09-04 15:22:20 +08:00

253 lines
9.5 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ThemeDemoPage> createState() => _ThemeDemoPageState();
}
class _ThemeDemoPageState extends State<ThemeDemoPage> {
/// 当前品牌(真实项目 → 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('本页主按钮'),
),
],
),
),
],
),
);
}
}