This commit is contained in:
2026-03-07 17:24:59 +08:00
parent 4418ebecac
commit b0ec8ab4bd
417 changed files with 42546 additions and 2 deletions

View File

@@ -0,0 +1,398 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_color.dart';
import 'package:flutter_dmzj/app/app_constant.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/app/controller/base_controller.dart';
import 'package:flutter_dmzj/app/dialog_utils.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_dmzj/app/utils.dart';
import 'package:flutter_dmzj/requests/news_request.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:flutter_dmzj/services/app_settings_service.dart';
import 'package:flutter_dmzj/services/db_service.dart';
import 'package:flutter_dmzj/services/user_service.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:universal_html/html.dart' as html;
import 'package:universal_html/parsing.dart';
import 'package:webview_flutter/webview_flutter.dart';
class NewsDetailController extends BaseController {
final String newsUrl;
final String title;
final int id;
final NewsRequest request = NewsRequest();
AppSettingsService get settings => AppSettingsService.instance;
NewsDetailController(
{required this.newsUrl, this.title = "资讯详情", required this.id}) {
newsTitle.value = title;
if (id == 0) {
newsId = int.tryParse(
RegExp(r"/(\d+).html").firstMatch(newsUrl)?.group(1) ?? "0") ??
0;
} else {
newsId = id;
}
}
WebViewController? webViewController =
(Platform.isAndroid || Platform.isIOS) ? WebViewController() : null;
/// 评论数
var commentAmount = 0.obs;
/// 点赞数
var moodAmount = 0.obs;
/// 是否点过赞
var liked = false.obs;
/// 是否已经收藏
var collected = false.obs;
var newsId = 0;
var newsTitle = "资讯详情".obs;
var htmlContent = "".obs;
var author = "".obs;
var photo = "".obs;
var src = "".obs;
var time = "".obs;
var images = <String>[];
@override
void onInit() {
liked.value = DBService.instance.newsLikeBox.containsKey(newsId);
if (Platform.isAndroid || Platform.isIOS) {
initWebView();
} else {
loadHtml();
}
// loadStat();
// checkCollected();
super.onInit();
}
var currentUrl = "";
void initWebView() {
webViewController!.setJavaScriptMode(JavaScriptMode.unrestricted);
webViewController!.setBackgroundColor(
Get.isDarkMode ? Colors.black : AppColor.backgroundColor);
webViewController!.setNavigationDelegate(
NavigationDelegate(
onPageStarted: (String url) {
pageLoadding.value = true;
},
onPageFinished: (String url) async {
try {
await setFontSize();
//防止亮瞎24K钛合金狗眼
if (Get.isDarkMode) {
await webViewController!.runJavaScript("""
document.body.style.background="#000000";
document.getElementsByClassName("min_box")[0].style.background="#000000";
document.getElementsByClassName("news_box")[0].style.color="#f1f2f6";
document.getElementsByClassName("min_box_tit")[0].style.color="#fff";
""");
}
//加载前5张图片
//当Web没有滚动条时图片不会加载这里手动给他加载出来
await webViewController!.runJavaScript("""
\$('.news_box img:lt(5)').each(function () {
\$(this).lazyload({
effect: "fadeIn"
});
});""");
//读取全部的图片
var imagesResult =
await webViewController?.runJavaScriptReturningResult('''
function getImgLinks(){
var imgLinks = [];
\$('img').each(function() {
var src = \$(this).attr('data-original');
if (src && src.startsWith('https://images')) {
imgLinks.push(src);
}
});
console.log(imgLinks);
return ${Platform.isIOS ? "JSON.stringify(imgLinks)" : "imgLinks"};
}
getImgLinks();
''');
if (imagesResult != null && imagesResult != "null") {
List list = json.decode(imagesResult.toString());
images = list.map((e) => e.toString()).toList();
}
} finally {
pageLoadding.value = false;
}
},
onWebResourceError: (WebResourceError error) {},
onNavigationRequest: (NavigationRequest request) async {
var result = await onTapUrl(request.url);
return result
? NavigationDecision.prevent
: NavigationDecision.navigate;
},
),
);
Log.d(newsUrl);
currentUrl = "https://v3api.zaimanhua.com/v3/article/show/$newsId.html";
webViewController!.loadRequest(Uri.parse(currentUrl));
}
void loadHtml() async {
try {
pageError.value = false;
pageLoadding.value = true;
var result = await Dio().get(
newsUrl,
options: Options(
responseType: ResponseType.plain,
),
);
final htmlDocument = parseHtmlDocument(result.data);
var news = htmlDocument.documentElement!.querySelector('.news_box');
htmlContent.value = news!.innerHtml ?? "";
author.value =
htmlDocument.documentElement?.querySelector('.txt1')?.innerText ?? "";
src.value =
htmlDocument.documentElement?.querySelector('.txt2')?.innerText ?? "";
time.value =
htmlDocument.documentElement?.querySelector('.txt3')?.innerText ?? "";
var imgList = htmlDocument.documentElement?.querySelectorAll('img');
var imagesList = <String>[];
for (html.Element img in imgList ?? []) {
var imgSrc = img.getAttribute("data-original");
if (imgSrc != null) {
imagesList.add(imgSrc);
}
}
images = imagesList;
} catch (e) {
pageError.value = true;
errorMsg.value = e.toString();
} finally {
pageLoadding.value = false;
}
}
void loadStat() async {
try {
var result = await request.stat(newsId);
commentAmount.value = result.commentAmount;
moodAmount.value = result.moodAmount;
newsTitle.value = result.title;
} catch (e) {
SmartDialog.showToast(e.toString());
SmartDialog.showToast("读取新闻数据失败:$e");
}
}
void checkCollected() async {
if (!UserService.instance.logined.value) {
return;
}
try {
collected.value = await request.checkCollect(newsId);
} catch (e) {
Log.logPrint(e);
SmartDialog.showToast("检查用户收藏状态失败:$e");
}
}
void refershContent() {
webViewController!.reload();
}
void collect() async {
if (!await UserService.instance.login()) {
return;
}
try {
SmartDialog.showLoading();
await (collected.value
? request.delCollect(newsId)
: request.collect(newsId));
collected.value = !collected.value;
} catch (e) {
Log.logPrint(e);
SmartDialog.showToast(e.toString());
} finally {
SmartDialog.dismiss(status: SmartStatus.loading);
}
}
void like() async {
if (liked.value) {
SmartDialog.showToast("已经点过赞了");
return;
}
try {
SmartDialog.showLoading();
await request.like(newsId);
liked.value = true;
moodAmount.value += 1;
DBService.instance.newsLikeBox.put(newsId, true);
} catch (e) {
SmartDialog.showToast(e.toString());
} finally {
SmartDialog.dismiss(status: SmartStatus.loading);
}
}
void share() {
Utils.share(newsUrl, content: title);
}
void comment() async {
AppNavigator.toComment(objId: newsId, type: AppConstant.kTypeNews);
}
void photoView() {
DialogUtils.showImageViewer(0, images);
}
void showImageView(String imgSrc) {
if (imgSrc.isEmpty) {
return;
}
if (images.contains(imgSrc)) {
DialogUtils.showImageViewer(
images.indexOf(imgSrc),
images,
);
} else {
DialogUtils.showImageViewer(0, [imgSrc]);
}
}
Future<bool> onTapUrl(url) async {
//iOS处理
if (url == currentUrl) {
return false;
}
var uri = Uri.parse(url);
Log.d(url);
if (uri.scheme == "dmzjimage") {
//打开图片
showImageView(uri.queryParameters['src'].toString());
return true;
} else if (uri.scheme == "dmzjandroid") {
var id = int.tryParse(uri.queryParameters["id"].toString()) ?? 0;
if (uri.path == "/cartoon_description") {
AppNavigator.toComicDetail(id);
} else {
AppNavigator.toNovelDetail(id);
}
return true;
} else if (uri.scheme == "https" || uri.scheme == "http") {
if (uri.path.contains("article/")) {
AppNavigator.toNewsDetail(url: url);
} else {
AppNavigator.toWebView(url);
}
return true;
} else {
SmartDialog.showToast("无法打开链接:$url");
return true;
}
}
void showSettings() {
AppNavigator.showBottomSheet(
SizedBox(
height: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ListTile(
title: Text("设置"),
trailing: IconButton(
onPressed: AppNavigator.closePage,
icon: Icon(Icons.close),
),
contentPadding: AppStyle.edgeInsetsL12,
),
Divider(
height: 1.0,
color: Colors.grey.withOpacity(.2),
),
Obx(
() => ListTile(
title: const Text("字体大小"),
leading: const Icon(Icons.text_fields_rounded),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
OutlinedButton(
onPressed: () {
settings.setNewsFontSize(
settings.newsFontSize.value + 1,
);
setFontSize();
},
child: const Icon(
Icons.add,
color: Colors.grey,
),
),
AppStyle.hGap12,
Text("${settings.newsFontSize.value}"),
AppStyle.hGap12,
OutlinedButton(
onPressed: () {
settings.setNewsFontSize(
settings.newsFontSize.value - 1,
);
setFontSize();
},
child: const Icon(
Icons.remove,
color: Colors.grey,
),
),
],
),
),
),
ListTile(
leading: const Icon(Icons.photo),
title: const Text("进入看图模式"),
onTap: () {
AppNavigator.closePage();
photoView();
},
trailing: const Icon(Icons.chevron_right),
),
],
),
),
);
}
Future setFontSize() async {
try {
if (webViewController == null) {
return;
}
await webViewController!.runJavaScript(
'''document.getElementsByClassName("news_box")[0].style.fontSize="${settings.newsFontSize}px";
document.getElementsByClassName("news_box")[0].style.lineHeight="1.6em";
''');
} catch (e) {
Log.logPrint(e);
}
}
}

