yumi-flutter/lib/modules/user/vip/vip_instruction_page.dart

492 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
class VipInstructionPage extends StatefulWidget {
const VipInstructionPage({super.key});
@override
State<VipInstructionPage> createState() => _VipInstructionPageState();
}
class _VipInstructionPageState extends State<VipInstructionPage> {
_VipInstructionTab _selectedTab = _VipInstructionTab.record;
static const List<_VipRecordRow> _recordRows = [
_VipRecordRow(
level: 1,
validDays: '30days',
type: 'Expiration',
time: '21/09/2029\n12:99:00',
),
_VipRecordRow(
level: 2,
validDays: '10days',
type: 'Card',
time: '21/09/2029\n12:99:00',
),
_VipRecordRow(
level: 3,
validDays: '30days',
type: 'Renew',
time: '21/09/2029\n12:99:00',
),
_VipRecordRow(
level: 4,
validDays: '30days',
type: 'Purchase',
time: '21/09/2029\n12:99:00',
),
_VipRecordRow(
level: 5,
validDays: '30days',
type: 'Upgrade',
time: '21/09/2029\n12:99:00',
),
];
static const List<_VipDescriptionSection> _descriptionSections = [
_VipDescriptionSection(
'Each time a VIP level is purchased, it is valid for 30 days, and can be manually renewed after expiration.',
),
_VipDescriptionSection(
'You can upgrade to a higher VIP level by paying the price difference. After upgrading, a new 30-day validity period will be given.',
bullets: [
'Price Difference = Target Price - Remaining Price for Current Days.',
'The upgrade takes effect immediately, and the remaining time of the previous level will be overwritten.',
],
),
_VipDescriptionSection(
'Once a higher-level VIP is activated, it is not possible to purchase a lower-level VIP.',
),
_VipDescriptionSection(
'Different levels cannot be stacked simultaneously; only one level can be activated at a time.',
),
_VipDescriptionSection(
'If using a card from your inventory to activate a new level:',
bullets: [
'If the new level is higher than the current level, it will directly upgrade, and the remaining time of the previous level will be cleared.',
'If the new level is lower than the current level, activation cannot be used.',
],
),
_VipDescriptionSection(
'After the VIP expires, the corresponding privileges will be lost, but the purchase records will not be cleared.',
),
_VipDescriptionSection(
'The platform reserves the final right to interpret the VIP rules.',
),
];
static const Color _tableLightColor = Color(0xFF262626);
static const Color _tableDarkColor = Color(0xFF191919);
static const Color _tableDividerColor = Color(0xFF333333);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: const [Color(0xFF0D2B22), Color(0xFF0E0E0E)],
stops: const [0, 0.40],
),
),
),
),
Positioned(
left: 0,
right: 0,
top: 0,
child: Image.asset(
'sc_images/vip/sc_vip_page_bg.png',
width: ScreenUtil().screenWidth,
fit: BoxFit.fitWidth,
alignment: Alignment.topCenter,
),
),
Positioned(
left: 0,
right: 0,
top: 0,
height: 300.w,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.36),
Colors.black.withValues(alpha: 0.62),
Colors.transparent,
],
stops: const [0, 0.52, 1],
),
),
),
),
),
SafeArea(
bottom: false,
child: Column(
children: [
_buildTopBar(context),
SizedBox(height: 6.w),
_buildTabs(),
Expanded(
child:
_selectedTab == _VipInstructionTab.record
? _buildRecordView()
: _buildDescriptionView(),
),
],
),
),
],
),
);
}
Widget _buildTopBar(BuildContext context) {
return SizedBox(
height: 44.w,
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: AlignmentDirectional.centerStart,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => Navigator.pop(context),
child: SizedBox(
width: 44.w,
height: 44.w,
child: Icon(
SCGlobalConfig.lang == 'ar'
? Icons.keyboard_arrow_right
: Icons.keyboard_arrow_left,
color: Colors.white,
size: 28.w,
),
),
),
),
Text(
'VIP Rules',
style: TextStyle(
color: Colors.white,
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildTabs() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildTab(_VipInstructionTab.record, 'Record'),
SizedBox(width: 45.5.w),
_buildTab(_VipInstructionTab.description, 'Description'),
],
);
}
Widget _buildTab(_VipInstructionTab tab, String title) {
final selected = _selectedTab == tab;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => _selectedTab = tab),
child: Column(
children: [
Text(
title,
style: TextStyle(
color:
selected ? Colors.white : Colors.white.withValues(alpha: 0.5),
fontFamily: 'Source Han Sans SC',
fontSize: 18.sp,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400,
height: 1,
),
),
SizedBox(height: 8.w),
AnimatedOpacity(
duration: const Duration(milliseconds: 160),
opacity: selected ? 1 : 0,
child: Container(
width: 18.w,
height: 4.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(999.w),
),
),
),
],
),
);
}
Widget _buildRecordView() {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.fromLTRB(16.w, 18.w, 16.w, 24.w),
child: _buildRecordTable(),
);
}
Widget _buildRecordTable() {
return ClipRRect(
borderRadius: BorderRadius.circular(12.w),
child: Container(
color: _tableDarkColor,
child: Column(
children: [
_buildTableRow(
const ['Level', 'Valid Days', 'type', 'Time'],
height: 38.5.w,
isHeader: true,
isLastRow: false,
),
for (int i = 0; i < _recordRows.length; i++)
_buildRecordDataRow(
_recordRows[i],
isLastRow: i == _recordRows.length - 1,
),
],
),
),
);
}
Widget _buildTableRow(
List<String> values, {
required double height,
required bool isHeader,
required bool isLastRow,
}) {
return SizedBox(
height: height,
child: Row(
children: List.generate(values.length, (index) {
return _buildTableCell(
columnIndex: index,
isLastColumn: index == values.length - 1,
isLastRow: isLastRow,
flex: _columnFlex(index),
child: Text(
values[index],
textAlign: TextAlign.center,
style: _tableTextStyle,
),
);
}),
),
);
}
Widget _buildRecordDataRow(_VipRecordRow row, {required bool isLastRow}) {
return SizedBox(
height: 76.w,
child: Row(
children: [
_buildTableCell(
columnIndex: 0,
isLastColumn: false,
isLastRow: isLastRow,
flex: _columnFlex(0),
child: Image.asset(
'sc_images/vip/sc_vip_badge_${row.level}.png',
width: 60.w,
height: 60.w,
fit: BoxFit.contain,
),
),
_buildTableCell(
columnIndex: 1,
isLastColumn: false,
isLastRow: isLastRow,
flex: _columnFlex(1),
child: Text(
row.validDays,
textAlign: TextAlign.center,
style: _tableTextStyle,
),
),
_buildTableCell(
columnIndex: 2,
isLastColumn: false,
isLastRow: isLastRow,
flex: _columnFlex(2),
child: Text(
row.type,
textAlign: TextAlign.center,
style: _tableTextStyle,
),
),
_buildTableCell(
columnIndex: 3,
isLastColumn: true,
isLastRow: isLastRow,
flex: _columnFlex(3),
child: Text(
row.time,
textAlign: TextAlign.center,
style: _tableTextStyle,
),
),
],
),
);
}
Widget _buildTableCell({
required int columnIndex,
required bool isLastColumn,
required bool isLastRow,
required int flex,
required Widget child,
}) {
return Expanded(
flex: flex,
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.w),
decoration: BoxDecoration(
color: columnIndex.isEven ? _tableLightColor : _tableDarkColor,
border: Border(
right:
isLastColumn
? BorderSide.none
: BorderSide(color: _tableDividerColor, width: 1.w),
bottom:
isLastRow
? BorderSide.none
: BorderSide(color: _tableDividerColor, width: 1.w),
),
),
child: child,
),
);
}
Widget _buildDescriptionView() {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.fromLTRB(16.w, 18.w, 16.w, 32.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < _descriptionSections.length; i++) ...[
_buildDescriptionSection(i + 1, _descriptionSections[i]),
SizedBox(height: 18.w),
],
],
),
);
}
Widget _buildDescriptionSection(int index, _VipDescriptionSection section) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$index. ${section.text}', style: _descriptionTextStyle),
if (section.bullets.isNotEmpty) ...[
SizedBox(height: 8.w),
for (final bullet in section.bullets) ...[
Padding(
padding: EdgeInsetsDirectional.only(start: 18.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('', style: _descriptionHighlightStyle),
SizedBox(width: 8.w),
Expanded(
child: Text(bullet, style: _descriptionHighlightStyle),
),
],
),
),
SizedBox(height: 4.w),
],
],
],
);
}
int _columnFlex(int index) {
switch (index) {
case 0:
return 92;
case 1:
return 100;
case 2:
return 114;
case 3:
default:
return 110;
}
}
TextStyle get _tableTextStyle => TextStyle(
color: Colors.white,
fontFamily: 'Source Han Sans SC',
fontSize: 12.sp,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400,
height: 14.5 / 12,
);
TextStyle get _descriptionTextStyle => TextStyle(
color: Colors.white,
fontFamily: 'Source Han Sans SC',
fontSize: 16.sp,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400,
height: 14.5 / 12,
);
TextStyle get _descriptionHighlightStyle => TextStyle(
color: const Color(0xFFFF9830),
fontFamily: 'Source Han Sans SC',
fontSize: 16.sp,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400,
height: 14.5 / 12,
);
}
enum _VipInstructionTab { record, description }
class _VipRecordRow {
const _VipRecordRow({
required this.level,
required this.validDays,
required this.type,
required this.time,
});
final int level;
final String validDays;
final String type;
final String time;
}
class _VipDescriptionSection {
const _VipDescriptionSection(this.text, {this.bullets = const []});
final String text;
final List<String> bullets;
}