hi-client/lib/app/widgets/hi_collapsible_list.dart

191 lines
6.3 KiB
Dart

// lib/app/widgets/hi_collapsible_list.dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'dart:math' as math; // 导入 math 库以使用 pi
import 'package:flutter/gestures.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
import 'package:url_launcher/url_launcher.dart';
/// 可折叠列表项的数据模型
class HICollapsibleItem {
final String title;
// 👇 核心改动 1: 将 content 类型从 String 改为 List<String>
final List<String> content;
const HICollapsibleItem({required this.title, required this.content});
}
/// 一个独立的、自定义样式的可折叠面板组件 (HI 前缀)
///
/// 它接收一个标题和一个内容字符串,并渲染成一个带边框、可展开/收起的面板。
class HICollapsibleItemWidget extends StatefulWidget {
const HICollapsibleItemWidget({
super.key,
required this.item,
this.initiallyExpanded = false,
});
/// 要显示的数据项
final HICollapsibleItem item;
/// 初始是否展开
final bool initiallyExpanded;
@override
State<HICollapsibleItemWidget> createState() => _HICollapsibleItemWidgetState();
}
class _HICollapsibleItemWidgetState extends State<HICollapsibleItemWidget> {
bool _isExpanded = false;
@override
void initState() {
super.initState();
_isExpanded = widget.initiallyExpanded;
}
List<TextSpan> _buildTextSpans(String text) {
final List<TextSpan> spans = [];
final RegExp linkRegExp = RegExp(r'\[link\](.*?)\[/link\]'); // 正则表达式匹配 [link]...[/link]
text.splitMapJoin(
linkRegExp,
onMatch: (Match match) {
final linkText = match.group(1)!; // 获取链接文本,例如 "点击这里"
spans.add(
TextSpan(
text: linkText,
style: const TextStyle(
color: const Color(0xFFADFF5B), // 链接颜色
),
recognizer: TapGestureRecognizer()
..onTap = () async {
final Uri url = Uri.parse('https://hifastvpn.com/help');
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
Get.snackbar('错误', '无法打开链接: $url');
}
},
),
);
return '';
},
onNonMatch: (String nonMatch) {
spans.add(TextSpan(text: nonMatch)); // 普通文本部分
return '';
},
);
return spans;
}
@override
Widget build(BuildContext context) {
final BorderRadius borderRadius = BorderRadius.circular(25);
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2.0),
borderRadius: borderRadius,
),
child: ClipRRect(
borderRadius: borderRadius,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ===== 标题部分 =====
GestureDetector(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
behavior: HitTestBehavior.translucent,
child: Padding(
padding: EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Row(
children: [
Expanded(
child: Text(
widget.item.title,
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
AnimatedRotation(
turns: _isExpanded ? 0.75 : 0.25,
duration: const Duration(milliseconds: 200),
child: KrLocalImage(
imageName: 'arrow-right-icon',
imageType: ImageType.svg,
color: Colors.white,
),
),
],
),
),
),
// ===== 内容部分 =====
AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: _isExpanded
? Container(
padding: EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widget.item.content.map((itemText) {
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(top: 11, right: 8),
child: Container(
width: 5.w,
height: 5.w,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
),
Expanded(
child: Text.rich(
TextSpan(
style: TextStyle(
color: Colors.white,
fontSize: 13,
height: 1.8,
fontWeight: FontWeight.w300,
),
children: _buildTextSpans(itemText),
),
),
),
],
),
);
}).toList(),
),
)
: const SizedBox.shrink(),
),
],
),
),
);
}
}