feat: 私信查看
This commit is contained in:
@ -321,4 +321,38 @@ class Api {
|
|||||||
|
|
||||||
// 获取指定分组下的up
|
// 获取指定分组下的up
|
||||||
static const String followUpGroup = '/x/relation/tag';
|
static const String followUpGroup = '/x/relation/tag';
|
||||||
|
|
||||||
|
/// 私聊
|
||||||
|
/// 'https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions?
|
||||||
|
/// session_type=1&
|
||||||
|
/// group_fold=1&
|
||||||
|
/// unfollow_fold=0&
|
||||||
|
/// sort_rule=2&
|
||||||
|
/// build=0&
|
||||||
|
/// mobi_app=web&
|
||||||
|
/// w_rid=8641d157fb9a9255eb2159f316ee39e2&
|
||||||
|
/// wts=1697305010
|
||||||
|
|
||||||
|
static const String sessionList =
|
||||||
|
'https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions';
|
||||||
|
|
||||||
|
/// 私聊用户信息
|
||||||
|
/// uids
|
||||||
|
/// build=0&mobi_app=web
|
||||||
|
static const String sessionAccountList =
|
||||||
|
'https://api.vc.bilibili.com/account/v1/user/cards';
|
||||||
|
|
||||||
|
/// https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs?
|
||||||
|
/// talker_id=400787461&
|
||||||
|
/// session_type=1&
|
||||||
|
/// size=20&
|
||||||
|
/// sender_device_id=1&
|
||||||
|
/// build=0&
|
||||||
|
/// mobi_app=web&
|
||||||
|
/// web_location=333.1296&
|
||||||
|
/// w_rid=cfe3bf58c9fe181bbf4dd6c75175e6b0&
|
||||||
|
/// wts=1697350697
|
||||||
|
|
||||||
|
static const String sessionMsg =
|
||||||
|
'https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs';
|
||||||
}
|
}
|
||||||
|
|||||||
80
lib/http/msg.dart
Normal file
80
lib/http/msg.dart
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import 'package:pilipala/http/api.dart';
|
||||||
|
import 'package:pilipala/http/init.dart';
|
||||||
|
import 'package:pilipala/models/msg/account.dart';
|
||||||
|
import 'package:pilipala/models/msg/session.dart';
|
||||||
|
import 'package:pilipala/utils/wbi_sign.dart';
|
||||||
|
|
||||||
|
class MsgHttp {
|
||||||
|
// 会话列表
|
||||||
|
static Future sessionList() async {
|
||||||
|
Map params = await WbiSign().makSign({
|
||||||
|
'session_type': 1,
|
||||||
|
'group_fold': 1,
|
||||||
|
'unfollow_fold': 0,
|
||||||
|
'sort_rule': 2,
|
||||||
|
'build': 0,
|
||||||
|
'mobi_app': 'web',
|
||||||
|
});
|
||||||
|
var res = await Request().get(Api.sessionList, data: params);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': SessionDataModel.fromJson(res.data['data']),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'date': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future accountList(uids) async {
|
||||||
|
var res = await Request().get(Api.sessionAccountList, data: {
|
||||||
|
'uids': uids,
|
||||||
|
'build': 0,
|
||||||
|
'mobi_app': 'web',
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': res.data['data']
|
||||||
|
.map<AccountListModel>((e) => AccountListModel.fromJson(e))
|
||||||
|
.toList(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'date': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future sessionMsg({
|
||||||
|
int? talkerId,
|
||||||
|
}) async {
|
||||||
|
Map params = await WbiSign().makSign({
|
||||||
|
'talker_id': talkerId,
|
||||||
|
'session_type': 1,
|
||||||
|
'size': 20,
|
||||||
|
'sender_device_id': 1,
|
||||||
|
'build': 0,
|
||||||
|
'mobi_app': 'web',
|
||||||
|
});
|
||||||
|
var res = await Request().get(Api.sessionMsg, data: params);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': SessionMsgDataModel.fromJson(res.data['data']),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'date': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
lib/models/msg/account.dart
Normal file
80
lib/models/msg/account.dart
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
class AccountListModel {
|
||||||
|
AccountListModel({
|
||||||
|
this.mid,
|
||||||
|
this.name,
|
||||||
|
this.sex,
|
||||||
|
this.face,
|
||||||
|
this.sign,
|
||||||
|
this.rank,
|
||||||
|
this.level,
|
||||||
|
this.silence,
|
||||||
|
this.vip,
|
||||||
|
this.pendant,
|
||||||
|
this.nameplate,
|
||||||
|
this.official,
|
||||||
|
this.birthday,
|
||||||
|
this.isFakeAccount,
|
||||||
|
this.isDeleted,
|
||||||
|
this.inRegAudit,
|
||||||
|
this.faceNft,
|
||||||
|
this.faceNftNew,
|
||||||
|
this.isSeniorMember,
|
||||||
|
this.digitalId,
|
||||||
|
this.digitalType,
|
||||||
|
this.attestation,
|
||||||
|
this.expertInfo,
|
||||||
|
this.honours,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? mid;
|
||||||
|
String? name;
|
||||||
|
String? sex;
|
||||||
|
String? face;
|
||||||
|
String? sign;
|
||||||
|
int? rank;
|
||||||
|
int? level;
|
||||||
|
int? silence;
|
||||||
|
Map? vip;
|
||||||
|
Map? pendant;
|
||||||
|
Map? nameplate;
|
||||||
|
Map? official;
|
||||||
|
int? birthday;
|
||||||
|
int? isFakeAccount;
|
||||||
|
int? isDeleted;
|
||||||
|
int? inRegAudit;
|
||||||
|
int? faceNft;
|
||||||
|
int? faceNftNew;
|
||||||
|
int? isSeniorMember;
|
||||||
|
String? digitalId;
|
||||||
|
int? digitalType;
|
||||||
|
Map? attestation;
|
||||||
|
Map? expertInfo;
|
||||||
|
Map? honours;
|
||||||
|
|
||||||
|
AccountListModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
mid = json['mid'];
|
||||||
|
name = json['name'] ?? '';
|
||||||
|
sex = json['sex'];
|
||||||
|
face = json['face'];
|
||||||
|
sign = json['sign'];
|
||||||
|
rank = json['rank'];
|
||||||
|
level = json['level'];
|
||||||
|
silence = json['silence'];
|
||||||
|
vip = json['vip'];
|
||||||
|
pendant = json['pendant'];
|
||||||
|
nameplate = json['nameplate'];
|
||||||
|
official = json['official'];
|
||||||
|
birthday = json['birthday'];
|
||||||
|
isFakeAccount = json['is_fake_account'];
|
||||||
|
isDeleted = json['is_deleted'];
|
||||||
|
inRegAudit = json['in_reg_audit'];
|
||||||
|
faceNft = json['face_nft'];
|
||||||
|
faceNftNew = json['face_nft_new'];
|
||||||
|
isSeniorMember = json['is_senior_member'];
|
||||||
|
digitalId = json['digital_id'];
|
||||||
|
digitalType = json['digital_type'];
|
||||||
|
attestation = json['attestation'];
|
||||||
|
expertInfo = json['expert_info'];
|
||||||
|
honours = json['honours'];
|
||||||
|
}
|
||||||
|
}
|
||||||
226
lib/models/msg/session.dart
Normal file
226
lib/models/msg/session.dart
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:pilipala/models/msg/account.dart';
|
||||||
|
|
||||||
|
class SessionDataModel {
|
||||||
|
SessionDataModel({
|
||||||
|
this.sessionList,
|
||||||
|
this.hasMore,
|
||||||
|
});
|
||||||
|
|
||||||
|
List? sessionList;
|
||||||
|
int? hasMore;
|
||||||
|
|
||||||
|
SessionDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
sessionList = json['session_list']
|
||||||
|
.map<SessionList>((e) => SessionList.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
hasMore = json['has_more'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionList {
|
||||||
|
SessionList({
|
||||||
|
this.talkerId,
|
||||||
|
this.sessionType,
|
||||||
|
this.atSeqno,
|
||||||
|
this.topTs,
|
||||||
|
this.groupName,
|
||||||
|
this.groupCover,
|
||||||
|
this.isFollow,
|
||||||
|
this.isDnd,
|
||||||
|
this.ackSeqno,
|
||||||
|
this.ackTs,
|
||||||
|
this.sessionTs,
|
||||||
|
this.unreadCount,
|
||||||
|
this.lastMsg,
|
||||||
|
this.groupType,
|
||||||
|
this.canFold,
|
||||||
|
this.status,
|
||||||
|
this.maxSeqno,
|
||||||
|
this.newPushMsg,
|
||||||
|
this.setting,
|
||||||
|
this.isGuardian,
|
||||||
|
this.isIntercept,
|
||||||
|
this.isTrust,
|
||||||
|
this.systemMsgType,
|
||||||
|
this.liveStatus,
|
||||||
|
this.bizMsgUnreadCount,
|
||||||
|
// this.userLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? talkerId;
|
||||||
|
int? sessionType;
|
||||||
|
int? atSeqno;
|
||||||
|
int? topTs;
|
||||||
|
String? groupName;
|
||||||
|
String? groupCover;
|
||||||
|
int? isFollow;
|
||||||
|
int? isDnd;
|
||||||
|
int? ackSeqno;
|
||||||
|
int? ackTs;
|
||||||
|
int? sessionTs;
|
||||||
|
int? unreadCount;
|
||||||
|
LastMsg? lastMsg;
|
||||||
|
int? groupType;
|
||||||
|
int? canFold;
|
||||||
|
int? status;
|
||||||
|
int? maxSeqno;
|
||||||
|
int? newPushMsg;
|
||||||
|
int? setting;
|
||||||
|
int? isGuardian;
|
||||||
|
int? isIntercept;
|
||||||
|
int? isTrust;
|
||||||
|
int? systemMsgType;
|
||||||
|
int? liveStatus;
|
||||||
|
int? bizMsgUnreadCount;
|
||||||
|
// int? userLabel;
|
||||||
|
AccountListModel? accountInfo;
|
||||||
|
|
||||||
|
SessionList.fromJson(Map<String, dynamic> json) {
|
||||||
|
talkerId = json["talker_id"];
|
||||||
|
sessionType = json["session_type"];
|
||||||
|
atSeqno = json["at_seqno"];
|
||||||
|
topTs = json["top_ts"];
|
||||||
|
groupName = json["group_name"];
|
||||||
|
groupCover = json["group_cover"];
|
||||||
|
isFollow = json["is_follow"];
|
||||||
|
isDnd = json["is_dnd"];
|
||||||
|
ackSeqno = json["ack_seqno"];
|
||||||
|
ackTs = json["ack_ts"];
|
||||||
|
sessionTs = json["session_ts"];
|
||||||
|
unreadCount = json["unread_count"];
|
||||||
|
lastMsg =
|
||||||
|
json["last_msg"] != null ? LastMsg.fromJson(json["last_msg"]) : null;
|
||||||
|
groupType = json["group_type"];
|
||||||
|
canFold = json["can_fold"];
|
||||||
|
status = json["status"];
|
||||||
|
maxSeqno = json["max_seqno"];
|
||||||
|
newPushMsg = json["new_push_msg"];
|
||||||
|
setting = json["setting"];
|
||||||
|
isGuardian = json["is_guardian"];
|
||||||
|
isIntercept = json["is_intercept"];
|
||||||
|
isTrust = json["is_trust"];
|
||||||
|
systemMsgType = json["system_msg_type"];
|
||||||
|
liveStatus = json["live_status"];
|
||||||
|
bizMsgUnreadCount = json["biz_msg_unread_count"];
|
||||||
|
// userLabel = json["user_label"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LastMsg {
|
||||||
|
LastMsg({
|
||||||
|
this.senderIid,
|
||||||
|
this.receiverType,
|
||||||
|
this.receiverId,
|
||||||
|
this.msgType,
|
||||||
|
this.content,
|
||||||
|
this.msgSeqno,
|
||||||
|
this.timestamp,
|
||||||
|
this.atUids,
|
||||||
|
this.msgKey,
|
||||||
|
this.msgStatus,
|
||||||
|
this.notifyCode,
|
||||||
|
this.newFaceVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? senderIid;
|
||||||
|
int? receiverType;
|
||||||
|
int? receiverId;
|
||||||
|
int? msgType;
|
||||||
|
Map? content;
|
||||||
|
int? msgSeqno;
|
||||||
|
int? timestamp;
|
||||||
|
String? atUids;
|
||||||
|
int? msgKey;
|
||||||
|
int? msgStatus;
|
||||||
|
String? notifyCode;
|
||||||
|
int? newFaceVersion;
|
||||||
|
|
||||||
|
LastMsg.fromJson(Map<String, dynamic> json) {
|
||||||
|
senderIid = json['sender_uid'];
|
||||||
|
receiverType = json['receiver_type'];
|
||||||
|
receiverId = json['receiver_id'];
|
||||||
|
msgType = json['msg_type'];
|
||||||
|
content = jsonDecode(json['content']);
|
||||||
|
msgSeqno = json['msg_seqno'];
|
||||||
|
timestamp = json['timestamp'];
|
||||||
|
atUids = json['at_uids'];
|
||||||
|
msgKey = json['msg_key'];
|
||||||
|
msgStatus = json['msg_status'];
|
||||||
|
notifyCode = json['notify_code'];
|
||||||
|
newFaceVersion = json['new_face_version'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionMsgDataModel {
|
||||||
|
SessionMsgDataModel({
|
||||||
|
this.messages,
|
||||||
|
this.hasMore,
|
||||||
|
this.minSeqno,
|
||||||
|
this.maxSeqno,
|
||||||
|
this.eInfos,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<MessageItem>? messages;
|
||||||
|
int? hasMore;
|
||||||
|
int? minSeqno;
|
||||||
|
int? maxSeqno;
|
||||||
|
List? eInfos;
|
||||||
|
|
||||||
|
SessionMsgDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
messages = json['messages']
|
||||||
|
.map<MessageItem>((e) => MessageItem.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
hasMore = json['has_more'];
|
||||||
|
minSeqno = json['min_seqno'];
|
||||||
|
maxSeqno = json['max_seqno'];
|
||||||
|
eInfos = json['e_infos'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessageItem {
|
||||||
|
MessageItem({
|
||||||
|
this.senderUid,
|
||||||
|
this.receiverType,
|
||||||
|
this.receiverId,
|
||||||
|
this.msgType,
|
||||||
|
this.content,
|
||||||
|
this.msgSeqno,
|
||||||
|
this.timestamp,
|
||||||
|
this.atUids,
|
||||||
|
this.msgKey,
|
||||||
|
this.msgStatus,
|
||||||
|
this.notifyCode,
|
||||||
|
this.newFaceVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? senderUid;
|
||||||
|
int? receiverType;
|
||||||
|
int? receiverId;
|
||||||
|
int? msgType;
|
||||||
|
Map? content;
|
||||||
|
int? msgSeqno;
|
||||||
|
int? timestamp;
|
||||||
|
List? atUids;
|
||||||
|
int? msgKey;
|
||||||
|
int? msgStatus;
|
||||||
|
String? notifyCode;
|
||||||
|
int? newFaceVersion;
|
||||||
|
|
||||||
|
MessageItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
senderUid = json['sender_uid'];
|
||||||
|
receiverType = json['receiver_type'];
|
||||||
|
receiverId = json['receiver_id'];
|
||||||
|
// 1 文本 2 图片 18 系统提示 10 系统通知
|
||||||
|
msgType = json['msg_type'];
|
||||||
|
content = jsonDecode(json['content']);
|
||||||
|
msgSeqno = json['msg_seqno'];
|
||||||
|
timestamp = json['timestamp'];
|
||||||
|
atUids = json['at_uids'];
|
||||||
|
msgKey = json['msg_key'];
|
||||||
|
msgStatus = json['msg_status'];
|
||||||
|
notifyCode = json['notify_code'];
|
||||||
|
newFaceVersion = json['new_face_version'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -129,7 +129,11 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Expanded(child: SearchPage()),
|
const Expanded(child: SearchPage()),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 6),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => Get.toNamed('/whisper'),
|
||||||
|
icon: const Icon(Icons.notifications_none)),
|
||||||
|
const SizedBox(width: 6),
|
||||||
Obx(
|
Obx(
|
||||||
() => ctr!.userLogin.value
|
() => ctr!.userLogin.value
|
||||||
? Stack(
|
? Stack(
|
||||||
|
|||||||
50
lib/pages/whisper/controller.dart
Normal file
50
lib/pages/whisper/controller.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/http/msg.dart';
|
||||||
|
import 'package:pilipala/models/msg/account.dart';
|
||||||
|
import 'package:pilipala/models/msg/session.dart';
|
||||||
|
|
||||||
|
class WhisperController extends GetxController {
|
||||||
|
RxList<SessionList> sessionList = <SessionList>[].obs;
|
||||||
|
RxList<AccountListModel> accountList = <AccountListModel>[].obs;
|
||||||
|
|
||||||
|
Future querySessionList() async {
|
||||||
|
var res = await MsgHttp.sessionList();
|
||||||
|
if (res['data'].sessionList.isNotEmpty) {
|
||||||
|
await queryAccountList(res['data'].sessionList);
|
||||||
|
// 将 accountList 转换为 Map 结构
|
||||||
|
Map<int, dynamic> accountMap = {};
|
||||||
|
for (var j in accountList) {
|
||||||
|
accountMap[j.mid!] = j;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历 sessionList,通过 mid 查找并赋值 accountInfo
|
||||||
|
for (var i in res['data'].sessionList) {
|
||||||
|
var accountInfo = accountMap[i.talkerId];
|
||||||
|
if (accountInfo != null) {
|
||||||
|
i.accountInfo = accountInfo;
|
||||||
|
}
|
||||||
|
if (i.talkerId == 844424930131966) {
|
||||||
|
i.accountInfo = AccountListModel(
|
||||||
|
name: 'UP主小助手',
|
||||||
|
face:
|
||||||
|
'https://message.biliimg.com/bfs/im/489a63efadfb202366c2f88853d2217b5ddc7a13.png',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (res['status']) {
|
||||||
|
sessionList.value = res['data'].sessionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future queryAccountList(sessionList) async {
|
||||||
|
List midsList = sessionList.map((e) => e.talkerId!).toList();
|
||||||
|
var res = await MsgHttp.accountList(midsList.join(','));
|
||||||
|
if (res['status']) {
|
||||||
|
accountList.value = res['data'];
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
lib/pages/whisper/index.dart
Normal file
4
lib/pages/whisper/index.dart
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
library whisper;
|
||||||
|
|
||||||
|
export './controller.dart';
|
||||||
|
export './view.dart';
|
||||||
178
lib/pages/whisper/view.dart
Normal file
178
lib/pages/whisper/view.dart
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
|
||||||
|
import 'controller.dart';
|
||||||
|
|
||||||
|
class WhisperPage extends StatefulWidget {
|
||||||
|
const WhisperPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WhisperPage> createState() => _WhisperPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WhisperPageState extends State<WhisperPage> {
|
||||||
|
late final WhisperController _whisperController =
|
||||||
|
Get.put(WhisperController());
|
||||||
|
late Future _futureBuilderFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_futureBuilderFuture = _whisperController.querySessionList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// LayoutBuilder(
|
||||||
|
// builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
// // 在这里根据父级容器的约束条件构建小部件树
|
||||||
|
// return Padding(
|
||||||
|
// padding: const EdgeInsets.only(left: 20, right: 20),
|
||||||
|
// child: SizedBox(
|
||||||
|
// height: constraints.maxWidth / 5,
|
||||||
|
// child: GridView.count(
|
||||||
|
// primary: false,
|
||||||
|
// crossAxisCount: 4,
|
||||||
|
// padding: const EdgeInsets.all(0),
|
||||||
|
// childAspectRatio: 1.25,
|
||||||
|
// children: [
|
||||||
|
// Column(
|
||||||
|
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
// children: [
|
||||||
|
// SizedBox(
|
||||||
|
// width: 36,
|
||||||
|
// height: 36,
|
||||||
|
// child: IconButton(
|
||||||
|
// style: ButtonStyle(
|
||||||
|
// padding:
|
||||||
|
// MaterialStateProperty.all(EdgeInsets.zero),
|
||||||
|
// backgroundColor:
|
||||||
|
// MaterialStateProperty.resolveWith((states) {
|
||||||
|
// return Theme.of(context)
|
||||||
|
// .colorScheme
|
||||||
|
// .primary
|
||||||
|
// .withOpacity(0.1);
|
||||||
|
// }),
|
||||||
|
// ),
|
||||||
|
// onPressed: () {},
|
||||||
|
// icon: Icon(
|
||||||
|
// Icons.message_outlined,
|
||||||
|
// size: 18,
|
||||||
|
// color: Theme.of(context).colorScheme.primary,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 6),
|
||||||
|
// const Text('回复我的', style: TextStyle(fontSize: 13))
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
Expanded(
|
||||||
|
child: FutureBuilder(
|
||||||
|
future: _futureBuilderFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
Map data = snapshot.data as Map;
|
||||||
|
if (data['status']) {
|
||||||
|
List sessionList = _whisperController.sessionList;
|
||||||
|
return Obx(
|
||||||
|
() => sessionList.isEmpty
|
||||||
|
? const SizedBox()
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: sessionList.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (_, int i) {
|
||||||
|
return ListTile(
|
||||||
|
onTap: () {
|
||||||
|
Get.toNamed(
|
||||||
|
'/whisperDetail?talkerId=${sessionList[i].talkerId}&name=${sessionList[i].accountInfo.name}');
|
||||||
|
},
|
||||||
|
leading: NetworkImgLayer(
|
||||||
|
width: 45,
|
||||||
|
height: 45,
|
||||||
|
type: 'avatar',
|
||||||
|
src: sessionList[i].accountInfo.face,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
sessionList[i].accountInfo.name,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
sessionList[i].lastMsg.content['text'] ??
|
||||||
|
sessionList[i]
|
||||||
|
.lastMsg
|
||||||
|
.content['content'] ??
|
||||||
|
sessionList[i]
|
||||||
|
.lastMsg
|
||||||
|
.content['title'],
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelMedium!
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.outline)),
|
||||||
|
trailing: Text(
|
||||||
|
Utils.dateFormat(
|
||||||
|
sessionList[i].lastMsg.timestamp),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelSmall!
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.outline),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 请求错误
|
||||||
|
return SizedBox();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 骨架屏
|
||||||
|
return SizedBox();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ListTile(
|
||||||
|
// onTap: () {},
|
||||||
|
// leading: CircleAvatar(),
|
||||||
|
// title: Text('钱瑞昌'),
|
||||||
|
// subtitle: Text('没事', style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
// trailing: Text('昨天'),
|
||||||
|
// ),
|
||||||
|
// ListTile(
|
||||||
|
// onTap: () {},
|
||||||
|
// leading: CircleAvatar(),
|
||||||
|
// title: Text('李天'),
|
||||||
|
// subtitle:
|
||||||
|
// Text('明天有空吗', style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
// trailing: Text('现在'),
|
||||||
|
// )
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/pages/whisperDetail/controller.dart
Normal file
24
lib/pages/whisperDetail/controller.dart
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/http/msg.dart';
|
||||||
|
import 'package:pilipala/models/msg/session.dart';
|
||||||
|
|
||||||
|
class WhisperDetailController extends GetxController {
|
||||||
|
late int talkerId;
|
||||||
|
RxString name = ''.obs;
|
||||||
|
RxList<MessageItem> messageList = <MessageItem>[].obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
talkerId = int.parse(Get.parameters['talkerId']!);
|
||||||
|
name.value = Get.parameters['name']!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future querySessionMsg() async {
|
||||||
|
var res = await MsgHttp.sessionMsg(talkerId: talkerId);
|
||||||
|
if (res['status']) {
|
||||||
|
messageList.value = res['data'].messages;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
lib/pages/whisperDetail/index.dart
Normal file
4
lib/pages/whisperDetail/index.dart
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
library whisper_detail;
|
||||||
|
|
||||||
|
export './controller.dart';
|
||||||
|
export './view.dart';
|
||||||
167
lib/pages/whisperDetail/view.dart
Normal file
167
lib/pages/whisperDetail/view.dart
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/pages/whisperDetail/controller.dart';
|
||||||
|
|
||||||
|
import 'widget/chat_item.dart';
|
||||||
|
|
||||||
|
class WhisperDetailPage extends StatefulWidget {
|
||||||
|
const WhisperDetailPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WhisperDetailPage> createState() => _WhisperDetailPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WhisperDetailPageState extends State<WhisperDetailPage> {
|
||||||
|
final WhisperDetailController _whisperDetailController =
|
||||||
|
Get.put(WhisperDetailController());
|
||||||
|
late Future _futureBuilderFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_futureBuilderFuture = _whisperDetailController.querySessionMsg();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
child: IconButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||||
|
backgroundColor:
|
||||||
|
MaterialStateProperty.resolveWith((states) {
|
||||||
|
return Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primaryContainer
|
||||||
|
.withOpacity(0.6);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
icon: Icon(
|
||||||
|
Icons.arrow_back_outlined,
|
||||||
|
size: 18,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Obx(
|
||||||
|
() => Text(
|
||||||
|
_whisperDetailController.name.value,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 36, height: 36),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: FutureBuilder(
|
||||||
|
future: _futureBuilderFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
Map data = snapshot.data as Map;
|
||||||
|
if (data['status']) {
|
||||||
|
List messageList = _whisperDetailController.messageList;
|
||||||
|
return Obx(
|
||||||
|
() => messageList.isEmpty
|
||||||
|
? const SizedBox()
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: messageList.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
reverse: true,
|
||||||
|
itemBuilder: (_, int i) {
|
||||||
|
if (i == 0) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
ChatItem(item: messageList[i]),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return ChatItem(item: messageList[i]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 请求错误
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 骨架屏
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
bottomNavigationBar: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: MediaQuery.of(context).padding.bottom + 70,
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 12,
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
width: 4,
|
||||||
|
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// IconButton(
|
||||||
|
// onPressed: () {},
|
||||||
|
// icon: Icon(
|
||||||
|
// Icons.add_circle_outline,
|
||||||
|
// color: Theme.of(context).colorScheme.outline,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.emoji_emotions_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Expanded(
|
||||||
|
// child: Container(
|
||||||
|
// height: 42,
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||||
|
// borderRadius: BorderRadius.circular(40.0),
|
||||||
|
// ),
|
||||||
|
// child: TextField(
|
||||||
|
// readOnly: true,
|
||||||
|
// style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
// decoration: const InputDecoration(
|
||||||
|
// border: InputBorder.none, // 移除默认边框
|
||||||
|
// hintText: '请输入内容', // 提示文本
|
||||||
|
// contentPadding: EdgeInsets.symmetric(
|
||||||
|
// horizontal: 12.0, vertical: 12.0), // 内边距
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
189
lib/pages/whisperDetail/widget/chat_item.dart
Normal file
189
lib/pages/whisperDetail/widget/chat_item.dart
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// ignore_for_file: must_be_immutable
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
|
||||||
|
class ChatItem extends StatelessWidget {
|
||||||
|
dynamic item;
|
||||||
|
|
||||||
|
ChatItem({
|
||||||
|
super.key,
|
||||||
|
this.item,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool isOwner = item.senderUid == 17340771;
|
||||||
|
bool isPic = item.msgType == 2;
|
||||||
|
bool isText = item.msgType == 1;
|
||||||
|
bool isSystem =
|
||||||
|
item.msgType == 18 || item.msgType == 10 || item.msgType == 13;
|
||||||
|
int msgType = item.msgType;
|
||||||
|
Map content = item.content ?? '';
|
||||||
|
return isSystem
|
||||||
|
? (msgType == 10
|
||||||
|
? SystemNotice(item: item)
|
||||||
|
: msgType == 13
|
||||||
|
? SystemNotice2(item: item)
|
||||||
|
: const SizedBox())
|
||||||
|
: Row(
|
||||||
|
children: [
|
||||||
|
if (!isOwner) const SizedBox(width: 12),
|
||||||
|
if (isOwner) const Spacer(),
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 300.0, // 设置最大宽度为200.0
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isOwner
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: const Radius.circular(16),
|
||||||
|
topRight: const Radius.circular(16),
|
||||||
|
bottomLeft: Radius.circular(isOwner ? 16 : 6),
|
||||||
|
bottomRight: Radius.circular(isOwner ? 6 : 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 12),
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: 8,
|
||||||
|
bottom: 6,
|
||||||
|
left: isPic ? 8 : 12,
|
||||||
|
right: isPic ? 8 : 12,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: isOwner
|
||||||
|
? CrossAxisAlignment.end
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
isText
|
||||||
|
? Text(
|
||||||
|
content['content'],
|
||||||
|
style: TextStyle(
|
||||||
|
color: isOwner
|
||||||
|
? Theme.of(context).colorScheme.onPrimary
|
||||||
|
: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSecondaryContainer),
|
||||||
|
)
|
||||||
|
: isPic
|
||||||
|
? NetworkImgLayer(
|
||||||
|
width: 220,
|
||||||
|
height:
|
||||||
|
220 * content['height'] / content['width'],
|
||||||
|
src: content['url'],
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
SizedBox(height: isPic ? 7 : 2),
|
||||||
|
Text(
|
||||||
|
Utils.dateFormat(item.timestamp),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||||
|
color: isOwner
|
||||||
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimary
|
||||||
|
.withOpacity(0.8)
|
||||||
|
: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSecondaryContainer
|
||||||
|
.withOpacity(0.8)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isOwner) const Spacer(),
|
||||||
|
if (isOwner) const SizedBox(width: 12),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SystemNotice extends StatelessWidget {
|
||||||
|
dynamic item;
|
||||||
|
SystemNotice({super.key, this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Map content = item.content ?? '';
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 300.0, // 设置最大宽度为200.0
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
bottomLeft: Radius.circular(6),
|
||||||
|
bottomRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 12),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(content['title'],
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium!
|
||||||
|
.copyWith(fontWeight: FontWeight.bold)),
|
||||||
|
Text(
|
||||||
|
Utils.dateFormat(item.timestamp),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelSmall!
|
||||||
|
.copyWith(color: Theme.of(context).colorScheme.outline),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
content['text'],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SystemNotice2 extends StatelessWidget {
|
||||||
|
dynamic item;
|
||||||
|
SystemNotice2({super.key, this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Map content = item.content ?? '';
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 300.0, // 设置最大宽度为200.0
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 12),
|
||||||
|
padding: const EdgeInsets.only(bottom: 6),
|
||||||
|
child: NetworkImgLayer(
|
||||||
|
width: 320,
|
||||||
|
height: 150,
|
||||||
|
src: content['pic_url'],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -37,6 +37,8 @@ import 'package:pilipala/pages/video/detail/replyReply/index.dart';
|
|||||||
import 'package:pilipala/pages/webview/index.dart';
|
import 'package:pilipala/pages/webview/index.dart';
|
||||||
import 'package:pilipala/pages/setting/index.dart';
|
import 'package:pilipala/pages/setting/index.dart';
|
||||||
import 'package:pilipala/pages/media/index.dart';
|
import 'package:pilipala/pages/media/index.dart';
|
||||||
|
import 'package:pilipala/pages/whisper/index.dart';
|
||||||
|
import 'package:pilipala/pages/whisperDetail/index.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
|
||||||
Box setting = GStrorage.setting;
|
Box setting = GStrorage.setting;
|
||||||
@ -122,6 +124,11 @@ class Routes {
|
|||||||
CustomGetPage(name: '/playSpeedSet', page: () => const PlaySpeedPage()),
|
CustomGetPage(name: '/playSpeedSet', page: () => const PlaySpeedPage()),
|
||||||
// 收藏搜索
|
// 收藏搜索
|
||||||
CustomGetPage(name: '/favSearch', page: () => const FavSearchPage()),
|
CustomGetPage(name: '/favSearch', page: () => const FavSearchPage()),
|
||||||
|
// 消息页面
|
||||||
|
CustomGetPage(name: '/whisper', page: () => const WhisperPage()),
|
||||||
|
// 私信详情
|
||||||
|
CustomGetPage(
|
||||||
|
name: '/whisperDetail', page: () => const WhisperDetailPage()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user