您当前的位置: 首页 > 慢生活 > 程序人生 网站首页程序人生
Flutter for React Native 开发者(token获取及loading效果与分页ui调整)
发布时间:2026-08-02 22:15:23编辑:雪饮阅读()
-
官网文档链接
https://docs.flutter.dev/flutter-for/react-native-devs
这篇文章的意思是给那些既懂安卓又懂ios的,反正就是给想要开发跨平台的开发者看的。
对应react native的开发者看的。
那么接下来我们尝试用react native运行一个安卓项目的hello world。
创建react native项目
npx @react-native-community/cli init DemoProject
很容易卡在
Installing dependencies
这里
可以删除原来的半成品项目,或者新建一个不同目录的项目,并追加可以查看详细情况的参数,以便于看到到底是卡住了,还是还在继续安装依赖。
npx @react-native-community/cli init ReactNativeDemoProject –verbose
这次看到请求超时了,换淘宝镜像,然后再次删除项目重新尝试
npm config set registry https://registry.npmmirror.com
然后运行到安卓
接下来,你要把你的安卓模拟器也就是android studio中的模拟器运行起来
然后
cd "C:\Users\Administrator\ReactNativeDemoProject" && npx react-native run-android
这次报错是没有JAVA_HOME环境变量。
那我下载的是这个
jdk-17.0.20_windows-x64_bin.msi
然后安装,然后配置环境变量到如D:\softwareInstall\jdk17
然后新开窗口再次运行
npx react-native run-android
运行过程中会发现
Downloading https://services.gradle.org/distributions/gradle-9.3.1-bin.zip
这个很容易超时,可以提前手动下载
然后在C:\Users\Administrator\ReactNativeDemoProject\android\gradle\wrapper\ gradle-wrapper.properties中手动关联本地路径
distributionUrl=file\:///D:/software/gradle/gradle-9.3.1-bin.zip
即使解决了这个,还有一些是需要翻墙才能搞定的maven依赖。
翻墙有时候还不一定生效,需要在这里C:\Users\Administrator\ReactNativeDemoProject\android\ gradle.properties中新增如下配置
systemProp.http.proxyHost=127.0.0.1
systemProp.http.proxyPort=7897
systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=7897
# 不需要代理时一定要删掉这几行!否则直接无限挂起
这里7897对应你的代理软件的端口,例如我这里用的是Clash Verge,其端口是7897
在设置里面可以看到

