show followee votes

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-12-11 20:55:44 +08:00
parent 2b4b1debe6
commit 74d5e03a34
4 changed files with 209 additions and 10 deletions

View File

@@ -974,4 +974,6 @@ class Api {
static const String sameFollowing = '/x/relation/same/followings';
static const String seasonStatus = '/pgc/view/web/season/user/status';
static const String followeeVotes = '${HttpString.tUrl}/vote_svr/v1/vote_svr/followee_votes';
}

View File

@@ -19,6 +19,7 @@ import 'package:PiliPlus/models_new/dynamic/dyn_reserve_info/data.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/topic_card_list.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/top_details.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/models_new/followee_votes/vote.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:PiliPlus/utils/wbi_sign.dart';
@@ -385,7 +386,7 @@ class DynamicsHttp {
static Future<LoadingState<VoteInfo>> doVote({
required int voteId,
required List<int> votes,
bool anonymity = false,
bool anonymous = false,
int? dynamicId,
}) async {
final csrf = Accounts.main.csrf;
@@ -393,7 +394,7 @@ class DynamicsHttp {
'vote_id': voteId,
'votes': votes,
'voter_uid': Accounts.main.mid,
'status': anonymity ? 1 : 0,
'status': anonymous ? 1 : 0,
'op_bit': 0,
'dynamic_id': dynamicId ?? 0,
'csrf_token': csrf,
@@ -654,4 +655,24 @@ class DynamicsHttp {
return Error(res.data['message']);
}
}
static Future<LoadingState<List<FolloweeVote>?>> followeeVotes({
required dynamic voteId,
}) async {
final res = await Request().get(
Api.followeeVotes,
queryParameters: {
'vote_id': voteId,
},
);
if (res.data['code'] == 0) {
return Success(
(res.data['data']?['votes'] as List?)
?.map((e) => FolloweeVote.fromJson(e))
.toList(),
);
} else {
return Error(res.data['message']);
}
}
}

View File

@@ -0,0 +1,23 @@
class FolloweeVote {
int uid;
String name;
String face;
List<int> votes;
int ctime;
FolloweeVote({
required this.uid,
required this.name,
required this.face,
required this.votes,
required this.ctime,
});
factory FolloweeVote.fromJson(Map<String, dynamic> json) => FolloweeVote(
uid: json['uid'],
name: json['name'],
face: json['face'],
votes: List<int>.from(json['votes']),
ctime: json['ctime'],
);
}

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/dialog/report.dart';
@@ -7,6 +8,8 @@ import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/badge_type.dart';
import 'package:PiliPlus/models/dynamics/vote_model.dart';
import 'package:PiliPlus/models_new/followee_votes/vote.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/date_utils.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:PiliPlus/utils/num_utils.dart';
@@ -28,7 +31,7 @@ class VotePanel extends StatefulWidget {
}
class _VotePanelState extends State<VotePanel> {
bool anonymity = false;
late bool anonymous = false;
late VoteInfo _voteInfo;
late final RxList<int> groupValue =
@@ -39,11 +42,20 @@ class _VotePanelState extends State<VotePanel> {
_voteInfo.endTime! * 1000 > DateTime.now().millisecondsSinceEpoch;
late bool _showPercentage = !_enabled;
late final _maxCnt = _voteInfo.choiceCnt ?? _voteInfo.options.length;
final isLogin = Accounts.main.isLogin;
late final Rxn<List<FolloweeVote>> followeeVote = Rxn<List<FolloweeVote>>();
@override
void initState() {
super.initState();
_voteInfo = widget.voteInfo;
if (isLogin) {
DynamicsHttp.followeeVotes(voteId: _voteInfo.voteId).then((res) {
if (mounted && res.isSuccess) {
followeeVote.value = res.data;
}
});
}
}
@override
@@ -102,7 +114,7 @@ class _VotePanelState extends State<VotePanel> {
? () async {
final res = await widget.callback(
groupValue.toSet(),
anonymity,
anonymous,
);
if (res.isSuccess) {
if (mounted) {
@@ -124,12 +136,153 @@ class _VotePanelState extends State<VotePanel> {
),
],
];
Widget title = Text(
_voteInfo.title ?? '',
style: theme.textTheme.titleMedium,
);
if (isLogin) {
title = Row(
spacing: 3,
crossAxisAlignment: .start,
children: [
Expanded(child: title),
Obx(() {
final list = followeeVote.value;
if (list != null && list.isNotEmpty) {
Widget child;
const size = 22.0;
const gap = 6.0;
const offset = size - gap;
if (list.length == 1) {
child = NetworkImgLayer(
src: list.first.face,
width: size,
height: size,
);
} else {
final decoration = BoxDecoration(
shape: .circle,
border: Border.all(color: theme.colorScheme.surface),
);
child = SizedBox(
height: size,
width: offset * min(3, list.length) + gap,
child: Stack(
clipBehavior: .none,
children: list
.take(3)
.indexed
.map(
(e) => Positioned(
top: 0,
left: e.$1 * offset,
bottom: 0,
child: DecoratedBox(
decoration: decoration,
child: Padding(
padding: const .all(.8),
child: NetworkImgLayer(
src: e.$2.face,
width: size - .8,
height: size - .8,
),
),
),
),
)
.toList(),
),
);
}
return GestureDetector(
behavior: .opaque,
onTap: () {
showDialog(
context: context,
builder: (context) {
final colorScheme = ColorScheme.of(context);
return AlertDialog(
clipBehavior: .hardEdge,
title: const Text('关注的人的投票'),
contentPadding: const .only(top: 10, bottom: 12),
constraints: const BoxConstraints(
minWidth: 280,
maxWidth: 420,
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: .min,
children: list
.map(
(e) => ListTile(
dense: true,
onTap: () =>
Get.toNamed('/member?mid=${e.uid}'),
leading: NetworkImgLayer(
src: e.face,
width: 40,
height: 40,
type: .avatar,
),
title: Text.rich(
style: const TextStyle(fontSize: 13),
TextSpan(
children: [
TextSpan(text: e.name),
TextSpan(
text: ' 投给了',
style: TextStyle(
fontSize: 12,
color: colorScheme.outline,
),
),
],
),
),
subtitle: Text(
style: const TextStyle(fontSize: 13),
e.votes
.map(
(vote) => _voteInfo.options
.firstWhereOrNull(
(e) => e.optIdx == vote,
)
?.optDesc,
)
.join(''),
),
),
)
.toList(),
),
),
);
},
);
},
child: Row(
mainAxisSize: .min,
children: [
child,
Icon(
size: 18,
color: theme.colorScheme.outline.withValues(alpha: .7),
Icons.keyboard_arrow_right,
),
],
),
);
}
return const SizedBox.shrink();
}),
],
);
}
Widget child = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_voteInfo.title != null)
Text(_voteInfo.title!, style: theme.textTheme.titleMedium),
title,
if (_voteInfo.desc != null)
Text(
_voteInfo.desc!,
@@ -196,8 +349,8 @@ class _VotePanelState extends State<VotePanel> {
),
CheckBoxText(
text: '匿名',
selected: anonymity,
onChanged: (val) => anonymity = val,
selected: anonymous,
onChanged: (val) => anonymous = val,
),
],
);
@@ -444,10 +597,10 @@ Future showVoteDialog(
padding: const EdgeInsets.all(24),
child: VotePanel(
voteInfo: voteInfo.data,
callback: (votes, anonymity) => DynamicsHttp.doVote(
callback: (votes, anonymous) => DynamicsHttp.doVote(
voteId: voteId,
votes: votes.toList(),
anonymity: anonymity,
anonymous: anonymous,
dynamicId: dynamicId,
),
),