492 lines
14 KiB
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;
|
|
}
|