174 lines
5.4 KiB
Dart
174 lines
5.4 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:image_picker/image_picker.dart';
|
||
|
||
/// 品牌配置编辑(启动图 + Logo)。
|
||
///
|
||
/// 通过 [ImagePicker] 选择本地图片,并用「手机外框」预览:
|
||
/// - 启动图为全屏背景;
|
||
/// - Logo 为右下角角标。
|
||
///
|
||
/// [splashUrl]/[logoUrl] 为本地图片路径(占位),由父级保存。
|
||
class BrandingEditor extends StatefulWidget {
|
||
const BrandingEditor({
|
||
super.key,
|
||
this.initialSplashUrl,
|
||
this.initialLogoUrl,
|
||
});
|
||
|
||
final String? initialSplashUrl;
|
||
final String? initialLogoUrl;
|
||
|
||
@override
|
||
State<BrandingEditor> createState() => BrandingEditorState();
|
||
}
|
||
|
||
/// 品牌编辑器状态(公开,供父级通过 GlobalKey 读取所选图片路径)。
|
||
class BrandingEditorState extends State<BrandingEditor> {
|
||
final ImagePicker _picker = ImagePicker();
|
||
String? _splashUrl;
|
||
String? _logoUrl;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_splashUrl = widget.initialSplashUrl;
|
||
_logoUrl = widget.initialLogoUrl;
|
||
}
|
||
|
||
Future<void> _pickSplash() async {
|
||
final xFile = await _picker.pickImage(source: ImageSource.gallery);
|
||
if (xFile != null && mounted) {
|
||
setState(() => _splashUrl = xFile.path);
|
||
}
|
||
}
|
||
|
||
Future<void> _pickLogo() async {
|
||
final xFile = await _picker.pickImage(source: ImageSource.gallery);
|
||
if (xFile != null && mounted) {
|
||
setState(() => _logoUrl = xFile.path);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: FilledButton.tonal(
|
||
onPressed: _pickSplash,
|
||
child: const Text('选择启动图'),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: FilledButton.tonal(
|
||
onPressed: _pickLogo,
|
||
child: const Text('选择 Logo'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
// 手机外框预览。
|
||
Center(
|
||
child: Container(
|
||
width: 200,
|
||
height: 380,
|
||
decoration: BoxDecoration(
|
||
color: Colors.black,
|
||
borderRadius: BorderRadius.circular(28),
|
||
border: Border.all(color: Colors.black87, width: 8),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.2),
|
||
blurRadius: 12,
|
||
offset: const Offset(0, 6),
|
||
),
|
||
],
|
||
),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: Stack(
|
||
children: [
|
||
// 启动图全屏背景。
|
||
if (_splashUrl != null && _splashUrl!.isNotEmpty)
|
||
Positioned.fill(
|
||
child: Image.network(
|
||
_splashUrl!,
|
||
fit: BoxFit.cover,
|
||
errorBuilder: (_, __, ___) => Container(
|
||
color: theme.colorScheme.primaryContainer,
|
||
),
|
||
),
|
||
)
|
||
else
|
||
Positioned.fill(
|
||
child: Container(
|
||
color: theme.colorScheme.primaryContainer,
|
||
child: Center(
|
||
child: Text(
|
||
'启动图预览',
|
||
style: TextStyle(
|
||
color: theme.colorScheme.onPrimaryContainer,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
// 顶部状态栏占位。
|
||
Positioned(
|
||
top: 8,
|
||
left: 0,
|
||
right: 0,
|
||
child: Container(
|
||
height: 18,
|
||
color: Colors.black.withOpacity(0.25),
|
||
),
|
||
),
|
||
// Logo 右下角角标。
|
||
if (_logoUrl != null && _logoUrl!.isNotEmpty)
|
||
Positioned(
|
||
right: 16,
|
||
bottom: 24,
|
||
child: Container(
|
||
width: 56,
|
||
height: 56,
|
||
decoration: const BoxDecoration(
|
||
color: Colors.white,
|
||
shape: BoxShape.circle,
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black26,
|
||
blurRadius: 6,
|
||
offset: Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
child: ClipOval(
|
||
child: Image.network(
|
||
_logoUrl!,
|
||
fit: BoxFit.cover,
|
||
errorBuilder: (_, __, ___) =>
|
||
const Icon(Icons.image, size: 28),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// 当前启动图本地路径(未选返回 null)。
|
||
String? get splashUrl => _splashUrl;
|
||
|
||
/// 当前 Logo 本地路径(未选返回 null)。
|
||
String? get logoUrl => _logoUrl;
|
||
}
|