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;
|
|||
|
|
}
|