201 lines
5.6 KiB
Dart
201 lines
5.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:ainoval/utils/web_theme.dart';
|
|
|
|
/// 文本编辑器操作按钮类型
|
|
enum EditorAction {
|
|
expand, // 展开
|
|
copy, // 复制
|
|
}
|
|
|
|
/// 自定义文本编辑器组件
|
|
/// 支持多行文本编辑、占位符和操作按钮
|
|
class CustomTextEditor extends StatefulWidget {
|
|
/// 构造函数
|
|
const CustomTextEditor({
|
|
super.key,
|
|
this.controller,
|
|
this.placeholder = '请输入内容...',
|
|
this.minLines = 3,
|
|
this.maxLines = 10,
|
|
this.showActions = true,
|
|
this.actions = const [EditorAction.expand, EditorAction.copy],
|
|
this.onExpand,
|
|
this.onCopy,
|
|
this.enabled = true,
|
|
this.readOnly = false,
|
|
});
|
|
|
|
/// 文本控制器
|
|
final TextEditingController? controller;
|
|
|
|
/// 占位符文字
|
|
final String placeholder;
|
|
|
|
/// 最小行数
|
|
final int minLines;
|
|
|
|
/// 最大行数
|
|
final int maxLines;
|
|
|
|
/// 是否显示操作按钮
|
|
final bool showActions;
|
|
|
|
/// 操作按钮列表
|
|
final List<EditorAction> actions;
|
|
|
|
/// 展开回调
|
|
final VoidCallback? onExpand;
|
|
|
|
/// 复制回调
|
|
final VoidCallback? onCopy;
|
|
|
|
/// 是否启用
|
|
final bool enabled;
|
|
|
|
/// 是否只读
|
|
final bool readOnly;
|
|
|
|
@override
|
|
State<CustomTextEditor> createState() => _CustomTextEditorState();
|
|
}
|
|
|
|
class _CustomTextEditorState extends State<CustomTextEditor> {
|
|
late TextEditingController _controller;
|
|
final FocusNode _focusNode = FocusNode();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = widget.controller ?? TextEditingController();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
if (widget.controller == null) {
|
|
_controller.dispose();
|
|
}
|
|
_focusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = WebTheme.isDarkMode(context);
|
|
|
|
return Column(
|
|
children: [
|
|
// 文本输入区域
|
|
Container(
|
|
constraints: BoxConstraints(
|
|
minHeight: widget.minLines * 24.0,
|
|
maxHeight: widget.maxLines * 24.0,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: widget.enabled
|
|
? Theme.of(context).colorScheme.surfaceContainer
|
|
: Theme.of(context).colorScheme.surfaceContainer,
|
|
border: Border.all(
|
|
color: Theme.of(context).colorScheme.outlineVariant,
|
|
width: 1,
|
|
),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: TextField(
|
|
controller: _controller,
|
|
focusNode: _focusNode,
|
|
enabled: widget.enabled,
|
|
readOnly: widget.readOnly,
|
|
maxLines: null,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
height: 1.5,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
decoration: InputDecoration(
|
|
hintText: _controller.text.isEmpty ? widget.placeholder : null,
|
|
hintStyle: TextStyle(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
fontSize: 14,
|
|
),
|
|
border: InputBorder.none,
|
|
enabledBorder: InputBorder.none,
|
|
focusedBorder: InputBorder.none,
|
|
contentPadding: const EdgeInsets.all(12),
|
|
isDense: true,
|
|
),
|
|
),
|
|
),
|
|
|
|
// 操作按钮区域
|
|
if (widget.showActions && widget.actions.isNotEmpty)
|
|
Container(
|
|
margin: const EdgeInsets.only(top: 4),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: widget.actions.map((action) => _buildActionButton(context, action, isDark)).toList(),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 构建操作按钮
|
|
Widget _buildActionButton(BuildContext context, EditorAction action, bool isDark) {
|
|
IconData icon;
|
|
String label;
|
|
VoidCallback? onPressed;
|
|
bool enabled = widget.enabled && !widget.readOnly;
|
|
|
|
switch (action) {
|
|
case EditorAction.expand:
|
|
icon = Icons.open_in_full;
|
|
label = '展开';
|
|
onPressed = enabled ? widget.onExpand : null;
|
|
break;
|
|
case EditorAction.copy:
|
|
icon = Icons.content_copy;
|
|
label = '复制';
|
|
onPressed = enabled ? widget.onCopy : null;
|
|
// 复制功能在有内容时才启用
|
|
enabled = enabled && _controller.text.isNotEmpty;
|
|
break;
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: 4),
|
|
child: Material(
|
|
type: MaterialType.transparency,
|
|
child: InkWell(
|
|
onTap: onPressed,
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
size: 12,
|
|
color: enabled
|
|
? Theme.of(context).colorScheme.onSurfaceVariant
|
|
: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: enabled
|
|
? Theme.of(context).colorScheme.onSurfaceVariant
|
|
: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |