news 2026/9/26 5:05:40

Flutter列表下拉刷新与上拉加载:原生方案完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter列表下拉刷新与上拉加载:原生方案完整指南

如果一个App里的列表页只能挑一个交互来做,我大概率会先做下拉刷新和上拉加载。这两个功能看着基础,真正落地时翻车率却高得吓人:刷新完列表直接给你弹回顶部,上拉加载同一页数据请求了三次,切到后台再回来还显示loading……这篇文章不聊空壳理论,直接把RefreshIndicator+ScrollController这套原生方案拆开揉碎,再给出一份能直接上生产环境的完整实现,顺带把我在 Flutter 列表场景里踩过的坑都列出来。适合正在做 Flutter 列表页、或者想把第三方加载库迁回原生方案的开发者参考。

先说结论:市面上那些花里胡哨的第三方下拉刷新库,能不用尽量不用。原生方案缺的只是“看起来好看的自定义动画”,而这些完全可以通过组合RefreshIndicator、ScrollController和自绘 Indicator 解决。真正考验人的不是刷新动画长什么样,而是分页状态怎么管、竞态怎么防、触底判断怎么不抖动。

1. 刷新加载的整体设计思路

1.1 先想清楚:刷新和加载到底在改变什么

很多开发者一上来就写RefreshIndicator(onRefresh: _refresh, child: ListView(...)),然后往ListView里塞一个FutureBuilder。表面看着能跑,但实际上把“刷新”和“加载”两件事混成了一件事,导致后面状态越来越乱。

下拉刷新和上拉加载虽然都涉及网络请求,但语义完全不同。下拉刷新是“重置式操作”,用户想表达的是:重新拉取最新数据,列表第一页覆盖旧数据。上拉加载是“追加式操作”,用户想表达的是:在现有数据后面继续拿下一页。如果用同一个函数处理,就会出现“刷新后页码忘记重置”“刷新结果直接 append 到旧列表后面”之类的低级问题。

我自己的习惯是:所有列表页的数据操作,都围绕三个状态变量展开——items(已加载的数据)、page(当前页码)、hasMore(是否还有下一页)。刷新操作负责把它们归零并重建,加载操作负责递增并追加。这两个方法永远独立实现,哪怕内部调的是同一个接口。

还有一个交互细节容易被忽略:下拉刷新成功后,列表应该保持当前的滚动位置,而不是“刷的一下”弹回顶部。很多第三方库为什么体验差?就是因为它们内部走的state = initialState再重新请求,数据一变,ListView的高度也跟着变,用户直接被甩到第一屏。原生RefreshIndicator本身不做这种坏事,问题通常出在开发者用setState把整个列表数据替换成空数组,再等新数据回来重新渲染,中间一帧列表变空,滚动位置就崩了。处理方式是:刷新前保留当前 items,请求成功后用新数据整体替换,并且刷新期间列表尾部显示 loading 状态而不是清空。

1.2 三种分页协议,对应不同的刷新加载写法

后端分页接口如果归纳一下,本质上就三种:页码分页、偏移分页、游标分页。这三者决定着你前端怎么维护page。

页码分页最常见,典型参数是page=1&pageSize=20,返回体里带total、hasMore。刷新时page重置为 1,加载时page + 1,逻辑最简单,没有歧义。

偏移分页的典型参数是offset=0&limit=20,隐藏含义是“从第 N 条开始取”。这类接口如果列表中间的数据被删除,offset 会漂移,下一页可能漏数据或重复数据。所以用偏移分页时,我一般会以当前items.length作为下一次请求的 offset,而不是用一个简单的计数器。

游标分页近年来用得越来越多,典型参数是cursor=xxx,返回体里带nextCursor。这类接口对前端最友好,因为不需要关心 page 概念,只要记住上一次返回的nextCursor作为下一次请求参数,直到nextCursor为空就说明没有更多。刷新时把 cursor 置空,加载时传nextCursor。

前端不管对接哪种协议,落到 UI 层都要收敛成同一套状态:items、hasMore、loadingMore、error。后端协议差异只在请求层处理,不要让分页协议影响你的 UI 层状态设计。这个习惯能让你在替换后端接口时,UI 代码基本不动。

1.3 为什么不推荐无脑上第三方库

pull_to_refresh、EasyRefresh这类库我曾经在生产项目里用得不少,它们确实提供了开箱即用的自定义头部、底部组件,初期接入很快。但用一段时间你会发现几个现实问题:

