| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- import 'dart:io';
- import 'package:cached_network_image/cached_network_image.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:flutter_screenutil/flutter_screenutil.dart';
- import 'package:fluttertoast/fluttertoast.dart';
- import 'package:image_picker/image_picker.dart';
- import 'package:news_app/constant/color_res.dart';
- import 'package:news_app/constant/size_res.dart';
- import 'package:news_app/http/http_util.dart';
- import 'package:news_app/widget/my_txt.dart';
- import '../../widget/auth_gesture_detector.dart';
- /// @author: bo.zeng
- /// @email: cnhbwds@gmail.com
- /// @date: 2025 2025/4/17 12:22
- /// @description:
- /// 图片上传状态
- enum UploadStatus {
- uploading, // 上传中
- success, // 上传成功
- failed, // 上传失败
- }
- /// 图片数据模型
- class ImageItem {
- final String localPath; // 本地路径
- String? remoteUrl; // 远程URL
- UploadStatus status; // 上传状态
- ImageItem({
- required this.localPath,
- this.remoteUrl,
- this.status = UploadStatus.uploading,
- });
- ImageItem copyWith({
- String? localPath,
- String? remoteUrl,
- UploadStatus? status,
- }) {
- return ImageItem(
- localPath: localPath ?? this.localPath,
- remoteUrl: remoteUrl ?? this.remoteUrl,
- status: status ?? this.status,
- );
- }
- }
- class CommentInputBarWidget extends ConsumerStatefulWidget {
- final FocusNode focusNode;
- final String id;
- final Function(String, List<String>)? onSend; // 改为传递URL列表
- final bool showImageUpload; // 是否显示图片上传按钮
- const CommentInputBarWidget(
- this.focusNode,
- this.onSend,
- this.id, {
- super.key,
- this.showImageUpload = false, // 默认不显示
- });
- @override
- ConsumerState<ConsumerStatefulWidget> createState() =>
- _CommentInputBarState();
- }
- class _CommentInputBarState extends ConsumerState<CommentInputBarWidget> {
- final TextEditingController _controller = TextEditingController();
- final List<ImageItem> _imageItems = [];
- static const int maxImageCount = 9;
- Future<void> _pickImage() async {
- final List<XFile> pickedFiles = await ImagePicker().pickMultiImage(
- imageQuality: 80,
- );
- if (pickedFiles.isNotEmpty) {
- // 计算还能添加多少张图片
- final remainingSlots = maxImageCount - _imageItems.length;
- if (remainingSlots <= 0) {
- // 已达到最大数量,不添加
- return;
- }
- // 添加新图片,最多添加到9张
- final imagesToAdd = pickedFiles.take(remainingSlots).toList();
- setState(() {
- for (var file in imagesToAdd) {
- _imageItems.add(ImageItem(localPath: file.path));
- }
- });
- // 立即上传图片
- _uploadImages(imagesToAdd.map((e) => e.path).toList());
- }
- }
- /// 上传图片
- Future<void> _uploadImages(List<String> filePaths) async {
- // 批量上传接口
- final urls = await HttpUtil().uploadCommentImages(filePaths);
- setState(() {
- // 更新上传状态
- int urlIndex = 0;
- for (int i = 0; i < _imageItems.length; i++) {
- if (_imageItems[i].status == UploadStatus.uploading &&
- filePaths.contains(_imageItems[i].localPath)) {
- if (urlIndex < urls.length) {
- _imageItems[i] = _imageItems[i].copyWith(
- remoteUrl: urls[urlIndex],
- status: UploadStatus.success,
- );
- urlIndex++;
- } else {
- _imageItems[i] = _imageItems[i].copyWith(
- status: UploadStatus.failed,
- );
- }
- }
- }
- });
- }
- /// 重试上传失败的图片
- Future<void> _retryUpload(int index) async {
- final item = _imageItems[index];
- setState(() {
- _imageItems[index] = item.copyWith(status: UploadStatus.uploading);
- });
- final url = await HttpUtil().uploadCommentImage(item.localPath);
- setState(() {
- if (url != null) {
- _imageItems[index] = item.copyWith(
- remoteUrl: url,
- status: UploadStatus.success,
- );
- } else {
- _imageItems[index] = item.copyWith(status: UploadStatus.failed);
- }
- });
- }
- void _removeImage(int index) {
- setState(() {
- _imageItems.removeAt(index);
- });
- }
- void _clearImages() {
- setState(() {
- _imageItems.clear();
- });
- }
- @override
- Widget build(BuildContext context) {
- double size = MediaQuery.of(context).padding.bottom;
- return Container(
- decoration: BoxDecoration(
- color: Colors.white,
- border: Border(top: BorderSide(color: colorE5E5E5, width: 0.5)),
- ),
- padding: EdgeInsets.only(
- top: horizontalPadding,
- bottom: size > 0 ? size : horizontalPadding,
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- mainAxisSize: MainAxisSize.min,
- children: [
- // 图片预览区域 - 支持多张图片
- if (_imageItems.isNotEmpty)
- Container(
- height: 60.h,
- margin: EdgeInsets.only(bottom: 10.w),
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: Row(
- children: List.generate(_imageItems.length, (index) {
- final item = _imageItems[index];
- return Container(
- width: 60.w,
- height: 60.h,
- margin: EdgeInsets.only(right: 8.w),
- child: Stack(
- alignment: Alignment.topRight,
- children: [
- // 显示图片(本地或远程)
- ClipRRect(
- borderRadius: BorderRadius.circular(4.r),
- child: item.status == UploadStatus.success && item.remoteUrl != null
- ? CachedNetworkImage(
- imageUrl: item.remoteUrl!,
- width: 60.w,
- height: 60.h,
- fit: BoxFit.cover,
- placeholder: (context, url) => Image.file(
- File(item.localPath),
- width: 60.w,
- height: 60.h,
- fit: BoxFit.cover,
- ),
- errorWidget: (context, url, error) => Image.file(
- File(item.localPath),
- width: 60.w,
- height: 60.h,
- fit: BoxFit.cover,
- ),
- )
- : Image.file(
- File(item.localPath),
- width: 60.w,
- height: 60.h,
- fit: BoxFit.cover,
- ),
- ),
- // 上传中遮罩
- if (item.status == UploadStatus.uploading)
- Container(
- width: 60.w,
- height: 60.h,
- decoration: BoxDecoration(
- color: Colors.black26,
- borderRadius: BorderRadius.circular(4.r),
- ),
- child: Center(
- child: SizedBox(
- width: 20.w,
- height: 20.w,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- valueColor:
- AlwaysStoppedAnimation<Color>(Colors.white),
- ),
- ),
- ),
- ),
- // 上传失败标记
- if (item.status == UploadStatus.failed)
- Container(
- width: 60.w,
- height: 60.h,
- decoration: BoxDecoration(
- color: Colors.black26,
- borderRadius: BorderRadius.circular(4.r),
- ),
- child: Center(
- child: GestureDetector(
- onTap: () => _retryUpload(index),
- child: Icon(
- Icons.refresh,
- color: Colors.white,
- size: 20.w,
- ),
- ),
- ),
- ),
- // 删除按钮
- GestureDetector(
- onTap: () => _removeImage(index),
- child: Container(
- margin: EdgeInsets.all(2.w),
- width: 16.w,
- height: 16.w,
- decoration: BoxDecoration(
- color: Colors.black54,
- shape: BoxShape.circle,
- ),
- child: Icon(
- Icons.close,
- color: Colors.white,
- size: 10.w,
- ),
- ),
- ),
- ],
- ),
- );
- }),
- ),
- ),
- ),
- Row(
- children: [
- Expanded(
- child: Container(
- height: 40.h,
- margin: EdgeInsets.only(right: 10.w),
- child: TextField(
- decoration: InputDecoration(
- fillColor: colorF5F7FA,
- filled: true,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(5.r),
- borderSide: BorderSide.none,
- ),
- contentPadding: EdgeInsets.symmetric(
- horizontal: 10.w,
- vertical: 20.h,
- ),
- hintText: '快来写下你的评论吧~',
- hintStyle: TextStyle(color: color7788A0, fontSize: 12.sp),
- ),
- focusNode: widget.focusNode,
- controller: _controller,
- ),
- ),
- ),
- // 图片上传按钮(仅指定话题显示)
- if (widget.showImageUpload)
- GestureDetector(
- onTap: _pickImage,
- child: Container(
- width: 40.w,
- height: 40.h,
- margin: EdgeInsets.only(right: 8.w),
- alignment: Alignment.center,
- child: Icon(
- Icons.image,
- color: _imageItems.isNotEmpty ? color188FFF : color7788A0,
- size: 24.w,
- ),
- ),
- ),
- AuthGestureDetector(
- onTap: () {
- // 校验:没有图片且没有文字时,不允许提交
- final hasImages = _imageItems.any(
- (item) => item.status == UploadStatus.success && item.remoteUrl != null);
- final text = _controller.text.trim();
- if (!hasImages && text.isEmpty) {
- // 没有图片且没有文字,弹出提醒
- Fluttertoast.showToast(
- msg: "请输入评论内容或上传图片",
- toastLength: Toast.LENGTH_SHORT,
- gravity: ToastGravity.CENTER,
- );
- return;
- }
- // 获取所有上传成功的图片URL
- final successUrls = _imageItems
- .where((item) =>
- item.status == UploadStatus.success && item.remoteUrl != null)
- .map((item) => item.remoteUrl!)
- .toList();
- widget.onSend?.call(_controller.text, successUrls);
- _controller.clear();
- _clearImages();
- },
- child: Container(
- width: 50.w,
- padding: EdgeInsets.symmetric(vertical: 5.h),
- alignment: Alignment.center,
- decoration: BoxDecoration(
- color: color188FFF,
- borderRadius: BorderRadius.circular(5.r),
- ),
- child: myTxt(text: "发送", color: Colors.white, fontSize: 12.sp),
- ),
- ),
- ],
- ),
- ],
- ),
- );
- }
- }
|