已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
132
apps/user_app/lib/ui/features/settings/login_password_page.dart
Normal file
132
apps/user_app/lib/ui/features/settings/login_password_page.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
// 功能描述:当前密码验证、新密码二次确认与服务端成功后重新登录;版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../app/dependencies.dart';
|
||||
import '../../../data/repositories/client_repository.dart';
|
||||
import '../../../data/services/api_client.dart';
|
||||
|
||||
class LoginPasswordPage extends StatefulWidget {
|
||||
const LoginPasswordPage({required this.repository, required this.session, super.key});
|
||||
final ClientRepository repository;
|
||||
final UserSession session;
|
||||
@override
|
||||
State<LoginPasswordPage> createState() => _LoginPasswordPageState();
|
||||
}
|
||||
|
||||
/// 密码仅保留在当前表单内存;失败不清空草稿,成功后清空并退出本机登录。
|
||||
class _LoginPasswordPageState extends State<LoginPasswordPage> {
|
||||
final _form = GlobalKey<FormState>();
|
||||
final _current = TextEditingController(),
|
||||
_next = TextEditingController(),
|
||||
_repeat = TextEditingController();
|
||||
final _visible = [false, false, false];
|
||||
bool _busy = false;
|
||||
String? _error;
|
||||
@override
|
||||
void dispose() {
|
||||
_current.dispose();
|
||||
_next.dispose();
|
||||
_repeat.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (_busy || !(_form.currentState?.validate() ?? false)) return;
|
||||
setState(() {
|
||||
_busy = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
await widget.repository.changeLoginPassword(_current.text, _next.text);
|
||||
if (!mounted) return;
|
||||
_current.clear();
|
||||
_next.clear();
|
||||
_repeat.clear();
|
||||
await widget.session.logout();
|
||||
} on SessionExpiredException {
|
||||
return;
|
||||
} catch (error) {
|
||||
if (mounted) setState(() => _error = error is ApiException ? error.message : '修改失败,请重试');
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _input(
|
||||
String label,
|
||||
TextEditingController controller,
|
||||
int index,
|
||||
String? Function(String?) validator,
|
||||
) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: TextFormField(
|
||||
key: ValueKey('password-$index'),
|
||||
controller: controller,
|
||||
enabled: !_busy,
|
||||
obscureText: !_visible[index],
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
validator: validator,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: _visible[index] ? '隐藏$label' : '显示$label',
|
||||
onPressed: _busy ? null : () => setState(() => _visible[index] = !_visible[index]),
|
||||
icon: Icon(_visible[index] ? Icons.visibility_off_outlined : Icons.visibility_outlined),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PopScope(
|
||||
canPop: !_busy,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('登录密码'),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
tooltip: '返回',
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => context.canPop() ? context.pop() : context.go('/settings'),
|
||||
),
|
||||
),
|
||||
body: Form(
|
||||
key: _form,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_input('当前密码', _current, 0, (value) => (value ?? '').isEmpty ? '请输入当前密码' : null),
|
||||
_input('新密码', _next, 1, (value) {
|
||||
final password = value ?? '';
|
||||
if (password.runes.length < 6) return '新密码至少6个字符';
|
||||
if (utf8.encode(password).length > 72) return '新密码过长,请缩短后重试';
|
||||
if (password == _current.text) return '新密码不能与当前密码相同';
|
||||
return null;
|
||||
}),
|
||||
_input('确认新密码', _repeat, 2, (value) => value != _next.text ? '两次新密码不一致' : null),
|
||||
if (_error != null)
|
||||
Semantics(
|
||||
liveRegion: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
_error!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: _busy ? null : _save,
|
||||
child: Text(_busy ? '正在修改…' : '确认修改并重新登录'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// 功能描述:支付密码首次设置、旧密码修改及验证码找回;版本:1.0.0。
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../data/repositories/client_repository.dart';
|
||||
import '../../../data/services/api_client.dart';
|
||||
import '../../../domain/models/client_models.dart';
|
||||
import '../../core/async_content.dart';
|
||||
|
||||
/// 只操作当前用户的钱包密码,余额和提现数据不允许由页面写入。
|
||||
class PaymentPasswordPage extends StatefulWidget {
|
||||
const PaymentPasswordPage({required this.repository, super.key});
|
||||
final ClientRepository repository;
|
||||
@override
|
||||
State<PaymentPasswordPage> createState() => _PaymentPasswordPageState();
|
||||
}
|
||||
|
||||
class _PaymentPasswordPageState extends State<PaymentPasswordPage> {
|
||||
final _form = GlobalKey<FormState>();
|
||||
final _old = TextEditingController(),
|
||||
_next = TextEditingController(),
|
||||
_repeat = TextEditingController(),
|
||||
_code = TextEditingController();
|
||||
bool _busy = false, _useCode = false;
|
||||
bool? _hasPassword;
|
||||
WalletSummary? _loaded;
|
||||
PaymentCodeRequest? _request;
|
||||
String? _error, _notice;
|
||||
Timer? _timer;
|
||||
DateTime? _resendAt;
|
||||
int get _remaining =>
|
||||
_resendAt == null ? 0 : _resendAt!.difference(DateTime.now()).inSeconds.clamp(0, 3600);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_old.dispose();
|
||||
_next.dispose();
|
||||
_repeat.dispose();
|
||||
_code.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 服务端状态缺失时进入可重试错误态,不能自行选择首次设置流程。
|
||||
Future<WalletSummary> _load() async {
|
||||
if (_busy && _loaded != null) return _loaded!;
|
||||
final wallet = await widget.repository.wallet();
|
||||
if (!mounted) return wallet;
|
||||
if (_busy && _loaded != null) return _loaded!;
|
||||
if (wallet.paymentPasswordSet == null) throw const ApiException(1714, '支付密码状态读取失败');
|
||||
_loaded = wallet;
|
||||
if (_hasPassword != wallet.paymentPasswordSet) {
|
||||
_hasPassword = wallet.paymentPasswordSet;
|
||||
_useCode = !_hasPassword!;
|
||||
_clearCode();
|
||||
_old.clear();
|
||||
}
|
||||
return wallet;
|
||||
}
|
||||
|
||||
void _clearCode() {
|
||||
_request = null;
|
||||
_code.clear();
|
||||
}
|
||||
|
||||
/// 不把Mock的验证码记录误报为短信已发送。
|
||||
Future<void> _sendCode() async {
|
||||
if (_busy || _remaining > 0) return;
|
||||
setState(() {
|
||||
_busy = true;
|
||||
_error = null;
|
||||
_notice = null;
|
||||
});
|
||||
try {
|
||||
final request = await widget.repository.requestPaymentPasswordCode(
|
||||
resetting: _hasPassword == true,
|
||||
);
|
||||
if (!mounted) return;
|
||||
_request = request;
|
||||
_code.clear();
|
||||
_resendAt = DateTime.now().add(Duration(seconds: request.retryAfter + 1));
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (!mounted || _remaining == 0) timer.cancel();
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
setState(
|
||||
() =>
|
||||
_notice = request.delivered ? '验证码已发送至 ${request.maskedPhone}' : '验证码请求已建立,但当前环境未发送短信',
|
||||
);
|
||||
} catch (error) {
|
||||
if (mounted) setState(() => _error = error is ApiException ? error.message : '验证码请求失败,请重试');
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (_busy || !(_form.currentState?.validate() ?? false)) return;
|
||||
if (_useCode && _request == null) {
|
||||
setState(() => _error = '请先获取验证码');
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_busy = true;
|
||||
_error = null;
|
||||
_notice = null;
|
||||
});
|
||||
try {
|
||||
final cleared = await widget.repository.setPaymentPassword(
|
||||
newPassword: _next.text,
|
||||
currentPassword: _useCode ? null : _old.text,
|
||||
requestIdentity: _useCode ? _request!.identity : null,
|
||||
code: _useCode ? _code.text : null,
|
||||
);
|
||||
if (!mounted) return;
|
||||
_old.clear();
|
||||
_next.clear();
|
||||
_repeat.clear();
|
||||
_clearCode();
|
||||
_timer?.cancel();
|
||||
_resendAt = null;
|
||||
setState(() {
|
||||
_hasPassword = true;
|
||||
_useCode = false;
|
||||
_notice = cleared ? '支付密码已更新' : '支付密码已更新,安全锁定可能仍在生效,请稍后再试';
|
||||
});
|
||||
} on SessionExpiredException {
|
||||
return;
|
||||
} catch (error) {
|
||||
if (mounted) setState(() => _error = error is ApiException ? error.message : '修改结果未确认,请稍后重试');
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _pin(
|
||||
String label,
|
||||
TextEditingController controller,
|
||||
String key, {
|
||||
bool confirmation = false,
|
||||
}) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: TextFormField(
|
||||
key: ValueKey(key),
|
||||
controller: controller,
|
||||
enabled: !_busy,
|
||||
obscureText: true,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(6),
|
||||
],
|
||||
decoration: InputDecoration(labelText: label),
|
||||
validator: (value) {
|
||||
if (!RegExp(r'^\d{6}$').hasMatch(value ?? '')) return '请输入6位数字支付密码';
|
||||
if (confirmation && value != _next.text) return '两次支付密码不一致';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PopScope(
|
||||
canPop: !_busy,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('支付密码'),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
tooltip: '返回',
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => context.canPop() ? context.pop() : context.go('/settings'),
|
||||
),
|
||||
),
|
||||
body: AsyncContent<WalletSummary>(
|
||||
load: _load,
|
||||
builder: (context, wallet) => Form(
|
||||
key: _form,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Text(
|
||||
_hasPassword == true ? (_useCode ? '找回支付密码' : '修改支付密码') : '设置支付密码',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (_useCode) ...[
|
||||
TextFormField(
|
||||
key: const ValueKey('payment-code'),
|
||||
controller: _code,
|
||||
enabled: !_busy,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(8),
|
||||
],
|
||||
decoration: const InputDecoration(labelText: '手机验证码'),
|
||||
validator: (v) => (v ?? '').isEmpty ? '请输入手机验证码' : null,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: _busy || _remaining > 0 ? null : _sendCode,
|
||||
child: Text(_remaining > 0 ? '$_remaining秒后重新获取' : '获取验证码'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
] else
|
||||
_pin('当前支付密码', _old, 'payment-old'),
|
||||
_pin('新支付密码(6位数字)', _next, 'payment-next'),
|
||||
_pin('确认支付密码', _repeat, 'payment-repeat', confirmation: true),
|
||||
if (_error != null || _notice != null)
|
||||
Semantics(
|
||||
liveRegion: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
_error ?? _notice!,
|
||||
style: TextStyle(
|
||||
color: _error == null ? null : Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
FilledButton(onPressed: _busy ? null : _save, child: Text(_busy ? '正在处理…' : '确认')),
|
||||
if (_hasPassword == true)
|
||||
TextButton(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => setState(() {
|
||||
_useCode = !_useCode;
|
||||
_clearCode();
|
||||
_old.clear();
|
||||
_error = null;
|
||||
_notice = null;
|
||||
}),
|
||||
child: Text(_useCode ? '使用当前支付密码修改' : '忘记支付密码?'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
277
apps/user_app/lib/ui/features/settings/settings_page.dart
Normal file
277
apps/user_app/lib/ui/features/settings/settings_page.dart
Normal file
@@ -0,0 +1,277 @@
|
||||
// 功能描述:按图31组织账户设置、协议、版本、图片缓存和退出操作;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../app/dependencies.dart';
|
||||
import '../../../data/repositories/client_repository.dart';
|
||||
import '../../../data/services/app_settings_service.dart';
|
||||
import '../../core/feature_entry.dart';
|
||||
import '../auth/login_support.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({required this.repository, required this.session, this.service, super.key});
|
||||
final ClientRepository repository;
|
||||
final UserSession session;
|
||||
final AppSettingsService? service;
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
/// 附属资料读取失败不阻塞清缓存或退出,未接入能力保持真实的未知状态。
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
late final AppSettingsService _service = widget.service ?? AppSettingsService();
|
||||
String _phone = '读取中', _version = '读取中';
|
||||
String? _notice;
|
||||
bool _busy = false, _phoneFailed = false;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPhone();
|
||||
_loadVersion();
|
||||
}
|
||||
|
||||
Future<void> _loadPhone() async {
|
||||
try {
|
||||
final profile = await widget.repository.profile();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_phone = profile.phone.replaceFirstMapped(
|
||||
RegExp(r'^(\d{3})\d{4}(\d{4})$'),
|
||||
(m) => '${m[1]}****${m[2]}',
|
||||
);
|
||||
_phoneFailed = false;
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_phone = '读取失败,点击重试';
|
||||
_phoneFailed = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadVersion() async {
|
||||
try {
|
||||
final version = await _service.version();
|
||||
if (mounted) setState(() => _version = version);
|
||||
} catch (error) {
|
||||
// 仅诊断非敏感的构建元数据错误,不输出会话或账号资料。
|
||||
debugPrint('构建版本读取失败:$error');
|
||||
if (mounted) setState(() => _version = '读取失败,点击重试');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirm(bool logout) async {
|
||||
if (_busy) return;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(logout ? '退出登录?' : '清除图片缓存?'),
|
||||
content: Text(logout ? '退出后再次办理业务需要登录。' : '清除可重新加载的图片缓存。账户资料、报修草稿和已下载文件会保留。'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('取消')),
|
||||
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('确认')),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
if (logout) {
|
||||
await widget.session.logout();
|
||||
} else {
|
||||
_service.clearImageCache();
|
||||
if (mounted) setState(() => _notice = '图片缓存已清除');
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _notice = '操作失败,请重试');
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _row(IconData icon, String title, {String? value, VoidCallback? action, Color? color}) =>
|
||||
InkWell(
|
||||
onTap: _busy ? null : action ?? () => showUnavailableFeature(context, title),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 48),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// 窄屏或放大文字时上下排列,避免手机号和设置名称拆成单字。
|
||||
final stacked =
|
||||
constraints.maxWidth < 290 || MediaQuery.textScalerOf(context).scale(14) > 16;
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: color ?? const Color(0xFF0064FF), size: 24),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: TextStyle(fontSize: 14, color: color)),
|
||||
if (stacked && value != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF777F8D)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (value != null && !stacked) ...[
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
value,
|
||||
textAlign: TextAlign.end,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF777F8D)),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.chevron_right, color: Color(0xFF9CA3AF), size: 20),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _group(String title, List<Widget> rows) => Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.fromLTRB(14, 10, 14, 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: const Color(0xFFE5E7EB)),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF777F8D),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
for (var i = 0; i < rows.length; i++) ...[if (i > 0) const Divider(height: 1), rows[i]],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('设置'),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
tooltip: '返回',
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.canPop() ? context.pop() : context.go('/me'),
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(14, 0, 14, 24),
|
||||
children: [
|
||||
_group('账号与安全', [
|
||||
_row(Icons.verified_user_outlined, '实名认证', value: '暂未开放'),
|
||||
_row(Icons.lock_outline, '登录密码', action: () => context.push('/settings/password')),
|
||||
_row(
|
||||
Icons.lock_outline,
|
||||
'支付密码',
|
||||
action: () => context.push('/settings/payment-password'),
|
||||
),
|
||||
_row(
|
||||
Icons.phone_android,
|
||||
'手机号',
|
||||
value: _phone,
|
||||
action: () => _phoneFailed ? _loadPhone() : showUnavailableFeature(context, '更换手机号'),
|
||||
),
|
||||
_row(Icons.desktop_windows_outlined, '设备登录管理', value: '暂未开放'),
|
||||
]),
|
||||
_group('消息通知', [
|
||||
// 尚无真实通知通道,不能用“不可关闭”暗示已经开启并正常送达。
|
||||
_row(Icons.notifications_none, '安全告警通知', value: '暂未开放', color: Colors.red),
|
||||
_row(Icons.sms_outlined, '订单通知', value: '暂未开放'),
|
||||
_row(Icons.mail_outline, '服务通知', value: '暂未开放'),
|
||||
_row(Icons.volume_up_outlined, '声音与振动', value: '暂未开放'),
|
||||
]),
|
||||
_group('设备与权限', [
|
||||
_row(Icons.bluetooth, '蓝牙权限', value: '暂未开放'),
|
||||
_row(Icons.location_on_outlined, '定位权限', value: '暂未开放'),
|
||||
_row(Icons.lock_outline, '远程控制二次确认', value: '暂未开放'),
|
||||
]),
|
||||
_group('通用', [
|
||||
_row(
|
||||
Icons.language,
|
||||
'语言',
|
||||
value: '简体中文',
|
||||
action: () => showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('语言'),
|
||||
content: const Text('简体中文'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('确定')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_row(Icons.shield_outlined, '隐私设置', value: '暂未开放'),
|
||||
_row(
|
||||
Icons.delete_outline,
|
||||
'清除缓存',
|
||||
value: '${(_service.imageCacheBytes / 1024 / 1024).toStringAsFixed(1)}MB 图片缓存',
|
||||
action: () => _confirm(false),
|
||||
),
|
||||
_row(Icons.info_outline, '版本', value: _version, action: _loadVersion),
|
||||
]),
|
||||
_group('帮助与协议', [
|
||||
_row(
|
||||
Icons.description_outlined,
|
||||
'用户协议',
|
||||
action: () => showPublishedAgreements(context, repository: widget.repository),
|
||||
),
|
||||
_row(
|
||||
Icons.privacy_tip_outlined,
|
||||
'隐私政策',
|
||||
action: () => showPublishedAgreements(context, repository: widget.repository),
|
||||
),
|
||||
_row(Icons.menu_book_outlined, '安全内容', action: () => context.push('/contents')),
|
||||
_row(Icons.person_off_outlined, '注销账号', value: '暂未开放', color: Colors.red),
|
||||
]),
|
||||
if (_notice != null)
|
||||
Semantics(
|
||||
liveRegion: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(_notice!),
|
||||
),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: _busy ? null : () => _confirm(true),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
side: const BorderSide(color: Colors.red),
|
||||
),
|
||||
child: const Text('退出登录'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'当前版本 $_version',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Color(0xFF777F8D), fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user