依赖的维护速度经常跟不上 Flutter 主版本迭代。Flutter 每升级一次,这些库总有几个小版本在边界情况下报错,尤其是涉及ScrollPosition、ScrollPhysics内部 API 变更的时候,你只能等作者更新,或者在 issue 里翻 workaround。我做过的项目里,不止一次因为第三方库在 Flutter 3.x 某次升级后出现触底判断失效,最后被迫花一晚上把库替换成原生方案。

第三方库为了适配各种自定义需求,往往会在列表外面包一层额外的ScrollView,甚至接管你的ScrollController。这在简单页面里没事,一旦页面里有NestedScrollView、CustomScrollView、多 Tab 复用同一个列表的场景,冲突会非常隐蔽。原生RefreshIndicator本身就是官方组件,所有参数都稳定,ScrollController在自己手里,问题排起来快得多。

2. 原生方案核心细节解析

2.1 RefreshIndicator 的触发原理与正确配置

RefreshIndicator在 iOS 和 Android 上的视觉差异很大,但在 Flutter 里它们共用同一个组件,只是内置动画的样式不同。它靠通知监听器监听子组件的ScrollNotification,当滚动位置为负且超过阈值时,进入armed状态,松手后触发onRefresh回调。

这里有一个默认配置容易踩坑:physics。如果你的列表数据不足一屏,ListView默认的ClampingScrollPhysics(Android)或BouncingScrollPhysics(iOS)可能根本拉不出 overscroll,这时候下拉刷新怎么拉都不触发。解决方案是在ListView上显式设置:

physics: const AlwaysScrollableScrollPhysics()

即使是空数据、短数据,也保证列表可以向下回弹,刷新手势才会被识别到。这个配置我在每个列表页都会统一加上,避免空态和短内容场景下刷不动。

另一个坑是onRefresh必须返回一个Future,并且这个 Future 只有在真正完成刷新后才能 resolve。如果你在onRefresh里直接调一个异步函数但没await,或者函数内部提前返回,指示器会在网络还没回来时就收回去,视觉上就是“转了一下就结束但数据没更新”。

Future<void> _onRefresh() async { await _cubit.refresh(); }

有些开发者喜欢在_onRefresh里setState,这没问题,但要保证异常别往上抛。RefreshIndicator的 Future 如果以 error 结束,控制台会直接打印一个未捕获的异常,甚至会触发全局错误上报。所以在刷新方法内部要自己 catch 掉异常,把错误状态写进业务状态里,UI 用 SnackBar 或列表 error view 展示。

2.2 触底判断的阈值怎么定

上拉加载最常见的实现方式是监听ScrollController,当滚动位置接近底部时触发加载。但判断条件写不好会出现两种极端:触发太早导致用户还没看到底部就加载了下一页,触发太晚导致白色空白长时间露出来。

我常用的判断公式是这样:

if (position.pixels >= position.maxScrollExtent - 300) { _loadMore(); }

300是触发距离,单位是逻辑像素。这个值不是拍脑袋定的,它取决于你列表页底部 Loading 组件的高度。如果底部加载指示器高约 80 像素,那么 300 的提前量意味着:用户滑到离底部还差一小段屏的位置时就开始加载下一页,等用户真正滑到底部时,新数据大概率已经渲染出来了,视觉上几乎无缝。

但300这个值不要写死在每个页面。如果列表项很高(比如大卡片、图片墙),一屏可能只能看到 3 条数据,300 像素的提前量会变成“提前了一整个屏”,导致用户根本还没看到底部,下一页就加载了。比较好的做法是:把触发距离做成页面的一个参数,根据列表项高度和底部组件高度共同决定,默认 200~300,高卡片列表适当缩小到 100 左右。

防抖动逻辑也必须加上,否则_loadMore会被连续触发。我在状态里加了一个原子锁:

if (_loadingMore || !_hasMore) return; _loadingMore = true;

请求回来后无论成功失败,都必须把_loadingMore恢复为false。这里最容易被忽略的是失败分支——很多新手只在 try 成功里复位锁,catch 里忘了,结果一次请求失败之后列表永远不再自动加载,只能靠手动重试按钮救回来。

2.3 列表尾部的三种状态,缺一不可

