Files
observer/flutter_app/lib/payment/paywall_screen.dart
T
admin a0b115d954 训练体系整合与标注单阶段化
- 标注:AI 预标注直写 labels_json(去候选确认两阶段);重叠去重(minIoU);全量标注按钮
- 训练:脚本迁移入 server/training/(Go 化 prepare_yolo/analyze_rfdetr,保留 train_server.py);tflite 产物自检并入训练流程(check_tflite)
- 数据目录/权重不进 git;.gitignore 迁移至仓库根
2026-08-26 18:22:56 +08:00

241 lines
7.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'models.dart';
import 'paywall_view_model.dart';
/// 付费墙:拉取套餐价格 → 选套餐 → 微信/支付宝支付 → 解锁进入相机
class PaywallScreen extends StatefulWidget {
const PaywallScreen({super.key});
@override
State<PaywallScreen> createState() => _PaywallScreenState();
}
class _PaywallScreenState extends State<PaywallScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<PaywallViewModel>().loadPlans();
});
}
@override
Widget build(BuildContext context) {
final vm = context.watch<PaywallViewModel>();
final loading = vm.state.state == PayState.loading;
return Scaffold(
appBar: AppBar(title: const Text('视野 · 会员'), centerTitle: true),
body: SafeArea(
child: Column(
children: [
const Padding(
padding: EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Text(
'开通会员解锁完整功能,按自然日计费,到期自动失效',
style: TextStyle(color: Colors.grey, fontSize: 13),
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 24),
children: [
_PlansSection(vm: vm, loading: loading),
const SizedBox(height: 24),
_PayButton(
label: '微信支付',
icon: Icons.wechat,
color: const Color(0xFF07C160),
enabled: !loading && vm.state.selectedPlan != null,
onTap: loading
? null
: () async {
final license = await vm.pay(PayChannel.wechat);
if (license != null && context.mounted) {
_onPaid(context);
}
},
),
const SizedBox(height: 12),
_PayButton(
label: '支付宝支付',
icon: Icons.account_balance_wallet,
color: const Color(0xFF1677FF),
enabled: !loading && vm.state.selectedPlan != null,
onTap: loading
? null
: () async {
final license = await vm.pay(PayChannel.alipay);
if (license != null && context.mounted) {
_onPaid(context);
}
},
),
const SizedBox(height: 12),
_PayButton(
label: '联系管理员',
icon: Icons.headset_mic,
color: Colors.grey.shade600,
enabled: true,
onTap: () async {
await Clipboard.setData(
const ClipboardData(text: 'wenwu901'),
);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已复制管理员微信号,请使用微信添加联系人'),
),
);
},
),
if (vm.state.state == PayState.failed)
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
vm.state.message ?? '支付失败',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
),
const SizedBox(height: 24),
],
),
),
],
),
),
);
}
/// 支付成功:返回主界面(主界面刷新展示新到期时间)
void _onPaid(BuildContext context) {
Navigator.of(context).pop();
}
}
/// 套餐区三态:加载中 / 失败可重试 / 套餐卡片列表
class _PlansSection extends StatelessWidget {
final PaywallViewModel vm;
final bool loading;
const _PlansSection({required this.vm, required this.loading});
@override
Widget build(BuildContext context) {
if (vm.plansLoading && vm.plans.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 48),
child: Center(child: CircularProgressIndicator()),
);
}
if (vm.plansError != null && vm.plans.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Column(
children: [
Text(
vm.plansError!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: () => context.read<PaywallViewModel>().loadPlans(),
child: const Text('重试'),
),
],
),
);
}
return Column(
children: [
...vm.plans.map((p) => _PlanCard(
plan: p,
selected: vm.state.selectedPlan?.id == p.id,
enabled: !loading,
onTap: () => vm.selectPlan(p),
)),
],
);
}
}
class _PlanCard extends StatelessWidget {
final Plan plan;
final bool selected;
final bool enabled;
final VoidCallback onTap;
const _PlanCard({
required this.plan,
required this.selected,
required this.enabled,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: selected ? 3 : 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: selected ? Colors.orange : Colors.grey.shade300,
width: selected ? 2 : 1,
),
),
child: ListTile(
onTap: enabled ? onTap : null,
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
title: Text(
plan.label,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 17),
),
subtitle: Text('${plan.days} 天 · 自然日'),
trailing: Text(
${plan.priceYuan}',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
);
}
}
class _PayButton extends StatelessWidget {
final String label;
final IconData icon;
final Color color;
final bool enabled;
final VoidCallback? onTap;
const _PayButton({
required this.label,
required this.icon,
required this.color,
required this.enabled,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 48,
child: FilledButton.icon(
style: FilledButton.styleFrom(
backgroundColor: color,
disabledBackgroundColor: Colors.grey.shade300,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
),
onPressed: enabled ? onTap : null,
icon: Icon(icon, size: 20),
label: Text(label, style: const TextStyle(fontSize: 16)),
),
);
}
}