您当前的位置: 首页 > 慢生活 > 程序人生 网站首页程序人生
flutter-添加交互- 教程(图片组件,javascript中blob的图片链接在flutter中的实现)
发布时间:2026-07-11 00:59:48编辑:雪饮阅读()
-
其实这个互动性,上篇中的button点击的事件就有实现的。就是那个搜索按钮点击的时候。
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;
}
/*
发起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": 100,
"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"},
],
},
];
try {
final ioClient = HttpClient()
..badCertificateCallback =
(X509Certificate cert, String host, int port) =>
true;
final client = IOClient(ioClient);
final response = await client.post(
Uri.parse(apiUrl),
headers: {
'authorization': auth,
'accept': 'application/json',
'Content-Type': 'application/json',
'origin': 'https://xn--10jong-2o7io6t.30gnao.space',
'referer': 'https://xn--10jong-2o7io6t.30gnao.space/',
'X-Bundle-Info': '1, com.xingba.web.online',
},
body: jsonEncode(requestBody),
);
client.close();
if (response.statusCode == 200) {
debugPrint('请求成功, 响应: ${response.body}');
} else {
debugPrint(
'请求失败, 状态码: ${response.statusCode}, 响应: ${response.body}',
);
}
} catch (e) {
debugPrint('请求异常: $e');
}
debugPrint(
'搜索按钮被点击, api: ${_apiController.text}, 关键词: ${_keywordController.text}',
);
},
child: const Text('搜索'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
debugPrint('屏蔽关键词管理按钮被点击');
},
child: const Text('屏蔽关键词管理'),
),
],
),
),
以下划线开头的成员或职业 ()是私人的。
也就是标准面向对象语言如java,php中private的那种修饰符的作用。
例如:
class SearchResultTable extends StatefulWidget {
const SearchResultTable({super.key});
@override
State<SearchResultTable> createState() => _SearchResultTableState();
}
那么接下来是我们的正事,也就是上篇中未完善的数据列表渲染,上篇中仅仅解决了请求406的错误。
首先我来说下,昨天那个406问题还有最优解
直接换成这样:
try {
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) {
debugPrint('请求成功, 响应: ${response.body}');
} else {
debugPrint(
'请求失败, 状态码: ${response.statusCode}, 响应: ${response.body}',
);
}
} catch (e) {
debugPrint('请求异常: $e');
}
也就是重新使用了标准http请求,仅仅只是多了一个X-Bundle-Info的请求头,这个请求头不常见(肯定是对方鉴权相关所自定义的),也是我测试原网页的请求发现的,所以这个才是之前406问题的核心。
那么接下来的核心是图片的渲染,图片渲染这里就稍微复杂点,因为我使用的那个接口中实际是将接口中返回的图片信息只是一部分,需要另外请求接口获取,并且获取的又是经过了简单加密的。
所以处理起来稍微复杂些,我仔细比对了下其网页端实现中实际最终得到的是blob的链接,例如:
blob:https://xn--10jong-2o7io6t.30gnao.space/43a57487-af61-4206-a4ef-49ce90fe80d1
那么核心代码分别是这几处:
//搜索
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;
}
/*
发起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"},
],
},
];
try {
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) {
debugPrint('请求成功, 响应: ${response.body}');
/*
响应体中
msg[0].count 为搜索结果总数
msg[0].list[x].title 为搜索结果中每个条目的标题
关于搜索结果中每个条目的图片
图片请求api
var imageApiUrl=msg[0].list[x].covers[0].resource.path+"/"+ msg[0].list[x].covers[0].resource.name;
图片请求头 x-bundle-info 为 1, com.xingba.web.online
图片请求成功后的请求结果中将响应体用类似JavaScript那种URL.createObjectURL(s)转换为图片url
var imageUrl=URL.createObjectURL(s);
然后在控制台中打印请求成功后的经过类似URL.createObjectURL处理过后的URL地址
*/
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();
});
_fetchImagesForResults(list, apiUrl, auth);
} else {
debugPrint(
'请求失败, 状态码: ${response.statusCode}, 响应: ${response.body}',
);
}
} catch (e) {
debugPrint('请求异常: $e');
}
debugPrint(
'搜索按钮被点击, api: ${_apiController.text}, 关键词: ${_keywordController.text}',
);
},
child: const Text('搜索'),
),
上面这个是点击搜索按钮后的,下面这个是之前的图片单元格修改后的
Expanded(
flex: 2,
child: _buildGridCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: imageBytes != null
? Image.memory(
imageBytes,
fit: BoxFit.contain,
width: double.infinity,
height: 120,
)
: const Center(child: Text('加载中...')),
),
rightBorder: true,
bottomBorder: true,
),
),
以及图片的最终核心处理函数
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() ?? '';
if (path.isNotEmpty && name.isNotEmpty) {
final imageUrl = 'https://media-server.ehgyz.com' + '$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;
}
// 去除前16字节和后16字节(与JS端解密逻辑一致)
final 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');
}
}
}
}
}
}
当然,之前的main.dart中也需要更新下
children: [
const Text('搜索结果页面'),
const Divider(),
const Expanded(child: SearchResultTable()),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
为了解决布局底部出现溢出问题,虽然现在依然有溢出问题,但是暂时先不处理。
关键字词:flutter,交互,教程,图片,组件,javascript,blob,链接,flutter