View File

@@ -0,0 +1,189 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/modules/news/detail/news_detail_controller.dart';
import 'package:flutter_dmzj/widgets/net_image.dart';
import 'package:flutter_dmzj/widgets/status/app_error_widget.dart';
import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart';
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
import 'package:get/get.dart';
import 'package:remixicon/remixicon.dart';
import 'package:webview_flutter/webview_flutter.dart';
class NewsDetailPage extends StatelessWidget {
final String newsUrl;
final int newsId;
final String title;
final NewsDetailController controller;
NewsDetailPage({
required this.newsUrl,
this.title = "资讯详情",
required this.newsId,
Key? key,
}) : controller = Get.put(
NewsDetailController(id: newsId, newsUrl: newsUrl, title: title),
tag: DateTime.now().millisecondsSinceEpoch.toString(),
),
super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Obx(() => Text(controller.newsTitle.value)),
actions: [
IconButton(
onPressed: controller.share,
icon: const Icon(Icons.share),
),
],
),
body: Stack(
children: [
(Platform.isAndroid || Platform.isIOS)
? Obx(
() => Offstage(
offstage: controller.pageLoadding.value,
child: WebViewWidget(
controller: controller.webViewController!,
),
),
)
: buildHtml(),
Obx(
() => Offstage(
offstage: !controller.pageLoadding.value,
child: const AppLoaddingWidget(),
),
),
Obx(
() => Offstage(
offstage: !controller.pageError.value,
child: AppErrorWidget(
errorMsg: controller.errorMsg.value,
onRefresh: controller.refershContent,
),
),
),
],
),
bottomNavigationBar: BottomAppBar(
child: SizedBox(
height: 48,
child: Row(
children: [
Expanded(
child: Obx(
() => TextButton.icon(
onPressed: controller.like,
icon: Icon(
controller.liked.value
? Remix.thumb_up_fill
: Remix.thumb_up_line,
size: 20,
),
label: Text(controller.moodAmount > 0
? "${controller.moodAmount}"
: "点赞"),
),
),
),
Expanded(
child: Obx(
() => TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.comment,
icon: const Icon(
Remix.chat_2_line,
size: 20,
),
label: Text(controller.commentAmount > 0
? "${controller.commentAmount}"
: "评论"),
),
),
),
// Expanded(
// child: Obx(
// () => TextButton.icon(
// style: TextButton.styleFrom(
// textStyle: const TextStyle(fontSize: 14),
// ),
// onPressed: controller.collect,
// icon: Icon(
// controller.collected.value
// ? Remix.star_fill
// : Remix.star_line,
// size: 20,
// ),
// label: Text(controller.collected.value ? "已收藏" : "收藏"),
// ),
// ),
// ),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.showSettings,
icon: const Icon(
Remix.settings_line,
size: 20,
),
label: const Text("设置"),
),
),
],
),
),
),
);
}
Widget buildHtml() {
return Obx(
() => ListView(
padding: AppStyle.edgeInsetsA12,
children: [
Text(
controller.title,
style: Get.textTheme.titleLarge,
),
AppStyle.vGap4,
Text(
"${controller.author.value} ${controller.src.value} ${controller.time.value}",
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
AppStyle.vGap12,
HtmlWidget(
controller.htmlContent.value,
textStyle: TextStyle(
fontSize: controller.settings.newsFontSize.value.toDouble(),
),
customWidgetBuilder: (e) {
if (e.localName == "img") {
var imgSrc = e.attributes["src"];
imgSrc ??= e.attributes["data-original"];
return GestureDetector(
child: NetImage(
imgSrc!,
borderRadius: 4,
),
onTap: () {
controller.showImageView(imgSrc ?? "");
},
);
}
return null;
},
onTapUrl: controller.onTapUrl,
),
],
),
);
}
}