上拉加载底部组件不能只做一个“加载中转圈”。我见过太多列表加载到最后一页后,底部一直挂着转圈,用户以为还在加载,其实是接口返回了hasMore=false,但代码里没做结束展示。底部组件至少要覆盖三种状态:

状态展示内容用户操作
加载中CircularProgressIndicator+ “加载中…”无需操作
没有更多“已经到底了”或品牌文案无需操作
加载失败“加载失败,点击重试”点击后重新触发加载

这里一个容易被吐槽的细节:加载失败时千万不要直接换成一整屏错误页。用户的心理预期是“我已经浏览了部分内容,只想继续加载”,你要做的只是保留已有数据、在底部给一个重试入口。一整屏错误会强迫用户退出列表页再进来,明显违背“沉浸式浏览”的交互直觉。

3. 从基础到可复用:完整实现

3.1 基础版:一根 ScrollController 打天下

先给出一份最朴素的实现。这段代码没有任何状态管理框架,适合中小项目快速落地,也方便理解核心机制。

class NewsListPage extends StatefulWidget { const NewsListPage({super.key}); @override State<NewsListPage> createState() => _NewsListPageState(); } class _NewsListPageState extends State<NewsListPage> { final ScrollController _controller = ScrollController(); final List<String> _items = []; int _page = 1; bool _loadingMore = false; bool _hasMore = true; String? _error; @override void initState() { super.initState(); _controller.addListener(_onScroll); _loadFirstPage(); } void _onScroll() { if (!_controller.hasClients) return; final position = _controller.position; if (position.pixels >= position.maxScrollExtent - 200) { _loadMore(); } } Future<void> _loadFirstPage() async { await _refresh(); } Future<void> _refresh() async { _page = 1; _hasMore = true; _error = null; try { final result = await ApiClient.fetchNews(page: _page); if (!mounted) return; setState(() { _items ..clear() ..addAll(result.items); _hasMore = result.hasMore; _page += 1; }); } catch (e) { if (!mounted) return; setState(() => _error = '刷新失败,请检查网络'); } } Future<void> _loadMore() async { if (_loadingMore || !_hasMore) return; _loadingMore = true; setState(() {}); try { final result = await ApiClient.fetchNews(page: _page); if (!mounted) return; setState(() { _items.addAll(result.items); _hasMore = result.hasMore; _page += 1; _error = null; }); } catch (e) { if (!mounted) return; setState(() => _error = '加载失败,点击重试'); } finally { if (mounted) setState(() => _loadingMore = false); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('资讯列表')), body: RefreshIndicator( onRefresh: _refresh, child: ListView.builder( controller: _controller, physics: const AlwaysScrollableScrollPhysics(), itemCount: _items.length + 1, itemBuilder: (context, index) { if (index >= _items.length) { return _buildFooter(); } return ListTile( title: Text(_items[index]), subtitle: const Text('这是一个列表项'), ); }, ), ), ); } Widget _buildFooter() { if (_error != null) { return TextButton( onPressed: _loadMore, child: Text(_error!), ); } if (_loadingMore) { return const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator()), ); } if (!_hasMore) { return const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: Text('已经到底了')), ); } return const SizedBox(height: 40); } }

这段代码有几个关键细节要说清楚。itemCount是_items.length + 1,多出来的 index 就是底部状态区,这样无论有没有更多数据,底部都能有一个统一出口。_loadFirstPage()在initState里直接调_refresh(),好处是首次加载和手动下拉刷新共用一套逻辑,不会出现两套初始化代码。

很多人会问,为什么_loadMore里的finally还包了一层setState?因为_loadingMore必须保证在成功、失败两种路径下都复位,否则一旦异常,下一次触底判断会因为锁没释放而永久失效。finally是最好用的兜底地方。mounted判断也必须加,异步请求回来如果页面已经 dispose,再调setState会直接抛异常,这是 Flutter 官方手册里反复强调的。

3.2 进阶版:用 Cubit 管理分页状态

基础版能跑,但状态一多就散落在setState里。页面里如果还有筛选条件、点赞操作、搜索联动,_page、_loadingMore、_hasMore这些变量会越加越多,逻辑线和时间线开始混乱。这时候就该状态管理上场了。我个人偏好Cubit,因为它比Bloc简单,又比setState严格。

先定义一个分页状态模型:

