fix(mobile): avatar URL handling + API paths + guide page layout

1. Avatar display:
   - Detect if _avatarSvg is URL or SVG string
   - Use Image.network for URLs, SvgPicture.string for SVG

2. API endpoints:
   - Remove /api/v1 prefix from endpoints (already in baseUrl)
   - Fixes duplicate /api/v1/api/v1 in requests

3. Guide page:
   - Restore BoxFit.cover for fullscreen images
   - Fix page indicator positioning with SafeArea

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-09 18:50:34 -08:00
parent cee2626a53
commit 91e54946ba
3 changed files with 134 additions and 102 deletions

View File

@ -1,38 +1,39 @@
/// API
///
/// : /api/v1 AppConfig.apiBaseUrl
/// baseUrl = 'https://rwaapi.szaiai.com/api/v1'
class ApiEndpoints { class ApiEndpoints {
ApiEndpoints._(); ApiEndpoints._();
// Base URL - // Base URL - (使 AppConfig)
static const String baseUrl = 'https://rwaapi.szaiai.com'; static const String baseUrl = 'https://rwaapi.szaiai.com';
// Base URL - // Base URL -
static const String baseUrlDev = 'https://rwaapi-dev.szaiai.com'; static const String baseUrlDev = 'https://rwaapi-dev.szaiai.com';
// API
static const String apiPrefix = '/api/v1';
// Auth (-> Identity Service) // Auth (-> Identity Service)
static const String auth = '$apiPrefix/auth'; static const String auth = '/auth';
static const String login = '$auth/login'; static const String login = '$auth/login';
static const String register = '$auth/register'; static const String register = '$auth/register';
static const String refreshToken = '$auth/refresh'; static const String refreshToken = '$auth/refresh';
static const String logout = '$auth/logout'; static const String logout = '$auth/logout';
// User (-> Identity Service) // User (-> Identity Service)
static const String user = '$apiPrefix/user'; static const String user = '/user';
static const String autoCreate = '$user/auto-create'; static const String autoCreate = '$user/auto-create';
static const String userWallet = '$user/wallet'; // static const String userWallet = '$user/wallet'; //
static const String me = '$apiPrefix/me'; // static const String me = '/me'; //
static const String profile = '$user/profile'; static const String profile = '$user/profile';
static const String updateProfile = '$user/profile/update'; static const String updateProfile = '$user/profile/update';
static const String updateAvatar = '$user/avatar'; static const String updateAvatar = '$user/avatar';
// Wallet (-> Wallet Service) // Wallet (-> Wallet Service)
static const String wallet = '$apiPrefix/wallet'; static const String wallet = '/wallet';
static const String createWallet = '$wallet/create'; static const String createWallet = '$wallet/create';
static const String importWallet = '$wallet/import'; static const String importWallet = '$wallet/import';
static const String balance = '$wallet/balance'; static const String balance = '$wallet/balance';
// Mining (-> Reward Service) // Mining (-> Reward Service)
static const String mining = '$apiPrefix/mining'; static const String mining = '/mining';
static const String miningStatus = '$mining/status'; static const String miningStatus = '$mining/status';
static const String startMining = '$mining/start'; static const String startMining = '$mining/start';
static const String stopMining = '$mining/stop'; static const String stopMining = '$mining/stop';
@ -40,33 +41,33 @@ class ApiEndpoints {
static const String hashPower = '$mining/hash-power'; static const String hashPower = '$mining/hash-power';
// Ranking (-> Leaderboard Service) // Ranking (-> Leaderboard Service)
static const String ranking = '$apiPrefix/ranking'; static const String ranking = '/ranking';
static const String dailyRanking = '$ranking/daily'; static const String dailyRanking = '$ranking/daily';
static const String weeklyRanking = '$ranking/weekly'; static const String weeklyRanking = '$ranking/weekly';
static const String monthlyRanking = '$ranking/monthly'; static const String monthlyRanking = '$ranking/monthly';
// Trading (-> Wallet Service) // Trading (-> Wallet Service)
static const String trading = '$apiPrefix/trading'; static const String trading = '/trading';
static const String exchange = '$trading/exchange'; static const String exchange = '$trading/exchange';
static const String settlement = '$trading/settlement'; static const String settlement = '$trading/settlement';
static const String transactions = '$trading/transactions'; static const String transactions = '$trading/transactions';
// Deposit (-> Wallet Service) // Deposit (-> Wallet Service)
static const String deposit = '$apiPrefix/deposit'; static const String deposit = '/deposit';
static const String depositAddress = '$deposit/address'; static const String depositAddress = '$deposit/address';
static const String confirmDeposit = '$deposit/confirm'; static const String confirmDeposit = '$deposit/confirm';
static const String depositRecords = '$deposit/records'; static const String depositRecords = '$deposit/records';
// Planting (-> Planting Service) // Planting (-> Planting Service)
static const String planting = '$apiPrefix/planting'; static const String planting = '/planting';
static const String plantingPrice = '$planting/price'; static const String plantingPrice = '$planting/price';
static const String submitPlanting = '$planting/submit'; static const String submitPlanting = '$planting/submit';
static const String provinces = '$planting/provinces'; static const String provinces = '$planting/provinces';
static const String cities = '$planting/cities'; static const String cities = '$planting/cities';
// Community & Referral (-> Referral Service) // Community & Referral (-> Referral Service)
static const String community = '$apiPrefix/community'; static const String community = '/community';
static const String referral = '$apiPrefix/referral'; static const String referral = '/referral';
static const String referralMe = '$referral/me'; // static const String referralMe = '$referral/me'; //
static const String referralDirects = '$referral/me/direct-referrals'; // static const String referralDirects = '$referral/me/direct-referrals'; //
static const String referralList = '$community/referrals'; static const String referralList = '$community/referrals';
@ -75,11 +76,11 @@ class ApiEndpoints {
static const String generateReferralLink = '$referral/generate-link'; static const String generateReferralLink = '$referral/generate-link';
// Authorization (-> Authorization Service) // Authorization (-> Authorization Service)
static const String authorizations = '$apiPrefix/authorizations'; static const String authorizations = '/authorizations';
static const String myAuthorizations = '$authorizations/my'; // static const String myAuthorizations = '$authorizations/my'; //
// Telemetry (-> Reporting Service) // Telemetry (-> Reporting Service)
static const String telemetry = '$apiPrefix/telemetry'; static const String telemetry = '/telemetry';
static const String telemetrySession = '$telemetry/session'; static const String telemetrySession = '$telemetry/session';
static const String telemetryHeartbeat = '$telemetry/heartbeat'; static const String telemetryHeartbeat = '$telemetry/heartbeat';
static const String telemetryEvents = '$telemetry/events'; static const String telemetryEvents = '$telemetry/events';

View File

@ -146,50 +146,45 @@ class _GuidePageState extends ConsumerState<GuidePage> {
} }
/// (1-4) - /// (1-4) -
/// 使 BoxFit.fitWidth /// 使 BoxFit.cover
Widget _buildGuidePage(GuidePageData data, int index) { Widget _buildGuidePage(GuidePageData data, int index) {
debugPrint('[GuidePage] _buildGuidePage() - 页面 ${index + 1}, 图片: ${data.imagePath}'); debugPrint('[GuidePage] _buildGuidePage() - 页面 ${index + 1}, 图片: ${data.imagePath}');
return Container( return Stack(
color: Colors.black, // fit: StackFit.expand,
child: Stack( children: [
fit: StackFit.expand, // - 使 cover
children: [ if (data.imagePath != null)
// - 使 fitWidth Image.asset(
if (data.imagePath != null) data.imagePath!,
LayoutBuilder( fit: BoxFit.cover,
builder: (context, constraints) { width: double.infinity,
debugPrint('[GuidePage] 页面 ${index + 1} 容器尺寸: ${constraints.maxWidth.toStringAsFixed(1)} x ${constraints.maxHeight.toStringAsFixed(1)}'); height: double.infinity,
debugPrint('[GuidePage] 页面 ${index + 1} 使用 BoxFit.fitWidth'); errorBuilder: (context, error, stackTrace) {
return Center( debugPrint('[GuidePage] 页面 ${index + 1} 图片加载失败: $error');
child: Image.asset( return Container(
data.imagePath!, color: const Color(0xFFFFF8E7),
fit: BoxFit.fitWidth, child: _buildPlaceholderImage(index),
width: double.infinity, );
errorBuilder: (context, error, stackTrace) { },
debugPrint('[GuidePage] 页面 ${index + 1} 图片加载失败: $error'); )
return Container( else
color: const Color(0xFFFFF8E7), Container(
child: _buildPlaceholderImage(index), color: const Color(0xFFFFF8E7),
); child: _buildPlaceholderImage(index),
},
),
);
},
)
else
Container(
color: const Color(0xFFFFF8E7),
child: _buildPlaceholderImage(index),
),
//
Positioned(
left: 0,
right: 0,
bottom: 80.h,
child: _buildPageIndicator(),
), ),
], // - 使 SafeArea UI
), Positioned(
left: 0,
right: 0,
bottom: 0,
child: SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: 40.h),
child: _buildPageIndicator(),
),
),
),
],
); );
} }
@ -423,42 +418,39 @@ class _WelcomePageContentState extends ConsumerState<_WelcomePageContent> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), onTap: () => FocusScope.of(context).unfocus(),
child: Container( child: Stack(
color: Colors.black, // fit: StackFit.expand,
child: Stack( children: [
fit: StackFit.expand, // - 使 cover
children: [ if (widget.backgroundImage != null)
// - 使 fitWidth Image.asset(
if (widget.backgroundImage != null) widget.backgroundImage!,
Center( fit: BoxFit.cover,
child: Image.asset( width: double.infinity,
widget.backgroundImage!, height: double.infinity,
fit: BoxFit.fitWidth, errorBuilder: (context, error, stackTrace) {
width: double.infinity, return Container(
errorBuilder: (context, error, stackTrace) { color: const Color(0xFFFFF8E7),
return Container( );
color: const Color(0xFFFFF8E7), },
); )
}, else
),
)
else
Container(
color: const Color(0xFFFFF8E7),
),
//
Container( Container(
decoration: BoxDecoration( color: const Color(0xFFFFF8E7),
gradient: LinearGradient( ),
begin: Alignment.topCenter, //
end: Alignment.bottomCenter, Container(
colors: [ decoration: BoxDecoration(
Colors.black.withValues(alpha: 0.3), gradient: LinearGradient(
Colors.black.withValues(alpha: 0.6), begin: Alignment.topCenter,
], end: Alignment.bottomCenter,
), colors: [
Colors.black.withValues(alpha: 0.3),
Colors.black.withValues(alpha: 0.6),
],
), ),
), ),
),
// //
SafeArea( SafeArea(
child: LayoutBuilder( child: LayoutBuilder(
@ -567,7 +559,6 @@ class _WelcomePageContentState extends ConsumerState<_WelcomePageContent> {
), ),
), ),
], ],
),
), ),
); );
} }

