show member guard

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-04-05 12:10:18 +08:00
parent 279f21857d
commit c0b55f9af3
15 changed files with 776 additions and 300 deletions

View File

@@ -5,11 +5,11 @@ import 'package:flutter/material.dart';
Widget avatars({ Widget avatars({
required ColorScheme colorScheme, required ColorScheme colorScheme,
required Iterable<Owner> users, required Iterable<Owner> users,
double gap = 6.0,
}) { }) {
const gap = 6.0;
const size = 22.0; const size = 22.0;
const padding = 0.8; const padding = 0.8;
const offset = size - gap; final offset = size - gap;
const imgSize = size - 2 * padding; const imgSize = size - 2 * padding;
if (users.length == 1) { if (users.length == 1) {
return NetworkImgLayer( return NetworkImgLayer(

View File

@@ -997,4 +997,7 @@ abstract final class Api {
static const String liveMedalWall = static const String liveMedalWall =
'${HttpString.liveBaseUrl}/xlive/web-ucenter/user/MedalWall'; '${HttpString.liveBaseUrl}/xlive/web-ucenter/user/MedalWall';
static const String memberGuard =
'${HttpString.liveBaseUrl}/xlive/app-ucenter/v1/guard/MainGuardCardAll';
} }

View File

@@ -21,6 +21,7 @@ import 'package:PiliPlus/models_new/member/coin_like_arc/data.dart';
import 'package:PiliPlus/models_new/member/search_archive/data.dart'; import 'package:PiliPlus/models_new/member/search_archive/data.dart';
import 'package:PiliPlus/models_new/member/season_web/data.dart'; import 'package:PiliPlus/models_new/member/season_web/data.dart';
import 'package:PiliPlus/models_new/member_card_info/data.dart'; import 'package:PiliPlus/models_new/member_card_info/data.dart';
import 'package:PiliPlus/models_new/member_guard/data.dart';
import 'package:PiliPlus/models_new/space/space/data.dart'; import 'package:PiliPlus/models_new/space/space/data.dart';
import 'package:PiliPlus/models_new/space/space_archive/data.dart'; import 'package:PiliPlus/models_new/space/space_archive/data.dart';
import 'package:PiliPlus/models_new/space/space_article/data.dart'; import 'package:PiliPlus/models_new/space/space_article/data.dart';
@@ -830,4 +831,23 @@ abstract final class MemberHttp {
return Error(res.data['message']); return Error(res.data['message']);
} }
} }
static Future<LoadingState<MemberGuardData>> memberGuard({
required Object ruid,
required int page,
}) async {
final res = await Request().get(
Api.memberGuard,
queryParameters: {
'page': page,
'page_size': 20,
'ruid': ruid,
},
);
if (res.data['code'] == 0) {
return Success(MemberGuardData.fromJson(res.data['data']));
} else {
return Error(res.data['message']);
}
}
} }

View File

@@ -0,0 +1,19 @@
import 'package:PiliPlus/models_new/member_guard/guard_top_list.dart';
class MemberGuardData {
List<GuardItem> guardTopList;
int? hasMore;
MemberGuardData({
required this.guardTopList,
this.hasMore,
});
factory MemberGuardData.fromJson(Map<String, dynamic> json) =>
MemberGuardData(
guardTopList: (json['guard_top_list'] as List<dynamic>)
.map((e) => GuardItem.fromJson(e as Map<String, dynamic>))
.toList(),
hasMore: json['has_more'] as int?,
);
}

View File

@@ -0,0 +1,20 @@
class GuardItem {
int uid;
String username;
String face;
int guardLevel;
GuardItem({
required this.uid,
required this.username,
required this.face,
required this.guardLevel,
});
factory GuardItem.fromJson(Map<String, dynamic> json) => GuardItem(
uid: json['uid'],
username: json['username'],
face: json['face'],
guardLevel: json['guard_level'],
);
}

View File