class NewsState extends Equatable { const NewsState({ this.items = const [], this.hasMore = true, this.loadingMore = false, this.loadMoreError = false, }); final List<NewsItem> items; final bool hasMore; final bool loadingMore; final bool loadMoreError; NewsState copyWith({ List<NewsItem>? items, bool? hasMore, bool? loadingMore, bool? loadMoreError, }) { return NewsState( items: items ?? this.items, hasMore: hasMore ?? this.hasMore, loadingMore: loadingMore ?? this.loadingMore, loadMoreError: loadMoreError ?? this.loadMoreError, ); } @override List<Object?> get props => [items, hasMore, loadingMore, loadMoreError]; }

再写 Cubit:

class NewsCubit extends Cubit<NewsState> { NewsCubit({required this.api}) : super(const NewsState()); final ApiClient api; int _page = 1; Future<void> refresh() async { _page = 1; try { final result = await api.fetchNews(page: _page); emit(NewsState( items: result.items, hasMore: result.hasMore, loadingMore: false, )); _page += 1; } catch (_) { emit(state.copyWith(loadMoreError: true)); } } Future<void> loadMore() async { if (state.loadingMore || !state.hasMore) return; emit(state.copyWith(loadingMore: true, loadMoreError: false)); try { final result = await api.fetchNews(page: _page); emit(state.copyWith( items: [...state.items, ...result.items], hasMore: result.hasMore, loadingMore: false, )); _page += 1; } catch (_) { emit(state.copyWith(loadingMore: false, loadMoreError: true)); } } }

用 Cubit 之后,UI 层只剩一个BlocBuilder:

BlocBuilder<NewsCubit, NewsState>( builder: (context, state) { return RefreshIndicator( onRefresh: () => context.read<NewsCubit>().refresh(), child: ListView.builder( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), itemCount: state.items.length + 1, itemBuilder: (context, index) { if (index >= state.items.length) { if (state.loadMoreError) { return TextButton( onPressed: () => context.read<NewsCubit>().loadMore(), child: const Text('加载失败,点击重试'), ); } if (state.loadingMore) { return const Center(child: CircularProgressIndicator()); } if (!state.hasMore) { return const Center(child: Text('已经到底了')); } return const SizedBox(height: 40); } return ListTile(title: Text(state.items[index].title)); }, ), ); }, )

这里有个测试上的好处:Cubit 的refresh()和loadMore()是纯异步方法,可以脱离 Widget 单独用bloc_test跑单测,构造各种失败场景验证状态是否正确,比如“第一页失败时loadMoreError是否为 true”“下一页返回空数组时hasMore是否被置为 false”。这是setState写法做不到的。

3.3 自定义刷新动画:两种实用路线

默认的RefreshIndicator转圈其实已经很顺眼,但很多产品和设计会要求“换掉那个丑转圈”。这里基本有两种路线。

一种是用RefreshIndicator自带的color、backgroundColor、strokeWidth参数去调样式,适合只改颜色的需求。另一种是自绘刷新头部组件。自绘的核心是理解RefreshIndicator只是监听ScrollNotification,自己写一个NotificationListener<ScrollNotification>来监听ScrollUpdateNotification的metrics.extentBefore(或pixels为负的差值)和ScrollActivityNotification来判断松手后的状态,再用一个AnimationController驱动自定义头部旋转、位移、文案变化。

如果你用的是CustomScrollView+SliverList,那RefreshIndicator也能包,但要注意它只能包一个可滚动子组件,所以CustomScrollView整体被包就没问题。另一种选择是CupertinoSliverRefreshControl,原生 iOS 风格的下拉刷新,在CustomScrollView的slivers里直接插入CupertinoSliverRefreshControl(onRefresh: ...),配合SliverOverlapInjector可以实现系统级的下拉反弹。两种组件我都在项目里用过,iOS 风格页面用CupertinoSliverRefreshControl的观感确实更自然。

自绘头部组件里有一个容易忽略的细节:动画状态切换的时机。下拉过程中头部跟着手指移动是“拖动态”,松手后进入“刷新态”,刷新结束进入“收尾态”。判定松手动作要用ScrollStartNotification的dragDetails和ScrollUpdateNotification的dragDetails区分是用户拖动还是惯性滚动,否则会出现“还没松手就开始转圈”的怪异表现。这块代码量比较大,一般不建议每个页面都手写,我的习惯是封装成一个CustomRefreshHeader,放到项目公共组件库里统一维护。

4. 高频问题与排查实录

4.1 常见问题速查表