View File

@@ -0,0 +1,67 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/controller/base_controller.dart';
import 'package:flutter_dmzj/app/event_bus.dart';
import 'package:flutter_dmzj/models/news/news_tag_model.dart';
import 'package:flutter_dmzj/modules/news/home/news_list_controller.dart';
import 'package:flutter_dmzj/requests/news_request.dart';
import 'package:get/get.dart';
class NewsHomeController extends GetxController
with GetTickerProviderStateMixin {
NewsRequest request = NewsRequest();
late TabController tabController;
var loadding = true;
List<NewsTagModel> categores = [];
var error = false;
var errorMsg = "";
StreamSubscription<dynamic>? streamSubscription;
@override
void onInit() {
streamSubscription = EventBus.instance.listen(
EventBus.kBottomNavigationBarClicked,
(index) {
if (index == 1) {
refreshOrScrollTop();
}
},
);
loadCategores();
super.onInit();
}
@override
void onClose() {
streamSubscription?.cancel();
super.onClose();
}
void loadCategores() async {
try {
loadding = true;
error = false;
update();
var category = await request.category();
category.insert(0, NewsTagModel(id: 0, name: "最新"));
tabController = TabController(length: category.length, vsync: this);
categores = category;
} catch (e) {
errorMsg = e.toString();
error = true;
} finally {
loadding = false;
update();
}
}
void refreshOrScrollTop() {
var tabIndex = tabController.index;
BasePageController controller;
controller = Get.find<NewsListController>(tag: "${categores[tabIndex].id}");
controller.scrollToTopOrRefresh();
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/platform_utils.dart';
import 'package:flutter_dmzj/modules/news/home/news_home_controller.dart';
import 'package:flutter_dmzj/modules/news/home/news_list_view.dart';
import 'package:flutter_dmzj/widgets/status/app_error_widget.dart';
import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart';
import 'package:flutter_dmzj/widgets/tab_appbar.dart';
import 'package:flutter_dmzj/widgets/windows_tab_page.dart';
import 'package:get/get.dart';
class NewsHomePage extends GetView<NewsHomeController> {
const NewsHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetBuilder<NewsHomeController>(
init: controller,
builder: (controller) {
if (controller.loadding) {
return const Scaffold(
body: AppLoaddingWidget(),
);
}
if (!controller.loadding && controller.error) {
return Scaffold(
body: AppErrorWidget(
errorMsg: controller.errorMsg,
onRefresh: controller.loadCategores,
),
);
}
if (PlatformUtils.isWindows) {
return WindowsTabPage(
tabs: controller.categores
.map((e) => WindowsTabItem(
label: e.name,
body: NewsListView(tag: e),
))
.toList(),
);
}
return Scaffold(
appBar: TabAppBar(
tabs: controller.categores.map((e) => Tab(text: e.name)).toList(),
controller: controller.tabController,
),
body: TabBarView(
controller: controller.tabController,
children:
controller.categores.map((e) => NewsListView(tag: e)).toList(),
),
);
},
);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter_dmzj/app/controller/base_controller.dart';
import 'package:flutter_dmzj/models/news/news_banner_model.dart';
import 'package:flutter_dmzj/models/news/news_list_item_model.dart';
import 'package:flutter_dmzj/models/news/news_tag_model.dart';
import 'package:flutter_dmzj/requests/news_request.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class NewsListController extends BasePageController<NewsListItemModel> {
final NewsRequest request = NewsRequest();
final NewsTagModel tag;
NewsListController(this.tag);
RxList<NewsBannerModel> banners = RxList<NewsBannerModel>();
@override
Future<List<NewsListItemModel>> getData(int page, int pageSize) async {
if (tag.id == 0 && page == 1) {
loadBanner();
}
return await request.getNewsList(tag.id, page);
}
void loadBanner() async {
try {
banners.value = await request.banner();
} catch (e) {
SmartDialog.showToast(e.toString());
}
}
void openBanner(NewsBannerModel item) {
AppNavigator.toNewsDetail(
url: item.objectUrl ?? "",
newsId: item.objectId ?? 0,
title: item.title,
);
}
}

View File

@@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/app/utils.dart';
import 'package:flutter_dmzj/models/news/news_tag_model.dart';
import 'package:flutter_dmzj/modules/news/home/news_list_controller.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart';
import 'package:flutter_dmzj/widgets/net_image.dart';
import 'package:flutter_dmzj/widgets/page_list_view.dart';
import 'package:flutter_swiper_view/flutter_swiper_view.dart';
import 'package:get/get.dart';
class NewsListView extends StatelessWidget {
final NewsTagModel tag;
final NewsListController controller;
NewsListView({Key? key, required this.tag})
: controller = Get.put(NewsListController(tag), tag: tag.id.toString()),
super(key: key);
@override
Widget build(BuildContext context) {
return KeepAliveWrapper(
child: PageListView(
pageController: controller,
firstRefresh: true,
separatorBuilder: (context, i) => Divider(
endIndent: 12,
indent: 12,
color: Colors.grey.withOpacity(.2),
height: 1,
),
header: tag.id == 0 ? buildBanner() : null,
itemBuilder: (context, i) {
var item = controller.list[i];
return InkWell(
onTap: () {
AppNavigator.toNewsDetail(
newsId: item.articleId.toInt(),
title: item.title,
url: item.pageUrl ?? "",
);
},
child: Container(
padding: AppStyle.edgeInsetsA12,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
NetImage(
item.rowPicUrl ?? "",
width: 100,
height: 62,
borderRadius: 4,
),
AppStyle.hGap12,
Expanded(
child: SizedBox(
height: 62,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Text(
item.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
Utils.formatTimestamp(item.createTime ?? 0),
style: const TextStyle(
color: Colors.grey, fontSize: 12),
),
// Row(
// children: <Widget>[
// const Icon(
// Icons.thumb_up,
// size: 12.0,
// color: Colors.grey,
// ),
// AppStyle.hGap4,
// Text(
// item.moodAmount.toString(),
// style: const TextStyle(
// color: Colors.grey,
// fontSize: 12,
// ),
// ),
// AppStyle.hGap8,
// const Icon(
// Icons.chat,
// size: 12.0,
// color: Colors.grey,
// ),
// AppStyle.hGap4,
// Text(
// item.commentAmount.toString(),
// style: const TextStyle(
// color: Colors.grey,
// fontSize: 12,
// ),
// )
// ],
// )
],
)
],
),
),
),
],
),
),
);
},
),
);
}
Widget buildBanner() {
return Padding(
padding: AppStyle.edgeInsetsH12.copyWith(bottom: 4),
child: Obx(
() => ClipRRect(
borderRadius: AppStyle.radius4,
child: AspectRatio(
aspectRatio: 75 / 40,
child: controller.banners.isEmpty
? const SizedBox()
: Swiper(
itemWidth: 750,
itemHeight: 400,
autoplay: true,
itemCount: controller.banners.length,
onTap: (i) {
controller.openBanner(controller.banners[i]);
},
itemBuilder: (_, i) => NetImage(
controller.banners[i].picUrl,
width: 750,
height: 400,
),
pagination: SwiperCustomPagination(
builder:
(BuildContext context, SwiperPluginConfig config) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.only(
left: 8,
right: 12,
top: 4,
bottom: 4,
),
//color: Colors.black12,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black38,
Colors.transparent,
],
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
controller
.banners[config.activeIndex].title,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14, color: Colors.white),
),
),
AppStyle.hGap8,
PageIndicator(
controller: config.pageController!,
count: config.itemCount,
size: 10,
layout: PageIndicatorLayout.SCALE,
),
],
),
),
);
},
),
),
),
),
),
);
}
}