This commit is contained in:
2026-08-24 13:45:14 +08:00
parent 75d3e93e6e
commit 7e9b32b379
12 changed files with 244 additions and 11 deletions
+9
View File
@@ -6,6 +6,7 @@ import 'auth/session_store.dart';
import 'camera/camera_screen.dart';
import 'container.dart';
import 'home/home_screen.dart';
import 'legal/terms_screen.dart';
import 'payment/paywall_screen.dart';
class ObserverApp extends StatelessWidget {
@@ -31,6 +32,7 @@ class ObserverApp extends StatelessWidget {
initialRoute: '/gate',
routes: {
'/gate': (_) => const StartupGate(),
'/terms': (_) => const TermsScreen(),
'/login': (_) => const AuthScreen(),
'/home': (_) => const HomeScreen(),
'/paywall': (_) => const PaywallScreen(),
@@ -57,6 +59,13 @@ class _StartupGateState extends State<StartupGate> {
}
Future<void> _check() async {
// 首次启动强制协议;同意后不再展示(拒绝则退出应用)
final accepted = await TermsScreen.readAccepted();
if (!mounted) return;
if (accepted != '1') {
Navigator.of(context).pushReplacementNamed('/terms');
return;
}
final session = context.read<SessionStore>();
final token = await session.readToken();
if (!mounted) return;
+3 -4
View File
@@ -1,10 +1,9 @@
/// 全局配置占位:接入真实支付前需替换以下值。
class AppConfig {
/// 后端服务器地址(订单创建/授权查询/套餐价格)
/// 默认 Android 模拟器 10.0.2.2iOS 模拟器构建时传
/// --dart-define=API_BASE_URL=http://127.0.0.1:8080
static const String apiBaseUrl =
String.fromEnvironment('API_BASE_URL', defaultValue: 'http://10.0.2.2:8080');
/// 默认指向线上域名;本地联调构建时用 --dart-define=API_BASE_URL=http://127.0.0.1:8080 覆盖
static const String apiBaseUrl = String.fromEnvironment('API_BASE_URL',
defaultValue: 'http://observer.redpowerfuture.com');
/// 微信开放平台 AppID(需在微信开放平台注册包名+签名)
static const String wechatAppId = 'wx0000000000000000';
+126
View File
@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show SystemNavigator;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:provider/provider.dart';
import '../auth/session_store.dart';
/// 使用协议页:首次启动强制展示,同意后记录(secure storage),拒绝则退出应用。
/// 用途声明:仅限野生动物观察,禁止近距离打扰与非法使用。
class TermsScreen extends StatefulWidget {
const TermsScreen({super.key});
static const _storage = FlutterSecureStorage();
static const acceptedKey = 'terms_accepted';
static Future<String?> readAccepted() => _storage.read(key: acceptedKey);
@override
State<TermsScreen> createState() => _TermsScreenState();
}
class _TermsScreenState extends State<TermsScreen> {
bool _saving = false;
static const String _content = '''
《视野》使用协议
感谢您使用「视野」动物观察应用。使用本应用前,请仔细阅读并同意以下条款:
一、用途限制
本应用仅用于野生动物观察、记录与科普学习,严禁用于任何非法用途。
二、禁止行为
1. 禁止近距离打扰、追逐、驱赶、恐吓野生动物;
2. 禁止投喂、诱捕、猎捕、伤害野生动物;
3. 禁止利用本应用从事盗猎、野生动物交易或其他违法犯罪活动;
4. 禁止进入自然保护区禁区、私人领地等未经许可的区域进行观察。
三、法律合规
您须遵守《中华人民共和国野生动物保护法》及相关法律法规。
因违法违规使用产生的一切后果由使用者自行承担。
四、安全提示
野外观察存在风险,请结伴而行、注意自身安全。因使用本应用产生的安全事故,本应用不承担责任。
五、识别结果说明
本应用的动物识别结果基于人工智能模型,可能存在误差或漏检,识别结果仅供参考,不构成科学鉴定或法律依据。
六、其他
本协议内容可能适时更新,更新后您继续使用即视为接受。''';
Future<void> _accept() async {
if (_saving) return;
setState(() => _saving = true);
await TermsScreen._storage.write(key: TermsScreen.acceptedKey, value: '1');
if (!mounted) return;
final token = await context.read<SessionStore>().readToken();
if (!mounted) return;
Navigator.of(context)
.pushReplacementNamed(token == null ? '/login' : '/home');
}
Future<void> _decline() async {
if (_saving) return;
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('需要同意协议'),
content: const Text('不同意《使用协议》将无法使用本应用。'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('再看看'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('退出'),
),
],
),
);
if (ok == true && mounted) await SystemNavigator.pop();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('使用协议')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Text(
_content,
style: const TextStyle(fontSize: 15, height: 1.7),
),
),
bottomNavigationBar: SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 16),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _saving ? null : _decline,
child: const Text('不同意'),
),
),
const SizedBox(width: 16),
Expanded(
child: FilledButton(
onPressed: _saving ? null : _accept,
child: _saving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('同意并继续'),
),
),
],
),
),
),
);
}
}