View File

@ -637,18 +637,58 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
} }
/// SVG或默认头像 /// SVG或默认头像
/// _avatarSvg URL SVG
Widget _buildSvgOrDefaultAvatar() { Widget _buildSvgOrDefaultAvatar() {
if (_avatarSvg != null) { if (_avatarSvg != null && _avatarSvg!.isNotEmpty) {
debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 使用SVG头像长度=${_avatarSvg!.length}'); debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 检查头像内容,长度=${_avatarSvg!.length}');
debugPrint('[ProfilePage] SVG前50字符: ${_avatarSvg!.substring(0, _avatarSvg!.length > 50 ? 50 : _avatarSvg!.length)}'); debugPrint('[ProfilePage] 内容前50字符: ${_avatarSvg!.substring(0, _avatarSvg!.length > 50 ? 50 : _avatarSvg!.length)}');
return SvgPicture.string(
_avatarSvg!, // URL
width: 80, if (_avatarSvg!.startsWith('http://') || _avatarSvg!.startsWith('https://')) {
height: 80, debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 检测到是URL使用网络图片');
fit: BoxFit.cover, return Image.network(
); _avatarSvg!,
width: 80,
height: 80,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
),
);
},
errorBuilder: (context, error, stackTrace) {
debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 网络图片加载失败: $error');
return const Icon(
Icons.person,
size: 40,
color: Color(0xFF8B5A2B),
);
},
);
}
// SVG SVG
if (_avatarSvg!.contains('<svg') || _avatarSvg!.startsWith('<?xml')) {
debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 检测到是SVG字符串使用SvgPicture');
return SvgPicture.string(
_avatarSvg!,
width: 80,
height: 80,
fit: BoxFit.cover,
);
}
debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 未知格式,使用默认图标');
} }
debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 无SVG使用默认图标'); debugPrint('[ProfilePage] _buildSvgOrDefaultAvatar() - 无头像数据,使用默认图标');
return const Icon( return const Icon(
Icons.person, Icons.person,
size: 40, size: 40,