AI 接入 V1.0
UI更新
This commit is contained in:
@@ -9,6 +9,7 @@ import '../../../utils/date_util.dart';
|
||||
import '../../../utils/toast.dart';
|
||||
import '../../../widgets/category_picker_grid.dart';
|
||||
import '../../../widgets/state_views.dart';
|
||||
import '../../../widgets/cute_icon.dart';
|
||||
|
||||
/// 手动记账表单(复刻 uniApp add.vue 手动 Tab)
|
||||
class ManualEntryTab extends ConsumerStatefulWidget {
|
||||
@@ -78,9 +79,9 @@ class _ManualEntryTabState extends ConsumerState<ManualEntryTab> {
|
||||
Widget _typeSelector(AddBillFormState form, AddBillFormNotifier notifier) {
|
||||
return Row(
|
||||
children: [
|
||||
_typeCard('📤', '支出', 1, form, notifier),
|
||||
_typeCard('expense', '支出', 1, form, notifier),
|
||||
const SizedBox(width: 10),
|
||||
_typeCard('📥', '收入', 2, form, notifier),
|
||||
_typeCard('income', '收入', 2, form, notifier),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -107,7 +108,7 @@ class _ManualEntryTabState extends ConsumerState<ManualEntryTab> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(icon, style: const TextStyle(fontSize: 20)),
|
||||
CuteIcon(icon, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
@@ -228,7 +229,7 @@ class _ManualEntryTabState extends ConsumerState<ManualEntryTab> {
|
||||
await notifier.submit();
|
||||
_amountController.clear();
|
||||
_descriptionController.clear();
|
||||
if (context.mounted) showToast(context, '✅ 记账成功');
|
||||
if (context.mounted) showToast(context, '记账成功', icon: 'check');
|
||||
} on ApiException catch (e) {
|
||||
if (context.mounted) showToast(context, e.message, error: true);
|
||||
} catch (_) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../../../core/network/api_exception.dart';
|
||||
import '../../../providers/ocr_provider.dart';
|
||||
import '../../../utils/toast.dart';
|
||||
import 'ocr_result_card.dart';
|
||||
import '../../../widgets/cute_icon.dart';
|
||||
|
||||
/// OCR 识别 Tab:选图 → 识别 → 结果列表(复刻 uniApp add.vue OCR Tab)
|
||||
class OcrTab extends ConsumerWidget {
|
||||
@@ -38,7 +39,7 @@ class OcrTab extends ConsumerWidget {
|
||||
Future<void> _saveDirectly(BuildContext context, WidgetRef ref) async {
|
||||
try {
|
||||
await ref.read(ocrProvider.notifier).saveDirectly();
|
||||
if (context.mounted) showToast(context, '✅ 已保存识别的账单');
|
||||
if (context.mounted) showToast(context, '已保存识别的账单', icon: 'check');
|
||||
} on ApiException catch (e) {
|
||||
if (context.mounted) showToast(context, e.message, error: true);
|
||||
} catch (_) {
|
||||
@@ -86,7 +87,7 @@ class OcrTab extends ConsumerWidget {
|
||||
child: const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('📷', style: TextStyle(fontSize: 36)),
|
||||
CuteIcon('camera', size: 40),
|
||||
SizedBox(height: 6),
|
||||
Text('点击选择图片', style: TextStyle(color: Color(0xFF8D6E63))),
|
||||
],
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../models/ai.dart';
|
||||
import '../../providers/ai_provider.dart';
|
||||
import '../../utils/toast.dart';
|
||||
import '../../widgets/state_views.dart';
|
||||
import '../../widgets/cute_icon.dart';
|
||||
|
||||
/// 助手:和 AI 对话,问账单、问笔记。
|
||||
///
|
||||
@@ -32,6 +33,17 @@ class _AssistantPageState extends ConsumerState<AssistantPage> {
|
||||
'我的笔记里有哪些关于 MySQL 的?',
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 打开就把上一次的对话展开,而不是每次都从空白开始。
|
||||
// 放进 postFrameCallback:initState 里改 provider 状态会撞上「构建期间改状态」。
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
ref.read(assistantProvider.notifier).restoreLastSessionIfNeeded();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_input.dispose();
|
||||
@@ -156,7 +168,7 @@ class _AssistantPageState extends ConsumerState<AssistantPage> {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 40, 24, 0),
|
||||
children: [
|
||||
const Center(child: Text('✨', style: TextStyle(fontSize: 44))),
|
||||
const Center(child: CuteIcon('assistant', size: 48)),
|
||||
const SizedBox(height: 14),
|
||||
const Center(
|
||||
child: Text(
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../utils/date_util.dart';
|
||||
import '../../utils/toast.dart';
|
||||
import '../../widgets/category_picker_grid.dart';
|
||||
import '../../widgets/state_views.dart';
|
||||
import '../../widgets/cute_icon.dart';
|
||||
|
||||
/// 账单编辑页(复刻 uniApp pages/bill/detail.vue 的编辑能力,并补上删除)
|
||||
class BillEditPage extends ConsumerStatefulWidget {
|
||||
@@ -117,9 +118,9 @@ class _BillEditPageState extends ConsumerState<BillEditPage> {
|
||||
Widget _typeSelector(BillEditState state, BillEditNotifier notifier) {
|
||||
return Row(
|
||||
children: [
|
||||
_typeCard('📤', '支出', 1, state, notifier),
|
||||
_typeCard('expense', '支出', 1, state, notifier),
|
||||
const SizedBox(width: 10),
|
||||
_typeCard('📥', '收入', 2, state, notifier),
|
||||
_typeCard('income', '收入', 2, state, notifier),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -149,7 +150,7 @@ class _BillEditPageState extends ConsumerState<BillEditPage> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(icon, style: const TextStyle(fontSize: 20)),
|
||||
CuteIcon(icon, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
@@ -314,7 +315,7 @@ class _BillEditPageState extends ConsumerState<BillEditPage> {
|
||||
try {
|
||||
await notifier.save();
|
||||
if (!mounted) return;
|
||||
showToast(context, '✅ 已保存');
|
||||
showToast(context, '已保存', icon: 'check');
|
||||
Navigator.of(context).pop();
|
||||
} on ApiException catch (e) {
|
||||
if (mounted) showToast(context, e.message, error: true);
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'bill_statistics_page.dart';
|
||||
import 'widgets/bill_group_card.dart';
|
||||
import 'widgets/budget_card.dart';
|
||||
import 'widgets/month_switcher.dart';
|
||||
import '../../widgets/cute_icon.dart';
|
||||
|
||||
/// 记账模块的首页:月份切换 + 本月汇总 + 预算环 + 按日期分组的账单列表。
|
||||
///
|
||||
@@ -184,7 +185,7 @@ class BillHomePage extends ConsumerWidget {
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('🎯 ', style: TextStyle(fontSize: 14)),
|
||||
CuteIcon('target', size: 15),
|
||||
Text(
|
||||
'仅可查看本月预算',
|
||||
style: TextStyle(fontSize: 13, color: Color(0xFF8D6E63)),
|
||||
|
||||
@@ -4,6 +4,7 @@ import '../../../models/bill.dart';
|
||||
import '../../../utils/amount_util.dart';
|
||||
import '../../../utils/bill_grouping.dart';
|
||||
import '../../../utils/date_util.dart';
|
||||
import '../../../widgets/cute_icon.dart';
|
||||
|
||||
/// 单日分组卡:组头(日期 + 分开的支出/收入小计)+ 账单条目
|
||||
class BillGroupCard extends StatelessWidget {
|
||||
@@ -91,7 +92,7 @@ class _BillTile extends StatelessWidget {
|
||||
color: Color(0xFFFBEEDD),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(bill.categoryIcon, style: const TextStyle(fontSize: 19)),
|
||||
child: CuteIcon(bill.categoryIcon, size: 21),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../models/budget.dart';
|
||||
import '../../../utils/amount_util.dart';
|
||||
import '../../../widgets/cute_icon.dart';
|
||||
import '../../../widgets/donut_chart.dart';
|
||||
import '../../../widgets/state_views.dart';
|
||||
|
||||
@@ -30,7 +31,7 @@ class BudgetCard extends StatelessWidget {
|
||||
|
||||
Widget _noBudget() {
|
||||
return const EmptyView(
|
||||
icon: '🎯',
|
||||
icon: 'target',
|
||||
title: '本月未设置预算',
|
||||
tip: '可在「我的 → 预算设置」中设置',
|
||||
padding: EdgeInsets.symmetric(vertical: 20),
|
||||
@@ -110,7 +111,9 @@ class BudgetCard extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('💰 日均可用 ', style: TextStyle(fontSize: 13, color: Color(0xFF6D4C41))),
|
||||
const CuteIcon('target', size: 15),
|
||||
const SizedBox(width: 5),
|
||||
const Text('日均可用 ', style: TextStyle(fontSize: 13, color: Color(0xFF6D4C41))),
|
||||
Text(
|
||||
'¥${formatAmount(info.remainingDaily)}',
|
||||
style: const TextStyle(
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/statistics_response.dart';
|
||||
import '../../../utils/amount_util.dart';
|
||||
import '../../../widgets/cute_icon.dart';
|
||||
|
||||
/// 分类排行:emoji + 名称 + 进度条 + 金额 + 占比
|
||||
class CategoryRankList extends StatelessWidget {
|
||||
@@ -50,7 +51,7 @@ class CategoryRankList extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(item.categoryIcon, style: const TextStyle(fontSize: 16)),
|
||||
CuteIcon(item.categoryIcon, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/module/app_module.dart';
|
||||
import '../../widgets/cute_icon.dart';
|
||||
import '../assistant/assistant_page.dart';
|
||||
|
||||
/// 首页仪表盘:AI 输入入口 + 模块卡片网格。
|
||||
@@ -106,7 +107,7 @@ class DashboardPage extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('✨', style: TextStyle(fontSize: 18)),
|
||||
const CuteIcon('assistant', size: 19),
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
@@ -175,7 +176,7 @@ class DashboardPage extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(module.icon, style: const TextStyle(fontSize: 24)),
|
||||
CuteIcon(module.icon, size: 26),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
module.name,
|
||||
|
||||
+44
-143
@@ -10,8 +10,13 @@ import '../../providers/auth_provider.dart';
|
||||
import '../../utils/crypto.dart';
|
||||
import '../../utils/toast.dart';
|
||||
import '../shell/main_shell.dart';
|
||||
import '../../widgets/cute_icon.dart';
|
||||
|
||||
/// 登录/注册页(复刻 uniApp pages/login/login.vue)
|
||||
/// 登录页(复刻 uniApp pages/login/login.vue)
|
||||
///
|
||||
/// **注册功能已下线**(2026-09-24):这是单人应用,开放注册等于任何人都能建号。
|
||||
/// App 端的入口和界面都去掉了;后端 `/api/auth/register` 按用户要求保留着,
|
||||
/// 真要加人时把入口加回来即可。
|
||||
class LoginPage extends ConsumerStatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@@ -21,15 +26,12 @@ class LoginPage extends ConsumerStatefulWidget {
|
||||
|
||||
class _LoginPageState extends ConsumerState<LoginPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool _isRegister = false;
|
||||
bool _showPassword = false;
|
||||
bool _loading = false;
|
||||
bool _agreeProtocol = false;
|
||||
bool _rememberMe = false;
|
||||
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _nicknameController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
late final AnimationController _floatController = AnimationController(
|
||||
@@ -53,7 +55,6 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
||||
_floatController.dispose();
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
_nicknameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -66,43 +67,24 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
||||
showToast(context, '请输入用户名和密码', error: true);
|
||||
return;
|
||||
}
|
||||
if (_isRegister) {
|
||||
if (password.length < 6) {
|
||||
showToast(context, '密码至少6位', error: true);
|
||||
return;
|
||||
}
|
||||
if (!_agreeProtocol) {
|
||||
showToast(context, '请先阅读并同意用户协议');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final api = ref.read(authApiProvider);
|
||||
final response = _isRegister
|
||||
? await api.register(
|
||||
username,
|
||||
password,
|
||||
_nicknameController.text.trim().isEmpty
|
||||
? username
|
||||
: _nicknameController.text.trim(),
|
||||
)
|
||||
: await api.login(username, password);
|
||||
final response = await ref.read(authApiProvider).login(username, password);
|
||||
|
||||
// 记住密码
|
||||
if (!_isRegister && _rememberMe) {
|
||||
if (_rememberMe) {
|
||||
await LocalStorage.instance.saveRememberedPassword(
|
||||
username,
|
||||
encryptPassword(password),
|
||||
);
|
||||
} else if (!_isRegister && !_rememberMe) {
|
||||
} else {
|
||||
await LocalStorage.instance.clearRememberedPassword();
|
||||
}
|
||||
|
||||
await ref.read(authProvider.notifier).loginSuccess(response);
|
||||
if (!mounted) return;
|
||||
showToast(context, _isRegister ? '🎉 注册成功' : '🚀 登录成功');
|
||||
showToast(context, '登录成功', icon: 'check');
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const MainShell()),
|
||||
(route) => false,
|
||||
@@ -308,90 +290,55 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSegmentTabs(),
|
||||
const SizedBox(height: 20),
|
||||
_inputField(
|
||||
icon: '👤',
|
||||
icon: 'user',
|
||||
controller: _usernameController,
|
||||
hint: '请输入用户名',
|
||||
maxLength: 20,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_inputField(
|
||||
icon: '🔒',
|
||||
icon: 'lock',
|
||||
controller: _passwordController,
|
||||
hint: '请输入密码',
|
||||
maxLength: 20,
|
||||
obscure: !_showPassword,
|
||||
suffix: IconButton(
|
||||
icon: Text(
|
||||
_showPassword ? '👁️' : '👁️🗨️',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
// 一个图标用颜色表示状态:显示中=品牌色,隐藏中=浅灰
|
||||
icon: CuteIcon(
|
||||
'eye',
|
||||
size: 18,
|
||||
tint: _showPassword
|
||||
? const Color(0xFFC8956E)
|
||||
: const Color(0xFFBCAAA4),
|
||||
),
|
||||
onPressed: () =>
|
||||
setState(() => _showPassword = !_showPassword),
|
||||
),
|
||||
),
|
||||
if (_isRegister) ...[
|
||||
const SizedBox(height: 14),
|
||||
_inputField(
|
||||
icon: '✨',
|
||||
controller: _nicknameController,
|
||||
hint: '请输入昵称(可选)',
|
||||
maxLength: 20,
|
||||
),
|
||||
],
|
||||
if (!_isRegister) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: Checkbox(
|
||||
value: _rememberMe,
|
||||
activeColor: const Color(0xFFC8956E),
|
||||
onChanged: (v) =>
|
||||
setState(() => _rememberMe = v ?? false),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: Checkbox(
|
||||
value: _rememberMe,
|
||||
activeColor: const Color(0xFFC8956E),
|
||||
onChanged: (v) =>
|
||||
setState(() => _rememberMe = v ?? false),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'记住密码',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF8D6E63),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'记住密码',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF8D6E63),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (_isRegister) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: Checkbox(
|
||||
value: _agreeProtocol,
|
||||
activeColor: const Color(0xFFC8956E),
|
||||
onChanged: (v) =>
|
||||
setState(() => _agreeProtocol = v ?? false),
|
||||
),
|
||||
),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'我已阅读并同意用户协议',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF8D6E63),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -414,9 +361,9 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_isRegister ? '注 册' : '登 录',
|
||||
style: const TextStyle(
|
||||
: const Text(
|
||||
'登 录',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 4,
|
||||
@@ -437,52 +384,6 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
||||
sigmaY: 10,
|
||||
);
|
||||
|
||||
// ---- 登录/注册切换 ----
|
||||
Widget _buildSegmentTabs() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x33C8956E),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Row(
|
||||
children: [
|
||||
_segment(
|
||||
'登录',
|
||||
!_isRegister,
|
||||
() => setState(() => _isRegister = false),
|
||||
),
|
||||
_segment('注册', _isRegister, () => setState(() => _isRegister = true)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _segment(String text, bool active, VoidCallback onTap) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: active ? const Color(0xFFC8956E) : Colors.transparent,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: active ? Colors.white : const Color(0xFF8D6E63),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _inputField({
|
||||
required String icon,
|
||||
@@ -501,7 +402,7 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(icon, style: const TextStyle(fontSize: 18)),
|
||||
CuteIcon(icon, size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../../providers/budget_provider.dart';
|
||||
import '../../utils/amount_input_formatter.dart';
|
||||
import '../../utils/amount_util.dart';
|
||||
import '../../utils/toast.dart';
|
||||
import '../../widgets/cute_icon.dart';
|
||||
import '../../widgets/state_views.dart';
|
||||
|
||||
/// 「我的」页面
|
||||
@@ -101,7 +102,7 @@ class MinePage extends ConsumerWidget {
|
||||
height: 60,
|
||||
decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.white),
|
||||
alignment: Alignment.center,
|
||||
child: const Text('👤', style: TextStyle(fontSize: 30)),
|
||||
child: const CuteIcon('user', size: 32),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
@@ -338,7 +339,9 @@ class MinePage extends ConsumerWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('💰 日均可用 ', style: TextStyle(fontSize: 13, color: Color(0xFF6D4C41))),
|
||||
const CuteIcon('target', size: 15),
|
||||
const SizedBox(width: 5),
|
||||
const Text('日均可用 ', style: TextStyle(fontSize: 13, color: Color(0xFF6D4C41))),
|
||||
Text(
|
||||
'¥${formatAmount(budget.remainingDaily)}',
|
||||
style: const TextStyle(
|
||||
@@ -408,7 +411,7 @@ class MinePage extends ConsumerWidget {
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(module.icon, style: const TextStyle(fontSize: 20)),
|
||||
CuteIcon(module.icon, size: 21),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -456,7 +459,7 @@ class MinePage extends ConsumerWidget {
|
||||
|
||||
try {
|
||||
await ref.read(accountProvider.notifier).setBalance(input);
|
||||
if (context.mounted) showToast(context, '✅ 余额已更新');
|
||||
if (context.mounted) showToast(context, '余额已更新', icon: 'check');
|
||||
} on ApiException catch (e) {
|
||||
if (context.mounted) showToast(context, e.message, error: true);
|
||||
} catch (_) {
|
||||
@@ -477,7 +480,7 @@ class MinePage extends ConsumerWidget {
|
||||
|
||||
try {
|
||||
await ref.read(budgetProvider.notifier).setMonthBudget(input);
|
||||
if (context.mounted) showToast(context, '✅ 预算已保存');
|
||||
if (context.mounted) showToast(context, '预算已保存', icon: 'check');
|
||||
} on ApiException catch (e) {
|
||||
if (context.mounted) showToast(context, e.message, error: true);
|
||||
} catch (_) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../models/note.dart';
|
||||
import '../../widgets/cute_icon.dart';
|
||||
|
||||
/// 目录选择面板
|
||||
///
|
||||
@@ -114,7 +115,7 @@ class _FolderPickerSheetState extends State<FolderPickerSheet> {
|
||||
children: [
|
||||
if (widget.showAll)
|
||||
_row(
|
||||
icon: '📚',
|
||||
icon: 'education',
|
||||
name: '全部',
|
||||
count: widget.tree.totalCount,
|
||||
depth: 0,
|
||||
@@ -122,7 +123,7 @@ class _FolderPickerSheetState extends State<FolderPickerSheet> {
|
||||
onTap: () => Navigator.pop(context, NoteLocation.all),
|
||||
),
|
||||
_row(
|
||||
icon: '📥',
|
||||
icon: 'income',
|
||||
name: '未分类',
|
||||
count: widget.tree.uncategorizedCount,
|
||||
depth: 0,
|
||||
@@ -153,7 +154,7 @@ class _FolderPickerSheetState extends State<FolderPickerSheet> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_row(
|
||||
icon: (open && hasChildren) ? '📂' : '📁',
|
||||
icon: (open && hasChildren) ? 'folder-open' : 'folder',
|
||||
name: node.name,
|
||||
count: node.noteCount,
|
||||
depth: depth,
|
||||
@@ -221,7 +222,7 @@ class _FolderPickerSheetState extends State<FolderPickerSheet> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(icon, style: const TextStyle(fontSize: 15)),
|
||||
CuteIcon(icon, size: 17),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
|
||||
@@ -231,7 +231,7 @@ class NoteDetailPage extends ConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Text(
|
||||
'🖼 图片加载失败',
|
||||
'图片加载失败',
|
||||
style: TextStyle(fontSize: 13, color: Color(0xFFBCAAA4)),
|
||||
),
|
||||
),
|
||||
@@ -277,7 +277,7 @@ class NoteDetailPage extends ConsumerWidget {
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: resolveAssetUrl(href)));
|
||||
Navigator.pop(sheetContext);
|
||||
showToast(context, '✅ 已复制链接');
|
||||
showToast(context, '已复制链接', icon: 'check');
|
||||
},
|
||||
style: FilledButton.styleFrom(backgroundColor: const Color(0xFFC8956E)),
|
||||
child: const Text('复制'),
|
||||
@@ -358,7 +358,7 @@ class NoteDetailPage extends ConsumerWidget {
|
||||
await ref.read(noteApiProvider).update(note.id, pinned: !note.pinned);
|
||||
ref.invalidate(noteDetailProvider(noteId));
|
||||
await ref.read(noteListProvider.notifier).refresh();
|
||||
if (context.mounted) showToast(context, note.pinned ? '已取消置顶' : '📌 已置顶');
|
||||
if (context.mounted) showToast(context, note.pinned ? '已取消置顶' : '已置顶', icon: 'pin');
|
||||
} on ApiException catch (e) {
|
||||
if (context.mounted) showToast(context, e.message, error: true);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ class _NoteEditPageState extends ConsumerState<NoteEditPage> {
|
||||
}
|
||||
if (!mounted) return;
|
||||
_saved = true;
|
||||
showToast(context, '✅ 已保存');
|
||||
showToast(context, '已保存', icon: 'check');
|
||||
Navigator.of(context).pop(true);
|
||||
} on ApiException catch (e) {
|
||||
if (mounted) showToast(context, e.message, error: true);
|
||||
@@ -246,7 +246,7 @@ class _NoteEditPageState extends ConsumerState<NoteEditPage> {
|
||||
final content = _contentController.text;
|
||||
if (content.trim().isEmpty) {
|
||||
return const Center(
|
||||
child: EmptyView(icon: '📄', title: '还没有内容', tip: '切回编辑模式写点什么吧'),
|
||||
child: EmptyView(icon: 'file', title: '还没有内容', tip: '切回编辑模式写点什么吧'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../utils/toast.dart';
|
||||
import '../../widgets/state_views.dart';
|
||||
import 'folder_picker_sheet.dart';
|
||||
import 'note_detail_page.dart';
|
||||
import '../../widgets/cute_icon.dart';
|
||||
|
||||
/// 笔记列表页
|
||||
///
|
||||
@@ -308,7 +309,7 @@ class _NoteListPageState extends ConsumerState<NoteListPage> {
|
||||
children: [
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.18),
|
||||
EmptyView(
|
||||
icon: state.hasFilter ? '🔍' : '📝',
|
||||
icon: state.hasFilter ? 'search' : 'note',
|
||||
title: state.hasFilter ? '没有匹配的笔记' : '还没有笔记',
|
||||
tip: state.hasFilter
|
||||
? '换个关键词或标签试试'
|
||||
@@ -383,7 +384,7 @@ class _NoteListPageState extends ConsumerState<NoteListPage> {
|
||||
Row(
|
||||
children: [
|
||||
if (note.pinned) ...[
|
||||
const Text('📌', style: TextStyle(fontSize: 13)),
|
||||
const CuteIcon('pin', size: 14),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Expanded(
|
||||
@@ -471,7 +472,7 @@ class _NoteListPageState extends ConsumerState<NoteListPage> {
|
||||
await ref.read(noteListProvider.notifier).refresh();
|
||||
ref.invalidate(noteTagsProvider);
|
||||
ref.invalidate(noteFolderTreeProvider); // 目录上的笔记数变了
|
||||
if (mounted) showToast(context, '✅ 已记录');
|
||||
if (mounted) showToast(context, '已记录', icon: 'check');
|
||||
} on ApiException catch (e) {
|
||||
if (mounted) showToast(context, e.message, error: true);
|
||||
} catch (_) {
|
||||
|
||||
Reference in New Issue
Block a user