现象原因解决方案
下拉刷新死都不触发列表数据不满一屏,默认 physics 不支持 overscroll显式设置AlwaysScrollableScrollPhysics
刷新转一圈就结束但数据没变onRefresh没返回 Future 或内部没 await方法改成 async,并await请求完成
上拉加载一次请求连续发好几次触底判断反复进入loadMore方法加_loadingMore锁,请求结束前不释放
网络失败后列表再也不自动加载catch 分支里忘复位 lock用finally统一复位
刷新后列表弹回顶部刷新时先清空 items 再等新数据刷新期间保留旧 items,请求完再替换
Web 端下拉刷新手感很怪Web 浏览器默认 overscroll 行为与 Flutter 冲突用Listener监听手势,或考虑 Web 端只保留加载按钮
切后台回来还在 loading页面 dispose 之后setState被调用所有异步回调加if (!mounted) return
底部“已经到底了”被下次加载覆盖页面重新加载时没有重置hasMore刷新时显式设置_hasMore = true

这张表里前四条是我在建列表组件时几乎必遇到的问题。尤其是第二条,很多人一看转圈收回去了就一直纠结为什么列表没变化,其实只是忘了返回Future。这个错误很隐蔽,因为代码不报错,编译不失败,只有运行时的行为不对。

4.2 竞态问题:慢网络下的数据错乱

上拉加载最常见的疑难杂症不是崩溃,而是数据错乱。典型的场景是这样的:用户在加载第 3 页时网络慢,等第 3 页还没回来,用户又下拉刷新了,刷新请求先发出去、后发出去都有可能,最后谁先回来决定了页面显示哪些数据。如果后发的刷新请求先回来,第 3 页加载再姗姗来迟,那页面上就会出现新旧数据混合的情况。

解决竞态的办法有几个层级。最轻量的是在刷新操作里直接跳过当前未完成的加载操作,用_loadingMore锁加一个请求序号:

int _requestSeq = 0; Future<void> _refresh() async { final seq = ++_requestSeq; // 请求发出去之前先取消掉逻辑上的加载 _loadingMore = false; // 请求结果回来时检查 seq 是否仍然是最新 }

每次请求响应回来后,先判断seq == _requestSeq,如果不相等直接丢弃结果。这个思路简单可靠,不需要引入额外的并发库。复杂一点的做法是用StreamController或RxDart的switchMap,让新的刷新请求自动取消旧的加载结果,但一般列表页里没必要引入这么重的异步模型。

另一种竞态是“刷新后页码和正在返回的旧页面错位”。比如刷新操作把_page重置为 1,但旧代码里正在等待page=2的响应,响应回来之后又_page += 1,导致下一页直接跳到了 page=3,跳页了。请求序号方案可以一并解决这个问题,因为过期的响应会被直接丢弃。

4.3 版本与性能:Flutter SDK 升级后的刷新加载坑

写这篇的时候我正好经历了 Flutter 3.x 的一次大版本升级,下拉刷新动画和性能问题在版本替换后暴露得很明显。如果你也在终端里看到诸如current configured Flutter SDK is not known to be fully supported或者You are applying Flutter's main Gradle plugin imperatively using the apply script这类提示,说明工程和当前 SDK 版本之间已经存在兼容性偏差,列表页的刷新动画虽然不是直接报错,但可能会出现 iOS 上回弹阻尼不正常、Android 上刷新指示器位置偏移等副作用。

遇到这类情况,我一般先做两件事:一是flutter upgrade到稳定渠道再看看,二是执行dart pub outdated检查项目里依赖包是否都在兼容区间。很多第三方列表库放出来的旧版本是几年没更新的,一旦 Flutter 主版本升级就彻底无法使用,这就是我在第一节强烈建议迁移到原生方案的原因。原生组件永远跟随 Flutter SDK 走,不存在“没人维护”的问题。

关于下拉刷新动画的帧率,Flutter 新版本默认用 Impeller 渲染引擎,刷新动画的圆环转圈在高刷新率设备上看起来更平滑。但在部分低端 iOS 设备上,Impeller 的着色器编译可能会出现首帧卡顿,导致下拉刷新出现一瞬间的掉帧。这种场景可以暂时通过关闭 Impeller 验证是不是渲染引擎的问题,确认后如果确实是 Impeller 的锅,就升级到修复版本而不是长期关闭,因为 Android 端 Impeller 带来的渲染一致性提升更明显。

Flutter Web 上刷新加载又是另一套表现,Web 端浏览器手势会和 Flutter 的 overscroll 冲突,尤其桌面浏览器里鼠标中键上下滚动,RefreshIndicator的表现会有违和感。我在 Web 端列表页通常不启用下拉刷新交互,改用右上角“刷新”按钮触发数据重置,上拉加载也换成显式的“加载更多”按钮,这样既符合 Web 使用习惯,又绕开了浏览器手势劫持的问题。这算是一个务实取舍,不要为了“移动端交互一致性”强行在 Web 上复刻所有手势。

写在最后的一点体会

这套原生刷新加载组合,我在自己维护的几个 App 项目里稳定跑了一年多,期间经历了一次 Flutter 主版本大升级,除了RefreshIndicator的默认样式微调,没有任何破坏性改动。换做以前用第三方库的时候,升级一次 Flutter SDK 往往要跟着改好几处 API,甚至要替换掉列表库本体,这份省心是实打实的。

如果你想复现文章里的方案,建议从基础版开始跑通,再逐步换到 Cubit 那版。先理解ScrollController的监听机制和状态锁的复位逻辑,再去考虑自定义动画和性能优化,顺序别反了。动手过程中如果遇到这里没写到的怪问题,多半出在你的列表嵌了多层可滚动组件,先用NotificationListener打印出ScrollNotification的事件类型,八成能从日志里直接看出端倪。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 5:04:50

Linux系统调用:从用户态到内核态的必经之路与实战排查

写这篇文章之前&#xff0c;我先说下我的结论&#xff1a;Linux系统调用是从用户态进入内核态的唯一合法通道&#xff0c;也是理解和排查Linux程序行为的一把钥匙。刚入门的时候&#xff0c;很多人觉得“系统调用”是个抽象又遥远的词——写个Hello World用printf&#xff0c;不…

作者头像 李华
网站建设 2026/9/26 5:04:19

从openclaw -h开始:部署、Channel接入与排错实战指南

我最早接触到 openclaw&#xff0c;是在一个技术群里看到有人甩了一张截图&#xff0c;内容就是openclaw -h的输出。当时我第一反应是&#xff1a;"这又是个什么新玩具&#xff1f;" 但仔细看了下帮助信息里的参数列表&#xff0c;发现它并不是普通的命令行小工具&am…

作者头像 李华
网站建设 2026/9/26 5:03:35

毕业论文神器!盘点2026年好评如潮的一键生成论文工具

一天写完毕业论文在2026年已不再是天方夜谭。最新测评显示&#xff0c;2026年最炸裂的一键生成论文工具&#xff0c;实测提速超300%&#xff0c;覆盖选题、查重、润色、排版全流程&#xff0c;高效搞定毕业论文&#xff0c;学生必备神器。 一、全流程王者&#xff1a;一站式搞定…

作者头像 李华
网站建设 2026/9/26 5:02:26

PHP仿土巴兔装修报价器源码解析:报价链路、部署与二次开发

简介&#xff1a;一份PHP仿土巴兔装修报价器源码包&#xff0c;面向具备PHP基础的家装行业开发者或学习者&#xff0c;用于快速搭建装修预算预估工具。资源内含2000个文件&#xff0c;约2.45MB&#xff0c;以2916个JSON数据文件为主体&#xff08;多用于城市、材料、项目等报价…

作者头像 李华
网站建设 2026/9/26 5:02:20

Substrate开发实战:从零构建区块链的完整指南

开头&#xff1a;先把这个词聊清楚如果你在搜索引擎里敲 "substrate" 这个词&#xff0c;会刷出来一堆八竿子打不着的玩意儿——生物学里的培养基底物、化学里的反应基材、半导体行业的晶圆衬底、甚至打印机的承印介质。但在过去几年&#xff0c;凡是在区块链技术圈子…

作者头像 李华
网站建设 2026/9/26 5:01:42

碳减排下综合能源服务商合作运行优化复现笔记

写这篇复现笔记之前&#xff0c;先交代一下背景。我前阵子接到一个活儿&#xff0c;要把《考虑碳减排的综合能源服务商合作运行优化策略》这篇EI论文的核心模型复现出来。论文的标题很长&#xff0c;但压缩成关键词就是两件事&#xff1a;碳减排成本怎么进模型、多个综合能源服…

作者头像 李华