feat: refine AI navigation and accessibility recovery
This commit is contained in:
@@ -743,7 +743,8 @@ class _ChatPageState extends State<ChatPage> {
|
||||
}
|
||||
|
||||
Widget _inputBar(BuildContext context) {
|
||||
final sendEnabled = _ctrl.text.trim().isNotEmpty && !_sending;
|
||||
final hasText = _ctrl.text.trim().isNotEmpty;
|
||||
final sendEnabled = hasText && !_sending;
|
||||
final accent = widget.aiMode ? AppTheme.ai : AppTheme.primary;
|
||||
return Container(
|
||||
color: context.jz.card,
|
||||
@@ -866,23 +867,59 @@ class _ChatPageState extends State<ChatPage> {
|
||||
),
|
||||
SizedBox(width: 6),
|
||||
],
|
||||
SizedBox(
|
||||
width: 50,
|
||||
height: 38,
|
||||
child: FilledButton(
|
||||
onPressed: sendEnabled ? _send : null,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: accent,
|
||||
disabledBackgroundColor: context.jz.line,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: ScaleTransition(
|
||||
scale: Tween<double>(begin: 0.9, end: 1).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'发送',
|
||||
style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700),
|
||||
),
|
||||
child: hasText
|
||||
? SizedBox(
|
||||
key: const ValueKey('send-message'),
|
||||
width: 50,
|
||||
height: 38,
|
||||
child: FilledButton(
|
||||
onPressed: sendEnabled ? _send : null,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: accent,
|
||||
disabledBackgroundColor: context.jz.line,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'发送',
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: PublicConfigApi.imageEnabled
|
||||
? SizedBox(
|
||||
key: const ValueKey('add-attachment'),
|
||||
width: 42,
|
||||
height: 42,
|
||||
child: IconButton(
|
||||
tooltip: '添加附件',
|
||||
onPressed: _sending ? null : _openAttachment,
|
||||
icon: AppIcons.icon(
|
||||
AppIcons.plus,
|
||||
size: 22,
|
||||
color: _sending ? context.jz.text3 : context.jz.text2,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('no-composer-action')),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -948,24 +985,6 @@ class _ChatPageState extends State<ChatPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (PublicConfigApi.imageEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ActionChip(
|
||||
avatar: AppIcons.icon(
|
||||
AppIcons.camera,
|
||||
size: 16,
|
||||
color: AppTheme.ai,
|
||||
),
|
||||
label: Text(
|
||||
'附件',
|
||||
style: TextStyle(fontSize: 11, color: context.jz.text2),
|
||||
),
|
||||
backgroundColor: context.jz.background,
|
||||
side: BorderSide.none,
|
||||
onPressed: _openAttachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1280,6 +1299,7 @@ class _ChatTabPageState extends State<ChatTabPage> {
|
||||
final status = switch (accessState) {
|
||||
AiAccessState.guest => '登录后可用',
|
||||
AiAccessState.reauthenticate => '需要重新登录',
|
||||
AiAccessState.aiDisabled => 'AI 功能已关闭',
|
||||
AiAccessState.cloudDisabled => '云连接已关闭',
|
||||
null => '在线',
|
||||
};
|
||||
|
||||
@@ -25,14 +25,6 @@ class _MainShellState extends State<MainShell> {
|
||||
return AnimatedBuilder(
|
||||
animation: SessionStore.instance,
|
||||
builder: (context, _) {
|
||||
final session = SessionStore.instance;
|
||||
final aiEnabled = session.aiEnabled;
|
||||
final showAiEntry = session.isGuest || aiEnabled;
|
||||
if (!showAiEntry && widget.shell.currentIndex == 2) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) widget.shell.goBranch(0);
|
||||
});
|
||||
}
|
||||
return Scaffold(
|
||||
body: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -58,12 +50,11 @@ class _MainShellState extends State<MainShell> {
|
||||
_tab('明细', AppIcons.home, 0),
|
||||
_tab('统计', AppIcons.chart, 1),
|
||||
SizedBox(width: 72, child: _fab()),
|
||||
if (showAiEntry)
|
||||
ValueListenableBuilder<CompanionDisplay>(
|
||||
valueListenable: PublicConfigApi.companionNotifier,
|
||||
builder: (_, companion, __) =>
|
||||
_tab(companion.name, AppIcons.chat, 2),
|
||||
),
|
||||
ValueListenableBuilder<CompanionDisplay>(
|
||||
valueListenable: PublicConfigApi.companionNotifier,
|
||||
builder: (_, companion, __) =>
|
||||
_tab(companion.name, AppIcons.chat, 2),
|
||||
),
|
||||
_tab('我的', AppIcons.user, 3),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -264,13 +264,6 @@ class _MePageState extends State<MePage> {
|
||||
'预算管理',
|
||||
onTap: () => context.push('/budget'),
|
||||
),
|
||||
_row(
|
||||
AppIcons.avatarAsset(PublicConfigApi.companionAvatarKey),
|
||||
context.jz.aiBackground,
|
||||
AppTheme.ai,
|
||||
session.aiEnabled ? 'AI 报告' : '报告',
|
||||
onTap: () => context.push('/report'),
|
||||
),
|
||||
_row(
|
||||
AppIcons.tag,
|
||||
context.jz.primaryBackground,
|
||||
|
||||
@@ -32,7 +32,7 @@ class _ScreenshotSettingsPageState extends State<ScreenshotSettingsPage>
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_check();
|
||||
_check(waitForConnection: true);
|
||||
_diagnosticRefreshTimer = Timer.periodic(
|
||||
const Duration(seconds: 2),
|
||||
(_) => _refreshRunningDiagnostic(),
|
||||
@@ -67,9 +67,11 @@ class _ScreenshotSettingsPageState extends State<ScreenshotSettingsPage>
|
||||
_check().whenComplete(() => _diagnosticRefreshInFlight = false);
|
||||
}
|
||||
|
||||
Future<void> _check() async {
|
||||
Future<void> _check({bool waitForConnection = false}) async {
|
||||
try {
|
||||
var status = await ScreenshotChannel.recognitionStatus();
|
||||
var status = waitForConnection
|
||||
? await ScreenshotChannel.waitForAccessibilityConnection()
|
||||
: await ScreenshotChannel.recognitionStatus();
|
||||
final invalid = <String>[
|
||||
if (status.accessibilityEvents &&
|
||||
(!status.accessibilityAuthorized ||
|
||||
@@ -180,7 +182,7 @@ class _ScreenshotSettingsPageState extends State<ScreenshotSettingsPage>
|
||||
|
||||
Future<void> _resumeAuthorization() async {
|
||||
if (_authorizing) return;
|
||||
await _check();
|
||||
await _check(waitForConnection: true);
|
||||
final key = _pendingAuthorizationKey;
|
||||
final status = _status;
|
||||
if (key == null || status == null) return;
|
||||
@@ -431,7 +433,13 @@ class _ScreenshotSettingsPageState extends State<ScreenshotSettingsPage>
|
||||
'仅在微信和支付宝疑似支付流程中读取可见文字,并按需在内存中进行本地截图 OCR;不保存图片、完整控件树,也不监听按键。',
|
||||
authorized: status!.accessibilityAuthorized,
|
||||
connected: status.accessibilityConnected,
|
||||
statusLabel: status.accessibilityConnectionLabel,
|
||||
onOpenSettings: ScreenshotChannel.openAccessibilitySettings,
|
||||
onRetry:
|
||||
status.accessibilityAuthorized &&
|
||||
!status.accessibilityConnected
|
||||
? () => _check(waitForConnection: true)
|
||||
: null,
|
||||
child: Column(
|
||||
children: [
|
||||
JzSwitchTile(
|
||||
@@ -888,7 +896,7 @@ class _BackgroundKeepAliveCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'无障碍和通知监听由 Android 独立轻量进程运行。请允许记之后台活动,'
|
||||
'无障碍和通知监听由 Android 系统持续绑定。请允许记之后台活动,'
|
||||
'${isVivo ? '并在 OriginOS 中开启自启动,' : ''}'
|
||||
'否则系统清理进程后可能暂时收不到支付事件。',
|
||||
style: TextStyle(
|
||||
@@ -932,6 +940,7 @@ class _RecognitionCard extends StatelessWidget {
|
||||
final String? statusLabel;
|
||||
final Widget child;
|
||||
final VoidCallback? onOpenSettings;
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
const _RecognitionCard({
|
||||
required this.icon,
|
||||
@@ -942,6 +951,7 @@ class _RecognitionCard extends StatelessWidget {
|
||||
required this.child,
|
||||
this.statusLabel,
|
||||
this.onOpenSettings,
|
||||
this.onRetry,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -1012,42 +1022,25 @@ class _RecognitionCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
child,
|
||||
if (onOpenSettings != null) ...[
|
||||
if (onOpenSettings != null || onRetry != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: '打开系统权限设置',
|
||||
child: InkWell(
|
||||
onTap: onOpenSettings,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.settings_outlined,
|
||||
size: 17,
|
||||
color: AppTheme.primary,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'系统权限设置',
|
||||
style: TextStyle(
|
||||
color: AppTheme.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
if (onRetry != null)
|
||||
TextButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh_rounded, size: 18),
|
||||
label: const Text('重新检测'),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onOpenSettings != null)
|
||||
TextButton.icon(
|
||||
onPressed: onOpenSettings,
|
||||
icon: const Icon(Icons.settings_outlined, size: 18),
|
||||
label: const Text('前往系统设置'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -16,7 +16,10 @@ import 'package:miaoji_zhang/shared/services/session_store.dart';
|
||||
enum _ReportKind { weekly, monthly, yearly }
|
||||
|
||||
class ReportPage extends StatefulWidget {
|
||||
const ReportPage({super.key});
|
||||
final String? initialPeriod;
|
||||
final DateTime? initialAnchor;
|
||||
|
||||
const ReportPage({super.key, this.initialPeriod, this.initialAnchor});
|
||||
|
||||
@override
|
||||
State<ReportPage> createState() => _ReportPageState();
|
||||
@@ -33,6 +36,12 @@ class _ReportPageState extends State<ReportPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_anchor = widget.initialAnchor ?? ShanghaiTime.now;
|
||||
_kind = switch (widget.initialPeriod) {
|
||||
'week' => _ReportKind.weekly,
|
||||
'year' => _ReportKind.yearly,
|
||||
_ => _ReportKind.monthly,
|
||||
};
|
||||
PublicConfigApi.companionNotifier.addListener(_refreshCompanion);
|
||||
CurrentLedgerStore.instance.addListener(_load);
|
||||
unawaited(_load());
|
||||
|
||||
@@ -3,9 +3,12 @@ import 'dart:async';
|
||||
import 'package:miaoji_zhang/shared/services/shanghai_time.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:miaoji_zhang/shared/api/api_client.dart';
|
||||
import 'package:miaoji_zhang/shared/api/business_api.dart';
|
||||
import 'package:miaoji_zhang/shared/api/config_api.dart';
|
||||
import 'package:miaoji_zhang/shared/services/current_ledger_store.dart';
|
||||
import 'package:miaoji_zhang/shared/services/session_store.dart';
|
||||
import 'package:miaoji_zhang/shared/theme/app_theme.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/app_controls.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/async_error_view.dart';
|
||||
@@ -161,8 +164,7 @@ class _StatsPageState extends State<StatsPage> {
|
||||
else ...[
|
||||
_categoryCard(stats),
|
||||
SizedBox(height: 10),
|
||||
if (stats.analysis case final analysis?)
|
||||
_analysisCard(analysis),
|
||||
_reportCard(stats.analysis),
|
||||
SizedBox(height: 10),
|
||||
_trendCard(stats),
|
||||
if (stats.byCategory.isNotEmpty) ...[
|
||||
@@ -318,41 +320,103 @@ class _StatsPageState extends State<StatsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _analysisCard(String text) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.aiBackground,
|
||||
Widget _reportCard(String? analysis) {
|
||||
final aiEnabled = SessionStore.instance.aiEnabled;
|
||||
final accent = aiEnabled ? AppTheme.ai : AppTheme.primary;
|
||||
final background = aiEnabled
|
||||
? context.jz.aiBackground
|
||||
: context.jz.primaryBackground;
|
||||
final copy = analysis?.trim().isNotEmpty == true
|
||||
? analysis!.trim()
|
||||
: '查看本周期的收支变化、分类排行和消费高峰。';
|
||||
final route = Uri(
|
||||
path: '/report',
|
||||
queryParameters: {'period': _period, 'date': _anchor.toIso8601String()},
|
||||
).toString();
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: aiEnabled ? '查看 AI 报告' : '查看周期报告',
|
||||
child: Material(
|
||||
color: background,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppTheme.ai.withValues(alpha: 0.12)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
child: InkWell(
|
||||
onTap: () => context.push(route),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.card,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: accent.withValues(alpha: 0.12)),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.auto_awesome_rounded,
|
||||
size: 16,
|
||||
color: AppTheme.ai,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: context.jz.card,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
aiEnabled
|
||||
? Icons.auto_awesome_rounded
|
||||
: Icons.assessment_outlined,
|
||||
size: 17,
|
||||
color: accent,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
aiEnabled
|
||||
? '${PublicConfigApi.companionName}的 AI 报告'
|
||||
: '周期报告',
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: accent,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
copy,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: context.jz.text2,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'查看完整报告',
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: accent,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 2),
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
size: 17,
|
||||
color: accent,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: context.jz.text2,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user