@@ -1,3 +1,5 @@
import 'package:PiliPlus/models/model_owner.dart';
class Elec { class Elec {
int? total; int? total;
List<ElecItem>? list; List<ElecItem>? list;
@@ -15,9 +17,11 @@ class Elec {
); );
} }
class ElecItem { class ElecItem extends Owner {
String? uname; String? uname;
String? avatar; String? avatar;
@override
String? get face => avatar;
ElecItem({ ElecItem({
this.uname, this.uname,

View File

@@ -2,20 +2,17 @@ import 'package:PiliPlus/models/model_owner.dart';
class Guard { class Guard {
String? uri; String? uri;
String? desc; Object? count;
List<Owner>? item; List<Owner>? item;
Guard({ Guard.fromJson(Map<String, dynamic> json) {
this.uri, uri = json['uri'] as String?;
this.desc, item = (json['item'] as List<dynamic>?)
this.item,
});
factory Guard.fromJson(Map<String, dynamic> json) => Guard(
uri: json['uri'] as String?,
desc: json['desc'] as String?,
item: (json['item'] as List<dynamic>?)
?.map((e) => Owner.fromJson(e as Map<String, dynamic>)) ?.map((e) => Owner.fromJson(e as Map<String, dynamic>))
.toList(), .toList();
); final String? desc = json['desc'];
if (desc != null) {
count = RegExp(r'^(\d+)').firstMatch(desc)?.group(1);
}
}
} }

View File

@@ -5,7 +5,9 @@ import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/common/member/tab_type.dart'; import 'package:PiliPlus/models/common/member/tab_type.dart';
import 'package:PiliPlus/models/model_owner.dart';
import 'package:PiliPlus/models_new/space/space/data.dart'; import 'package:PiliPlus/models_new/space/space/data.dart';
import 'package:PiliPlus/models_new/space/space/elec.dart';
import 'package:PiliPlus/models_new/space/space/live.dart'; import 'package:PiliPlus/models_new/space/space/live.dart';
import 'package:PiliPlus/models_new/space/space/setting.dart'; import 'package:PiliPlus/models_new/space/space/setting.dart';
import 'package:PiliPlus/models_new/space/space/tab2.dart'; import 'package:PiliPlus/models_new/space/space/tab2.dart';
@@ -43,7 +45,13 @@ class MemberController extends CommonDataController<SpaceData, SpaceData?>
bool? hasSeasonOrSeries; bool? hasSeasonOrSeries;
late bool hasCharge = false; List<ElecItem>? charges;
int? chargeCount;
bool get hasCharge => chargeCount != null && chargeCount! > 0;
List<Owner>? guards;
Object? guardCount;
bool get hasGuard => guards?.isNotEmpty ?? false;
final fromViewAid = Get.parameters['from_view_aid']; final fromViewAid = Get.parameters['from_view_aid'];
@@ -58,21 +66,30 @@ class MemberController extends CommonDataController<SpaceData, SpaceData?>
@override @override
bool customHandleResponse(bool isRefresh, Success<SpaceData> response) { bool customHandleResponse(bool isRefresh, Success<SpaceData> response) {
final data = response.response; final data = response.response;
username = data.card?.name ?? ''; final card = data.card;
isFollowed = data.card?.relation?.isFollowed; username = card?.name ?? '';
hasCharge = (data.elec?.total ?? 0) > 0; isFollowed = card?.relation?.isFollowed;
// charge
final elec = data.elec;
charges = elec?.list;
chargeCount = elec?.total;
// guard
final guard = data.guard;
guards = guard?.item;
guardCount = guard?.count;
if (data.relation == -1) { if (data.relation == -1) {
relation.value = 128; relation.value = 128;
} else { } else {
relation.value = data.card?.relation?.isFollow == 1 relation.value = card?.relation?.isFollow == 1
? data.relSpecial == 1 ? data.relSpecial == 1
? -10 ? -10
: data.card?.relation?.status ?? 2 : card?.relation?.status ?? 2
: 0; : 0;
} }
tab2 = data.tab2; tab2 = data.tab2;
live = data.live; live = data.live;
silence = data.card?.silence; silence = card?.silence;
if ((data.ugcSeason?.count != null && data.ugcSeason?.count != 0) || if ((data.ugcSeason?.count != null && data.ugcSeason?.count != 0) ||
data.series?.item?.isNotEmpty == true) { data.series?.item?.isNotEmpty == true) {
hasSeasonOrSeries = true; hasSeasonOrSeries = true;

View File

@@ -103,6 +103,10 @@ class _MemberPageState extends State<MemberPage> {
silence: _userController.silence, silence: _userController.silence,
headerControllerBuilder: getHeaderController, headerControllerBuilder: getHeaderController,
showLiveMedalWall: _showLiveMedalWall, showLiveMedalWall: _showLiveMedalWall,
charges: _userController.charges,
chargeCount: _userController.chargeCount,
guards: _userController.guards,
guardCount: _userController.guardCount,
), ),
), ),
), ),
@@ -218,23 +222,38 @@ class _MemberPageState extends State<MemberPage> {
], ],
), ),
), ),
if (_userController.hasCharge) // if (_userController.hasCharge)
PopupMenuItem( // PopupMenuItem(
onTap: () => Get.toNamed( // onTap: () => UpowerRankPage.toUpowerRank(
'/upowerRank', // mid: _userController.mid,
parameters: { // name: _userController.username ?? '',
'mid': _userController.mid.toString(), // count: _userController.chargeCount,
}, // ),
), // child: const Row(
child: const Row( // mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, // children: [
children: [ // Icon(Icons.electric_bolt, size: 19),
Icon(Icons.electric_bolt, size: 19), // SizedBox(width: 10),
SizedBox(width: 10), // Text('充电排行榜'),
Text('充电排行榜'), // ],
], // ),
), // ),
), // if (_userController.hasGuard)
// PopupMenuItem(
// onTap: () => MemberGuard.toMemberGuard(
// mid: _userController.mid,
// name: _userController.username ?? '',
// count: _userController.guardCount,
// ),
// child: const Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Icon(Icons.anchor, size: 19),
// SizedBox(width: 10),
// Text('大航海舰队'),
// ],
// ),
// ),
if (Get.isRegistered<MemberContributeCtr>(tag: _heroTag)) if (Get.isRegistered<MemberContributeCtr>(tag: _heroTag))
PopupMenuItem( PopupMenuItem(
onTap: _toWebArchive, onTap: _toWebArchive,

View File

@@ -7,7 +7,9 @@ import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart';
import 'package:PiliPlus/models/common/member/user_info_type.dart'; import 'package:PiliPlus/models/common/member/user_info_type.dart';
import 'package:PiliPlus/models/model_owner.dart';
import 'package:PiliPlus/models_new/space/space/card.dart'; import 'package:PiliPlus/models_new/space/space/card.dart';
import 'package:PiliPlus/models_new/space/space/elec.dart';
import 'package:PiliPlus/models_new/space/space/followings_followed_upper.dart'; import 'package:PiliPlus/models_new/space/space/followings_followed_upper.dart';
import 'package:PiliPlus/models_new/space/space/images.dart'; import 'package:PiliPlus/models_new/space/space/images.dart';
import 'package:PiliPlus/models_new/space/space/live.dart'; import 'package:PiliPlus/models_new/space/space/live.dart';
@@ -18,6 +20,8 @@ import 'package:PiliPlus/pages/follow/view.dart';
import 'package:PiliPlus/pages/follow_type/followed/view.dart'; import 'package:PiliPlus/pages/follow_type/followed/view.dart';
import 'package:PiliPlus/pages/member/widget/header_layout_widget.dart'; import 'package:PiliPlus/pages/member/widget/header_layout_widget.dart';
import 'package:PiliPlus/pages/member/widget/medal_widget.dart'; import 'package:PiliPlus/pages/member/widget/medal_widget.dart';
import 'package:PiliPlus/pages/member_guard/view.dart';
import 'package:PiliPlus/pages/member_upower_rank/view.dart';
import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/app_scheme.dart';
import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart';
@@ -47,6 +51,10 @@ class UserInfoCard extends StatelessWidget {
this.silence, this.silence,
required this.headerControllerBuilder, required this.headerControllerBuilder,
required this.showLiveMedalWall, required this.showLiveMedalWall,
required this.charges,
required this.chargeCount,
required this.guards,
required this.guardCount,
}); });
final bool isOwner; final bool isOwner;
@@ -58,6 +66,10 @@ class UserInfoCard extends StatelessWidget {
final int? silence; final int? silence;
final ValueGetter<PageController> headerControllerBuilder; final ValueGetter<PageController> headerControllerBuilder;
final VoidCallback showLiveMedalWall; final VoidCallback showLiveMedalWall;
final List<ElecItem>? charges;
final Object? chargeCount;
final List<Owner>? guards;
final Object? guardCount;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -132,7 +144,22 @@ class UserInfoCard extends StatelessWidget {
BuildContext context, BuildContext context,
ColorScheme colorScheme, ColorScheme colorScheme,
bool isLight, bool isLight,
bool isPortrait,
) { ) {
return [
_buildName(context, colorScheme),
if (card.officialVerify?.desc?.isNotEmpty ?? false)
_buildVerify(colorScheme),
if (card.sign?.isNotEmpty ?? false) _buildSign(),
?_buildChargeAndGuard(colorScheme, isPortrait),
if (card.followingsFollowedUpper?.items?.isNotEmpty ?? false)
_buildFollowedUp(colorScheme, card.followingsFollowedUpper!),
_buildExtraInfo(colorScheme),
if (silence == 1) _buildBanWidget(colorScheme, isLight),
];
}
Widget _buildName(BuildContext context, ColorScheme colorScheme) {
Widget? liveMedal; Widget? liveMedal;
if (card.liveFansWearing?.detailV2 case final detailV2?) { if (card.liveFansWearing?.detailV2 case final detailV2?) {
Color? nameColor; Color? nameColor;
@@ -161,212 +188,210 @@ class UserInfoCard extends StatelessWidget {
} }
} }
} }
return [ return Padding(
Padding( padding: const .only(left: 20, right: 20),
padding: const EdgeInsets.only(left: 20, right: 20), child: Wrap(
child: Wrap( spacing: 8,
spacing: 8, runSpacing: 8,
runSpacing: 8, crossAxisAlignment: .center,
crossAxisAlignment: WrapCrossAlignment.center, children: [
children: [ GestureDetector(
GestureDetector( onTap: () => Utils.copyText(card.name!),
onTap: () => Utils.copyText(card.name!), child: Text(
card.name!,
strutStyle: const StrutStyle(
height: 1,
leading: 0,
fontSize: 17,
fontWeight: .bold,
),
style: TextStyle(
height: 1,
fontSize: 17,
fontWeight: .bold,
color: (card.vip?.status ?? -1) > 0 && card.vip?.type == 2
? colorScheme.vipColor
: null,
),
),
),
Image.asset(
Utils.levelName(
card.levelInfo!.currentLevel!,
isSeniorMember: card.levelInfo?.identity == 2,
),
height: 11,
cacheHeight: 11.cacheSize(context),
semanticLabel: '等级${card.levelInfo?.currentLevel}',
),
if (card.vip?.status == 1)
Container(
padding: const .symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
borderRadius: Style.mdRadius,
color: colorScheme.vipColor,
),
child: Text( child: Text(
card.name!, card.vip?.label?.text ?? '大会员',
strutStyle: const StrutStyle( strutStyle: const StrutStyle(
height: 1, height: 1,
leading: 0, leading: 0,
fontSize: 17, fontSize: 10,
fontWeight: FontWeight.bold, fontWeight: .bold,
), ),
style: TextStyle( style: const TextStyle(
height: 1, height: 1,
fontSize: 17, fontSize: 10,
fontWeight: FontWeight.bold, color: Colors.white,
color: (card.vip?.status ?? -1) > 0 && card.vip?.type == 2 fontWeight: .bold,
? colorScheme.vipColor
: null,
), ),
), ),
), ),
Image.asset( // if (card.nameplate?.imageSmall?.isNotEmpty ?? false)
Utils.levelName( // CachedNetworkImage(
card.levelInfo!.currentLevel!, // imageUrl: ImageUtils.thumbnailUrl(card.nameplate!.imageSmall!),
isSeniorMember: card.levelInfo?.identity == 2, // height: 20,
// placeholder: (context, url) {
// return const SizedBox.shrink();
// },
// ),
?liveMedal,
],
),
);
}
Widget _buildVerify(ColorScheme colorScheme) {
return Container(
margin: const .only(left: 20, top: 8, right: 20),
padding: const .symmetric(horizontal: 5, vertical: 2),
decoration: BoxDecoration(
borderRadius: const .all(.circular(12)),
color: colorScheme.onInverseSurface,
),
child: Text.rich(
TextSpan(
children: [
if (card.officialVerify?.spliceTitle?.isNotEmpty ?? false) ...[
WidgetSpan(
alignment: .middle,
child: DecoratedBox(
decoration: BoxDecoration(
shape: .circle,
color: colorScheme.surface,
),
child: Icon(
Icons.offline_bolt,
color: card.officialVerify?.type == 0
? const Color(0xFFFFCC00)
: Colors.lightBlueAccent,
size: 18,
),
),
),
const TextSpan(text: ' '),
],
TextSpan(
text: card.officialVerify!.spliceTitle!,
style: TextStyle(
fontSize: 12,
fontWeight: .bold,
color: colorScheme.onSurface.withValues(alpha: 0.7),
), ),
height: 11,
cacheHeight: 11.cacheSize(context),
semanticLabel: '等级${card.levelInfo?.currentLevel}',
), ),
if (card.vip?.status == 1)
Container(
padding: const .symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
borderRadius: Style.mdRadius,
color: colorScheme.vipColor,
),
child: Text(
card.vip?.label?.text ?? '大会员',
strutStyle: const StrutStyle(
height: 1,
leading: 0,
fontSize: 10,
fontWeight: FontWeight.bold,
),
style: const TextStyle(
height: 1,
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
// if (card.nameplate?.imageSmall?.isNotEmpty == true)
// CachedNetworkImage(
// imageUrl: ImageUtils.thumbnailUrl(card.nameplate!.imageSmall!),
// height: 20,
// placeholder: (context, url) {
// return const SizedBox.shrink();
// },
// ),
?liveMedal,
], ],
), ),
), ),
if (card.officialVerify?.desc?.isNotEmpty == true) );
Container( }
margin: const EdgeInsets.only(left: 20, top: 8, right: 20),
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), Widget _buildSign() {
decoration: BoxDecoration( return Padding(
borderRadius: const BorderRadius.all(Radius.circular(12)), padding: const .only(left: 20, top: 6, right: 20),
color: colorScheme.onInverseSurface, child: SelectableText(
), card.sign!.trim().replaceAll(RegExp(r'\n{2,}'), '\n'),
child: Text.rich( style: const TextStyle(fontSize: 14),
TextSpan( ),
children: [ );
if (card.officialVerify?.spliceTitle?.isNotEmpty == true) ...[ }
WidgetSpan(
alignment: PlaceholderAlignment.middle, Widget _buildExtraInfo(ColorScheme colorScheme) {
child: DecoratedBox( return Padding(
decoration: BoxDecoration( padding: const .only(left: 20, top: 6, right: 20),
shape: BoxShape.circle, child: Wrap(
color: colorScheme.surface, spacing: 10,
), runSpacing: 8,
child: Icon( crossAxisAlignment: .center,
Icons.offline_bolt, children: [
color: card.officialVerify?.type == 0 GestureDetector(
? const Color(0xFFFFCC00) onTap: () => Utils.copyText(card.mid.toString()),
: Colors.lightBlueAccent, child: Text(
size: 18, 'UID: ${card.mid}',
), style: TextStyle(fontSize: 12, color: colorScheme.outline),
),
),
const TextSpan(
text: ' ',
),
],
TextSpan(
text: card.officialVerify!.spliceTitle!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
), ),
), ),
), ...?card.spaceTag?.map(
if (card.sign?.isNotEmpty == true) (item) {
Padding( final hasUri = item.uri?.isNotEmpty ?? false;
padding: const EdgeInsets.only(left: 20, top: 6, right: 20), final child = Text(
child: SelectableText( item.title ?? '',
card.sign!.trim().replaceAll(RegExp(r'\n{2,}'), '\n'),
style: const TextStyle(fontSize: 14),
),
),
if (card.followingsFollowedUpper?.items?.isNotEmpty == true) ...[
const SizedBox(height: 6),
_buildFollowedUp(colorScheme, card.followingsFollowedUpper!),
],
Padding(
padding: const EdgeInsets.only(left: 20, top: 6, right: 20),
child: Wrap(
spacing: 10,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
GestureDetector(
onTap: () => Utils.copyText(card.mid.toString()),
child: Text(
'UID: ${card.mid}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: colorScheme.outline, color: hasUri ? colorScheme.secondary : colorScheme.outline,
), ),
);
if (hasUri) {
return GestureDetector(
onTap: () => PiliScheme.routePushFromUrl(item.uri!),
child: child,
);
}
return child;
},
),
],
),
);
}
Widget _buildBanWidget(ColorScheme colorScheme, bool isLight) {
return Container(
width: .infinity,
decoration: BoxDecoration(
borderRadius: const .all(.circular(6)),
color: isLight ? colorScheme.errorContainer : colorScheme.error,
),
margin: const .only(left: 20, top: 8, right: 20),
padding: const .symmetric(horizontal: 8, vertical: 4),
child: Text.rich(
TextSpan(
children: [
WidgetSpan(
alignment: .middle,
child: Icon(
Icons.info,
size: 17,
color: isLight
? colorScheme.onErrorContainer
: colorScheme.onError,
), ),
), ),
...?card.spaceTag?.map( TextSpan(
(item) { text: ' 该账号封禁中',
final hasUri = item.uri?.isNotEmpty == true; style: TextStyle(
final child = Text( color: isLight
item.title ?? '', ? colorScheme.onErrorContainer
style: TextStyle( : colorScheme.onError,
fontSize: 12, ),
color: hasUri ? colorScheme.secondary : colorScheme.outline,
),
);
if (hasUri) {
return GestureDetector(
onTap: () => PiliScheme.routePushFromUrl(item.uri!),
child: child,
);
}
return child;
},
), ),
], ],
), ),
), ),
if (silence == 1) );
Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6)),
color: isLight ? colorScheme.errorContainer : colorScheme.error,
),
margin: const EdgeInsets.only(left: 20, top: 8, right: 20),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text.rich(
TextSpan(
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.info,
size: 17,
color: isLight
? colorScheme.onErrorContainer
: colorScheme.onError,
),
),
TextSpan(
text: ' 该账号封禁中',
style: TextStyle(
color: isLight
? colorScheme.onErrorContainer
: colorScheme.onError,
),
),
],
),
),
),
];
} }
Column _buildRight(ColorScheme colorScheme) => Column( Column _buildRight(ColorScheme colorScheme) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Row( Row(
children: UserInfoType.values children: UserInfoType.values
@@ -392,7 +417,7 @@ class UserInfoCard extends StatelessWidget {
const SizedBox(height: 5), const SizedBox(height: 5),
Row( Row(
spacing: 10, spacing: 10,
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
if (!isOwner) if (!isOwner)
IconButton.outlined( IconButton.outlined(
@@ -417,9 +442,9 @@ class UserInfoCard extends StatelessWidget {
width: 1.0, width: 1.0,
color: colorScheme.outline.withValues(alpha: 0.3), color: colorScheme.outline.withValues(alpha: 0.3),
), ),
padding: .zero,
tapTargetSize: .padded, tapTargetSize: .padded,
padding: EdgeInsets.zero, visualDensity: .compact,
visualDensity: VisualDensity.compact,
), ),
), ),
Expanded( Expanded(
@@ -440,7 +465,7 @@ class UserInfoCard extends StatelessWidget {
children: [ children: [
if (relation != 0 && relation != 128) ...[ if (relation != 0 && relation != 128) ...[
WidgetSpan( WidgetSpan(
alignment: PlaceholderAlignment.middle, alignment: .middle,
child: Icon( child: Icon(
Icons.sort, Icons.sort,
size: 16, size: 16,
@@ -512,8 +537,8 @@ class UserInfoCard extends StatelessWidget {
) { ) {
final imgUrls = images.collectionTopSimple?.top?.imgUrls; final imgUrls = images.collectionTopSimple?.top?.imgUrls;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
HeaderLayoutWidget( HeaderLayoutWidget(
header: imgUrls != null && imgUrls.isNotEmpty header: imgUrls != null && imgUrls.isNotEmpty
@@ -533,8 +558,8 @@ class UserInfoCard extends StatelessWidget {
actions: _buildRight(scheme), actions: _buildRight(scheme),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
..._buildLeft(context, scheme, isLight), ..._buildLeft(context, scheme, isLight, true),
if (card.prInfo?.content?.isNotEmpty == true) if (card.prInfo?.content?.isNotEmpty ?? false)
buildPrInfo(context, scheme, isLight, card.prInfo!), buildPrInfo(context, scheme, isLight, card.prInfo!),
const SizedBox(height: 5), const SizedBox(height: 5),
], ],
@@ -673,8 +698,8 @@ class UserInfoCard extends StatelessWidget {
: null, : null,
colorBlendMode: filter colorBlendMode: filter
? isLight ? isLight
? BlendMode.lighten ? .lighten
: BlendMode.darken : .darken
: null, : null,
fadeInDuration: const Duration(milliseconds: 120), fadeInDuration: const Duration(milliseconds: 120),
fadeOutDuration: const Duration(milliseconds: 120), fadeOutDuration: const Duration(milliseconds: 120),
@@ -699,8 +724,8 @@ class UserInfoCard extends StatelessWidget {
: null; : null;
Widget child = Container( Widget child = Container(
margin: const EdgeInsets.only(top: 8), margin: const .only(top: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), padding: const .symmetric(horizontal: 16, vertical: 10),
color: Utils.parseColor(isLight ? prInfo.bgColor : prInfo.bgColorNight), color: Utils.parseColor(isLight ? prInfo.bgColor : prInfo.bgColorNight),
child: Row( child: Row(
children: [ children: [
@@ -721,7 +746,7 @@ class UserInfoCard extends StatelessWidget {
style: TextStyle(fontSize: 13, color: textColor), style: TextStyle(fontSize: 13, color: textColor),
), ),
), ),
if (prInfo.url?.isNotEmpty == true) ...[ if (prInfo.url?.isNotEmpty ?? false) ...[
const SizedBox(width: 10), const SizedBox(width: 10),
Icon( Icon(
Icons.keyboard_arrow_right, Icons.keyboard_arrow_right,
@@ -731,7 +756,7 @@ class UserInfoCard extends StatelessWidget {
], ],
), ),
); );
if (prInfo.url?.isNotEmpty == true) { if (prInfo.url?.isNotEmpty ?? false) {
return GestureDetector( return GestureDetector(
onTap: () => PageUtils.handleWebview(prInfo.url!), onTap: () => PageUtils.handleWebview(prInfo.url!),
child: child, child: child,
@@ -742,8 +767,8 @@ class UserInfoCard extends StatelessWidget {
Column _buildH(BuildContext context, ColorScheme scheme, bool isLight) => Column _buildH(BuildContext context, ColorScheme scheme, bool isLight) =>
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
// _buildHeader(context), // _buildHeader(context),
const SizedBox(height: kToolbarHeight), const SizedBox(height: kToolbarHeight),
@@ -751,7 +776,7 @@ class UserInfoCard extends StatelessWidget {
children: [ children: [
const SizedBox(width: 20), const SizedBox(width: 20),
Padding( Padding(
padding: EdgeInsets.only( padding: .only(
top: 10, top: 10,
bottom: card.prInfo?.content?.isNotEmpty == true ? 0 : 10, bottom: card.prInfo?.content?.isNotEmpty == true ? 0 : 10,
), ),
@@ -761,27 +786,110 @@ class UserInfoCard extends StatelessWidget {
Expanded( Expanded(
flex: 5, flex: 5,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
const SizedBox(height: 10), const SizedBox(height: 10),
..._buildLeft(context, scheme, isLight), ..._buildLeft(context, scheme, isLight, false),
const SizedBox(height: 5), const SizedBox(height: 5),
], ],
), ),
), ),
Expanded( Expanded(flex: 3, child: _buildRight(scheme)),
flex: 3,
child: _buildRight(scheme),
),
const SizedBox(width: 20), const SizedBox(width: 20),
], ],
), ),
if (card.prInfo?.content?.isNotEmpty == true) if (card.prInfo?.content?.isNotEmpty ?? false)
buildPrInfo(context, scheme, isLight, card.prInfo!), buildPrInfo(context, scheme, isLight, card.prInfo!),
], ],
); );
Widget _buildChargeItem(
ColorScheme colorScheme,
List<Owner>? list,
Object? count,
String desc,
VoidCallback onTap,
) {
return GestureDetector(
onTap: onTap,
child: Row(
mainAxisSize: .min,
children: [
avatars(
gap: 10,
colorScheme: colorScheme,
users: list!.take(3),
),
const SizedBox(width: 4),
Text.rich(
TextSpan(
children: [
TextSpan(
text: NumUtils.numFormat(count),
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
TextSpan(
text: desc,
style: TextStyle(
fontSize: 13,
color: colorScheme.outline,
),
),
],
),
),
Icon(
Icons.keyboard_arrow_right,
size: 20,
color: colorScheme.outline,
),
],
),
);
}
Widget? _buildChargeAndGuard(ColorScheme colorScheme, bool isPortrait) {
final children = [
if (charges?.isNotEmpty ?? false)
_buildChargeItem(
colorScheme,
charges,
chargeCount,
'人为TA充电',
() => UpowerRankPage.toUpowerRank(
mid: card.mid!,
name: card.name!,
count: chargeCount,
),
),
if (guards?.isNotEmpty ?? false)
_buildChargeItem(
colorScheme,
guards,
guardCount,
'人加入大航海',
() => MemberGuard.toMemberGuard(
mid: card.mid!,
name: card.name!,
count: guardCount,
),
),
];
if (children.isNotEmpty) {
return Padding(
padding: const .only(left: 20, right: 20, top: 6),
child: isPortrait
? Row(mainAxisAlignment: .spaceBetween, children: children)
: Wrap(spacing: 10, runSpacing: 6, children: children),
);
}
return null;
}
Widget _buildFollowedUp( Widget _buildFollowedUp(
ColorScheme colorScheme, ColorScheme colorScheme,
FollowingsFollowedUpper item, FollowingsFollowedUpper item,
@@ -789,34 +897,39 @@ class UserInfoCard extends StatelessWidget {
var list = item.items!; var list = item.items!;
final flag = list.length > 3; final flag = list.length > 3;
if (flag) list = list.sublist(0, 3); if (flag) list = list.sublist(0, 3);
Widget child = Row( Widget child = Padding(
mainAxisSize: MainAxisSize.min, padding: const .only(left: 20, top: 6, right: 20),
children: [ child: Row(
const SizedBox(width: 20), mainAxisSize: .min,
avatars(colorScheme: colorScheme, users: list), children: [
const SizedBox(width: 4), avatars(
Flexible( gap: 10,
child: Text( colorScheme: colorScheme,
list.map((e) => e.name).join(''), users: list,
maxLines: 1, ),
overflow: TextOverflow.ellipsis, const SizedBox(width: 4),
style: TextStyle( Flexible(
fontSize: 13, child: Text(
color: colorScheme.onSurfaceVariant, list.map((e) => e.name).join(''),
maxLines: 1,
overflow: .ellipsis,
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
),
), ),
), ),
), Text(
Text( '${flag ? '${item.items!.length}' : ''}TA',
'${flag ? '${item.items!.length}' : ''}TA ', style: TextStyle(fontSize: 13, color: colorScheme.outline),
style: TextStyle(fontSize: 13, color: colorScheme.outline), ),
), Icon(
Icon( Icons.keyboard_arrow_right,
Icons.keyboard_arrow_right, size: 20,
size: 20, color: colorScheme.outline,
color: colorScheme.outline, ),
), ],
const SizedBox(width: 10), ),
],
); );
return GestureDetector( return GestureDetector(
onTap: () => FollowedPage.toFollowedPage(mid: card.mid, name: card.name), onTap: () => FollowedPage.toFollowedPage(mid: card.mid, name: card.name),

View File

@@ -0,0 +1,44 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models_new/member_guard/data.dart';
import 'package:PiliPlus/models_new/member_guard/guard_top_list.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/get_navigation.dart';
class MemberGuardController
extends CommonListController<MemberGuardData, GuardItem> {
@override
void onInit() {
super.onInit();
queryData();
}
final Object ruid = Get.arguments['ruid'];
late List<GuardItem> tops;
@override
List<GuardItem>? getDataList(MemberGuardData response) {
return response.guardTopList;
}
@override
bool customHandleResponse(bool isRefresh, Success<MemberGuardData> response) {
if (response.response.hasMore != 1) {
isEnd = true;
}
if (isRefresh) {
final list = response.response.guardTopList;
tops = list.take(3).toList();
if (list.length > 3) {
list.removeRange(0, 3);
}
}
return false;
}
@override
Future<LoadingState<MemberGuardData>> customGetData() =>
MemberHttp.memberGuard(ruid: ruid, page: page);
}

View File

@@ -0,0 +1,206 @@
import 'package:PiliPlus/common/widgets/flutter/list_tile.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/member_guard/guard_top_list.dart';
import 'package:PiliPlus/pages/member_guard/controller.dart';
import 'package:PiliPlus/utils/extension/widget_ext.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:flutter/material.dart' hide ListTile;
import 'package:get/get.dart';
class MemberGuard extends StatefulWidget {
const MemberGuard({super.key});
@override
State<MemberGuard> createState() => _MemberGuardState();
static Future<void>? toMemberGuard({
required Object mid,
required String name,
required Object? count,
}) {
return Get.toNamed(
'/memberGuard',
arguments: {
'ruid': mid,
'name': name,
'count': count,
},
);
}
}
class _MemberGuardState extends State<MemberGuard> {
late final String _userName;
late final Object? _count;
late final MemberGuardController _controller;
@override
void initState() {
super.initState();
final args = Get.arguments;
_userName = args['name'];
_count = args['count'];
_controller = Get.put(
MemberGuardController(),
tag: args['ruid'].toString(),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('$_userName的舰队${_count == null ? '' : '($_count)'}'),
),
body: refreshIndicator(
onRefresh: _controller.onRefresh,
child: CustomScrollView(
slivers: [
ViewSliverSafeArea(
sliver: Obx(() => _buildBody(_controller.loadingState.value)),
),
],
),
).constraintWidth(),
);
}
Widget _buildBody(LoadingState<List<GuardItem>?> state) {
switch (state) {
case Loading():
return linearLoading;
case Success<List<GuardItem>?>(:final response):
return SliverMainAxisGroup(
slivers: [
_buildTopItems(),
if (response!.isNotEmpty)
SliverPadding(
padding: const .only(top: 10),
sliver: SliverList.separated(
itemCount: response.length,
itemBuilder: (context, index) {
if (index == response.length - 1) {
_controller.onLoadMore();
}
final item = response[index];
return ListTile(
safeArea: false,
visualDensity: .comfortable,
onTap: () => Get.toNamed('/member?mid=${item.uid}'),
leading: _avatar(item.face, 32, item.guardLevel),
title: Text(
item.username,
style: const TextStyle(fontSize: 14),
),
);
},
separatorBuilder: (_, _) => const SizedBox(height: 4),
),
),
],
);
case Error(:final errMsg):
return HttpError(errMsg: errMsg, onReload: _controller.onReload);
}
}
Widget _buildTopItem(GuardItem item, double size) {
final child = GestureDetector(
behavior: .opaque,
onTap: () => Get.toNamed('/member?mid=${item.uid}'),
child: Padding(
padding: const .symmetric(vertical: 10.0),
child: Column(
spacing: 5,
mainAxisSize: .min,
children: [
SizedBox(
height: 67.5, // 50 * 1.35
child: Align(
alignment: .bottomCenter,
child: _avatar(item.face, size, item.guardLevel),
),
),
Text(item.username, maxLines: 1, overflow: .ellipsis),
],
),
),
);
if (PlatformUtils.isDesktop) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: child,
);
}
return child;
}
Widget _buildTopItems() {
final Widget first;
final Widget second;
final Widget third;
if (_controller.tops.firstOrNull case final item?) {
first = _buildTopItem(item, 50);
} else {
first = const SizedBox.shrink();
}
if (_controller.tops.elementAtOrNull(1) case final item?) {
second = _buildTopItem(item, 42);
} else {
second = const SizedBox.shrink();
}
if (_controller.tops.elementAtOrNull(2) case final item?) {
third = _buildTopItem(item, 42);
} else {
third = const SizedBox.shrink();
}
return SliverToBoxAdapter(
child: Row(
children: [
Expanded(child: second),
Expanded(child: first),
Expanded(child: third),
],
),
);
}
static String? _pendantUrl(int guardLevel) => switch (guardLevel) {
1 =>
'https://i0.hdslb.com/bfs/live/a454275dea465ac15a03f121f0d7edaf96e30bcf.png',
2 =>
'https://i0.hdslb.com/bfs/live/3b46129e796df42ec7356fcba77c8a79d47db682.png',
3 =>
'https://i0.hdslb.com/bfs/live/80f732943cc3367029df65e267960d56736a82ee.png',
_ => null,
};
static Widget _avatar(String url, double size, int guardLevel) {
final pendentSize = 1.35 * size;
return Stack(
clipBehavior: .none,
alignment: .center,
children: [
NetworkImgLayer(
src: url,
width: size,
height: size,
type: .avatar,
),
NetworkImgLayer(
type: .emote,
width: pendentSize,
height: pendentSize,
src: _pendantUrl(guardLevel),
getPlaceHolder: () => const SizedBox.shrink(),
),
],
);
}
}

View File

@@ -8,13 +8,15 @@ import 'package:get/get.dart';
class UpowerRankController class UpowerRankController
extends CommonListController<UpowerRankData, UpowerRankInfo> { extends CommonListController<UpowerRankData, UpowerRankInfo> {
UpowerRankController({this.privilegeType, required this.upMid}); UpowerRankController({
int? privilegeType; this.privilegeType,
required this.upMid,
});
final String upMid; final String upMid;
final Rx<String?> name = Rx<String?>(null); final int? privilegeType;
final Rx<List<LevelInfo>?> tabs = Rx<List<LevelInfo>?>(null);
int? memberTotal; late final Rx<List<LevelInfo>?> tabs = Rx<List<LevelInfo>?>(null);
@override @override
void onInit() { void onInit() {
@@ -25,11 +27,11 @@ class UpowerRankController
@override @override
List<UpowerRankInfo>? getDataList(UpowerRankData response) { List<UpowerRankInfo>? getDataList(UpowerRankData response) {
isEnd = true; isEnd = true;
memberTotal = response.memberTotal ?? 0; if (privilegeType == null &&
if (response.levelInfo != null && response.levelInfo!.length > 1) { response.levelInfo != null &&
response.levelInfo!.length > 1) {
tabs.value = response.levelInfo; tabs.value = response.levelInfo;
} }
name.value = response.upInfo!.nickname;
return response.rankInfo; return response.rankInfo;
} }

View File

@@ -10,39 +10,58 @@ import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models_new/upower_rank/rank_info.dart'; import 'package:PiliPlus/models_new/upower_rank/rank_info.dart';
import 'package:PiliPlus/pages/member_upower_rank/controller.dart'; import 'package:PiliPlus/pages/member_upower_rank/controller.dart';
import 'package:PiliPlus/utils/extension/widget_ext.dart'; import 'package:PiliPlus/utils/extension/widget_ext.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart' hide ListTile; import 'package:flutter/material.dart' hide ListTile;
import 'package:get/get.dart'; import 'package:get/get.dart';
class UpowerRankPage extends StatefulWidget { class UpowerRankPage extends StatefulWidget {
const UpowerRankPage({super.key, this.upMid, this.tag, this.privilegeType}); const UpowerRankPage({
super.key,
this.privilegeType,
});
final String? upMid;
final String? tag;
final int? privilegeType; final int? privilegeType;
@override @override
State<UpowerRankPage> createState() => _UpowerRankPageState(); State<UpowerRankPage> createState() => _UpowerRankPageState();
static Future<void>? toUpowerRank({
required Object mid,
required String name,
required Object? count,
}) {
return Get.toNamed(
'/upowerRank',
arguments: {
'mid': mid,
'name': name,
'count': count,
},
);
}
} }
class _UpowerRankPageState extends State<UpowerRankPage> class _UpowerRankPageState extends State<UpowerRankPage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
late final _upMid = Get.parameters['mid']!; String? _name;
late final String _tag; Object? _count;
late final String _upMid;
late final UpowerRankController _controller; late final UpowerRankController _controller;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_tag = widget.privilegeType == null final params = Get.arguments;
? Utils.generateRandomString(8) _upMid = params['mid']!.toString();
: '${widget.tag}${widget.privilegeType}'; if (widget.privilegeType == null) {
_name = params['name'];
_count = params['count'];
}
_controller = Get.put( _controller = Get.put(
UpowerRankController( UpowerRankController(
privilegeType: widget.privilegeType, privilegeType: widget.privilegeType,
upMid: widget.upMid ?? _upMid, upMid: _upMid,
), ),
tag: _tag, tag: '$_upMid${widget.privilegeType}',
); );
} }
@@ -70,14 +89,7 @@ class _UpowerRankPageState extends State<UpowerRankPage>
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
title: Obx(() { title: Text('$_name的充电排行榜${_count == null ? '' : '($_count)'}'),
final name = _controller.name.value;
return name == null
? const SizedBox.shrink()
: Text(
'$name 充电排行榜${_controller.memberTotal == 0 ? '' : '(${_controller.memberTotal})'}',
);
}),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Get.toNamed( onPressed: () => Get.toNamed(
@@ -126,7 +138,7 @@ class _UpowerRankPageState extends State<UpowerRankPage>
} else { } else {
Get.find<UpowerRankController>( Get.find<UpowerRankController>(
tag: tag:
'$_tag${tabs[index].privilegeType}', '$_upMid${tabs[index].privilegeType}',
).animateToTop(); ).animateToTop();
} }
} catch (_) {} } catch (_) {}
@@ -141,8 +153,6 @@ class _UpowerRankPageState extends State<UpowerRankPage>
.skip(1) .skip(1)
.map( .map(
(e) => UpowerRankPage( (e) => UpowerRankPage(
upMid: _upMid,
tag: _tag,
privilegeType: e.privilegeType, privilegeType: e.privilegeType,
), ),
), ),
@@ -170,7 +180,7 @@ class _UpowerRankPageState extends State<UpowerRankPage>
) { ) {
late final width = MediaQuery.textScalerOf(context).scale(32); late final width = MediaQuery.textScalerOf(context).scale(32);
return switch (loadingState) { return switch (loadingState) {
Loading() => linearLoading, Loading() => const SliverFillRemaining(child: m3eLoading),
Success<List<UpowerRankInfo>?>(:final response) => Success<List<UpowerRankInfo>?>(:final response) =>
response != null && response.isNotEmpty response != null && response.isNotEmpty
? SliverList.builder( ? SliverList.builder(

View File

@@ -34,6 +34,7 @@ import 'package:PiliPlus/pages/main_reply/view.dart';
import 'package:PiliPlus/pages/match_info/view.dart'; import 'package:PiliPlus/pages/match_info/view.dart';
import 'package:PiliPlus/pages/member/view.dart'; import 'package:PiliPlus/pages/member/view.dart';
import 'package:PiliPlus/pages/member_dynamics/view.dart'; import 'package:PiliPlus/pages/member_dynamics/view.dart';
import 'package:PiliPlus/pages/member_guard/view.dart';
import 'package:PiliPlus/pages/member_profile/view.dart'; import 'package:PiliPlus/pages/member_profile/view.dart';
import 'package:PiliPlus/pages/member_search/view.dart'; import 'package:PiliPlus/pages/member_search/view.dart';
import 'package:PiliPlus/pages/member_upower_rank/view.dart'; import 'package:PiliPlus/pages/member_upower_rank/view.dart';
@@ -196,5 +197,6 @@ class Routes {
GetPage(name: '/myReply', page: () => const MyReply()), GetPage(name: '/myReply', page: () => const MyReply()),
GetPage(name: '/videoWeb', page: () => const MemberVideoWeb()), GetPage(name: '/videoWeb', page: () => const MemberVideoWeb()),
GetPage(name: '/ssWeb', page: () => const MemberSSWeb()), GetPage(name: '/ssWeb', page: () => const MemberSSWeb()),
GetPage(name: '/memberGuard', page: () => const MemberGuard()),
]; ];
} }