接下来报错:
SDK location not found. Define a valid SDK location with an ANDROID_HOME environment variable or by setting the sdk.dir path in your project's local properties file at 'C:\Users\Administrator\ReactNativeDemoProject\android\local.properties'.
也就是没有设置安卓的环境变量。
所以需要手动指定android的sdk的路径,则在文件C:\Users\Administrator\ReactNativeDemoProject\android\ local.properties(没有则新建)中新增配置如:
sdk.dir=D:\\softwareInstall\\androidSdk
现在又卡在这里
> Configure project :app
This version only understands SDK XML versions up to 3 but an SDK XML file of version 4 was encountered. This can happen if you use versions of Android Studio and the command-line tools that were released at different times.
Checking the license for package NDK (Side by side) 27.1.12297006 in D:\softwareInstall\androidSdk\licenses
License for package NDK (Side by side) 27.1.12297006 accepted.
Preparing "Install NDK (Side by side) 27.1.12297006 v.27.1.12297006".
<-------------> 0% CONFIGURING [15m 41s]
> root project > :app
然后我终止后,我在android studio中发现实际下载的是
https://dl.google.com/android/repository/android-ndk-r27b-windows.zip
这个挺大的。
不过我还是在android studio中进行下载,因为我觉得这样会能看到进度。
下载完成后,再次运行,出现如下
C:\Users\Administrator\ReactNativeDemoProject>npx react-native run-android
'"adb"' �����ڲ����ⲿ���Ҳ���ǿ����еij���
�������ļ�
info Launching emulator...
error Failed to launch emulator. Reason: No emulators found as an output of `emulator -list-avds`.
warn Please launch an emulator manually or connect a device. Otherwise app may fail to launch.
info Installing the app...
Starting a Gradle Daemon (subsequent builds will be faster)
Checking the license for package Android SDK Build-Tools 35 in D:\softwareInstall\androidSdk\licenses
License for package Android SDK Build-Tools 35 accepted.
Preparing "Install Android SDK Build-Tools 35 v.35.0.0".
<=============> 100% CONFIGURING [9m 58s]
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
可以看到很多idle,这个不用管,还要继续耐心等待,过一会儿就会再次出现变动,Build-Tools 下载完成后还要校验文件哈希、解压、写入 SDK 目录、更新 sdk 配置文件等。
耐心的结果是又报这个错误
BUILD SUCCESSFUL in 30m 50s
82 actionable tasks: 72 executed, 10 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.3.1/userguide/configuration_cache_enabling.html
info Connecting to the development server...
warn Failed to connect to development server using "adb reverse": spawnSync adb ENOENT
info Starting the app...
'adb' 不是内部或外部命令,也不是可运行的程序
或批处理文件。
error Failed to start the app.
Error: Command failed with exit code 1: adb shell am start -n com.reactnativedemoproject/com.reactnativedemoproject.MainActivity -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
at makeError (C:\Users\Administrator\ReactNativeDemoProject\node_modules\execa\lib\error.js:60:11)
at module.exports.sync (C:\Users\Administrator\ReactNativeDemoProject\node_modules\execa\index.js:194:17)
at tryLaunchAppOnDevice (C:\Users\Administrator\ReactNativeDemoProject\node_modules\@react-native-community\cli-platform-android\build\commands\runAndroid\tryLaunchAppOnDevice.js:52:22)
at C:\Users\Administrator\ReactNativeDemoProject\node_modules\@react-native-community\cli-platform-android\build\commands\runAndroid\runOnAllDevices.js:90:39
at Array.forEach (<anonymous>)
at runOnAllDevices (C:\Users\Administrator\ReactNativeDemoProject\node_modules\@react-native-community\cli-platform-android\build\commands\runAndroid\runOnAllDevices.js:85:48)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async Command.handleAction (C:\Users\Administrator\ReactNativeDemoProject\node_modules\@react-native-community\cli\build\index.js:139:9)
info Run CLI with --verbose flag for more details.
在系统环境变量path中新增我们的adb所在路径如
D:\softwareInstall\androidSdk\platform-tools
然后再次新窗口再次运行试试。
现在这次能运行了,但是又报错:
2:57
5G.t
Unable to load script.
Make sure you're running Metro or that your bundle
'index.android.bundle' is packaged correctly for
release.
The device must either be USB connected (with
bundler set to "localhost:8081") or be on the same
Wi-Fi network as your computer (with bundler set to
your computer IP) to connect to Metro.
If you're using USB on a physical device,make sure
you also run this command:
adb reverse tcp:8081 tcp:8081
loadJSBundleFromAssets
ReactInstance.kt
access$loadJSBundleFromAssets
ReactInstance.kt:86
loadScriptFromAssets
ReactInstance.kt:336
loadScript
JSBundleLoader.kt:33
loadJSBundle
ReactInstance.kt:315
getOrCreateReactInstanceTask$lambda$45$lambda
$41
ReactHostImpl.kt:1072
$r8$1ambda$SmdYI7igN6ETqUXj13FigYM4DHc
ReactHostImpl.kt
then
D8s$SyntheticClass
completeImmediately$lambda$3
Task.kt:360
DISMISS
RELOAD
(ESC)
(R, R)

