feat(mining-app): improve UX with non-blocking splash and skeleton loading
- Optimize splash page: reduce wait to 500ms, refresh token in background - Cache SharedPreferences instance to avoid blocking API requests - Add global 401 handler to auto-redirect to login page - Create shimmer loading components (ShimmerLoading, ShimmerBox, skeletons) - Replace CircularProgressIndicator with skeleton screens across all pages - Add keepAlive + auto-invalidation (5min) to providers to reduce API calls - Fix trading page: invalidate globalStateProvider after trade for data sync Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
|
@ -765,7 +765,8 @@
|
||||||
"Bash(git -C \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\" push)",
|
"Bash(git -C \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\" push)",
|
||||||
"Bash(git -C \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\" add frontend/mining-app/lib/presentation/pages/splash/splash_page.dart frontend/mining-app/lib/presentation/providers/user_providers.dart)",
|
"Bash(git -C \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\" add frontend/mining-app/lib/presentation/pages/splash/splash_page.dart frontend/mining-app/lib/presentation/providers/user_providers.dart)",
|
||||||
"Bash(git -C \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\" commit -m \"$\\(cat <<''EOF''\nfix\\(mining-app\\): update splash page theme and fix token refresh\n\n- Update splash_page.dart to orange theme \\(#FF6B00\\) matching other pages\n- Change app name from \"榴莲挖矿\" to \"榴莲生态\"\n- Fix refreshTokenIfNeeded to properly throw on failure instead of\n silently calling logout \\(which caused Riverpod ref errors\\)\n- Clear local storage directly on refresh failure without remote API call\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
"Bash(git -C \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\" commit -m \"$\\(cat <<''EOF''\nfix\\(mining-app\\): update splash page theme and fix token refresh\n\n- Update splash_page.dart to orange theme \\(#FF6B00\\) matching other pages\n- Change app name from \"榴莲挖矿\" to \"榴莲生态\"\n- Fix refreshTokenIfNeeded to properly throw on failure instead of\n silently calling logout \\(which caused Riverpod ref errors\\)\n- Clear local storage directly on refresh failure without remote API call\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||||
"Bash(python3 -c \" import sys content = sys.stdin.read\\(\\) old = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' new = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' print\\(content.replace\\(old, new\\)\\) \")"
|
"Bash(python3 -c \" import sys content = sys.stdin.read\\(\\) old = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' new = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' print\\(content.replace\\(old, new\\)\\) \")",
|
||||||
|
"Bash(git rm:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 536 B |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
|
@ -5,9 +5,9 @@
|
||||||
android:viewportWidth="40"
|
android:viewportWidth="40"
|
||||||
android:viewportHeight="40">
|
android:viewportHeight="40">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFF3F4F6"
|
android:fillColor="#FFFFEDD5"
|
||||||
android:pathData="M0 19.59c0-10.82 8.77-19.59 19.59-19.59h-0.01c10.82 0 19.59 8.77 19.59 19.59v0.82c0 10.82-8.77 19.59-19.59 19.59"/>
|
android:pathData="M0 20c0-11.05 8.95-20 20-20h0c11.05 0 20 8.95 20 20v0c0 11.05-8.95 20-20 20h0c-11.05 0-20-8.95-20-20z"/>
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFF6B00"
|
android:fillColor="#FFFF6B00"
|
||||||
android:pathData="M24.41 14.84l1.46 1.46-4.88 4.87-3.28-3.28c-0.38-0.42-1.03-0.42-1.4 0l-6 6c-0.43 0.38-0.43 1.03 0 1.4 0.37 0.38 1.03 0.38 1.4 0l5.3-5.29 3.28 3.28c0.37 0.42 1.03 0.42 1.4 0l5.58-5.58 1.46 1.46c0.32 0.28 0.84 0.09 0.84-0.38v-4.26c0-0.29-0.19-0.52-0.47-0.52H24.8c-0.42 0-0.66 0.56-0.38 0.84Z"/>
|
android:pathData="M13.52 24.8l4.41-4.4 2.7 2.73c0.35 0.3 0.9 0.3 1.2-0.04l5.98-6.72c0.28-0.35 0.28-0.86-0.04-1.17-0.3-0.32-0.9-0.32-1.2 0.03l-5.32 5.98-2.73-2.73c-0.36-0.32-0.86-0.32-1.18 0l-5.07 5.07c-0.36 0.36-0.36 0.86 0 1.18l0.07 0.07c0.32 0.36 0.82 0.36 1.18 0Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="40dp"
|
||||||
|
android:height="40dp"
|
||||||
|
android:viewportWidth="40"
|
||||||
|
android:viewportHeight="40">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFDCFCE7"
|
||||||
|
android:pathData="M0 20c0-11.05 8.95-20 20-20h0c11.05 0 20 8.95 20 20v0c0 11.05-8.95 20-20 20h0c-11.05 0-20-8.95-20-20z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF16A34A"
|
||||||
|
android:pathData="M22.9 18c-0.13-1.98-0.87-3.98-2.2-5.62-0.35-0.39-0.97-0.39-1.29 0-1.4 1.64-2.14 3.64-2.3 5.63 1.05 0.58 2.03 1.33 2.89 2.22 0.86-0.9 1.84-1.64 2.9-2.22ZM20 22.9c-1.64-2.5-4.3-4.23-7.34-4.5-0.55-0.08-0.98 0.39-0.94 0.9 0.39 4.02 3.05 7.34 6.64 8.63 0.55 0.2 1.1 0.31 1.64 0.43 0.59-0.12 1.13-0.27 1.64-0.43 3.63-1.29 6.29-4.61 6.64-8.63 0.08-0.51-0.39-0.98-0.9-0.9-3.08 0.27-5.74 2-7.38 4.5Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="40dp"
|
||||||
|
android:height="40dp"
|
||||||
|
android:viewportWidth="40"
|
||||||
|
android:viewportHeight="40">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFEDD5"
|
||||||
|
android:pathData="M0 20c0-11.05 8.95-20 20-20h0c11.05 0 20 8.95 20 20v0c0 11.05-8.95 20-20 20h0c-11.05 0-20-8.95-20-20z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFF6B00"
|
||||||
|
android:pathData="M15 11.68v5L18.32 20 15 23.32v0.04 4.96h10v-4.96-0.04L21.68 20 25 16.68v-5H15Zm8.32 12.07v2.93h-6.64v-2.93L20 20.43l3.32 3.32ZM20 19.57l-3.32-3.32v-2.93h6.64v2.93L20 19.57Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="36dp"
|
||||||
|
android:height="36dp"
|
||||||
|
android:viewportWidth="36"
|
||||||
|
android:viewportHeight="36">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF9FAFB"
|
||||||
|
android:pathData="M0 8c0-4.42 3.58-8 8-8h20c4.42 0 8 3.58 8 8v20c0 4.42-3.58 8-8 8h-20c-4.42 0-8-3.58-8-8z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF4B5563"
|
||||||
|
android:pathData="M18.67 17.33c-1.34-0.45-1.97-0.74-1.97-1.44 0-0.74 0.8-1.02 1.34-1.02 0.98 0 1.33 0.74 1.44 0.99l1.2-0.5c-0.15-0.35-0.64-1.44-1.94-1.68v-0.92h-1.48v0.95c-1.86 0.39-1.86 2.15-1.86 2.22 0 1.68 1.69 2.18 2.5 2.46 1.19 0.42 1.72 0.8 1.72 1.54 0 0.85-0.81 1.2-1.51 1.2-1.34 0-1.76-1.4-1.8-1.58l-1.23 0.52c0.46 1.62 1.69 2.08 2.18 2.22v0.95h1.48v-0.92c0.31-0.07 2.18-0.45 2.18-2.42 0-1.02-0.46-1.94-2.25-2.57Zm-7.42 7.42H9.74v-4.5h4.5v1.51h-1.87c1.24 1.8 3.27 2.99 5.63 2.99 3.73 0 6.75-3.02 6.75-6.75h1.51c0 4.57-3.69 8.26-8.26 8.26-2.78 0-5.27-1.4-6.75-3.51v2ZM9.74 18c0-4.57 3.69-8.26 8.26-8.26 2.78 0 5.27 1.4 6.75 3.51v-2h1.51v4.5h-4.5v-1.51h1.86c-1.23-1.8-3.26-2.99-5.62-2.99-3.73 0-6.75 3.02-6.75 6.75H9.74Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="36dp"
|
||||||
|
android:height="36dp"
|
||||||
|
android:viewportWidth="36"
|
||||||
|
android:viewportHeight="36">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF9FAFB"
|
||||||
|
android:pathData="M0 8c0-4.42 3.58-8 8-8h20c4.42 0 8 3.58 8 8v20c0 4.42-3.58 8-8 8h-20c-4.42 0-8-3.58-8-8z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF4B5563"
|
||||||
|
android:pathData="M13.89 16.49h-1.52v5.27h1.52V16.5Zm4.5 0h-1.52v5.27h1.52V16.5Zm6.36 6.75H10.51v1.51h14.24v-1.51Zm-1.86-6.75h-1.52v5.27h1.52V16.5Zm-5.28-5.03l3.9 2.04h-7.8l3.9-2.04Zm0-1.72l-7.1 3.76v1.51h14.24V13.5l-7.14-3.76Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF3F4F6"
|
||||||
|
android:pathData="M0 16c0-8.84 7.16-16 16-16h16c8.84 0 16 7.16 16 16v16c0 8.84-7.16 16-16 16h-16c-8.84 0-16-7.16-16-16z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFF6B00"
|
||||||
|
android:pathData="M30 24.98h-5.02V30c0 0.56-0.42 0.98-0.98 0.98s-0.98-0.42-0.98-0.98v-5.02H18c-0.56 0-0.98-0.42-0.98-0.98s0.42-0.98 0.98-0.98h5.02V18c0-0.56 0.42-0.98 0.98-0.98s0.98 0.42 0.98 0.98v5.02H30c0.56 0 0.98 0.42 0.98 0.98s-0.42 0.98-0.98 0.98Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF3F4F6"
|
||||||
|
android:pathData="M0 16c0-8.84 7.16-16 16-16h16c8.84 0 16 7.16 16 16v16c0 8.84-7.16 16-16 16h-16c-8.84 0-16-7.16-16-16z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFF6B00"
|
||||||
|
android:pathData="M30 24.98H18c-0.56 0-0.98-0.42-0.98-0.98s0.42-0.98 0.98-0.98h12c0.56 0 0.98 0.42 0.98 0.98s-0.42 0.98-0.98 0.98Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF3F4F6"
|
||||||
|
android:pathData="M0 16c0-8.84 7.16-16 16-16h16c8.84 0 16 7.16 16 16v16c0 8.84-7.16 16-16 16h-16c-8.84 0-16-7.16-16-16z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFF6B00"
|
||||||
|
android:pathData="M18.14 23.86l-2.77 2.81c-0.18 0.19-0.18 0.47 0 0.7l2.77 2.77c0.33 0.33 0.84 0.1 0.84-0.33v-1.83h6c0.57 0 1.04-0.42 1.04-0.98s-0.47-0.98-1.04-0.98h-6v-1.83c0-0.42-0.51-0.66-0.84-0.33Zm14.53-3.19l-2.81-2.81c-0.28-0.33-0.84-0.1-0.84 0.33v1.83h-6c-0.57 0-1.04 0.42-1.04 0.98s0.47 0.98 1.04 0.98h6v1.83c0 0.42 0.51 0.66 0.84 0.33l2.77-2.81c0.23-0.19 0.23-0.47 0.04-0.66Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF3F4F6"
|
||||||
|
android:pathData="M0 16c0-8.84 7.16-16 16-16h16c8.84 0 16 7.16 16 16v16c0 8.84-7.16 16-16 16h-16c-8.84 0-16-7.16-16-16z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFF6B00"
|
||||||
|
android:pathData="M28.6 21H27v-5.02c0-0.51-0.47-0.98-0.98-0.98h-4.04C21.47 15 21 15.47 21 15.98V21h-1.6c-0.88 0-1.35 1.08-0.7 1.69l4.6 4.6c0.37 0.41 1.03 0.41 1.4 0l4.6-4.6c0.6-0.61 0.18-1.69-0.7-1.69Zm-11.58 9.98c0 0.57 0.42 1.04 0.98 1.04h12c0.56 0 0.98-0.47 0.98-1.04 0-0.51-0.42-0.98-0.98-0.98H18c-0.56 0-0.98 0.47-0.98 0.98Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="36dp"
|
||||||
android:height="24dp"
|
android:height="36dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="36"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="36">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF1F2937"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M21 12.23C21 6.75 16.73 3 12 3c-4.69 0-9 3.66-9 9.28-0.6 0.33-0.98 0.99-0.98 1.74v1.96C2.02 17.11 2.9 18 3.98 18c0.57 0 1.04-0.47 1.04-0.98v-4.83c0-3.85 2.95-7.17 6.75-7.27 3.98-0.14 7.21 3.05 7.21 6.99v7.07H12c-0.56 0-0.98 0.47-0.98 1.04 0 0.51 0.42 0.98 0.98 0.98h6.98c1.13 0 2.02-0.9 2.02-2.02v-1.21c0.6-0.29 0.98-0.9 0.98-1.64v-2.3c0-0.7-0.37-1.31-0.98-1.6ZM8.02 12.98C8.02 12.47 8.44 12 9 12s0.98 0.47 0.98 0.98c0 0.57-0.42 1.04-0.98 1.04s-0.98-0.47-0.98-1.04Zm6 0c0-0.51 0.42-0.98 0.98-0.98s0.98 0.47 0.98 0.98c0 0.57-0.42 1.04-0.98 1.04s-0.98-0.47-0.98-1.04ZM18 11.02C17.53 8.2 15.05 6 12.05 6 9 6 5.77 8.53 6 12.47c2.48-1.03 4.36-3.24 4.88-5.9 1.3 2.62 3.98 4.45 7.12 4.45Z"/>
|
android:pathData="M0 18c0-9.94 8.06-18 18-18h0c9.94 0 18 8.06 18 18v0c0 9.94-8.06 18-18 18h0c-9.94 0-18-8.06-18-18z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF4B5563"
|
||||||
|
android:pathData="M24.21 18.82c0-0.27 0.04-0.55 0.04-0.82 0-0.27-0.04-0.55-0.04-0.82l1.76-1.37c0.16-0.11 0.2-0.35 0.08-0.54l-1.68-2.9c-0.08-0.11-0.2-0.19-0.35-0.19-0.04 0-0.12 0-0.16 0.04l-2.07 0.82c-0.43-0.31-0.9-0.63-1.4-0.82l-0.32-2.19c-0.04-0.2-0.2-0.35-0.39-0.35h-3.36c-0.2 0-0.35 0.16-0.39 0.35l-0.31 2.19c-0.51 0.2-0.98 0.5-1.4 0.82l-2.08-0.82c-0.08-0.04-0.12-0.04-0.16-0.04-0.15 0-0.27 0.08-0.35 0.2l-1.68 2.89c-0.11 0.2-0.07 0.43 0.08 0.54l1.76 1.37c0 0.27-0.04 0.55-0.04 0.82 0 0.27 0.04 0.55 0.04 0.82l-1.76 1.37c-0.15 0.11-0.2 0.35-0.08 0.54l1.68 2.9c0.08 0.11 0.2 0.19 0.35 0.19 0.04 0 0.12 0 0.16-0.04l2.07-0.82c0.43 0.31 0.9 0.63 1.4 0.82l0.32 2.19c0.04 0.2 0.2 0.35 0.39 0.35h3.36c0.2 0 0.35-0.16 0.39-0.35l0.31-2.19c0.51-0.2 0.98-0.5 1.4-0.82l2.08 0.82c0.08 0.04 0.12 0.04 0.16 0.04 0.15 0 0.27-0.08 0.35-0.2l1.68-2.89c0.11-0.2 0.07-0.43-0.08-0.54l-1.76-1.37Zm-1.68-1.45c0.04 0.28 0.04 0.47 0.04 0.63 0 0.16 0 0.35-0.04 0.63l-0.12 0.93 0.75 0.59 0.9 0.7-0.6 0.98-1.05-0.43-0.86-0.31-0.74 0.54c-0.35 0.28-0.7 0.47-1.05 0.63L18.9 22.6l-0.16 0.94-0.15 1.13H17.4l-0.15-1.13-0.12-0.94-0.9-0.35c-0.35-0.16-0.7-0.35-1.01-0.63l-0.78-0.54-0.86 0.35-1.06 0.43-0.58-1.02 0.9-0.7 0.74-0.59-0.12-0.93c-0.04-0.28-0.04-0.47-0.04-0.63 0-0.16 0-0.35 0.04-0.63l0.12-0.93-0.75-0.59-0.9-0.7 0.6-0.98 1.05 0.43 0.86 0.31 0.74-0.54c0.35-0.28 0.7-0.47 1.05-0.63l0.86-0.35 0.16-0.94 0.15-1.13h1.18l0.15 1.13 0.12 0.94 0.9 0.35c0.35 0.16 0.7 0.35 1.01 0.63l0.75 0.54 0.9-0.35 1.05-0.43 0.58 1.02-0.9 0.7-0.74 0.59 0.12 0.93ZM18 14.69c-1.84 0-3.32 1.48-3.32 3.32 0 1.84 1.48 3.32 3.32 3.32 1.84 0 3.32-1.48 3.32-3.32 0-1.84-1.48-3.32-3.32-3.32Zm0 5c-0.9 0-1.68-0.78-1.68-1.68 0-0.9 0.78-1.68 1.68-1.68 0.9 0 1.68 0.78 1.68 1.68 0 0.9-0.78 1.68-1.68 1.68Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="14dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="14"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF9CA3AF"
|
||||||
|
android:pathData="M7 6.5c2.21 0 4.18 1.23 5.14 3.2-0.96 1.97-2.93 3.23-5.14 3.23-2.21 0-4.18-1.26-5.14-3.23C2.82 7.73 4.79 6.5 7 6.5Zm0-1.18c-2.93 0-5.41 1.84-6.43 4.38 1.02 2.57 3.5 4.37 6.43 4.37 2.93 0 5.41-1.8 6.43-4.37C12.4 7.16 9.93 5.32 7 5.32Zm0 2.93c0.8 0 1.45 0.66 1.45 1.45 0 0.82-0.66 1.48-1.45 1.48-0.8 0-1.45-0.66-1.45-1.48 0-0.8 0.66-1.45 1.45-1.45Zm0-1.18c-1.45 0-2.63 1.18-2.63 2.63S5.55 12.32 7 12.32s2.63-1.17 2.63-2.62c0-1.45-1.18-2.63-2.63-2.63Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="14dp"
|
|
||||||
android:height="20dp"
|
|
||||||
android:viewportWidth="14"
|
|
||||||
android:viewportHeight="20">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFF6B00"
|
|
||||||
android:pathData="M7 4.18c-3.23 0-5.82 2.6-5.82 5.82 0 3.23 2.6 5.82 5.82 5.82 3.23 0 5.82-2.6 5.82-5.82 0-3.23-2.6-5.82-5.82-5.82Zm0 8.75c-0.33 0-0.57-0.28-0.57-0.6V10c0-0.33 0.24-0.57 0.57-0.57 0.33 0 0.57 0.24 0.57 0.57v2.32c0 0.33-0.24 0.6-0.57 0.6Zm0.57-4.68H6.43V7.07h1.14v1.18Z"/>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="14dp"
|
android:width="20dp"
|
||||||
android:height="20dp"
|
android:height="20dp"
|
||||||
android:viewportWidth="14"
|
android:viewportWidth="20"
|
||||||
android:viewportHeight="20">
|
android:viewportHeight="20">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFF6B00"
|
android:fillColor="#FF4B5563"
|
||||||
android:pathData="M5.41 6.91c-0.21 0.22-0.21 0.6 0 0.82L7.68 10l-2.27 2.27c-0.21 0.22-0.21 0.6 0 0.82 0.25 0.22 0.6 0.22 0.82 0l2.68-2.68c0.25-0.22 0.25-0.6 0-0.82L6.23 6.91c-0.21-0.22-0.57-0.22-0.82 0Z"/>
|
android:pathData="M10 18.32c0.9 0 1.68-0.74 1.68-1.64H8.32c0 0.9 0.78 1.64 1.68 1.64Zm5-5V9.18c0-2.58-1.37-4.73-3.75-5.27V3.32c0-0.66-0.55-1.25-1.25-1.25S8.75 2.66 8.75 3.32v0.59C6.37 4.45 5 6.6 5 9.18v4.14L3.32 15v0.82h13.36V15L15 13.32Zm-1.68 0.86H6.68v-5c0-2.07 1.25-3.75 3.32-3.75s3.32 1.68 3.32 3.75v5Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="14dp"
|
||||||
android:height="24dp"
|
android:height="20dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="14"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="20">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFF6B00"
|
android:fillColor="#FF10B981"
|
||||||
android:pathData="M18.98 5.02h-1.96V3.98c0-0.51-0.47-0.98-1.04-0.98H8.02C7.45 3 6.98 3.47 6.98 3.98v1.04H5.02C3.89 5.02 3 5.9 3 6.98v1.04c0 2.53 1.92 4.59 4.4 4.92 0.62 1.5 1.97 2.62 3.62 2.95V19h-3c-0.57 0-1.04 0.46-1.04 1.03 0 0.51 0.47 0.98 1.04 0.98h7.96c0.57 0 1.04-0.47 1.04-0.98 0-0.57-0.47-1.04-1.04-1.04h-3V15.9c1.64-0.33 3-1.45 3.61-2.95C19.08 12.6 21 10.54 21 8.02V6.98c0-1.07-0.9-1.96-2.02-1.96Zm-13.96 3V6.98h1.96v3.85C5.86 10.4 5.02 9.28 5.02 8.02Zm13.96 0c0 1.26-0.84 2.39-1.96 2.8V6.99h1.96v1.04Z"/>
|
android:pathData="M9.82 7l0.84 0.84-2.84 2.84-1.91-1.91c-0.22-0.25-0.6-0.25-0.82 0l-3.5 3.5c-0.25 0.22-0.25 0.6 0 0.82 0.21 0.22 0.6 0.22 0.82 0L5.5 10l1.91 1.91c0.22 0.25 0.6 0.25 0.82 0l3.25-3.25 0.85 0.85c0.2 0.16 0.5 0.05 0.5-0.22V6.8c0-0.16-0.12-0.3-0.28-0.3h-2.51C9.79 6.5 9.65 6.83 9.82 7Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="32dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="32">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF9CA3AF"
|
android:fillColor="#FF9CA3AF"
|
||||||
android:pathData="M6.14 11.86l-2.76 2.81c-0.2 0.19-0.2 0.47 0 0.7l2.76 2.77c0.33 0.33 0.84 0.1 0.84-0.33v-1.83h6c0.57 0 1.04-0.42 1.04-0.98s-0.47-0.98-1.04-0.98h-6v-1.83c0-0.42-0.51-0.66-0.84-0.33Zm14.53-3.19l-2.81-2.81c-0.28-0.33-0.84-0.1-0.84 0.33v1.83h-6c-0.57 0-1.04 0.42-1.04 0.98s0.47 0.98 1.04 0.98h6v1.83c0 0.42 0.51 0.66 0.84 0.33l2.77-2.81c0.23-0.19 0.23-0.47 0.04-0.66Z"/>
|
android:pathData="M15.98 16.98c3.1-2.8 6-5.43 6-7.68 0-1.83-1.45-3.28-3.28-3.28-1.03 0-2.06 0.46-2.72 1.21-0.65-0.75-1.64-1.21-2.67-1.21-1.87 0-3.33 1.45-3.33 3.28 0 2.25 2.91 4.87 6 7.68Zm-2.67-9c0.42 0 0.9 0.24 1.17 0.57l1.5 1.78 1.55-1.78c0.28-0.33 0.75-0.57 1.17-0.57 0.75 0 1.32 0.57 1.32 1.32 0 1.12-2.07 3.18-4.04 5.01C14.06 12.48 12 10.42 12 9.3c0-0.75 0.56-1.32 1.31-1.32Zm5.67 12h-1.96c0-1.17-0.75-2.25-1.88-2.67l-6.19-2.3H0.98v10.97h6v-1.4l7.04 1.92 7.96-2.48v-1.04c0-1.64-1.3-3-3-3ZM3 24.02v-7.04h2.02v7.04H3Zm10.97 0.37l-6.99-1.92v-5.49h1.64l5.82 2.2c0.33 0.1 0.56 0.43 0.56 0.8 0 0-1.97-0.04-2.3-0.14l-2.39-0.8-0.6 1.93 2.34 0.8c0.51 0.14 1.07 0.23 1.6 0.23h5.33c0.43 0 0.75 0.23 0.94 0.56l-5.95 1.83Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="32dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="32">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF9CA3AF"
|
android:fillColor="#FF9CA3AF"
|
||||||
android:pathData="M9.98 15.98V8.02C9.98 6.89 10.88 6 12 6h9V5.02C21 3.89 20.1 3 18.98 3H5.02C3.89 3 3 3.9 3 5.02v13.96C3 20.11 3.9 21 5.02 21h13.96c1.13 0 2.02-0.9 2.02-2.02V18h-9c-1.13 0-2.02-0.9-2.02-2.02Zm3-7.96C12.47 8.02 12 8.44 12 9v6c0 0.56 0.47 0.98 0.98 0.98h9V8.02h-9Zm3 5.48c-0.8 0-1.5-0.66-1.5-1.5s0.7-1.5 1.5-1.5c0.85 0 1.5 0.66 1.5 1.5s-0.65 1.5-1.5 1.5Z"/>
|
android:pathData="M6.98 15.02L3 19l3.98 3.98v-3h7.04v-1.96H6.98v-3ZM21 13l-3.98-3.98v3H9.98v1.96h7.04v3L21 13Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="32dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="32">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF9CA3AF"
|
android:fillColor="#FF9CA3AF"
|
||||||
android:pathData="M12 12c2.2 0 3.98-1.78 3.98-3.98S14.2 3.98 12 3.98 8.02 5.81 8.02 8.02C8.02 10.22 9.8 12 12 12Zm0 2.02c-2.67 0-8.02 1.3-8.02 3.98v0.98c0 0.57 0.47 1.04 1.04 1.04h13.96c0.57 0 1.04-0.47 1.04-1.04V18c0-2.67-5.35-3.98-8.02-3.98Z"/>
|
android:pathData="M12 10c1.08 0 2.02 0.9 2.02 2.02 0 1.07-0.94 1.96-2.02 1.96s-2.02-0.89-2.02-1.96C9.98 10.89 10.92 10 12 10Zm0 9.98c2.72 0 5.81 1.32 6 2.02H6c0.23-0.7 3.33-2.02 6-2.02Zm0-12c-2.2 0-3.98 1.83-3.98 4.04C8.02 14.22 9.8 16 12 16s3.98-1.78 3.98-3.98S14.2 7.98 12 7.98Zm0 10.04c-2.67 0-8.02 1.3-8.02 3.98v2.02h16.04V22c0-2.67-5.35-3.98-8.02-3.98Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="14dp"
|
||||||
android:height="24dp"
|
android:height="20dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="14"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="20">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFF6B00"
|
android:fillColor="#FFD1D5DB"
|
||||||
android:pathData="M8.02 2.02C6.89 2.02 6 2.9 6 3.98v3.2c0 0.5 0.23 1.02 0.6 1.4L9.99 12l-3.37 3.42C6.23 15.8 6 16.32 6 16.82v3.2c0 1.07 0.9 1.96 2.02 1.96h7.96c1.13 0 2.02-0.89 2.02-1.96v-3.2c0-0.5-0.19-1.02-0.56-1.4L14.02 12l3.37-3.42C17.81 8.2 18 7.68 18 7.18v-3.2c0-1.07-0.9-1.96-2.02-1.96H8.02Zm7.96 14.9v2.06c0 0.57-0.42 1.04-0.98 1.04H9c-0.56 0-0.98-0.47-0.98-1.04v-2.06c0-0.28 0.09-0.51 0.28-0.7l3.7-3.7 3.7 3.7c0.2 0.19 0.28 0.42 0.28 0.7Z"/>
|
android:pathData="M3.64 14.81l1.04 1.01L10.5 10 4.68 4.18l-1.04 1L8.42 10l-4.78 4.81Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="12dp"
|
||||||
|
android:height="10dp"
|
||||||
|
android:viewportWidth="12"
|
||||||
|
android:viewportHeight="10">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF10B981"
|
||||||
|
android:pathData="M4.45 8.75c-0.15 0-0.27-0.14-0.25-0.27l0.39-2.64H3.13c-0.38 0-0.14-0.31-0.14-0.33 0.53-0.94 1.33-2.3 2.36-4.14 0.04-0.08 0.12-0.12 0.22-0.12 0.13 0 0.25 0.14 0.23 0.27L5.43 4.16h1.45c0.17 0 0.27 0.08 0.17 0.27l-2.4 4.2C4.6 8.71 4.53 8.75 4.45 8.75Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="14dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="14"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFD1D5DB"
|
||||||
|
android:pathData="M3.64 18.81l1.04 1.01L10.5 14 4.68 8.18l-1.04 1L8.42 14l-4.78 4.81Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -6,6 +6,13 @@ import '../error/exceptions.dart';
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
final Dio dio;
|
final Dio dio;
|
||||||
|
|
||||||
|
// 缓存 SharedPreferences 实例,避免每次请求都阻塞读取
|
||||||
|
static SharedPreferences? _prefsCache;
|
||||||
|
static Future<SharedPreferences>? _prefsFuture;
|
||||||
|
|
||||||
|
// 全局回调,用于处理 401 跳转登录
|
||||||
|
static void Function()? onUnauthorized;
|
||||||
|
|
||||||
ApiClient({required this.dio}) {
|
ApiClient({required this.dio}) {
|
||||||
dio.options = BaseOptions(
|
dio.options = BaseOptions(
|
||||||
baseUrl: AppConstants.baseUrl,
|
baseUrl: AppConstants.baseUrl,
|
||||||
|
|
@ -20,17 +27,38 @@ class ApiClient {
|
||||||
// 添加请求拦截器,自动注入 token
|
// 添加请求拦截器,自动注入 token
|
||||||
dio.interceptors.add(InterceptorsWrapper(
|
dio.interceptors.add(InterceptorsWrapper(
|
||||||
onRequest: (options, handler) async {
|
onRequest: (options, handler) async {
|
||||||
// 每次请求都从存储中读取最新的 token,确保登录后立即生效
|
// 使用缓存的 SharedPreferences 实例,避免阻塞
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await _getPrefs();
|
||||||
final token = prefs.getString('access_token');
|
final token = prefs.getString('access_token');
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
options.headers['Authorization'] = 'Bearer $token';
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
}
|
}
|
||||||
handler.next(options);
|
handler.next(options);
|
||||||
},
|
},
|
||||||
|
onError: (error, handler) {
|
||||||
|
// 全局处理 401 错误
|
||||||
|
if (error.response?.statusCode == 401) {
|
||||||
|
onUnauthorized?.call();
|
||||||
|
}
|
||||||
|
handler.next(error);
|
||||||
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取缓存的 SharedPreferences 实例
|
||||||
|
static Future<SharedPreferences> _getPrefs() async {
|
||||||
|
if (_prefsCache != null) return _prefsCache!;
|
||||||
|
_prefsFuture ??= SharedPreferences.getInstance();
|
||||||
|
_prefsCache = await _prefsFuture;
|
||||||
|
return _prefsCache!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除缓存(登录/登出时调用)
|
||||||
|
static void clearPrefsCache() {
|
||||||
|
_prefsCache = null;
|
||||||
|
_prefsFuture = null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Response> get(
|
Future<Response> get(
|
||||||
String path, {
|
String path, {
|
||||||
Map<String, dynamic>? queryParameters,
|
Map<String, dynamic>? queryParameters,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'core/di/injection.dart';
|
import 'core/di/injection.dart';
|
||||||
import 'core/router/app_router.dart';
|
import 'core/router/app_router.dart';
|
||||||
import 'core/constants/app_colors.dart';
|
import 'core/constants/app_colors.dart';
|
||||||
|
import 'core/network/api_client.dart';
|
||||||
|
import 'core/router/routes.dart';
|
||||||
|
import 'presentation/providers/user_providers.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
@ -17,11 +20,31 @@ void main() async {
|
||||||
runApp(const ProviderScope(child: MiningApp()));
|
runApp(const ProviderScope(child: MiningApp()));
|
||||||
}
|
}
|
||||||
|
|
||||||
class MiningApp extends ConsumerWidget {
|
class MiningApp extends ConsumerStatefulWidget {
|
||||||
const MiningApp({super.key});
|
const MiningApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<MiningApp> createState() => _MiningAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MiningAppState extends ConsumerState<MiningApp> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// 设置 401 全局处理回调
|
||||||
|
ApiClient.onUnauthorized = _handleUnauthorized;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleUnauthorized() {
|
||||||
|
// 清除用户状态
|
||||||
|
ref.read(userNotifierProvider.notifier).logout();
|
||||||
|
// 跳转到登录页
|
||||||
|
final router = ref.read(appRouterProvider);
|
||||||
|
router.go(Routes.login);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final router = ref.watch(appRouterProvider);
|
final router = ref.watch(appRouterProvider);
|
||||||
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import '../../../core/constants/app_colors.dart';
|
||||||
import '../../../core/utils/format_utils.dart';
|
import '../../../core/utils/format_utils.dart';
|
||||||
import '../../providers/user_providers.dart';
|
import '../../providers/user_providers.dart';
|
||||||
import '../../providers/mining_providers.dart';
|
import '../../providers/mining_providers.dart';
|
||||||
|
import '../../widgets/shimmer_loading.dart';
|
||||||
|
|
||||||
class AssetPage extends ConsumerWidget {
|
class AssetPage extends ConsumerWidget {
|
||||||
const AssetPage({super.key});
|
const AssetPage({super.key});
|
||||||
|
|
@ -14,7 +15,6 @@ class AssetPage extends ConsumerWidget {
|
||||||
static const Color _grayText = Color(0xFF6B7280);
|
static const Color _grayText = Color(0xFF6B7280);
|
||||||
static const Color _darkText = Color(0xFF1F2937);
|
static const Color _darkText = Color(0xFF1F2937);
|
||||||
static const Color _bgGray = Color(0xFFF3F4F6);
|
static const Color _bgGray = Color(0xFFF3F4F6);
|
||||||
static const Color _lightGray = Color(0xFFF9FAFB);
|
|
||||||
static const Color _riverBed = Color(0xFF4B5563);
|
static const Color _riverBed = Color(0xFF4B5563);
|
||||||
static const Color _serenade = Color(0xFFFFF7ED);
|
static const Color _serenade = Color(0xFFFFF7ED);
|
||||||
static const Color _feta = Color(0xFFF0FDF4);
|
static const Color _feta = Color(0xFFF0FDF4);
|
||||||
|
|
@ -64,7 +64,7 @@ class AssetPage extends ConsumerWidget {
|
||||||
// 资产列表
|
// 资产列表
|
||||||
accountAsync.when(
|
accountAsync.when(
|
||||||
data: (account) => _buildAssetList(account),
|
data: (account) => _buildAssetList(account),
|
||||||
loading: () => _buildLoadingCard(),
|
loading: () => _buildAssetListSkeleton(),
|
||||||
error: (_, __) => const SizedBox.shrink(),
|
error: (_, __) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
@ -74,7 +74,7 @@ class AssetPage extends ConsumerWidget {
|
||||||
// 账户列表
|
// 账户列表
|
||||||
accountAsync.when(
|
accountAsync.when(
|
||||||
data: (account) => _buildAccountList(account),
|
data: (account) => _buildAccountList(account),
|
||||||
loading: () => _buildLoadingCard(),
|
loading: () => _buildAssetListSkeleton(),
|
||||||
error: (_, __) => const SizedBox.shrink(),
|
error: (_, __) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 100),
|
const SizedBox(height: 100),
|
||||||
|
|
@ -767,13 +767,18 @@ class AssetPage extends ConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLoadingCard() {
|
Widget _buildLoadingCard() {
|
||||||
return Container(
|
return const AssetCardSkeleton();
|
||||||
padding: const EdgeInsets.all(32),
|
}
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
Widget _buildAssetListSkeleton() {
|
||||||
borderRadius: BorderRadius.circular(16),
|
return Column(
|
||||||
),
|
children: const [
|
||||||
child: const Center(child: CircularProgressIndicator()),
|
AssetItemSkeleton(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
AssetItemSkeleton(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
AssetItemSkeleton(),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import '../../../domain/entities/contribution.dart';
|
||||||
import '../../../domain/entities/contribution_record.dart';
|
import '../../../domain/entities/contribution_record.dart';
|
||||||
import '../../providers/user_providers.dart';
|
import '../../providers/user_providers.dart';
|
||||||
import '../../providers/contribution_providers.dart';
|
import '../../providers/contribution_providers.dart';
|
||||||
|
import '../../widgets/shimmer_loading.dart';
|
||||||
|
|
||||||
class ContributionPage extends ConsumerWidget {
|
class ContributionPage extends ConsumerWidget {
|
||||||
const ContributionPage({super.key});
|
const ContributionPage({super.key});
|
||||||
|
|
@ -75,7 +76,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const PageSkeleton(),
|
||||||
error: (error, _) => Center(
|
error: (error, _) => Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|
@ -382,12 +383,16 @@ class ContributionPage extends ConsumerWidget {
|
||||||
}).toList(),
|
}).toList(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Padding(
|
loading: () => Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 20),
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||||
child: SizedBox(
|
child: ShimmerLoading(
|
||||||
width: 24,
|
child: Column(
|
||||||
height: 24,
|
children: const [
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
ShimmerBox(height: 48),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
ShimmerBox(height: 48),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
error: (error, _) => Padding(
|
error: (error, _) => Padding(
|
||||||
|
|
|
||||||
|
|
@ -25,28 +25,21 @@ class _SplashPageState extends ConsumerState<SplashPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initialize() async {
|
Future<void> _initialize() async {
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
// 最多等 500ms 展示品牌,然后立即跳转,不阻塞用户
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
// 检查用户登录状态
|
|
||||||
final userState = ref.read(userNotifierProvider);
|
final userState = ref.read(userNotifierProvider);
|
||||||
|
|
||||||
if (userState.isLoggedIn) {
|
if (userState.isLoggedIn) {
|
||||||
// 已登录,尝试刷新token
|
// 立即跳转,不等待 token 刷新
|
||||||
try {
|
|
||||||
await ref.read(userNotifierProvider.notifier).refreshTokenIfNeeded();
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
context.go(Routes.contribution);
|
context.go(Routes.contribution);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
// 后台刷新 token,失败了由 API 拦截器处理 401
|
||||||
// token刷新失败,跳转到登录页
|
ref.read(userNotifierProvider.notifier).refreshTokenIfNeeded();
|
||||||
if (mounted) {
|
|
||||||
context.go(Routes.login);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 未登录,跳转到登录页
|
|
||||||
context.go(Routes.login);
|
context.go(Routes.login);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import '../../../core/utils/format_utils.dart';
|
||||||
import '../../providers/mining_providers.dart';
|
import '../../providers/mining_providers.dart';
|
||||||
import '../../providers/user_providers.dart';
|
import '../../providers/user_providers.dart';
|
||||||
import '../../providers/trading_providers.dart';
|
import '../../providers/trading_providers.dart';
|
||||||
|
import '../../widgets/shimmer_loading.dart';
|
||||||
|
|
||||||
class TradingPage extends ConsumerStatefulWidget {
|
class TradingPage extends ConsumerStatefulWidget {
|
||||||
const TradingPage({super.key});
|
const TradingPage({super.key});
|
||||||
|
|
@ -767,14 +768,25 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLoadingCard() {
|
Widget _buildLoadingCard() {
|
||||||
return Container(
|
return ShimmerLoading(
|
||||||
|
child: Container(
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: const Center(child: CircularProgressIndicator()),
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: const [
|
||||||
|
ShimmerBox(width: 100, height: 16),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
ShimmerBox(width: 150, height: 28),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
ShimmerBox(width: 80, height: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -830,7 +842,9 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
);
|
);
|
||||||
if (success) {
|
if (success) {
|
||||||
_amountController.clear();
|
_amountController.clear();
|
||||||
|
// 交易成功后刷新所有相关数据
|
||||||
ref.invalidate(shareAccountProvider(accountSequence));
|
ref.invalidate(shareAccountProvider(accountSequence));
|
||||||
|
ref.invalidate(globalStateProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../domain/entities/contribution.dart';
|
import '../../domain/entities/contribution.dart';
|
||||||
import '../../domain/entities/contribution_record.dart';
|
import '../../domain/entities/contribution_record.dart';
|
||||||
|
|
@ -15,8 +16,21 @@ final contributionRepositoryProvider = Provider<ContributionRepository>((ref) {
|
||||||
|
|
||||||
final contributionProvider = FutureProvider.family<Contribution?, String>(
|
final contributionProvider = FutureProvider.family<Contribution?, String>(
|
||||||
(ref, accountSequence) async {
|
(ref, accountSequence) async {
|
||||||
|
// 空字符串不请求
|
||||||
|
if (accountSequence.isEmpty) return null;
|
||||||
|
|
||||||
final useCase = ref.watch(getUserContributionUseCaseProvider);
|
final useCase = ref.watch(getUserContributionUseCaseProvider);
|
||||||
final result = await useCase(accountSequence);
|
final result = await useCase(accountSequence);
|
||||||
|
|
||||||
|
// 保持 provider 活跃,避免重复请求
|
||||||
|
ref.keepAlive();
|
||||||
|
|
||||||
|
// 5 分钟后自动失效
|
||||||
|
final timer = Timer(const Duration(minutes: 5), () {
|
||||||
|
ref.invalidateSelf();
|
||||||
|
});
|
||||||
|
ref.onDispose(() => timer.cancel());
|
||||||
|
|
||||||
return result.fold(
|
return result.fold(
|
||||||
(failure) => throw Exception(failure.message),
|
(failure) => throw Exception(failure.message),
|
||||||
(contribution) => contribution,
|
(contribution) => contribution,
|
||||||
|
|
@ -52,12 +66,25 @@ class ContributionRecordsParams {
|
||||||
/// 贡献值记录 Provider
|
/// 贡献值记录 Provider
|
||||||
final contributionRecordsProvider = FutureProvider.family<ContributionRecordsPage?, ContributionRecordsParams>(
|
final contributionRecordsProvider = FutureProvider.family<ContributionRecordsPage?, ContributionRecordsParams>(
|
||||||
(ref, params) async {
|
(ref, params) async {
|
||||||
|
// 空字符串不请求
|
||||||
|
if (params.accountSequence.isEmpty) return null;
|
||||||
|
|
||||||
final repository = ref.watch(contributionRepositoryProvider);
|
final repository = ref.watch(contributionRepositoryProvider);
|
||||||
final result = await repository.getContributionRecords(
|
final result = await repository.getContributionRecords(
|
||||||
params.accountSequence,
|
params.accountSequence,
|
||||||
page: params.page,
|
page: params.page,
|
||||||
pageSize: params.pageSize,
|
pageSize: params.pageSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 保持 provider 活跃
|
||||||
|
ref.keepAlive();
|
||||||
|
|
||||||
|
// 5 分钟后自动失效
|
||||||
|
final timer = Timer(const Duration(minutes: 5), () {
|
||||||
|
ref.invalidateSelf();
|
||||||
|
});
|
||||||
|
ref.onDispose(() => timer.cancel());
|
||||||
|
|
||||||
return result.fold(
|
return result.fold(
|
||||||
(failure) => throw Exception(failure.message),
|
(failure) => throw Exception(failure.message),
|
||||||
(records) => records,
|
(records) => records,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../domain/entities/share_account.dart';
|
import '../../domain/entities/share_account.dart';
|
||||||
import '../../domain/entities/global_state.dart';
|
import '../../domain/entities/global_state.dart';
|
||||||
|
|
@ -14,11 +15,24 @@ final getGlobalStateUseCaseProvider = Provider<GetGlobalState>((ref) {
|
||||||
return getIt<GetGlobalState>();
|
return getIt<GetGlobalState>();
|
||||||
});
|
});
|
||||||
|
|
||||||
// State Providers
|
// State Providers - 使用 keepAlive 缓存数据,避免重复请求
|
||||||
final shareAccountProvider = FutureProvider.family<ShareAccount?, String>(
|
final shareAccountProvider = FutureProvider.family<ShareAccount?, String>(
|
||||||
(ref, accountSequence) async {
|
(ref, accountSequence) async {
|
||||||
|
// 空字符串不请求
|
||||||
|
if (accountSequence.isEmpty) return null;
|
||||||
|
|
||||||
final useCase = ref.watch(getShareAccountUseCaseProvider);
|
final useCase = ref.watch(getShareAccountUseCaseProvider);
|
||||||
final result = await useCase(accountSequence);
|
final result = await useCase(accountSequence);
|
||||||
|
|
||||||
|
// 保持 provider 活跃,避免每次 rebuild 都重新请求
|
||||||
|
ref.keepAlive();
|
||||||
|
|
||||||
|
// 5 分钟后自动失效,允许刷新
|
||||||
|
final timer = Timer(const Duration(minutes: 5), () {
|
||||||
|
ref.invalidateSelf();
|
||||||
|
});
|
||||||
|
ref.onDispose(() => timer.cancel());
|
||||||
|
|
||||||
return result.fold(
|
return result.fold(
|
||||||
(failure) => throw Exception(failure.message),
|
(failure) => throw Exception(failure.message),
|
||||||
(account) => account,
|
(account) => account,
|
||||||
|
|
@ -29,6 +43,16 @@ final shareAccountProvider = FutureProvider.family<ShareAccount?, String>(
|
||||||
final globalStateProvider = FutureProvider<GlobalState?>((ref) async {
|
final globalStateProvider = FutureProvider<GlobalState?>((ref) async {
|
||||||
final useCase = ref.watch(getGlobalStateUseCaseProvider);
|
final useCase = ref.watch(getGlobalStateUseCaseProvider);
|
||||||
final result = await useCase();
|
final result = await useCase();
|
||||||
|
|
||||||
|
// 保持 provider 活跃
|
||||||
|
ref.keepAlive();
|
||||||
|
|
||||||
|
// 5 分钟后自动失效
|
||||||
|
final timer = Timer(const Duration(minutes: 5), () {
|
||||||
|
ref.invalidateSelf();
|
||||||
|
});
|
||||||
|
ref.onDispose(() => timer.cancel());
|
||||||
|
|
||||||
return result.fold(
|
return result.fold(
|
||||||
(failure) => throw Exception(failure.message),
|
(failure) => throw Exception(failure.message),
|
||||||
(state) => state,
|
(state) => state,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 骨架屏加载组件 - 提供更好的加载体验
|
||||||
|
class ShimmerLoading extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
|
const ShimmerLoading({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.isLoading = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShimmerLoading> createState() => _ShimmerLoadingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShimmerLoadingState extends State<ShimmerLoading>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 1500),
|
||||||
|
vsync: this,
|
||||||
|
)..repeat();
|
||||||
|
_animation = Tween<double>(begin: -2, end: 2).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!widget.isLoading) return widget.child;
|
||||||
|
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _animation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return ShaderMask(
|
||||||
|
shaderCallback: (bounds) {
|
||||||
|
return LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: const [
|
||||||
|
Color(0xFFEBEBF4),
|
||||||
|
Color(0xFFF4F4F4),
|
||||||
|
Color(0xFFEBEBF4),
|
||||||
|
],
|
||||||
|
stops: [
|
||||||
|
_animation.value - 1,
|
||||||
|
_animation.value,
|
||||||
|
_animation.value + 1,
|
||||||
|
].map((e) => e.clamp(0.0, 1.0)).toList(),
|
||||||
|
).createShader(bounds);
|
||||||
|
},
|
||||||
|
blendMode: BlendMode.srcATop,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 骨架屏占位框
|
||||||
|
class ShimmerBox extends StatelessWidget {
|
||||||
|
final double? width;
|
||||||
|
final double height;
|
||||||
|
final double borderRadius;
|
||||||
|
|
||||||
|
const ShimmerBox({
|
||||||
|
super.key,
|
||||||
|
this.width,
|
||||||
|
required this.height,
|
||||||
|
this.borderRadius = 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE5E7EB),
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 资产卡片骨架屏
|
||||||
|
class AssetCardSkeleton extends StatelessWidget {
|
||||||
|
const AssetCardSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ShimmerLoading(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.04),
|
||||||
|
blurRadius: 30,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const ShimmerBox(width: 80, height: 16),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const ShimmerBox(width: 180, height: 36),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const ShimmerBox(width: 120, height: 14),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const ShimmerBox(width: 100, height: 24, borderRadius: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 资产项目骨架屏
|
||||||
|
class AssetItemSkeleton extends StatelessWidget {
|
||||||
|
const AssetItemSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ShimmerLoading(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 2,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const ShimmerBox(width: 40, height: 40, borderRadius: 20),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const ShimmerBox(width: 60, height: 16),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const ShimmerBox(width: 100, height: 20),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const ShimmerBox(width: 80, height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const ShimmerBox(width: 14, height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 收益统计骨架屏
|
||||||
|
class EarningsStatsSkeleton extends StatelessWidget {
|
||||||
|
const EarningsStatsSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ShimmerLoading(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 2,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 4,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE5E7EB),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const ShimmerBox(width: 60, height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: const [
|
||||||
|
ShimmerBox(width: 50, height: 12),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
ShimmerBox(width: 70, height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
height: 40,
|
||||||
|
color: const Color(0xFFE5E7EB),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: const [
|
||||||
|
ShimmerBox(width: 50, height: 12),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
ShimmerBox(width: 70, height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
height: 40,
|
||||||
|
color: const Color(0xFFE5E7EB),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: const [
|
||||||
|
ShimmerBox(width: 50, height: 12),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
ShimmerBox(width: 70, height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 页面骨架屏
|
||||||
|
class PageSkeleton extends StatelessWidget {
|
||||||
|
const PageSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: const [
|
||||||
|
AssetCardSkeleton(),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
AssetItemSkeleton(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
AssetItemSkeleton(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
AssetItemSkeleton(),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
EarningsStatsSkeleton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||