diff --git a/lib/common/widgets/badge.dart b/lib/common/widgets/badge.dart index baa2bff2..a8f2fc67 100644 --- a/lib/common/widgets/badge.dart +++ b/lib/common/widgets/badge.dart @@ -1,37 +1,5 @@ import 'package:flutter/material.dart'; -// Widget pBadge( -// text, -// context, -// double? top, -// double? right, -// double? bottom, -// double? left, { -// type = 'primary', -// }) { -// Color bgColor = Theme.of(context).colorScheme.primary; -// Color color = Theme.of(context).colorScheme.onPrimary; -// if (type == 'gray') { -// bgColor = Colors.black54.withOpacity(0.4); -// color = Colors.white; -// } -// return Positioned( -// top: top, -// left: left, -// right: right, -// bottom: bottom, -// child: Container( -// padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 6), -// decoration: -// BoxDecoration(borderRadius: BorderRadius.circular(4), color: bgColor), -// child: Text( -// text, -// style: TextStyle(fontSize: 11, color: color), -// ), -// ), -// ); -// } - class PBadge extends StatelessWidget { final String? text; final double? top; diff --git a/lib/common/widgets/content_container.dart b/lib/common/widgets/content_container.dart new file mode 100644 index 00000000..076a02e9 --- /dev/null +++ b/lib/common/widgets/content_container.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class ContentContainer extends StatelessWidget { + final Widget? contentWidget; + final Widget? bottomWidget; + final bool isScrollable; + final Clip? childClipBehavior; + + const ContentContainer( + {Key? key, + this.contentWidget, + this.bottomWidget, + this.isScrollable = true, + this.childClipBehavior}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return SingleChildScrollView( + clipBehavior: childClipBehavior ?? Clip.hardEdge, + physics: isScrollable ? null : NeverScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: constraints.copyWith( + minHeight: constraints.maxHeight, + maxHeight: double.infinity, + ), + child: IntrinsicHeight( + child: Column( + children: [ + if (contentWidget != null) + Expanded( + child: contentWidget!, + ) + else + Spacer(), + if (bottomWidget != null) bottomWidget!, + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index 6d4fa61a..551f4063 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -17,6 +17,10 @@ class VideoCardH extends StatelessWidget { final Function()? longPress; final Function()? longPressEnd; final String source; + final bool showOwner; + final bool showView; + final bool showDanmaku; + final bool showPubdate; const VideoCardH({ Key? key, @@ -24,6 +28,10 @@ class VideoCardH extends StatelessWidget { this.longPress, this.longPressEnd, this.source = 'normal', + this.showOwner = true, + this.showView = true, + this.showDanmaku = true, + this.showPubdate = false, }) : super(key: key); @override @@ -103,7 +111,14 @@ class VideoCardH extends StatelessWidget { }, ), ), - VideoContent(videoItem: videoItem, source: source) + VideoContent( + videoItem: videoItem, + source: source, + showOwner: showOwner, + showView: showView, + showDanmaku: showDanmaku, + showPubdate: showPubdate, + ) ], ), ); @@ -119,8 +134,20 @@ class VideoContent extends StatelessWidget { // ignore: prefer_typing_uninitialized_variables final videoItem; final String source; - const VideoContent( - {super.key, required this.videoItem, this.source = 'normal'}); + final bool showOwner; + final bool showView; + final bool showDanmaku; + final bool showPubdate; + + const VideoContent({ + super.key, + required this.videoItem, + this.source = 'normal', + this.showOwner = true, + this.showView = true, + this.showDanmaku = true, + this.showPubdate = false, + }); @override Widget build(BuildContext context) { @@ -179,34 +206,40 @@ class VideoContent extends StatelessWidget { // ), // ), // const SizedBox(height: 4), - Row( - children: [ - Text( - videoItem.owner.name, - style: TextStyle( - fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.outline, + if (showPubdate) + Text( + Utils.dateFormat(videoItem.pubdate!), + style: TextStyle( + fontSize: 11, color: Theme.of(context).colorScheme.outline), + ), + if (showOwner) + Row( + children: [ + Text( + videoItem.owner.name, + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), ), - ), - ], - ), + ], + ), Row( children: [ - StatView( - theme: 'gray', - view: videoItem.stat.view, - ), - const SizedBox(width: 8), - StatDanMu( - theme: 'gray', - danmu: videoItem.stat.danmaku, - ), - // Text( - // Utils.dateFormat(videoItem.pubdate!), - // style: TextStyle( - // fontSize: 11, - // color: Theme.of(context).colorScheme.outline), - // ) + if (showView) ...[ + StatView( + theme: 'gray', + view: videoItem.stat.view, + ), + const SizedBox(width: 8), + ], + if (showDanmaku) + StatDanMu( + theme: 'gray', + danmu: videoItem.stat.danmaku, + ), + const Spacer(), // SizedBox( // width: 20, diff --git a/lib/http/api.dart b/lib/http/api.dart index 042d8e11..af7d8b9e 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -215,7 +215,7 @@ class Api { // 粉丝 // vmid 用户id pn 页码 ps 每页个数,最大50 order: desc // order_type 排序规则 最近访问传空,最常访问传 attention - static const String fans = 'https://api.bilibili.com/x/relation/fans'; + static const String fans = '/x/relation/fans'; // 直播 // ?page=1&page_size=30&platform=web @@ -372,4 +372,36 @@ class Api { /// local_id static const getWebKey = 'https://passport.bilibili.com/x/passport-login/web/key'; + + /// 置顶视频 + static const getTopVideoApi = '/x/space/top/arc'; + + /// 主页 - 最近投币的视频 + /// vmid + /// gaia_source = main_web + /// web_location + /// w_rid + /// wts + static const getRecentCoinVideoApi = '/x/space/coin/video'; + + /// 最近点赞的视频 + static const getRecentLikeVideoApi = '/x/space/like/video'; + + /// 最近追番 + static const getRecentBangumiApi = '/x/space/bangumi/follow/list'; + + /// 用户专栏 + static const getMemberSeasonsApi = '/x/polymer/web-space/home/seasons_series'; + + /// 获赞数 播放数 + static const getMemberViewApi = '/x/space/upstat'; + + /// 查询某个专栏 + /// mid + /// season_id + /// sort_reverse + /// page_num + /// page_size + static const getSeasonDetailApi = + '/x/polymer/web-space/seasons_archives_list'; } diff --git a/lib/http/member.dart b/lib/http/member.dart index a48dbffd..9dc6da85 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -1,8 +1,12 @@ +import 'dart:ffi'; + import 'package:pilipala/http/index.dart'; import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/models/member/archive.dart'; +import 'package:pilipala/models/member/coin.dart'; import 'package:pilipala/models/member/info.dart'; +import 'package:pilipala/models/member/seasons.dart'; import 'package:pilipala/models/member/tags.dart'; import 'package:pilipala/utils/wbi_sign.dart'; @@ -215,4 +219,144 @@ class MemberHttp { }; } } + + // 获取up置顶 + static Future getTopVideo(String? vmid) async { + var res = await Request().get(Api.getTopVideoApi); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'] + .map((e) => MemberTagItemModel.fromJson(e)) + .toList() + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 获取uo专栏 + static Future getMemberSeasons(int? mid, int? pn, int? ps) async { + var res = await Request().get(Api.getMemberSeasonsApi, data: { + 'mid': mid, + 'page_num': pn, + 'page_size': ps, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists']) + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 最近投币 + static Future getRecentCoinVideo({required int mid}) async { + Map params = await WbiSign().makSign({ + 'mid': mid, + 'gaia_source': 'main_web', + 'web_location': 333.999, + }); + var res = await Request().get( + Api.getRecentCoinVideoApi, + data: { + 'vmid': mid, + 'gaia_source': 'main_web', + 'web_location': 333.999, + 'w_rid': params['w_rid'], + 'wts': params['wts'], + }, + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'] + .map((e) => MemberCoinsDataModel.fromJson(e)) + .toList(), + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 最近点赞 + static Future getRecentLikeVideo({required int mid}) async { + Map params = await WbiSign().makSign({ + 'mid': mid, + 'gaia_source': 'main_web', + 'web_location': 333.999, + }); + var res = await Request().get( + Api.getRecentLikeVideoApi, + data: { + 'vmid': mid, + 'gaia_source': 'main_web', + 'web_location': 333.999, + 'w_rid': params['w_rid'], + 'wts': params['wts'], + }, + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists']) + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 查看某个专栏 + static Future getSeasonDetail({ + required int mid, + required int seasonId, + bool sortReverse = false, + required int pn, + required int ps, + }) async { + var res = await Request().get( + Api.getSeasonDetailApi, + data: { + 'mid': mid, + 'season_id': seasonId, + 'sort_reverse': sortReverse, + 'page_num': pn, + 'page_size': ps, + }, + ); + if (res.data['code'] == 0) { + try { + return { + 'status': true, + 'data': MemberSeasonsList.fromJson(res.data['data']) + }; + } catch (err) { + print(err); + } + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/models/member/coin.dart b/lib/models/member/coin.dart new file mode 100644 index 00000000..701131f1 --- /dev/null +++ b/lib/models/member/coin.dart @@ -0,0 +1,89 @@ +class MemberCoinsDataModel { + MemberCoinsDataModel({ + this.aid, + this.bvid, + this.cid, + this.coins, + this.copyright, + this.ctime, + this.desc, + this.duration, + this.owner, + this.pic, + this.pubLocation, + this.pubdate, + this.resourceType, + this.state, + this.subtitle, + this.time, + this.title, + this.tname, + this.videos, + this.view, + this.danmaku, + }); + + int? aid; + String? bvid; + int? cid; + int? coins; + int? copyright; + int? ctime; + String? desc; + int? duration; + Owner? owner; + String? pic; + String? pubLocation; + int? pubdate; + String? resourceType; + int? state; + String? subtitle; + int? time; + String? title; + String? tname; + int? videos; + int? view; + int? danmaku; + + MemberCoinsDataModel.fromJson(Map json) { + aid = json['aid']; + bvid = json['bvid']; + cid = json['cid']; + coins = json['coins']; + copyright = json['copyright']; + ctime = json['ctime']; + desc = json['desc']; + duration = json['duration']; + owner = Owner.fromJson(json['owner']); + pic = json['pic']; + pubLocation = json['pub_location']; + pubdate = json['pubdate']; + resourceType = json['resource_type']; + state = json['state']; + subtitle = json['subtitle']; + time = json['time']; + title = json['title']; + tname = json['tname']; + videos = json['videos']; + view = json['stat']['view']; + danmaku = json['stat']['danmaku']; + } +} + +class Owner { + Owner({ + this.mid, + this.name, + this.face, + }); + + int? mid; + String? name; + String? face; + + Owner.fromJson(Map json) { + mid = json['mid']; + name = json['name']; + face = json['face']; + } +} diff --git a/lib/models/member/seasons.dart b/lib/models/member/seasons.dart new file mode 100644 index 00000000..70230367 --- /dev/null +++ b/lib/models/member/seasons.dart @@ -0,0 +1,108 @@ +class MemberSeasonsDataModel { + MemberSeasonsDataModel({ + this.page, + this.seasonsList, + }); + + Map? page; + List? seasonsList; + + MemberSeasonsDataModel.fromJson(Map json) { + page = json['page']; + seasonsList = json['seasons_list'] != null + ? json['seasons_list'] + .map((e) => MemberSeasonsList.fromJson(e)) + .toList() + : []; + } +} + +class MemberSeasonsList { + MemberSeasonsList({ + this.archives, + this.meta, + this.recentAids, + this.page, + }); + + List? archives; + MamberMeta? meta; + List? recentAids; + Map? page; + + MemberSeasonsList.fromJson(Map json) { + archives = json['archives'] != null + ? json['archives'] + .map((e) => MemberArchiveItem.fromJson(e)) + .toList() + : []; + meta = MamberMeta.fromJson(json['meta']); + page = json['page']; + } +} + +class MemberArchiveItem { + MemberArchiveItem({ + this.aid, + this.bvid, + this.ctime, + this.duration, + this.pic, + this.cover, + this.pubdate, + this.view, + this.title, + }); + + int? aid; + String? bvid; + int? ctime; + int? duration; + String? pic; + String? cover; + int? pubdate; + int? view; + String? title; + + MemberArchiveItem.fromJson(Map json) { + aid = json['aid']; + bvid = json['bvid']; + ctime = json['ctime']; + duration = json['duration']; + pic = json['pic']; + cover = json['pic']; + pubdate = json['pubdate']; + view = json['stat']['view']; + title = json['title']; + } +} + +class MamberMeta { + MamberMeta({ + this.cover, + this.description, + this.mid, + this.name, + this.ptime, + this.seasonId, + this.total, + }); + + String? cover; + String? description; + int? mid; + String? name; + int? ptime; + int? seasonId; + int? total; + + MamberMeta.fromJson(Map json) { + cover = json['cover']; + description = json['description']; + mid = json['mid']; + name = json['name']; + ptime = json['ptime']; + seasonId = json['season_id']; + total = json['total']; + } +} diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index 317d47e9..4f5527ff 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -95,8 +95,12 @@ class _PlDanmakuState extends State { // 根据position判断是否有已缓存弹幕。没有则请求对应段 int segIndex = (currentPosition / (6 * 60 * 1000)).ceil(); segIndex = segIndex < 1 ? 1 : segIndex; - if (ctr.dmSegList[segIndex - 1].elems.isEmpty && - !ctr.hasrequestSeg.contains(segIndex - 1)) { + print('🌹🌹: ${segIndex}'); + print('🌹🌹: ${ctr.dmSegList.length}'); + print('🌹🌹: ${ctr.hasrequestSeg.contains(segIndex - 1)}'); + if (segIndex - 1 >= ctr.dmSegList.length || + (ctr.dmSegList[segIndex - 1].elems.isEmpty && + !ctr.hasrequestSeg.contains(segIndex - 1))) { ctr.hasrequestSeg.add(segIndex - 1); ctr.currentSegIndex = segIndex; EasyThrottle.throttle('follow', const Duration(seconds: 1), () { diff --git a/lib/pages/member/archive/index.dart b/lib/pages/member/archive/index.dart deleted file mode 100644 index 8be45429..00000000 --- a/lib/pages/member/archive/index.dart +++ /dev/null @@ -1,4 +0,0 @@ -library archive_panel; - -export './controller.dart'; -export 'index.dart'; diff --git a/lib/pages/member/archive/view.dart b/lib/pages/member/archive/view.dart deleted file mode 100644 index f34a396c..00000000 --- a/lib/pages/member/archive/view.dart +++ /dev/null @@ -1,240 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:loading_more_list/loading_more_list.dart'; -import 'package:pilipala/common/widgets/video_card_h.dart'; -import 'package:pilipala/models/member/archive.dart'; -import 'package:pilipala/pages/member/archive/index.dart'; -import 'package:pilipala/utils/utils.dart'; -import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; - -class ArchivePanel extends StatefulWidget { - final int? mid; - const ArchivePanel({super.key, this.mid}); - - @override - State createState() => _ArchivePanelState(); -} - -class _ArchivePanelState extends State - with AutomaticKeepAliveClientMixin { - DateTime lastRefreshTime = DateTime.now(); - late final LoadMoreListSource source; - late final ArchiveController _archiveController; - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - print('🐶🐶: ${widget.mid}'); - _archiveController = Get.put(ArchiveController(widget.mid), - tag: Utils.makeHeroTag(widget.mid)); - source = LoadMoreListSource(_archiveController); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return PullToRefreshNotification( - onRefresh: () async { - await Future.delayed(const Duration(seconds: 1)); - return true; - }, - maxDragOffset: 50, - child: GlowNotificationWidget( - Column( - children: [ - // 下拉刷新指示器 - // PullToRefreshContainer( - // (PullToRefreshScrollNotificationInfo? info) { - // return PullToRefreshHeader(info, lastRefreshTime); - // }, - // ), - Padding( - padding: - const EdgeInsets.only(left: 14, top: 8, bottom: 8, right: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('排序方式'), - SizedBox( - height: 35, - width: 85, - child: TextButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - onPressed: () { - // _archiveController.order = 'click'; - // _archiveController.pn = 1; - _archiveController.toggleSort(); - source.refresh(true); - // LoadMoreListSource().loadData(); - }, - child: Obx( - () => AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: - (Widget child, Animation animation) { - return ScaleTransition( - scale: animation, child: child); - }, - child: Text( - _archiveController.currentOrder['label']!, - key: ValueKey( - _archiveController.currentOrder['label']!), - ), - ), - ), - ), - ), - ], - ), - ), - Expanded( - child: LoadingMoreList( - ListConfig( - sourceList: source, - itemBuilder: - (BuildContext c, VListItemModel item, int index) { - if (index == 0) { - return Column( - children: [ - const SizedBox(height: 6), - VideoCardH(videoItem: item) - ], - ); - } else { - return VideoCardH(videoItem: item); - } - }, - indicatorBuilder: _buildIndicator, - ), - ), - ) - ], - ), - showGlowLeading: false, - ), - ); - } - - Widget _buildIndicator(BuildContext context, IndicatorStatus status) { - TextStyle style = - TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline); - Widget? widget; - switch (status) { - case IndicatorStatus.none: - widget = Container(height: 0.0); - break; - case IndicatorStatus.loadingMoreBusying: - widget = Text('加载中...', style: style); - widget = _setbackground(false, widget, height: 60.0); - break; - case IndicatorStatus.fullScreenBusying: - widget = Text('加载中...', style: style); - widget = _setbackground(true, widget); - break; - case IndicatorStatus.error: - - /// TODO 异常逻辑 - widget = Text('没有更多了', style: style); - widget = _setbackground(false, widget); - - widget = GestureDetector( - onTap: () {}, - child: widget, - ); - - break; - case IndicatorStatus.fullScreenError: - - /// TODO 异常逻辑 - widget = Text('没有更多了', style: style); - widget = _setbackground(true, widget); - widget = GestureDetector( - onTap: () {}, - child: widget, - ); - break; - case IndicatorStatus.noMoreLoad: - widget = Text('没有更多了', style: style); - widget = _setbackground(false, widget, height: 60.0); - break; - case IndicatorStatus.empty: - widget = Text('用户没有投稿', style: style); - widget = _setbackground(true, widget); - break; - } - return widget; - } - - Widget _setbackground(bool full, Widget widget, {double height = 100}) { - widget = Padding( - padding: height == double.infinity - ? EdgeInsets.zero - : EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), - child: Container( - width: double.infinity, - height: height, - color: Theme.of(context).colorScheme.background, - alignment: Alignment.center, - child: widget, - ), - ); - return widget; - } - - Widget getIndicator(BuildContext context) { - final TargetPlatform platform = Theme.of(context).platform; - return platform == TargetPlatform.iOS - ? const CupertinoActivityIndicator( - animating: true, - radius: 16.0, - ) - : CircularProgressIndicator( - strokeWidth: 2.0, - valueColor: - AlwaysStoppedAnimation(Theme.of(context).primaryColor), - ); - } -} - -class LoadMoreListSource extends LoadingMoreBase { - late ArchiveController ctr; - LoadMoreListSource(this.ctr); - bool forceRefresh = false; - - @override - Future loadData([bool isloadMoreAction = false]) async { - bool isSuccess = false; - var res = await ctr.getMemberArchive(); - if (res['status']) { - if (ctr.pn == 2) { - clear(); - } - addAll(res['data'].list.vlist); - } - if (length < res['data'].page['count']) { - isSuccess = true; - } else { - isSuccess = false; - } - return isSuccess; - } - - @override - Future refresh([bool clearBeforeRequest = false]) async { - // _hasMore = true; - // pageindex = 1; - // //force to refresh list when you don't want clear list before request - // //for the case, if your list already has 20 items. - forceRefresh = !clearBeforeRequest; - var result = await super.refresh(clearBeforeRequest); - - forceRefresh = false; - return result; - } -} diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index 0d40ec65..49a448f1 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -6,6 +6,7 @@ import 'package:pilipala/http/member.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/member/archive.dart'; +import 'package:pilipala/models/member/coin.dart'; import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:share_plus/share_plus.dart'; @@ -20,9 +21,10 @@ class MemberController extends GetxController { late int ownerMid; // 投稿列表 RxList? archiveList = [VListItemModel()].obs; - var userInfo; + dynamic userInfo; RxInt attribute = (-1).obs; RxString attributeText = '关注'.obs; + RxList recentCoinsList = [].obs; @override void onInit() { @@ -55,14 +57,6 @@ class MemberController extends GetxController { return res; } - // Future getMemberCardInfo() async { - // var res = await MemberHttp.memberCardInfo(mid: mid); - // if (res['status']) { - // print(userStat); - // } - // return res; - // } - // 关注/取关up Future actionRelationMod() async { if (userInfo == null) { @@ -173,4 +167,35 @@ class MemberController extends GetxController { void shareUser() { Share.share('${memberInfo.value.name} - https://space.bilibili.com/$mid'); } + + // 请求专栏 + Future getMemberSeasons() async { + if (userInfo == null) return; + var res = await MemberHttp.getMemberSeasons(mid, 1, 10); + if (!res['status']) { + SmartDialog.showToast("用户专栏请求异常:${res['msg']}"); + } + return res; + } + + // 请求投币视频 + Future getRecentCoinVideo() async { + if (userInfo == null) return; + var res = await MemberHttp.getRecentCoinVideo(mid: mid); + recentCoinsList.value = res['data']; + return res; + } + + // 跳转查看动态 + void pushDynamicsPage() => Get.toNamed('/memberDynamics?mid=$mid'); + + // 跳转查看投稿 + void pushArchivesPage() => Get.toNamed('/memberArchive?mid=$mid'); + + // 跳转查看专栏 + void pushSeasonsPage() {} + // 跳转查看最近投币 + void pushRecentCoinsPage() async { + if (recentCoinsList.isNotEmpty) {} + } } diff --git a/lib/pages/member/dynamic/controller.dart b/lib/pages/member/dynamic/controller.dart deleted file mode 100644 index 056240ad..00000000 --- a/lib/pages/member/dynamic/controller.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:get/get.dart'; -import 'package:pilipala/http/member.dart'; - -class MemberDynamicPanelController extends GetxController { - MemberDynamicPanelController(this.mid); - int? mid; - String offset = ''; - int count = 0; - bool hasMore = true; - - @override - void onInit() { - super.onInit(); - mid ??= int.parse(Get.parameters['mid']!); - } - - Future getMemberDynamic() async { - if (!hasMore) { - return {'status': false}; - } - var res = await MemberHttp.memberDynamic( - offset: offset, - mid: mid, - ); - if (res['status']) { - offset = res['data'].offset; - hasMore = res['data'].hasMore; - } - return res; - } -} diff --git a/lib/pages/member/dynamic/view.dart b/lib/pages/member/dynamic/view.dart deleted file mode 100644 index 15d7376e..00000000 --- a/lib/pages/member/dynamic/view.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:loading_more_list/loading_more_list.dart'; -import 'package:pilipala/models/dynamics/result.dart'; -import 'package:pilipala/pages/dynamics/widgets/dynamic_panel.dart'; -import 'package:pilipala/utils/utils.dart'; - -import 'controller.dart'; - -class MemberDynamicPanel extends StatefulWidget { - final int? mid; - const MemberDynamicPanel({super.key, this.mid}); - - @override - State createState() => _MemberDynamicPanelState(); -} - -class _MemberDynamicPanelState extends State - with AutomaticKeepAliveClientMixin { - DateTime lastRefreshTime = DateTime.now(); - late final LoadMoreListSource source; - late final MemberDynamicPanelController _dynamicController; - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - _dynamicController = Get.put(MemberDynamicPanelController(widget.mid), - tag: Utils.makeHeroTag(widget.mid)); - source = LoadMoreListSource(_dynamicController); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return LoadingMoreList( - ListConfig( - sourceList: source, - itemBuilder: (BuildContext c, DynamicItemModel item, int index) { - return DynamicPanel(item: item); - }, - indicatorBuilder: _buildIndicator, - ), - ); - } - - Widget _buildIndicator(BuildContext context, IndicatorStatus status) { - TextStyle style = - TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline); - Widget? widget; - switch (status) { - case IndicatorStatus.none: - widget = Container(height: 0.0); - break; - case IndicatorStatus.loadingMoreBusying: - widget = Text('加载中...', style: style); - widget = _setbackground(false, widget, height: 60.0); - break; - case IndicatorStatus.fullScreenBusying: - widget = Text('加载中...', style: style); - widget = _setbackground(true, widget); - break; - case IndicatorStatus.error: - - /// TODO 异常逻辑 - widget = Text('没有更多了', style: style); - widget = _setbackground(false, widget); - - widget = GestureDetector( - onTap: () {}, - child: widget, - ); - - break; - case IndicatorStatus.fullScreenError: - - /// TODO 异常逻辑 - widget = Text('没有更多了', style: style); - widget = _setbackground(true, widget); - widget = GestureDetector( - onTap: () {}, - child: widget, - ); - break; - case IndicatorStatus.noMoreLoad: - widget = Text('没有更多了', style: style); - widget = _setbackground(false, widget, height: 60.0); - break; - case IndicatorStatus.empty: - widget = Text('用户没有投稿', style: style); - widget = _setbackground(true, widget); - break; - } - return widget; - } - - Widget _setbackground(bool full, Widget widget, {double height = 100}) { - widget = Padding( - padding: height == double.infinity - ? EdgeInsets.zero - : EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), - child: Container( - width: double.infinity, - height: height, - color: Theme.of(context).colorScheme.background, - alignment: Alignment.center, - child: widget, - ), - ); - return widget; - } - - Widget getIndicator(BuildContext context) { - final TargetPlatform platform = Theme.of(context).platform; - return platform == TargetPlatform.iOS - ? const CupertinoActivityIndicator( - animating: true, - radius: 16.0, - ) - : CircularProgressIndicator( - strokeWidth: 2.0, - valueColor: - AlwaysStoppedAnimation(Theme.of(context).primaryColor), - ); - } -} - -class LoadMoreListSource extends LoadingMoreBase { - late MemberDynamicPanelController ctr; - LoadMoreListSource(this.ctr); - - @override - Future loadData([bool isloadMoreAction = false]) async { - bool isSuccess = false; - var res = await ctr.getMemberDynamic(); - if (res['status']) { - addAll(res['data'].items); - } - try { - if (res['data'].hasMore) { - isSuccess = true; - } else { - isSuccess = false; - } - } catch (_) {} - - return isSuccess; - } -} diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 6fb8e228..08645777 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -1,16 +1,16 @@ import 'dart:async'; -import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; -import 'package:pilipala/pages/member/archive/view.dart'; -import 'package:pilipala/pages/member/dynamic/index.dart'; import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/utils/utils.dart'; +import 'widgets/conis.dart'; import 'widgets/profile.dart'; +import 'widgets/seasons.dart'; class MemberPage extends StatefulWidget { const MemberPage({super.key}); @@ -23,9 +23,10 @@ class _MemberPageState extends State with SingleTickerProviderStateMixin { late String heroTag; late MemberController _memberController; - Future? _futureBuilderFuture; + late Future _futureBuilderFuture; + late Future _memberSeasonsFuture; + late Future _memberCoinsFuture; final ScrollController _extendNestCtr = ScrollController(); - late TabController _tabController; final StreamController appbarStream = StreamController(); late int mid; @@ -35,12 +36,13 @@ class _MemberPageState extends State mid = int.parse(Get.parameters['mid']!); heroTag = Get.arguments['heroTag'] ?? Utils.makeHeroTag(mid); _memberController = Get.put(MemberController(), tag: heroTag); - _tabController = TabController(length: 3, vsync: this, initialIndex: 2); _futureBuilderFuture = _memberController.getInfo(); + _memberSeasonsFuture = _memberController.getMemberSeasons(); + _memberCoinsFuture = _memberController.getRecentCoinVideo(); _extendNestCtr.addListener( () { double offset = _extendNestCtr.position.pixels; - if (offset > 230) { + if (offset > 100) { appbarStream.add(true); } else { appbarStream.add(false); @@ -59,183 +61,222 @@ class _MemberPageState extends State Widget build(BuildContext context) { return Scaffold( primary: true, - body: ExtendedNestedScrollView( - controller: _extendNestCtr, - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { - return [ - SliverAppBar( - pinned: false, - primary: true, - elevation: 0, - scrolledUnderElevation: 1, - forceElevated: innerBoxIsScrolled, - expandedHeight: 290, - titleSpacing: 0, - title: StreamBuilder( - stream: appbarStream.stream, - initialData: false, - builder: (context, AsyncSnapshot snapshot) { - return AnimatedOpacity( - opacity: snapshot.data ? 1 : 0, - curve: Curves.easeOut, - duration: const Duration(milliseconds: 500), - child: Row( - children: [ - Row( - children: [ - Obx( - () => NetworkImgLayer( - width: 35, - height: 35, - type: 'avatar', - src: _memberController.face.value, - ), + body: Column( + children: [ + AppBar( + title: StreamBuilder( + stream: appbarStream.stream, + initialData: false, + builder: (context, AsyncSnapshot snapshot) { + return AnimatedOpacity( + opacity: snapshot.data ? 1 : 0, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 500), + child: Row( + children: [ + Row( + children: [ + Obx( + () => NetworkImgLayer( + width: 35, + height: 35, + type: 'avatar', + src: _memberController.face.value, ), - const SizedBox(width: 10), - Obx( - () => Text( - _memberController.memberInfo.value.name ?? '', - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onBackground, - fontSize: 14), - ), + ), + const SizedBox(width: 10), + Obx( + () => Text( + _memberController.memberInfo.value.name ?? '', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onBackground, + fontSize: 14), ), - ], - ) - ], - ), - ); - }, - ), - actions: [ - IconButton( - onPressed: () => Get.toNamed( - '/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'), - icon: const Icon(Icons.search_outlined), - ), - PopupMenuButton( - icon: const Icon(Icons.more_vert), - itemBuilder: (BuildContext context) => [ - if (_memberController.ownerMid != - _memberController.mid) ...[ - PopupMenuItem( - onTap: () => _memberController.blockUser(), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.block, size: 19), - const SizedBox(width: 10), - Text(_memberController.attribute.value != 128 - ? '加入黑名单' - : '移除黑名单'), - ], - ), + ), + ], ) ], + ), + ); + }, + ), + actions: [ + IconButton( + onPressed: () => Get.toNamed( + '/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'), + icon: const Icon(Icons.search_outlined), + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (BuildContext context) => [ + if (_memberController.ownerMid != _memberController.mid) ...[ PopupMenuItem( - onTap: () => _memberController.shareUser(), + onTap: () => _memberController.blockUser(), child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.share_outlined, size: 19), + const Icon(Icons.block, size: 19), const SizedBox(width: 10), - Text(_memberController.ownerMid != - _memberController.mid - ? '分享UP主' - : '分享我的主页'), + Text(_memberController.attribute.value != 128 + ? '加入黑名单' + : '移除黑名单'), ], ), - ), + ) ], + PopupMenuItem( + onTap: () => _memberController.shareUser(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.share_outlined, size: 19), + const SizedBox(width: 10), + Text(_memberController.ownerMid != _memberController.mid + ? '分享UP主' + : '分享我的主页'), + ], + ), + ), + ], + ), + const SizedBox(width: 4), + ], + ), + Expanded( + child: SingleChildScrollView( + controller: _extendNestCtr, + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + 20, ), - const SizedBox(width: 4), - ], - flexibleSpace: FlexibleSpaceBar( - background: Stack( + child: Column( children: [ + profileWidget(), + + /// 动态链接 + ListTile( + onTap: _memberController.pushDynamicsPage, + title: const Text('Ta的动态'), + trailing: + const Icon(Icons.arrow_forward_outlined, size: 19), + ), + + /// 视频 + ListTile( + onTap: _memberController.pushArchivesPage, + title: const Text('Ta的投稿'), + trailing: + const Icon(Icons.arrow_forward_outlined, size: 19), + ), + + /// 专栏 + ListTile( + onTap: () {}, + title: const Text('Ta的专栏'), + ), + MediaQuery.removePadding( + removeTop: true, + removeBottom: true, + context: context, + child: Padding( + padding: const EdgeInsets.only( + left: StyleString.safeSpace, + right: StyleString.safeSpace, + ), + child: FutureBuilder( + future: _memberSeasonsFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done) { + if (snapshot.data == null) { + return const SizedBox(); + } + if (snapshot.data['status']) { + Map data = snapshot.data as Map; + if (data['data'].seasonsList.isEmpty) { + return commenWidget('用户没有设置专栏'); + } else { + return MemberSeasonsPanel(data: data['data']); + } + } else { + // 请求错误 + return const SizedBox(); + } + } else { + return const SizedBox(); + } + }, + ), + ), + ), + + /// 收藏 + + /// 追番 + /// 最近投币 Obx( - () => _memberController.face.value != '' - ? Positioned.fill( - bottom: 10, - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - fit: BoxFit.fitWidth, - image: NetworkImage( - _memberController.face.value), - alignment: Alignment.topCenter, - isAntiAlias: true, - ), - ), - foregroundDecoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Theme.of(context) - .colorScheme - .background - .withOpacity(0.44), - Theme.of(context).colorScheme.background, - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: const [0.0, 0.46], - ), - ), - ), + () => _memberController.recentCoinsList.isNotEmpty + ? ListTile( + onTap: () {}, + title: const Text('最近投币的视频'), + // trailing: const Icon(Icons.arrow_forward_outlined, + // size: 19), ) : const SizedBox(), ), - Positioned( - left: 0, - right: 0, - bottom: 0, - height: 20, - child: Container( - color: Theme.of(context).colorScheme.background, + MediaQuery.removePadding( + removeTop: true, + removeBottom: true, + context: context, + child: Padding( + padding: const EdgeInsets.only( + left: StyleString.safeSpace, + right: StyleString.safeSpace, + ), + child: FutureBuilder( + future: _memberCoinsFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done) { + if (snapshot.data == null) { + return const SizedBox(); + } + if (snapshot.data['status']) { + Map data = snapshot.data as Map; + return MemberCoinsPanel(data: data['data']); + } else { + // 请求错误 + return const SizedBox(); + } + } else { + return const SizedBox(); + } + }, + ), ), ), - profileWidget(), + // 最近点赞 + // ListTile( + // onTap: () {}, + // title: const Text('最近点赞的视频'), + // trailing: + // const Icon(Icons.arrow_forward_outlined, size: 19), + // ), ], ), ), ), - ]; - }, - pinnedHeaderSliverHeightBuilder: () { - return MediaQuery.of(context).padding.top + kToolbarHeight; - }, - onlyOneScrollInBody: true, - body: Column( - children: [ - SizedBox( - width: double.infinity, - height: 50, - child: TabBar(controller: _tabController, tabs: const [ - Tab(text: '主页'), - Tab(text: '动态'), - Tab(text: '投稿'), - ]), - ), - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - const Text('主页'), - MemberDynamicPanel(mid: mid), - ArchivePanel(mid: mid), - ], - )) - ], - ), + ), + ], ), ); } Widget profileWidget() { return Padding( - padding: const EdgeInsets.only(left: 18, right: 18), + padding: const EdgeInsets.only(left: 18, right: 18, bottom: 20), child: FutureBuilder( future: _futureBuilderFuture, builder: (context, snapshot) { @@ -250,7 +291,7 @@ class _MemberPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ profile(_memberController), - const SizedBox(height: 14), + const SizedBox(height: 20), Row( children: [ Flexible( @@ -260,7 +301,7 @@ class _MemberPageState extends State overflow: TextOverflow.ellipsis, style: Theme.of(context) .textTheme - .bodyLarge! + .titleMedium! .copyWith(fontWeight: FontWeight.bold), )), const SizedBox(width: 2), @@ -332,29 +373,11 @@ class _MemberPageState extends State softWrap: true, ), ], - const SizedBox(height: 4), + const SizedBox(height: 6), if (_memberController.memberInfo.value.sign != '') SelectableText( _memberController.memberInfo.value.sign!, - maxLines: _memberController - .memberInfo.value.official!['title'] != - '' - ? 1 - : 2, - style: const TextStyle( - overflow: TextOverflow.ellipsis), - onTap: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - content: SelectableText(_memberController - .memberInfo.value.sign!), - ); - }, - ); - }, - ) + ), ], ), ], @@ -371,4 +394,22 @@ class _MemberPageState extends State ), ); } + + Widget commenWidget(msg) { + return Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 30, + ), + child: Center( + child: Text( + msg, + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.outline), + ), + ), + ); + } } diff --git a/lib/pages/member/widgets/conis.dart b/lib/pages/member/widgets/conis.dart new file mode 100644 index 00000000..57a8a583 --- /dev/null +++ b/lib/pages/member/widgets/conis.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/models/member/coin.dart'; +import 'package:pilipala/pages/member_coin/widgets/item.dart'; + +class MemberCoinsPanel extends StatelessWidget { + final List? data; + const MemberCoinsPanel({super.key, this.data}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, boxConstraints) { + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, // Use a fixed count for GridView + crossAxisSpacing: StyleString.safeSpace, + mainAxisSpacing: StyleString.safeSpace, + childAspectRatio: 0.94, + ), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: data!.length, + itemBuilder: (context, i) { + return MemberCoinsItem(coinItem: data![i]); + }, + ); + }, + ); + } +} diff --git a/lib/pages/member/widgets/profile.dart b/lib/pages/member/widgets/profile.dart index 22e88106..a8791631 100644 --- a/lib/pages/member/widgets/profile.dart +++ b/lib/pages/member/widgets/profile.dart @@ -11,7 +11,7 @@ Widget profile(ctr, {loadingStatus = false}) { return Builder( builder: ((context) { return Padding( - padding: EdgeInsets.only(top: 3 * MediaQuery.of(context).padding.top), + padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top - 20), child: Row( children: [ Hero( @@ -78,7 +78,8 @@ Widget profile(ctr, {loadingStatus = false}) { mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.only(left: 10, right: 10), + padding: + const EdgeInsets.only(top: 10, left: 10, right: 10), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, diff --git a/lib/pages/member/widgets/seasons.dart b/lib/pages/member/widgets/seasons.dart new file mode 100644 index 00000000..68c4077f --- /dev/null +++ b/lib/pages/member/widgets/seasons.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/badge.dart'; +import 'package:pilipala/models/member/seasons.dart'; +import 'package:pilipala/pages/member_seasons/widgets/item.dart'; + +class MemberSeasonsPanel extends StatelessWidget { + final MemberSeasonsDataModel? data; + const MemberSeasonsPanel({super.key, this.data}); + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: data!.seasonsList!.length, + itemBuilder: (context, index) { + MemberSeasonsList item = data!.seasonsList![index]; + return Padding( + padding: const EdgeInsets.only(bottom: 12, right: 4), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12, left: 4), + child: Row( + children: [ + Text( + item.meta!.name!, + maxLines: 1, + style: Theme.of(context).textTheme.titleSmall!, + ), + const SizedBox(width: 10), + PBadge( + stack: 'relative', + size: 'small', + text: item.meta!.total.toString(), + ), + const Spacer(), + SizedBox( + width: 35, + height: 35, + child: IconButton( + onPressed: () => Get.toNamed( + '/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'), + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + icon: const Icon( + Icons.arrow_forward, + size: 20, + ), + ), + ) + ], + ), + ), + LayoutBuilder( + builder: (context, boxConstraints) { + return GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, // Use a fixed count for GridView + crossAxisSpacing: StyleString.safeSpace, + mainAxisSpacing: StyleString.safeSpace, + childAspectRatio: 0.94, + ), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: item.archives!.length, + itemBuilder: (context, i) { + return MemberSeasonsItem(seasonItem: item.archives![i]); + }, + ); + }, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/member/archive/controller.dart b/lib/pages/member_archive/controller.dart similarity index 54% rename from lib/pages/member/archive/controller.dart rename to lib/pages/member_archive/controller.dart index 3fc90328..785cd764 100644 --- a/lib/pages/member/archive/controller.dart +++ b/lib/pages/member_archive/controller.dart @@ -1,9 +1,11 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/member.dart'; +import 'package:pilipala/models/member/archive.dart'; -class ArchiveController extends GetxController { - ArchiveController(this.mid); - int? mid; +class MemberArchiveController extends GetxController { + final ScrollController scrollController = ScrollController(); + late int mid; int pn = 1; int count = 0; RxMap currentOrder = {}.obs; @@ -12,20 +14,27 @@ class ArchiveController extends GetxController { {'type': 'click', 'label': '最多播放'}, {'type': 'stow', 'label': '最多收藏'}, ]; + RxList archivesList = [].obs; @override void onInit() { super.onInit(); - mid ??= int.parse(Get.parameters['mid']!); - print('🐶🐶: $mid'); + mid = int.parse(Get.parameters['mid']!); currentOrder.value = orderList.first; } // 获取用户投稿 - Future getMemberArchive() async { + Future getMemberArchive(type) async { + if (type == 'onRefresh') { + pn = 1; + } var res = await MemberHttp.memberArchive( - mid: mid, pn: pn, order: currentOrder['type']!); + mid: mid, + pn: pn, + order: currentOrder['type']!, + ); if (res['status']) { + archivesList.addAll(res['data'].list.vlist); count = res['data'].page['count']; pn += 1; } @@ -34,11 +43,16 @@ class ArchiveController extends GetxController { toggleSort() async { pn = 1; - int index = orderList.indexOf(currentOrder.value); + int index = orderList.indexOf(currentOrder); if (index == orderList.length - 1) { currentOrder.value = orderList.first; } else { currentOrder.value = orderList[index + 1]; } } + + // 上拉加载 + Future onLoad() async { + getMemberArchive('onLoad'); + } } diff --git a/lib/pages/member_archive/index.dart b/lib/pages/member_archive/index.dart new file mode 100644 index 00000000..4c7551da --- /dev/null +++ b/lib/pages/member_archive/index.dart @@ -0,0 +1,4 @@ +library member_archive; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/member_archive/view.dart b/lib/pages/member_archive/view.dart new file mode 100644 index 00000000..21773f2d --- /dev/null +++ b/lib/pages/member_archive/view.dart @@ -0,0 +1,120 @@ +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/video_card_h.dart'; +import 'controller.dart'; + +class MemberArchivePage extends StatefulWidget { + const MemberArchivePage({super.key}); + + @override + State createState() => _MemberArchivePageState(); +} + +class _MemberArchivePageState extends State { + final MemberArchiveController _memberArchivesController = + Get.put(MemberArchiveController()); + late Future _futureBuilderFuture; + late ScrollController scrollController; + + @override + void initState() { + super.initState(); + _futureBuilderFuture = + _memberArchivesController.getMemberArchive('onRefresh'); + scrollController = _memberArchivesController.scrollController; + scrollController.addListener( + () { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle( + 'member_archives', const Duration(milliseconds: 500), () { + _memberArchivesController.onLoad(); + }); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('他的投稿'), + // actions: [ + // Obx( + // () => PopupMenuButton( + // padding: EdgeInsets.zero, + // tooltip: '投稿排序', + // icon: Icon( + // Icons.more_vert_outlined, + // color: Theme.of(context).colorScheme.outline, + // ), + // position: PopupMenuPosition.under, + // onSelected: (String type) {}, + // itemBuilder: (BuildContext context) => >[ + // for (var i in _memberArchivesController.orderList) ...[ + // PopupMenuItem( + // onTap: () {}, + // value: _memberArchivesController.currentOrder['label'], + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Text(i['label']!), + // if (_memberArchivesController.currentOrder['label'] == + // i['label']) ...[ + // const SizedBox(width: 10), + // const Icon(Icons.done, size: 20), + // ], + // ], + // ), + // ), + // ] + // ], + // ), + // ), + // ], + ), + body: CustomScrollView( + controller: _memberArchivesController.scrollController, + slivers: [ + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data != null) { + Map data = snapshot.data as Map; + List list = _memberArchivesController.archivesList; + if (data['status']) { + return Obx( + () => list.isNotEmpty + ? SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return VideoCardH( + videoItem: list[index], + showOwner: false, + showPubdate: true, + ); + }, + childCount: list.length, + ), + ) + : const SliverToBoxAdapter(), + ); + } else { + return const SliverToBoxAdapter(); + } + } else { + return const SliverToBoxAdapter(); + } + } else { + return const SliverToBoxAdapter(); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/member_coin/controller.dart b/lib/pages/member_coin/controller.dart new file mode 100644 index 00000000..dc3f5d8f --- /dev/null +++ b/lib/pages/member_coin/controller.dart @@ -0,0 +1,3 @@ +import 'package:get/get.dart'; + +class MemberCoinController extends GetxController {} diff --git a/lib/pages/member/dynamic/index.dart b/lib/pages/member_coin/index.dart similarity index 68% rename from lib/pages/member/dynamic/index.dart rename to lib/pages/member_coin/index.dart index de6f3f89..3009dd2b 100644 --- a/lib/pages/member/dynamic/index.dart +++ b/lib/pages/member_coin/index.dart @@ -1,4 +1,4 @@ -library dynamic_panel; +library member_coin; export './controller.dart'; export './view.dart'; diff --git a/lib/pages/member_coin/view.dart b/lib/pages/member_coin/view.dart new file mode 100644 index 00000000..f74880a5 --- /dev/null +++ b/lib/pages/member_coin/view.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class MemberCoinPage extends StatefulWidget { + const MemberCoinPage({super.key}); + + @override + State createState() => _MemberCoinPageState(); +} + +class _MemberCoinPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold(); + } +} diff --git a/lib/pages/member_coin/widgets/item.dart b/lib/pages/member_coin/widgets/item.dart new file mode 100644 index 00000000..ea6e7ee1 --- /dev/null +++ b/lib/pages/member_coin/widgets/item.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/badge.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/common/widgets/stat/view.dart'; +import 'package:pilipala/http/search.dart'; +import 'package:pilipala/models/member/coin.dart'; +import 'package:pilipala/utils/utils.dart'; + +class MemberCoinsItem extends StatelessWidget { + final MemberCoinsDataModel coinItem; + + const MemberCoinsItem({ + Key? key, + required this.coinItem, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(coinItem.aid); + return Card( + elevation: 0, + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + child: InkWell( + onTap: () async { + int cid = + await SearchHttp.ab2c(aid: coinItem.aid, bvid: coinItem.bvid); + Get.toNamed('/video?bvid=${coinItem.bvid}&cid=$cid', + arguments: {'videoItem': coinItem, 'heroTag': heroTag}); + }, + child: Column( + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder(builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + NetworkImgLayer( + src: coinItem.pic, + width: maxWidth, + height: maxHeight, + ), + if (coinItem.duration != null) + PBadge( + bottom: 6, + right: 6, + type: 'gray', + text: Utils.timeFormat(coinItem.duration), + ) + ], + ); + }), + ), + Padding( + padding: const EdgeInsets.fromLTRB(5, 6, 0, 0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + coinItem.title!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + StatView( + view: coinItem.view, + theme: 'gray', + ), + const Spacer(), + Text( + Utils.CustomStamp_str( + timestamp: coinItem.pubdate, date: 'MM-DD'), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.outline, + ), + ), + const SizedBox(width: 6) + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/member_dynamics/controller.dart b/lib/pages/member_dynamics/controller.dart new file mode 100644 index 00000000..b6cd1b74 --- /dev/null +++ b/lib/pages/member_dynamics/controller.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/member.dart'; +import 'package:pilipala/models/dynamics/result.dart'; + +class MemberDynamicsController extends GetxController { + final ScrollController scrollController = ScrollController(); + late int mid; + String offset = ''; + int count = 0; + bool hasMore = true; + RxList dynamicsList = [].obs; + + @override + void onInit() { + super.onInit(); + mid = int.parse(Get.parameters['mid']!); + } + + Future getMemberDynamic(type) async { + if (type == 'onRefresh') { + offset = ''; + dynamicsList.clear(); + } + var res = await MemberHttp.memberDynamic( + offset: offset, + mid: mid, + ); + if (res['status']) { + dynamicsList.addAll(res['data'].items); + offset = res['data'].offset; + hasMore = res['data'].hasMore; + } + return res; + } + + // 上拉加载 + Future onLoad() async { + getMemberDynamic('onLoad'); + } +} diff --git a/lib/pages/member_dynamics/index.dart b/lib/pages/member_dynamics/index.dart new file mode 100644 index 00000000..52951909 --- /dev/null +++ b/lib/pages/member_dynamics/index.dart @@ -0,0 +1,4 @@ +library member_dynamics; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/member_dynamics/view.dart b/lib/pages/member_dynamics/view.dart new file mode 100644 index 00000000..0682c5a6 --- /dev/null +++ b/lib/pages/member_dynamics/view.dart @@ -0,0 +1,86 @@ +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/pages/member_dynamics/index.dart'; + +import '../dynamics/widgets/dynamic_panel.dart'; + +class MemberDynamicsPage extends StatefulWidget { + const MemberDynamicsPage({super.key}); + + @override + State createState() => _MemberDynamicsPageState(); +} + +class _MemberDynamicsPageState extends State { + final MemberDynamicsController _memberDynamicController = + Get.put(MemberDynamicsController()); + late Future _futureBuilderFuture; + late ScrollController scrollController; + + @override + void initState() { + super.initState(); + _futureBuilderFuture = + _memberDynamicController.getMemberDynamic('onRefresh'); + scrollController = _memberDynamicController.scrollController; + scrollController.addListener( + () { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle( + 'member_dynamics', const Duration(milliseconds: 500), () { + _memberDynamicController.onLoad(); + }); + } + }, + ); + } + + @override + void dispose() { + _memberDynamicController.scrollController.removeListener(() {}); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('他的动态'), + ), + body: CustomScrollView( + controller: _memberDynamicController.scrollController, + slivers: [ + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + List list = _memberDynamicController.dynamicsList; + if (data['status']) { + return Obx( + () => list.isNotEmpty + ? SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return DynamicPanel(item: list[index]); + }, + childCount: list.length, + ), + ) + : const SliverToBoxAdapter(), + ); + } else { + return const SliverToBoxAdapter(); + } + } else { + return const SliverToBoxAdapter(); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/member_like/controller.dart b/lib/pages/member_like/controller.dart new file mode 100644 index 00000000..1d2e9c7c --- /dev/null +++ b/lib/pages/member_like/controller.dart @@ -0,0 +1,3 @@ +import 'package:get/get.dart'; + +class MemberLikeController extends GetxController {} diff --git a/lib/pages/member_like/index.dart b/lib/pages/member_like/index.dart new file mode 100644 index 00000000..e84771e3 --- /dev/null +++ b/lib/pages/member_like/index.dart @@ -0,0 +1,4 @@ +library member_like; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/member_like/view.dart b/lib/pages/member_like/view.dart new file mode 100644 index 00000000..ca33d569 --- /dev/null +++ b/lib/pages/member_like/view.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class MemberLikePage extends StatefulWidget { + const MemberLikePage({super.key}); + + @override + State createState() => _MemberLikePageState(); +} + +class _MemberLikePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold(); + } +} diff --git a/lib/pages/member_seasons/controller.dart b/lib/pages/member_seasons/controller.dart new file mode 100644 index 00000000..82ef0af0 --- /dev/null +++ b/lib/pages/member_seasons/controller.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/member.dart'; +import 'package:pilipala/models/member/seasons.dart'; + +class MemberSeasonsController extends GetxController { + final ScrollController scrollController = ScrollController(); + late int mid; + late int seasonId; + int pn = 1; + int ps = 30; + int count = 0; + RxList seasonsList = [].obs; + late Map page; + + @override + void onInit() { + super.onInit(); + mid = int.parse(Get.parameters['mid']!); + seasonId = int.parse(Get.parameters['seasonId']!); + } + + // 获取专栏详情 + Future getSeasonDetail(type) async { + if (type == 'onRefresh') { + pn = 1; + } + var res = await MemberHttp.getSeasonDetail( + mid: mid, + seasonId: seasonId, + pn: pn, + ps: ps, + sortReverse: false, + ); + if (res['status']) { + seasonsList.addAll(res['data'].archives); + page = res['data'].page; + pn += 1; + } + return res; + } + + // 上拉加载 + Future onLoad() async { + getSeasonDetail('onLoad'); + } +} diff --git a/lib/pages/member_seasons/index.dart b/lib/pages/member_seasons/index.dart new file mode 100644 index 00000000..4a4dc63d --- /dev/null +++ b/lib/pages/member_seasons/index.dart @@ -0,0 +1,4 @@ +library member_seasons; + +export 'controller.dart'; +export 'view.dart'; diff --git a/lib/pages/member_seasons/view.dart b/lib/pages/member_seasons/view.dart new file mode 100644 index 00000000..97a43358 --- /dev/null +++ b/lib/pages/member_seasons/view.dart @@ -0,0 +1,103 @@ +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; +import 'controller.dart'; +import 'widgets/item.dart'; + +class MemberSeasonsPage extends StatefulWidget { + const MemberSeasonsPage({super.key}); + + @override + State createState() => _MemberSeasonsPageState(); +} + +class _MemberSeasonsPageState extends State { + final MemberSeasonsController _memberSeasonsController = + Get.put(MemberSeasonsController()); + late Future _futureBuilderFuture; + late ScrollController scrollController; + + @override + void initState() { + super.initState(); + _futureBuilderFuture = + _memberSeasonsController.getSeasonDetail('onRefresh'); + scrollController = _memberSeasonsController.scrollController; + scrollController.addListener( + () { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle( + 'member_archives', const Duration(milliseconds: 500), () { + _memberSeasonsController.onLoad(); + }); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('他的专栏'), + ), + body: Padding( + padding: const EdgeInsets.only( + left: StyleString.safeSpace, + right: StyleString.safeSpace, + ), + child: SingleChildScrollView( + controller: _memberSeasonsController.scrollController, + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data != null) { + Map data = snapshot.data as Map; + List list = _memberSeasonsController.seasonsList; + if (data['status']) { + return Obx( + () => list.isNotEmpty + ? LayoutBuilder( + builder: (context, boxConstraints) { + return GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: StyleString.safeSpace, + mainAxisSpacing: StyleString.safeSpace, + childAspectRatio: 0.94, + ), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: _memberSeasonsController + .seasonsList.length, + itemBuilder: (context, i) { + return MemberSeasonsItem( + seasonItem: _memberSeasonsController + .seasonsList[i], + ); + }, + ); + }, + ) + : const SizedBox(), + ); + } else { + return const SizedBox(); + } + } else { + return const SizedBox(); + } + } else { + return const SizedBox(); + } + }, + ), + ), + ), + ); + } +} diff --git a/lib/pages/member_seasons/widgets/item.dart b/lib/pages/member_seasons/widgets/item.dart new file mode 100644 index 00000000..157adb66 --- /dev/null +++ b/lib/pages/member_seasons/widgets/item.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/badge.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/common/widgets/stat/view.dart'; +import 'package:pilipala/http/search.dart'; +import 'package:pilipala/utils/utils.dart'; + +class MemberSeasonsItem extends StatelessWidget { + final dynamic seasonItem; + + const MemberSeasonsItem({ + Key? key, + required this.seasonItem, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(seasonItem.aid); + return Card( + elevation: 0, + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + child: InkWell( + onTap: () async { + int cid = + await SearchHttp.ab2c(aid: seasonItem.aid, bvid: seasonItem.bvid); + Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid', + arguments: {'videoItem': seasonItem, 'heroTag': heroTag}); + }, + child: Column( + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder(builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: seasonItem.pic, + width: maxWidth, + height: maxHeight, + ), + ), + if (seasonItem.duration != null) + PBadge( + bottom: 6, + right: 6, + type: 'gray', + text: Utils.timeFormat(seasonItem.duration), + ) + ], + ); + }), + ), + Padding( + padding: const EdgeInsets.fromLTRB(5, 6, 0, 0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + seasonItem.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + StatView( + view: seasonItem.view, + theme: 'gray', + ), + const Spacer(), + Text( + Utils.CustomStamp_str( + timestamp: seasonItem.pubdate, date: 'MM-DD'), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.outline, + ), + ), + const SizedBox(width: 6) + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 4590ec44..16cacdbf 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -76,6 +76,7 @@ class VideoIntroController extends GetxController { if (Get.arguments.containsKey('videoItem')) { preRender = true; var args = Get.arguments['videoItem']; + var keys = Get.arguments.keys.toList(); videoItem!['pic'] = args.pic; if (args.title is String) { videoItem!['title'] = args.title; @@ -86,11 +87,9 @@ class VideoIntroController extends GetxController { } videoItem!['title'] = str; } - if (args.stat != null) { - videoItem!['stat'] = args.stat; - } - videoItem!['pubdate'] = args.pubdate; - videoItem!['owner'] = args.owner; + videoItem!['stat'] = keys.contains('stat') && args.stat; + videoItem!['pubdate'] = keys.contains('pubdate') && args.pubdate; + videoItem!['owner'] = keys.contains('owner') && args.owner; } } userLogin = userInfo != null; diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index eb954510..2a9b774d 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -247,7 +247,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { padding: const EdgeInsets.only( left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10), sliver: SliverToBoxAdapter( - child: !loadingStatus || videoItem.isNotEmpty + child: !loadingStatus ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -277,7 +277,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { children: [ StatView( theme: 'gray', - view: !widget.loadingStatus + view: !loadingStatus ? widget.videoDetail!.stat!.view : videoItem['stat'].view, size: 'medium', @@ -285,7 +285,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { const SizedBox(width: 10), StatDanMu( theme: 'gray', - danmu: !widget.loadingStatus + danmu: !loadingStatus ? widget.videoDetail!.stat!.danmaku : videoItem['stat'].danmaku, size: 'medium', @@ -293,7 +293,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { const SizedBox(width: 10), Text( Utils.dateFormat( - !widget.loadingStatus + !loadingStatus ? widget.videoDetail!.pubdate : videoItem['pubdate'], formatType: 'detail'), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 6accf4f1..cec93c26 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -21,7 +21,12 @@ import 'package:pilipala/pages/later/index.dart'; import 'package:pilipala/pages/liveRoom/view.dart'; import 'package:pilipala/pages/login/index.dart'; import 'package:pilipala/pages/member/index.dart'; +import 'package:pilipala/pages/member_archive/index.dart'; +import 'package:pilipala/pages/member_coin/index.dart'; +import 'package:pilipala/pages/member_dynamics/index.dart'; +import 'package:pilipala/pages/member_like/index.dart'; import 'package:pilipala/pages/member_search/index.dart'; +import 'package:pilipala/pages/member_seasons/index.dart'; import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/searchResult/index.dart'; @@ -125,6 +130,19 @@ class Routes { CustomGetPage(name: '/favSearch', page: () => const FavSearchPage()), // 登录页面 CustomGetPage(name: '/loginPage', page: () => const LoginPage()), + // 用户动态 + CustomGetPage( + name: '/memberDynamics', page: () => const MemberDynamicsPage()), + // 用户投稿 + CustomGetPage( + name: '/memberArchive', page: () => const MemberArchivePage()), + // 用户最近投币 + CustomGetPage(name: '/memberCoin', page: () => const MemberCoinPage()), + // 用户最近喜欢 + CustomGetPage(name: '/memberLike', page: () => const MemberLikePage()), + // 用户专栏 + CustomGetPage( + name: '/memberSeasons', page: () => const MemberSeasonsPage()), ]; } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 8982c178..19979df6 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -146,7 +146,7 @@ class Utils { int.parse(MM) == DateTime.now().month) { // 当天 if (int.parse(DD) == DateTime.now().day) { - return date.split(' ')[1]; + return '今天'; } } return date;