据说是没有开启这个
npx react-native start
但运行这个又报错:
Chat
思考过程
这是一个非常常见的 React Native 错误: "Unable to load script" 。意思是应用无法从 Metro bundler 加载 JavaScript bundle。
## 错误原因分析
应用启动时,需要从 Metro bundler(默认在 localhost:8081 )下载 JS bundle,但连接失败了。常见原因有:
### 1. Metro Bundler 没有运行
运行 npx react-native run-android 时,Metro 应该自动启动。如果没有,需要手动启动:
### 2. ADB 反向端口未设置
模拟器通常不需要,但如果是真机需要执行:
### 3. 端口 8081 被占用
检查端口是否被其他进程占用:
如果被占用,可以杀掉该进程或换端口:
### 4. 模拟器网络问题
模拟器一般使用 10.0.2.2 来访问宿主机的 localhost 。如果自动配置失败,可以在模拟器中打开开发者菜单(按 Ctrl+M 或摇晃设备),进入 Dev Settings → Debug server host & port for device ,设置为:
## 推荐排查步骤
1. 先确认 Metro 是否在运行 — 看终端是否有 Metro 的启动日志
2. 如果 Metro 没启动 ,先运行 npx react-native start ,等它显示 "Ready" 后,再开一个终端运行 npx react-native run-android
3. 如果 Metro 已启动但仍报错 ,在模拟器上按 R R (Reload)重试,或执行 adb reverse tcp:8081 tcp:8081
最大概率是 Metro Bundler 没有成功启动 ,先确认这一点。
23:07
user37810542461
我允许第一个又报这个错误:C:\Users\Administrator\ReactNativeDemoProject>npx react-native start
error configs.toReversed is not a function.
TypeError: configs.toReversed is not a function
at mergeConfig (C:\Users\Administrator\ReactNativeDemoProject\node_modules\metro-config\src\loadConfig.js:202:35)
at Object.getDefaultConfig (C:\Users\Administrator\ReactNativeDemoProject\node_modules\@react-native\metro-config\dist\index.js:97:39)
at loadMetroConfig (C:\Users\Administrator\ReactNativeDemoProject\node_modules\@react-native\community-cli-plugin\dist\utils\loadMetroConfig.js:66:39)
at Object.runServer [as func] (C:\Users\Administrator\ReactNativeDemoProject\node_modules\@react-native\community-cli-plugin\dist\commands\start\runServer.js:56:58)
at Command.handleAction (C:\Users\Administrator\ReactNativeDemoProject\node_modules\@react-native-community\cli\build\index.js:139:23)
at Command.listener [as _actionHandler] (C:\Users\Administrator\ReactNativeDemoProject\node_modules\commander\lib\command.js:482:17)
at C:\Users\Administrator\ReactNativeDemoProject\node_modules\commander\lib\command.js:1283:65
at Command._chainOrCall (C:\Users\Administrator\ReactNativeDemoProject\node_modules\commander\lib\command.js:1177:12)
at Command._parseCommand (C:\Users\Administrator\ReactNativeDemoProject\node_modules\commander\lib\command.js:1283:27)
at C:\Users\Administrator\ReactNativeDemoProject\node_modules\commander\lib\command.js:1081:27
因为我的node版本是18,这个报错是因为Array.prototype.toReversed() 是 Node.js 20+ 才支持的方法。你当前的 Node.js 版本低于 20。
所以我安装了node20
然后再次启动服务
C:\Users\Administrator\ReactNativeDemoProject>npx react-native start
Welcome to React Native v0.86
Starting dev server on http://localhost:8081
▒▒▓▓▓▓▒▒
▒▓▓▓▒▒░░▒▒▓▓▓▒
▒▓▓▓▓░░░▒▒▒▒░░░▓▓▓▓▒
▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓
▓▓░░░░░▒▓▓▓▓▓▓▒░░░░░▓▓
▓▓░░▓▓▒░░░▒▒░░░▒▓▒░░▓▓
▓▓░░▓▓▓▓▓▒▒▒▒▓▓▓▓▒░░▓▓
▓▓░░▓▓▓▓▓▓▓▓▓▓▓▓▓▒░░▓▓
▓▓▒░░▒▒▓▓▓▓▓▓▓▓▒░░░▒▓▓
▒▓▓▓▒░░░▒▓▓▒░░░▒▓▓▓▒
▒▓▓▓▒░░░░▒▓▓▓▒
▒▒▓▓▓▓▒▒
Welcome to Metro v0.84.4
Fast - Scalable - Integrated
INFO Dev server ready. Press Ctrl+C to exit.
INFO Key commands available:
r - reload app(s)
d - open Dev Menu
j - open DevTools
然后关闭,然后重新运行
npx react-native run-android
就成功了。
那么react native就暂时先研究到这里,接下来我们继续完善我们之前的一些问题。
运行到真机过程中若发现仅仅安装成功,但主界面只有一个flutter图标,则可以尝试
adb kill-server
adb start-server
adb devices
确认设备连接正常后,重新运行
flutter clean
flutter pub get
flutter run -v
那接下来就是回到之前我们的正题,删除掉主界面最底部的默认的那个加号默认demo的功能。
main.dart:
import 'package:flutter/material.dart';
import 'info_table.dart';
import 'col_span_table.dart';
import 'col_span_table2.dart';
import 'search_result_table.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: .center,
children: [
const Text('搜索结果页面'),
const Divider(),
const Expanded(child: SearchResultTable()),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
);
}
}
以及对搜索结果页面的token获取进行优化,还有请求加载loading效果以及搜索结果列表展示ui与分页信息展示根据实际数据量及总页码数进行区分ui展示。
search_result_table.dart:
import 'dart:convert' show jsonEncode, jsonDecode;
import 'dart:typed_data';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'blocked_keywords_button.dart';
import 'detail_page.dart';
class SearchResultTable extends StatefulWidget {
const SearchResultTable({super.key});
@override
State<SearchResultTable> createState() => _SearchResultTableState();
}
class _SearchResultTableState extends State<SearchResultTable> {
final TextEditingController _apiController = TextEditingController(
text: "https://search-server.xoczl.net/Search/Social/Query",
);
final TextEditingController _keywordController = TextEditingController(
text: "搭讪",
);
final TextEditingController authorizationController = TextEditingController();
List<dynamic> _searchResults = [];
int _resultCount = 0;
bool _hasSearched = false;
int _currentPage = 1;
final Map<int, Uint8List> _imageDataUris = {};
List<String> _blockedKeywords = [];
String? _syncMessage;
void _onBlockedKeywordsChanged(List<String> keywords) {
_blockedKeywords = keywords;
}
@override
void initState() {
super.initState();
_fetchToken();
}
void showLoadingDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: false, // 禁止点击外部关闭
builder: (_) => const PopScope(
canPop: false, // 拦截返回键
child: Center(child: CircularProgressIndicator()),
),
);
}
// 关闭loading
void dismissLoading(BuildContext context) {
Navigator.pop(context);
}
Future<void> _fetchToken() async {
try {
showLoadingDialog(context);
final response = await http.get(
Uri.parse('https://xn--10jong-2o7io6t.30gnao.space/SearchHomePage'),
);
if (response.statusCode == 200) {
final body = response.body;
final regex = RegExp(
r'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+',
);
final match = regex.firstMatch(body);
if (match != null) {
setState(() {
authorizationController.text = match.group(0)!;
});
if (mounted) {
dismissLoading(context);
}
} else {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('提取token失败')));
dismissLoading(context);
}
}
} else {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('提取token失败')));
dismissLoading(context);
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('提取token失败')));
dismissLoading(context);
}
}
}
@override
void dispose() {
_apiController.dispose();
_keywordController.dispose();
authorizationController.dispose();
super.dispose();
}
Widget _buildGridCell({
required Widget child,
bool rightBorder = false,
bool bottomBorder = false,
bool topBorder = false,
}) {
return Container(
decoration: BoxDecoration(
border: Border(
right: rightBorder ? const BorderSide() : BorderSide.none,
bottom: bottomBorder ? const BorderSide() : BorderSide.none,
top: topBorder ? const BorderSide() : BorderSide.none,
),
),
child: child,
);
}
Widget _buildHeaderCell(String text) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(text, style: const TextStyle(fontWeight: FontWeight.bold)),
),
);
}
//数据同步后的请求
Future<void> dataSyncAfterDataList(
List<dynamic> requestBody,
String auth,
List<String> blockedKeywords,
) async {
try {
requestBody[0]['blocked_keyword_list'] = blockedKeywords;
final response = await http.post(
Uri.parse("http://localhost:8082/index/dataSyncAfterDataList"),
headers: {
'authorization': auth,
'Content-Type': 'application/json',
'origin': 'https://xn--10jong-2o7io6t.30gnao.space',
'X-Bundle-Info': '1, com.xingba.web.online',
},
body: jsonEncode(requestBody),
);
if (response.statusCode == 200) {
debugPrint('请求成功, 响应: ${response.body}');
final responseBody = jsonDecode(response.body);
final msg = responseBody['msg'];
final count = msg[0]['count'] as int;
final list = msg[0]['list'] as List<dynamic>;
setState(() {
_resultCount = count;
_searchResults = list;
_imageDataUris.clear();
_hasSearched = true;
});
_fetchImagesForResults(list, "null", auth);
} else {
debugPrint('请求失败, 状态码: ${response.statusCode}, 响应: ${response.body}');
}
} catch (e) {
debugPrint('请求异常: $e');
}
}
//原请求
Future<void> originRequest(
String apiUrl,
List<dynamic> requestBody,
String auth,
) async {
try {
showLoadingDialog(context);
final response = await http.post(
Uri.parse(apiUrl),
headers: {
'authorization': auth,
'Content-Type': 'application/json',
'origin': 'https://xn--10jong-2o7io6t.30gnao.space',
'X-Bundle-Info': '1, com.xingba.web.online',
},
body: jsonEncode(requestBody),
);
if (response.statusCode == 200) {
if (mounted) {
dismissLoading(context);
}
debugPrint('请求成功, 响应: ${response.body}');
final responseBody = jsonDecode(response.body);
final msg = responseBody['msg'];
if (msg is! List || msg.isEmpty) {
if (msg is Map && msg['message'] != null) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(msg['message'].toString())));
}
return;
}
final count = msg[0]['count'] as int;
final list = msg[0]['list'] as List<dynamic>;
setState(() {
_resultCount = count;
_searchResults = list;
_imageDataUris.clear();
_hasSearched = true;
});
_fetchImagesForResults(list, apiUrl, auth);
} else {
if (mounted) {
dismissLoading(context);
}
debugPrint('请求失败, 状态码: ${response.statusCode}, 响应: ${response.body}');
}
} catch (e) {
if (mounted) {
dismissLoading(context);
}
debugPrint('请求异常: $e');
}
}
//同步数据请求(按页循环同步,直到没有更多数据)
Future<bool> asyncRequest(
List<dynamic> requestBody,
String auth,
int? totalPage,
) async {
try {
var info = '正在同步第${requestBody[0]['page']}页数据';
if (totalPage != null) {
info += ',预计共${totalPage}页';
}
setState(() {
_syncMessage = info;
});
final response = await http.post(
Uri.parse("http://localhost:8082/index/dataSync"),
headers: {
'authorization': auth,
'Content-Type': 'application/json',
'origin': 'https://xn--10jong-2o7io6t.30gnao.space',
'X-Bundle-Info': '1, com.xingba.web.online',
},
body: jsonEncode(requestBody),
);
if (response.statusCode == 200) {
debugPrint('同步请求成功, 响应: ${response.body}');
final responseBody = jsonDecode(response.body);
final msg = responseBody['msg'];
final list = msg[0]['list'] as List<dynamic>;
totalPage =
((msg[0]['count'] as num) / (requestBody[0]['limit'] as num))
.ceil();
if (list.isNotEmpty) {
// 还有数据,页码+1,继续同步下一页
requestBody[0]['page'] = (requestBody[0]['page'] as int) + 1;
await asyncRequest(requestBody, auth, totalPage);
}
// 当前页无数据,同步完成
return true;
} else {
debugPrint('同步请求失败, 状态码: ${response.statusCode}, 响应: ${response.body}');
return false;
}
} catch (e) {
debugPrint('同步请求异常: $e');
return false;
}
}
//前置请求
Future<void> preRequest(
List<dynamic> requestBody,
String apiUrl,
String auth,
List<String> blockedKeywords,
) async {
if (blockedKeywords.isNotEmpty) {
// 有屏蔽关键词:先按页同步数据到本地数据库
bool syncComplete = await asyncRequest(requestBody, auth, null);
if (syncComplete) {
setState(() {
_syncMessage = null;
});
// 同步完成后,调用筛选接口
await dataSyncAfterDataList(requestBody, auth, blockedKeywords);
}
} else {
// 无屏蔽关键词:直接请求原始API
await originRequest(apiUrl, requestBody, auth);
}
}
Future<void> _fetchImagesForResults(
List<dynamic> list,
String apiUrl,
String auth,
) async {
for (int i = 0; i < list.length; i++) {
final item = list[i];
final covers = item['covers'];
if (covers != null && covers is List && covers.isNotEmpty) {
final resource = covers[0]['resource'];
final file = covers[0]['file'];
if (file == null) {
continue;
}
if (file != null) {
final hash = file['hash']?.toString() ?? '';
if (hash.isNotEmpty) {
if (hash != "2bf933b436b05c58") {
// continue;
}
}
}
if (resource != null) {
final path = resource['path']?.toString() ?? '';
final name = resource['name']?.toString() ?? '';
final rawStorageType = resource['storage_type'];
final int storage_type = rawStorageType is int
? rawStorageType
: int.tryParse(rawStorageType?.toString() ?? '') ?? 0;
if (path.isNotEmpty && name.isNotEmpty) {
final baseUrl = storage_type == 4
? 'https://photos.7ujx6.com'
: 'https://media-server.ehgyz.com';
final imageUrl = '$baseUrl$path/$name';
debugPrint('图片[$i] 请求URL: $imageUrl');
try {
final imageResponse = await http.get(
Uri.parse(imageUrl),
headers: {
'referer': 'https://xn--10jong-2o7io6t.30gnao.space/',
'origin': 'https://xn--10jong-2o7io6t.30gnao.space',
'x-bundle-info': '1, com.xingba.web.online',
'authorization': auth,
},
);
if (imageResponse.statusCode == 200) {
final contentType = imageResponse.headers['content-type'] ?? '';
final imageBytes = imageResponse.bodyBytes;
debugPrint(
'图片[$i] content-type: $contentType, 大小: ${imageBytes.length} bytes',
);
if (!contentType.startsWith('image/')) {
// 服务器返回的不是图片,打印前200字符看看实际返回了什么
final bodyText = String.fromCharCodes(
imageBytes.length > 200
? imageBytes.sublist(0, 200)
: imageBytes,
);
debugPrint('图片[$i] 非图片响应: $bodyText');
return;
}
// 验证图片数据是否有效(检查文件头)
if (imageBytes.length < 32) {
debugPrint('图片[$i] 数据太小: ${imageBytes.length} bytes');
return;
}
// storage_type == 4 时不裁剪,否则去除前16字节和后16字节(与JS端解密逻辑一致)
Uint8List trimmedBytes;
if (storage_type == 4) {
trimmedBytes = imageBytes;
} else {
trimmedBytes = imageBytes.sublist(16, imageBytes.length - 16);
}
debugPrint('图片[$i] 裁剪后大小: ${trimmedBytes.length} bytes');
if (mounted) {
setState(() {
_imageDataUris[i] = trimmedBytes;
});
}
} else {
debugPrint('图片[$i] 请求失败, 状态码: ${imageResponse.statusCode}');
}
} catch (e) {
debugPrint('图片[$i]请求失败: $e');
}
}
}
}
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(border: Border.all()),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
ElevatedButton(
onPressed: _fetchToken,
child: const Text('获取token'),
),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _apiController,
decoration: const InputDecoration(
hintText: '请输入api地址',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _keywordController,
decoration: const InputDecoration(
hintText: '请输入关键词',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: authorizationController,
decoration: const InputDecoration(
hintText: '请输入authorization',
border: OutlineInputBorder(),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
//搜索
ElevatedButton(
onPressed: () async {
if (_apiController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入api地址')),
);
return;
}
if (_keywordController.text.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('请输入关键词')));
return;
}
debugPrint('屏蔽关键词列表: $_blockedKeywords');
/*
发起http的post请求
请求头需要包含authorization
请求头content-type的值为application/json
请求的json对象格式为:
var jsonObject=[{"name":"social.post","limit":100,"page":1,"or":[{"field":"title","op":"$like","value":"%kasumi"},{"field":"tags","op":"$in","value":["kasumi"]}],"and":[{"field":"categories_id","op":"$nin","value":["137","257","347","264","764","931","97","101","365","534","115","361","335","389","336","425","367","456","930","85","2","382","78","252","154","1108","917","416","344","1107","337","74","99","281","93","230","454","166","175","397","452","414","285","928","766","1009","288","113","322","49","1106","426","81","758","332","104","88","364","1011","333","57","240","695","86","396","677","41","265","959","102","56","358","221","301","929","242","187","224","789","340","349","55","231","402","105","169","461","346","50","182","210","676","48","292","95","413","131","334","35","348","310","455","43","246","77","401","330","293","355","84","161","23","312","1012","359","771","87","124","163","395","1214","123","927","926","388","39","934","202","274","453","214","286","932","375","405","212","404","38","7","46","40","363","268","1110","75","350","262","218","394","92","37","247","128","675","933"]},{"field":"status","op":"$in","value":[1,9]}],"sort":[{"field":"create_time","order":"DESC"}]}];
这个json对象中仅仅用输入的关键词替换这个对象中的kasumi
*/
final keyword = _keywordController.text;
final apiUrl = _apiController.text;
final auth = authorizationController.text;
final requestBody = [
{
"name": "social.post",
"limit": 10,
"page": 1,
"or": [
{
"field": "title",
"op": "\$like",
"value": "%$keyword",
},
{
"field": "tags",
"op": "\$in",
"value": [keyword],
},
],
"and": [
{
"field": "categories_id",
"op": "\$nin",
"value": [
"137",
"257",
"347",
"264",
"764",
"931",
"97",
"101",
"365",
"534",
"115",
"361",
"335",
"389",
"336",
"425",
"367",
"456",
"930",
"85",
"2",
"382",
"78",
"252",
"154",
"1108",
"917",
"416",
"344",
"1107",
"337",
"74",
"99",
"281",
"93",
"230",
"454",
"166",
"175",
"397",
"452",
"414",
"285",
"928",
"766",
"1009",
"288",
"113",
"322",
"49",
"1106",
"426",
"81",
"758",
"332",
"104",
"88",
"364",
"1011",
"333",
"57",
"240",
"695",
"86",
"396",
"677",
"41",
"265",
"959",
"102",
"56",
"358",
"221",
"301",
"929",
"242",
"187",
"224",
"789",
"340",
"349",
"55",
"231",
"402",
"105",
"169",
"461",
"346",
"50",
"182",
"210",
"676",
"48",
"292",
"95",
"413",
"131",
"334",
"35",
"348",
"310",
"455",
"43",
"246",
"77",
"401",
"330",
"293",
"355",
"84",
"161",
"23",
"312",
"1012",
"359",
"771",
"87",
"124",
"163",
"395",
"1214",
"123",
"927",
"926",
"388",
"39",
"934",
"202",
"274",
"453",
"214",
"286",
"932",
"375",
"405",
"212",
"404",
"38",
"7",
"46",
"40",
"363",
"268",
"1110",
"75",
"350",
"262",
"218",
"394",
"92",
"37",
"247",
"128",
"675",
"933",
],
},
{
"field": "status",
"op": "\$in",
"value": [1, 9],
},
],
"sort": [
{"field": "create_time", "order": "DESC"},
],
},
];
setState(() {
_hasSearched = false;
_currentPage = 1;
});
await preRequest(
requestBody,
apiUrl,
auth,
_blockedKeywords,
);
debugPrint(
'搜索按钮被点击, api: ${_apiController.text}, 关键词: ${_keywordController.text}',
);
},
child: const Text('搜索'),
),
const SizedBox(width: 8),
BlockedKeywordsButton(onChanged: _onBlockedKeywordsChanged),
],
),
),
),
if (_syncMessage != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(width: 8),
Text(
_syncMessage!,
style: const TextStyle(color: Colors.blue),
),
],
),
),
if (_hasSearched && _resultCount > 0)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
const Text(
'搜索结果',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
),
if (_hasSearched && _resultCount > 0) const Divider(),
if (_hasSearched && _resultCount > 0)
Row(
children: [
Expanded(
flex: 2,
child: _buildGridCell(
child: _buildHeaderCell('标题'),
rightBorder: true,
bottomBorder: true,
topBorder: true,
),
),
Expanded(
flex: 2,
child: _buildGridCell(
child: _buildHeaderCell('图片'),
rightBorder: true,
bottomBorder: true,
topBorder: true,
),
),
Expanded(
child: _buildGridCell(
child: _buildHeaderCell('操作'),
bottomBorder: true,
topBorder: true,
),
),
],
),
if (_hasSearched && _resultCount > 0)
Flexible(
child: ListView.builder(
itemCount: _searchResults.length,
itemBuilder: (context, index) {
final item = _searchResults[index];
final title = item['title']?.toString() ?? '';
final imageBytes = _imageDataUris[index];
final id = item['id']?.toString() ?? '';
final type = item['type']?.toInt() ?? -1;
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 2,
child: _buildGridCell(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(title),
),
),
rightBorder: true,
bottomBorder: true,
),
),
Expanded(
flex: 2,
child: _buildGridCell(
child: Center(
child: Padding(
padding: const EdgeInsets.all(4.0),
child: imageBytes != null
? Image.memory(
imageBytes,
fit: BoxFit.contain,
height: 80,
)
: const Text('加载中...'),
),
),
rightBorder: true,
bottomBorder: true,
),
),
Expanded(
child: _buildGridCell(
child: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size(60, 32),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
tapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(
id: id,
type: type,
title: title,
auth: authorizationController.text,
imageBytes: _imageDataUris[index],
),
),
);
},
child: const Text('详情'),
),
),
bottomBorder: true,
),
),
],
),
);
},
),
),
if (_hasSearched && _resultCount > 0)
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_resultCount > 0)
Text(
'共$_resultCount条记录,共${(_resultCount / 100).ceil()}页',
),
if ((_resultCount / 100).ceil() > 1) ...[
const SizedBox(width: 16),
ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.zero,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
debugPrint('下一页按钮被点击');
},
child: const Text('下一页'),
),
],
if (_currentPage > 1) ...[
const SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.zero,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
debugPrint('上一页按钮被点击');
},
child: const Text('上一页'),
),
],
],
),
),
),
],
),
],
),
);
}
}
关键字词:flutter,native,react,token,loading,分页,开发者
上一篇:Flutter for iOS 开发者(视频播放详情页)
下一篇:返回列表