diff --git a/lib/http/api.dart b/lib/http/api.dart
index 42819d7d..4e5dca54 100644
--- a/lib/http/api.dart
+++ b/lib/http/api.dart
@@ -578,4 +578,7 @@ class Api {
/// 稍后再看&收藏夹视频列表
static const String mediaList = '/x/v2/medialist/resource/list';
+
+ /// 用户专栏
+ static const String opusList = '/x/polymer/web-dynamic/v1/opus/feed/space';
}
diff --git a/lib/http/member.dart b/lib/http/member.dart
index e87aa42e..459d6747 100644
--- a/lib/http/member.dart
+++ b/lib/http/member.dart
@@ -1,5 +1,8 @@
+import 'dart:convert';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
+import 'package:html/parser.dart';
+import 'package:pilipala/models/member/article.dart';
import 'package:pilipala/models/member/like.dart';
import '../common/constants.dart';
import '../models/dynamics/result.dart';
@@ -556,4 +559,60 @@ class MemberHttp {
};
}
}
+
+ static Future getWWebid({required int mid}) async {
+ var res = await Request().get('https://space.bilibili.com/$mid/article');
+ String? headContent = parse(res.data).head?.outerHtml;
+ final regex = RegExp(
+ r'');
+ if (headContent != null) {
+ final match = regex.firstMatch(headContent);
+ if (match != null && match.groupCount >= 1) {
+ final content = match.group(1);
+ String decodedString = Uri.decodeComponent(content!);
+ Map map = jsonDecode(decodedString);
+ return {'status': true, 'data': map['access_id']};
+ } else {
+ return {'status': false, 'data': '请检查登录状态'};
+ }
+ }
+ return {'status': false, 'data': '请检查登录状态'};
+ }
+
+ // 获取用户专栏
+ static Future getMemberArticle({
+ required int mid,
+ required int pn,
+ required String wWebid,
+ String? offset,
+ }) async {
+ Map params = await WbiSign().makSign({
+ 'host_mid': mid,
+ 'page': pn,
+ 'offset': offset,
+ 'web_location': 333.999,
+ 'w_webid': wWebid,
+ });
+ var res = await Request().get(Api.opusList, data: {
+ 'host_mid': mid,
+ 'page': pn,
+ 'offset': offset,
+ 'web_location': 333.999,
+ 'w_webid': wWebid,
+ 'w_rid': params['w_rid'],
+ 'wts': params['wts'],
+ });
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': MemberArticleDataModel.fromJson(res.data['data'])
+ };
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'] ?? '请求异常',
+ };
+ }
+ }
}
diff --git a/lib/models/member/article.dart b/lib/models/member/article.dart
new file mode 100644
index 00000000..8489385e
--- /dev/null
+++ b/lib/models/member/article.dart
@@ -0,0 +1,46 @@
+class MemberArticleDataModel {
+ MemberArticleDataModel({
+ this.hasMore,
+ this.items,
+ this.offset,
+ this.updateNum,
+ });
+
+ bool? hasMore;
+ List? items;
+ String? offset;
+ int? updateNum;
+
+ MemberArticleDataModel.fromJson(Map json) {
+ hasMore = json['has_more'];
+ items = json['items']
+ .map((e) => MemberArticleItemModel.fromJson(e))
+ .toList();
+ offset = json['offset'];
+ updateNum = json['update_num'];
+ }
+}
+
+class MemberArticleItemModel {
+ MemberArticleItemModel({
+ this.content,
+ this.cover,
+ this.jumpUrl,
+ this.opusId,
+ this.stat,
+ });
+
+ String? content;
+ Map? cover;
+ String? jumpUrl;
+ String? opusId;
+ Map? stat;
+
+ MemberArticleItemModel.fromJson(Map json) {
+ content = json['content'];
+ cover = json['cover'];
+ jumpUrl = json['jump_url'];
+ opusId = json['opus_id'];
+ stat = json['stat'];
+ }
+}
diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart
index ada869b5..3b7f24a4 100644
--- a/lib/pages/member/controller.dart
+++ b/lib/pages/member/controller.dart
@@ -240,4 +240,6 @@ class MemberController extends GetxController {
}
void pushfavPage() => Get.toNamed('/fav?mid=$mid');
+ // 跳转图文专栏
+ void pushArticlePage() => Get.toNamed('/memberArticle?mid=$mid');
}
diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart
index be0ddedc..2939628c 100644
--- a/lib/pages/member/view.dart
+++ b/lib/pages/member/view.dart
@@ -170,32 +170,44 @@ class _MemberPageState extends State
),
/// 视频
- Obx(() => ListTile(
- onTap: _memberController.pushArchivesPage,
- title: Text(
- '${_memberController.isOwner.value ? '我' : 'Ta'}的投稿'),
- trailing: const Icon(Icons.arrow_forward_outlined,
- size: 19),
- )),
+ Obx(
+ () => ListTile(
+ onTap: _memberController.pushArchivesPage,
+ title: Text(
+ '${_memberController.isOwner.value ? '我' : 'Ta'}的投稿'),
+ trailing:
+ const Icon(Icons.arrow_forward_outlined, size: 19),
+ ),
+ ),
/// 他的收藏夹
- Obx(() => ListTile(
- onTap: _memberController.pushfavPage,
- title: Text(
- '${_memberController.isOwner.value ? '我' : 'Ta'}的收藏'),
- trailing: const Icon(Icons.arrow_forward_outlined,
- size: 19),
- )),
+ Obx(
+ () => ListTile(
+ onTap: _memberController.pushfavPage,
+ title: Text(
+ '${_memberController.isOwner.value ? '我' : 'Ta'}的收藏'),
+ trailing:
+ const Icon(Icons.arrow_forward_outlined, size: 19),
+ ),
+ ),
/// 专栏
- Obx(() => ListTile(
+ Obx(
+ () => ListTile(
+ onTap: _memberController.pushArticlePage,
title: Text(
- '${_memberController.isOwner.value ? '我' : 'Ta'}的专栏'))),
+ '${_memberController.isOwner.value ? '我' : 'Ta'}的专栏'),
+ trailing:
+ const Icon(Icons.arrow_forward_outlined, size: 19),
+ ),
+ ),
/// 合集
- Obx(() => ListTile(
- title: Text(
- '${_memberController.isOwner.value ? '我' : 'Ta'}的合集'))),
+ Obx(
+ () => ListTile(
+ title: Text(
+ '${_memberController.isOwner.value ? '我' : 'Ta'}的合集')),
+ ),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
diff --git a/lib/pages/member_article/controller.dart b/lib/pages/member_article/controller.dart
new file mode 100644
index 00000000..cffce2fe
--- /dev/null
+++ b/lib/pages/member_article/controller.dart
@@ -0,0 +1,68 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/http/member.dart';
+import 'package:pilipala/models/member/article.dart';
+
+class MemberArticleController extends GetxController {
+ final ScrollController scrollController = ScrollController();
+ late int mid;
+ int pn = 1;
+ String? offset;
+ bool hasMore = true;
+ String? wWebid;
+ RxBool isLoading = false.obs;
+ RxList articleList = [].obs;
+
+ @override
+ void onInit() {
+ super.onInit();
+ mid = int.parse(Get.parameters['mid']!);
+ }
+
+ // 获取wWebid
+ Future getWWebid() async {
+ var res = await MemberHttp.getWWebid(mid: mid);
+ if (res['status']) {
+ wWebid = res['data'];
+ } else {
+ wWebid = '-1';
+ SmartDialog.showToast(res['msg']);
+ }
+ }
+
+ Future getMemberArticle(type) async {
+ if (isLoading.value) {
+ return;
+ }
+ isLoading.value = true;
+ if (wWebid == null) {
+ await getWWebid();
+ }
+ if (type == 'init') {
+ pn = 1;
+ articleList.clear();
+ }
+ var res = await MemberHttp.getMemberArticle(
+ mid: mid,
+ pn: pn,
+ offset: offset,
+ wWebid: wWebid!,
+ );
+ if (res['status']) {
+ offset = res['data'].offset;
+ hasMore = res['data'].hasMore!;
+ if (type == 'init') {
+ articleList.value = res['data'].items;
+ }
+ if (type == 'onLoad') {
+ articleList.addAll(res['data'].items);
+ }
+ pn += 1;
+ } else {
+ SmartDialog.showToast(res['msg']);
+ }
+ isLoading.value = false;
+ return res;
+ }
+}
diff --git a/lib/pages/member_article/index.dart b/lib/pages/member_article/index.dart
new file mode 100644
index 00000000..bfc2f344
--- /dev/null
+++ b/lib/pages/member_article/index.dart
@@ -0,0 +1,4 @@
+library member_article;
+
+export './controller.dart';
+export './view.dart';
diff --git a/lib/pages/member_article/view.dart b/lib/pages/member_article/view.dart
new file mode 100644
index 00000000..e23e208d
--- /dev/null
+++ b/lib/pages/member_article/view.dart
@@ -0,0 +1,176 @@
+import 'package:easy_debounce/easy_throttle.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/common/skeleton/skeleton.dart';
+import 'package:pilipala/common/widgets/http_error.dart';
+import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/common/widgets/no_data.dart';
+import 'package:pilipala/utils/utils.dart';
+
+import 'controller.dart';
+
+class MemberArticlePage extends StatefulWidget {
+ const MemberArticlePage({super.key});
+
+ @override
+ State createState() => _MemberArticlePageState();
+}
+
+class _MemberArticlePageState extends State {
+ late MemberArticleController _memberArticleController;
+ late Future _futureBuilderFuture;
+ late ScrollController scrollController;
+ late int mid;
+
+ @override
+ void initState() {
+ super.initState();
+ mid = int.parse(Get.parameters['mid']!);
+ final String heroTag = Utils.makeHeroTag(mid);
+ _memberArticleController = Get.put(MemberArticleController(), tag: heroTag);
+ _futureBuilderFuture = _memberArticleController.getMemberArticle('init');
+ scrollController = _memberArticleController.scrollController;
+
+ scrollController.addListener(_scrollListener);
+ }
+
+ void _scrollListener() {
+ if (scrollController.position.pixels >=
+ scrollController.position.maxScrollExtent - 200) {
+ EasyThrottle.throttle(
+ 'member_archives', const Duration(milliseconds: 500), () {
+ _memberArticleController.getMemberArticle('onLoad');
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ titleSpacing: 0,
+ centerTitle: false,
+ title: const Text('Ta的图文', style: TextStyle(fontSize: 16)),
+ ),
+ body: FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (BuildContext context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ if (snapshot.data != null) {
+ return _buildContent(snapshot.data as Map);
+ } else {
+ return _buildError(snapshot.data['msg']);
+ }
+ } else {
+ return ListView.builder(
+ itemCount: 10,
+ itemBuilder: (BuildContext context, int index) {
+ return _buildSkeleton();
+ },
+ );
+ }
+ },
+ ),
+ );
+ }
+
+ Widget _buildContent(Map data) {
+ RxList list = _memberArticleController.articleList;
+ if (data['status']) {
+ return Obx(
+ () => list.isNotEmpty
+ ? ListView.separated(
+ controller: scrollController,
+ itemCount: list.length,
+ separatorBuilder: (BuildContext context, int index) {
+ return Divider(
+ height: 10,
+ color: Theme.of(context).dividerColor.withOpacity(0.15),
+ );
+ },
+ itemBuilder: (BuildContext context, int index) {
+ return _buildListItem(list[index]);
+ },
+ )
+ : const CustomScrollView(
+ physics: NeverScrollableScrollPhysics(),
+ slivers: [
+ NoData(),
+ ],
+ ),
+ );
+ } else {
+ return _buildError(data['msg']);
+ }
+ }
+
+ Widget _buildListItem(dynamic item) {
+ return ListTile(
+ onTap: () {
+ Get.toNamed('/opus', parameters: {
+ 'title': item.content,
+ 'id': item.opusId,
+ 'articleType': 'opus',
+ });
+ },
+ leading: NetworkImgLayer(
+ width: 50,
+ height: 50,
+ type: 'emote',
+ src: item.cover['url'],
+ ),
+ title: Text(
+ item.content,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ subtitle: Padding(
+ padding: const EdgeInsets.only(top: 4),
+ child: Text(
+ '${item.stat["like"]}人点赞',
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildError(String errMsg) {
+ return CustomScrollView(
+ physics: const NeverScrollableScrollPhysics(),
+ slivers: [
+ SliverToBoxAdapter(
+ child: HttpError(
+ errMsg: errMsg,
+ fn: () {},
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildSkeleton() {
+ return Skeleton(
+ child: ListTile(
+ leading: Container(
+ width: 50,
+ height: 50,
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.onInverseSurface,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ title: Container(
+ height: 16,
+ color: Theme.of(context).colorScheme.onInverseSurface,
+ ),
+ subtitle: Container(
+ height: 11,
+ color: Theme.of(context).colorScheme.onInverseSurface,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart
index 136de91f..a679fd79 100644
--- a/lib/router/app_pages.dart
+++ b/lib/router/app_pages.dart
@@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/pages/fav_edit/index.dart';
import 'package:pilipala/pages/follow_search/view.dart';
+import 'package:pilipala/pages/member_article/index.dart';
import 'package:pilipala/pages/message/at/index.dart';
import 'package:pilipala/pages/message/like/index.dart';
import 'package:pilipala/pages/message/reply/index.dart';
@@ -186,6 +187,9 @@ class Routes {
name: '/messageSystem', page: () => const MessageSystemPage()),
// 收藏夹编辑
CustomGetPage(name: '/favEdit', page: () => const FavEditPage()),
+ // 用户专栏
+ CustomGetPage(
+ name: '/memberArticle', page: () => const MemberArticlePage()),
];
}