马良AI写作初始化仓库
This commit is contained in:
857
AINoval/lib/screens/subscription/subscription_screen.dart
Normal file
857
AINoval/lib/screens/subscription/subscription_screen.dart
Normal file
@@ -0,0 +1,857 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
import 'package:ainoval/widgets/common/app_sidebar.dart';
|
||||
import 'package:ainoval/widgets/common/user_avatar_menu.dart';
|
||||
import 'package:ainoval/screens/settings/settings_panel.dart';
|
||||
import 'package:ainoval/screens/editor/managers/editor_state_manager.dart';
|
||||
import 'package:ainoval/models/editor_settings.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/subscription_repository.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/payment_repository.dart';
|
||||
import 'package:ainoval/models/admin/subscription_models.dart';
|
||||
|
||||
class SubscriptionScreen extends StatefulWidget {
|
||||
const SubscriptionScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SubscriptionScreen> createState() => _SubscriptionScreenState();
|
||||
}
|
||||
|
||||
class _SubscriptionScreenState extends State<SubscriptionScreen> {
|
||||
bool _isSidebarExpanded = true;
|
||||
final _subRepo = PublicSubscriptionRepository();
|
||||
final _payRepo = PaymentRepository();
|
||||
bool _loading = true;
|
||||
String? _error;
|
||||
List<SubscriptionPlan> _plans = const [];
|
||||
|
||||
BillingCycle _selectedCycle = BillingCycle.monthly;
|
||||
static const double _featureColumnWidth = 240.0;
|
||||
static const double _planColumnWidth = 220.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
setState(() { _loading = true; _error = null; });
|
||||
try {
|
||||
final plans = await _subRepo.listActivePlans();
|
||||
if (!mounted) return;
|
||||
setState(() { _plans = plans; });
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
// 带上具体异常信息,便于排查是否为鉴权/解析问题
|
||||
setState(() { _error = '加载订阅信息失败: $e'; });
|
||||
} finally {
|
||||
if (mounted) setState(() { _loading = false; });
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
return Scaffold(
|
||||
backgroundColor: WebTheme.getBackgroundColor(context),
|
||||
body: Row(
|
||||
children: [
|
||||
AppSidebar(
|
||||
isExpanded: _isSidebarExpanded,
|
||||
currentRoute: 'my_subscription',
|
||||
onExpandedChanged: (v) => setState(() { _isSidebarExpanded = v; }),
|
||||
onNavigate: (route) {
|
||||
if (route == 'my_subscription') return;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
// Top Bar
|
||||
Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: WebTheme.getBorderColor(context), width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'订阅与升级',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: Icon(isDark ? Icons.light_mode : Icons.dark_mode, size: 20),
|
||||
onPressed: () {},
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
UserAvatarMenu(
|
||||
size: 16,
|
||||
onOpenSettings: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (dialogContext) => Dialog(
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
backgroundColor: Colors.transparent,
|
||||
child: SettingsPanel(
|
||||
stateManager: EditorStateManager(),
|
||||
userId: '',
|
||||
onClose: () => Navigator.of(dialogContext).pop(),
|
||||
editorSettings: const EditorSettings(),
|
||||
onEditorSettingsChanged: (_) {},
|
||||
initialCategoryIndex: SettingsPanel.accountManagementCategoryIndex,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Ultra-Modern Hero
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 80),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Ultra-large main title
|
||||
Text(
|
||||
'创作升级',
|
||||
style: TextStyle(
|
||||
fontSize: 72,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: WebTheme.getTextColor(context),
|
||||
height: 0.9,
|
||||
letterSpacing: -2.0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Minimal subtitle
|
||||
Text(
|
||||
'选择适合你的方案',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 64),
|
||||
// Ultra-simple toggle
|
||||
_ultraSimpleToggle(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Content
|
||||
Expanded(
|
||||
child: _loading
|
||||
? _skeletonContent(context)
|
||||
: _error != null
|
||||
? _errorView(context, _error!)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
// Ultra-clean plans section
|
||||
Center(child: _plansSection(context)),
|
||||
const SizedBox(height: 80),
|
||||
// Modern comparison section
|
||||
_modernComparisonSection(context),
|
||||
const SizedBox(height: 120),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Widget _plansSection(BuildContext context) {
|
||||
final filtered = _filteredPlans();
|
||||
if (filtered.isEmpty) {
|
||||
return const SizedBox(height: 200);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 1200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 900) {
|
||||
// 窄屏:单列栈叠,卡片自适应宽度
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: filtered
|
||||
.map((plan) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: _ultraCleanCard(context, plan),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
// 宽屏:使用 Wrap 实现响应式多列,避免 Row+Expanded 在滚动视图中的无限宽问题
|
||||
return Wrap(
|
||||
spacing: 32,
|
||||
runSpacing: 24,
|
||||
children: filtered
|
||||
.map((plan) => SizedBox(
|
||||
width: 360,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: _ultraCleanCard(context, plan),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _ultraCleanCard(BuildContext context, SubscriptionPlan p) {
|
||||
final feats = p.features ?? const {};
|
||||
final recommended = p.recommended;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(40),
|
||||
decoration: BoxDecoration(
|
||||
color: recommended
|
||||
? WebTheme.getTextColor(context).withOpacity(0.04)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: recommended
|
||||
? Border.all(
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.08),
|
||||
width: 1,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Minimal badge for recommended
|
||||
if (recommended) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getTextColor(context),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'推荐',
|
||||
style: TextStyle(
|
||||
color: WebTheme.getBackgroundColor(context),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
// Plan name - ultra large
|
||||
Text(
|
||||
p.planName,
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: WebTheme.getTextColor(context),
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Price - massive and clean
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
'¥${p.price.toInt()}',
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: WebTheme.getTextColor(context),
|
||||
letterSpacing: -1.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'/ ${p.billingCycle == BillingCycle.monthly ? "月" : "年"}',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Minimal feature list - only top 3
|
||||
...(_getTopFeatures(feats).take(3).map((feature) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
feature,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: WebTheme.getTextColor(context),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
))),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Single CTA button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _buyPlan(p, PayChannel.wechat),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: WebTheme.getTextColor(context),
|
||||
foregroundColor: WebTheme.getBackgroundColor(context),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text(
|
||||
'立即选择',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
List<String> _getTopFeatures(Map<String, dynamic> features) {
|
||||
final List<String> topFeatures = [];
|
||||
|
||||
// Only show the most important features in a clean way
|
||||
if (features['ai.daily.calls'] != null) {
|
||||
final calls = features['ai.daily.calls'];
|
||||
topFeatures.add(calls == -1 ? '无限AI调用' : '每日${calls}次AI调用');
|
||||
}
|
||||
|
||||
if (features['novel.max.count'] != null) {
|
||||
final count = features['novel.max.count'];
|
||||
topFeatures.add(count == -1 ? '无限小说项目' : '最多${count}个小说项目');
|
||||
}
|
||||
|
||||
if (features['import.daily.limit'] != null) {
|
||||
final limit = features['import.daily.limit'];
|
||||
topFeatures.add(limit == -1 ? '无限导入' : '每日导入${limit}次');
|
||||
}
|
||||
|
||||
// Add default features if none specified
|
||||
if (topFeatures.isEmpty) {
|
||||
topFeatures.addAll([
|
||||
'核心创作功能',
|
||||
'云端同步备份',
|
||||
'多设备支持',
|
||||
]);
|
||||
}
|
||||
|
||||
return topFeatures;
|
||||
}
|
||||
|
||||
Widget _modernComparisonSection(BuildContext context) {
|
||||
final filtered = _filteredPlans();
|
||||
if (filtered.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 1200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Column(
|
||||
children: [
|
||||
// Section title
|
||||
Text(
|
||||
'功能对比',
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: WebTheme.getTextColor(context),
|
||||
letterSpacing: -1.0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 64),
|
||||
// Table-style comparison
|
||||
_buildComparisonTable(context, filtered),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildComparisonTable(BuildContext context, List<SubscriptionPlan> plans) {
|
||||
final featureGroups = {
|
||||
'创作功能': [
|
||||
{'key': 'ai.daily.calls', 'name': 'AI 每日调用次数'},
|
||||
{'key': 'novel.max.count', 'name': '小说项目数量'},
|
||||
{'key': 'import.daily.limit', 'name': '导入限制'},
|
||||
{'key': 'export.formats', 'name': '导出格式'},
|
||||
],
|
||||
'AI 集成': [
|
||||
{'key': 'ai.scene.summary', 'name': 'AI 场景摘要'},
|
||||
{'key': 'ai.character.extraction', 'name': 'AI 角色提取'},
|
||||
{'key': 'ai.story.generation', 'name': 'AI 故事生成'},
|
||||
],
|
||||
'协作功能': [
|
||||
{'key': 'collaboration.viewer', 'name': '邀请查看者'},
|
||||
{'key': 'collaboration.editor', 'name': '邀请编辑者'},
|
||||
{'key': 'collaboration.team', 'name': '团队协作'},
|
||||
],
|
||||
'支持服务': [
|
||||
{'key': 'priority.support', 'name': '优先客服支持'},
|
||||
{'key': 'advanced.features', 'name': '高级功能'},
|
||||
],
|
||||
};
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getCardColor(context),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: WebTheme.getBorderColor(context).withOpacity(0.5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||
child: IntrinsicWidth(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header row
|
||||
_buildTableHeader(context, plans),
|
||||
// Feature groups
|
||||
...featureGroups.entries.map((group) =>
|
||||
_buildFeatureGroup(context, group.key, group.value, plans)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTableHeader(BuildContext context, List<SubscriptionPlan> plans) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: WebTheme.getBorderColor(context).withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Feature column header
|
||||
SizedBox(
|
||||
width: _featureColumnWidth,
|
||||
child: Text(
|
||||
'功能',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Plan headers
|
||||
...plans.map((plan) {
|
||||
final isRecommended = plan.recommended;
|
||||
return SizedBox(
|
||||
width: _planColumnWidth,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isRecommended
|
||||
? WebTheme.getTextColor(context).withOpacity(0.04)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isRecommended
|
||||
? Border.all(
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.25),
|
||||
width: 2,
|
||||
)
|
||||
: Border.all(
|
||||
color: Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
plan.planName,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (isRecommended) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getTextColor(context),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'推荐',
|
||||
style: TextStyle(
|
||||
color: WebTheme.getBackgroundColor(context),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_getPlanDescription(plan),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeatureGroup(BuildContext context, String groupName, List<Map<String, String>> features, List<SubscriptionPlan> plans) {
|
||||
return Column(
|
||||
children: [
|
||||
// Group header
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.02),
|
||||
child: Text(
|
||||
groupName,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Feature rows
|
||||
...features.map((feature) =>
|
||||
_buildFeatureRow(context, feature['name']!, feature['key']!, plans)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeatureRow(BuildContext context, String featureName, String featureKey, List<SubscriptionPlan> plans) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: WebTheme.getBorderColor(context).withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Feature name
|
||||
SizedBox(
|
||||
width: _featureColumnWidth,
|
||||
child: Text(
|
||||
featureName,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Plan values
|
||||
...plans.map((plan) {
|
||||
final isRecommended = plan.recommended;
|
||||
final featureValue = (plan.features ?? {})[featureKey];
|
||||
return SizedBox(
|
||||
width: _planColumnWidth,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isRecommended
|
||||
? WebTheme.getTextColor(context).withOpacity(0.06)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: isRecommended
|
||||
? Border.all(
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.15),
|
||||
width: 1,
|
||||
)
|
||||
: Border.all(
|
||||
color: Colors.transparent,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: _buildFeatureIcon(context, featureValue),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeatureIcon(BuildContext context, dynamic value) {
|
||||
if (value == null || (value is num && value == 0)) {
|
||||
return Text(
|
||||
'—',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
);
|
||||
} else if (value is bool) {
|
||||
return Icon(
|
||||
value ? Icons.check : Icons.close,
|
||||
color: value
|
||||
? const Color(0xFF10B981)
|
||||
: WebTheme.getSecondaryTextColor(context),
|
||||
size: 18,
|
||||
);
|
||||
} else if (value is num) {
|
||||
if (value < 0) {
|
||||
return Text(
|
||||
'无限制',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF10B981),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
value.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return Text(
|
||||
value.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _getPlanDescription(SubscriptionPlan plan) {
|
||||
if (plan.description != null && plan.description!.isNotEmpty) {
|
||||
return plan.description!;
|
||||
}
|
||||
// Default descriptions based on plan name
|
||||
switch (plan.planName.toLowerCase()) {
|
||||
case 'basic':
|
||||
case '基础版':
|
||||
return '适合初学者,满足基本创作需求';
|
||||
case 'pro':
|
||||
case '专业版':
|
||||
return '适合专业作者,提供高级功能';
|
||||
case 'premium':
|
||||
case '高级版':
|
||||
return '适合团队协作,功能最全面';
|
||||
default:
|
||||
return '为创作者量身定制的方案';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Widget _ultraSimpleToggle(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Monthly
|
||||
GestureDetector(
|
||||
onTap: () => setState(() { _selectedCycle = BillingCycle.monthly; }),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
child: Text(
|
||||
'月付',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: _selectedCycle == BillingCycle.monthly ? FontWeight.w700 : FontWeight.w400,
|
||||
color: _selectedCycle == BillingCycle.monthly
|
||||
? WebTheme.getTextColor(context)
|
||||
: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 20,
|
||||
color: WebTheme.getBorderColor(context),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
// Yearly
|
||||
GestureDetector(
|
||||
onTap: () => setState(() { _selectedCycle = BillingCycle.yearly; }),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'年付',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: _selectedCycle == BillingCycle.yearly ? FontWeight.w700 : FontWeight.w400,
|
||||
color: _selectedCycle == BillingCycle.yearly
|
||||
? WebTheme.getTextColor(context)
|
||||
: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
if (_selectedCycle == BillingCycle.yearly) ...[
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF10B981),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'省17%',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<SubscriptionPlan> _filteredPlans() {
|
||||
final list = _plans.where((p) => p.billingCycle == _selectedCycle).toList();
|
||||
list.sort((a, b) {
|
||||
if (a.recommended != b.recommended) return a.recommended ? -1 : 1;
|
||||
return b.priority.compareTo(a.priority);
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
Widget _skeletonContent(BuildContext context) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _errorView(BuildContext context, String message) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: WebTheme.getSecondaryTextColor(context)),
|
||||
const SizedBox(height: 8),
|
||||
Text(message, style: TextStyle(color: WebTheme.getTextColor(context))),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton(onPressed: _loadData, child: const Text('重试'))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _buyPlan(SubscriptionPlan p, PayChannel channel) async {
|
||||
try {
|
||||
final order = await _payRepo.createPayment(planId: p.id!, channel: channel);
|
||||
if (order.paymentUrl.isNotEmpty) {
|
||||
final uri = Uri.parse(order.paymentUrl);
|
||||
if (await canLaunchUrl(uri)) await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('创建订单失败: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user