Files
CloudHome-Ops/lib/widgets/branding_editor.dart

174 lines
5.4 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: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;
}