Просмотр исходного кода

弹窗完善。
评论的展示与提交。
评论的弹窗与自定义评分控件。

liukai месяцев назад: 8
Родитель
Сommit
24f315a62b

+ 86 - 43
packages/cpt_job/lib/modules/job_applied/job_applied_controller.dart

@@ -1,3 +1,4 @@
+import 'package:cpt_job/widget/applied_staff_reviews.dart';
 import 'package:domain/entity/response/job_list_applied_staff_list_entity.dart';
 import 'package:domain/repository/job_repository.dart';
 import 'package:get/get.dart';
@@ -7,6 +8,7 @@ import 'package:plugin_platform/engine/toast/toast_engine.dart';
 import 'package:plugin_platform/http/dio/dio_cancelable_mixin.dart';
 import 'package:shared/utils/date_time_utils.dart';
 import 'package:shared/utils/log_utils.dart';
+import 'package:widgets/dialog/app_default_dialog.dart';
 import 'package:widgets/load_state_layout.dart';
 import 'package:widgets/widget_export.dart';
 
@@ -167,8 +169,39 @@ class JobAppliedController extends GetxController with DioCancelableMixin {
   }
 
   /// 展示评论的弹窗
-  void showRemarkDialog(JobListAppliedStaffListRows data) {
-    ToastEngine.show("展示评论的弹窗");
+  void showRemarkDialog(JobListAppliedStaffListRows data) async {
+    //请求接口获取到已评论的数据
+    var result = await _jobRepository.fetchAppliedStaffReviewView(data.appliedId.toString());
+
+    if (result.isSuccess) {
+      //接口数据获取成功,展示弹窗
+      DialogEngine.show(
+        widget: AppliedStaffReviews(
+          appliedReviews: result.data!,
+          confirmAction: (attitudeRate, performanceRate, experienceRate, groomingRate, content) async {
+
+            //请求接口,提交评论
+            var submitResult = await _jobRepository.remarkAppliedSingleStaffSubmit(
+              data.appliedId.toString(),
+              attitudeRate,
+              groomingRate,
+              performanceRate,
+              experienceRate,
+              content,
+            );
+
+            if (submitResult.isSuccess) {
+              NotifyEngine.showSuccess("Successful".tr);
+            } else {
+              ToastEngine.show(submitResult.errorMsg ?? "Network Load Error".tr);
+            }
+          },
+        ),
+      );
+    } else {
+      ToastEngine.show(result.errorMsg ?? "Network Load Error".tr);
+      return;
+    }
   }
 
   /// 去展示员工状态的审核流程页面
@@ -179,7 +212,7 @@ class JobAppliedController extends GetxController with DioCancelableMixin {
   /// Item选中与未选中设置
   void doSelectedOrNot(JobListAppliedStaffListRows data) {
     //只有 Approve = 3 的状态才能选中
-    if (data.status == 3){
+    if (data.status == 3) {
       data.isSelected = !data.isSelected;
       update();
     }
@@ -188,42 +221,43 @@ class JobAppliedController extends GetxController with DioCancelableMixin {
   /// 批量修改的弹窗
   void showBatchModifyDialog() async {
     if (state.jobInfo == null) return;
-    DialogEngine.show(
-        widget: AppliedButchModify(
-            selectedStartDate: DateTimeUtils.getDateTime(state.jobInfo?.startTime ?? ""),
-            selectedEndDate: DateTimeUtils.getDateTime(state.jobInfo?.endTime ?? ""),
-            confirmAction: (start, end) {
-              requestBatchModify(start, end);
-            }));
-  }
-
-  /// 执行批量修改的请求
-  void requestBatchModify(DateTime start, DateTime end) async {
+    //找出已经选中的员工(只有状态为3 Approve的状态才能修改)
     var selectedList = state.datas.where((element) => element.isSelected && element.status == 3).toList(growable: false);
     if (selectedList.isNotEmpty) {
       var ids = selectedList.map((e) => e.appliedId.toString()).toList(growable: false);
       var separatedIds = ids.join(',');
 
-      //执行请求
-      var result = await _jobRepository.batchEditStaffCheckTime(
-        state.jobId,
-        separatedIds,
-        DateTimeUtils.formatDate(start),
-        DateTimeUtils.formatDate(end),
-        cancelToken: cancelToken,
-      );
+      DialogEngine.show(
+          widget: AppliedButchModify(
+              selectedStartDate: DateTimeUtils.getDateTime(state.jobInfo?.startTime ?? ""),
+              selectedEndDate: DateTimeUtils.getDateTime(state.jobInfo?.endTime ?? ""),
+              confirmAction: (start, end) {
+                requestBatchModify(start, end, separatedIds);
+              }));
+    } else {
+      ToastEngine.show("Please select the applied record".tr);
+    }
+  }
 
-      if (result.isSuccess) {
-        NotifyEngine.showSuccess("Successful".tr);
+  /// 执行批量修改的请求
+  void requestBatchModify(DateTime start, DateTime end, String separatedIds) async {
+    //执行请求
+    var result = await _jobRepository.batchEditStaffCheckTime(
+      state.jobId,
+      separatedIds,
+      DateTimeUtils.formatDate(start),
+      DateTimeUtils.formatDate(end),
+      cancelToken: cancelToken,
+    );
 
-        //调用接口刷新指定的Staff的信息
-        fetchItemByIdAndRefreshItem(separatedIds);
-      } else {
-        ToastEngine.show(result.errorMsg ?? "Network Load Error".tr);
-        return;
-      }
+    if (result.isSuccess) {
+      NotifyEngine.showSuccess("Successful".tr);
+
+      //调用接口刷新指定的Staff的信息
+      fetchItemByIdAndRefreshItem(separatedIds);
     } else {
-      ToastEngine.show("Please select the applied record".tr);
+      ToastEngine.show(result.errorMsg ?? "Network Load Error".tr);
+      return;
     }
   }
 
@@ -235,18 +269,27 @@ class JobAppliedController extends GetxController with DioCancelableMixin {
       var ids = selectedList.map((e) => e.appliedId.toString()).toList(growable: false);
       var separatedIds = ids.join(',');
 
-      //执行请求
-      var result = await _jobRepository.submitBatchStaffApprove(separatedIds, cancelToken: cancelToken);
-
-      if (result.isSuccess) {
-        NotifyEngine.showSuccess("Successful".tr);
-
-        //调用接口刷新指定的Staff的信息
-        fetchItemByIdAndRefreshItem(separatedIds);
-      } else {
-        ToastEngine.show(result.errorMsg ?? "Network Load Error".tr);
-        return;
-      }
+      // Are you sure 的弹窗
+      DialogEngine.show(
+        widget: AppDefaultDialog(
+          title: "Message".tr,
+          message: "Are you sure you want to setting approved?".tr,
+          confirmAction: () async {
+            //执行请求
+            var result = await _jobRepository.submitBatchStaffApprove(separatedIds, cancelToken: cancelToken);
+
+            if (result.isSuccess) {
+              NotifyEngine.showSuccess("Successful".tr);
+
+              //调用接口刷新指定的Staff的信息
+              fetchItemByIdAndRefreshItem(separatedIds);
+            } else {
+              ToastEngine.show(result.errorMsg ?? "Network Load Error".tr);
+              return;
+            }
+          },
+        ),
+      );
     } else {
       ToastEngine.show("Please select the applied record".tr);
     }

+ 313 - 0
packages/cpt_job/lib/widget/applied_staff_reviews.dart

@@ -0,0 +1,313 @@
+import 'dart:ui';
+
+import 'package:cs_resources/generated/assets.dart';
+import 'package:domain/entity/response/job_list_remark_view_entity.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:plugin_platform/engine/toast/toast_engine.dart';
+import 'package:shared/utils/util.dart';
+import 'package:widgets/ext/ex_widget.dart';
+import 'package:cs_resources/constants/color_constants.dart';
+import 'package:widgets/my_text_view.dart';
+import 'package:widgets/shatter/rating_widget.dart';
+import 'package:widgets/widget_export.dart';
+
+/**
+ * 员工的评价弹窗
+ */
+class AppliedStaffReviews extends StatefulWidget {
+  JobListRemarkViewEntity appliedReviews;
+  void Function(String attitudeRate, String performanceRate, String experienceRate, String groomingRate, String content)? confirmAction;
+
+  AppliedStaffReviews({required this.appliedReviews, this.confirmAction});
+
+  @override
+  State<AppliedStaffReviews> createState() => _AppliedStaffReviewsState();
+}
+
+class _AppliedStaffReviewsState extends State<AppliedStaffReviews> {
+  late JobListRemarkViewEntity reviews;
+  late String attitudeRate;
+  late String groomingRate;
+  late String performanceRate;
+  late String experienceRate;
+  late String content;
+  late TextEditingController _controller;
+  late FocusNode _focusNode;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController();
+    _focusNode = FocusNode();
+
+    reviews = widget.appliedReviews;
+    attitudeRate = reviews.attitudeRate.toString();
+    groomingRate = reviews.groomingRate.toString();
+    performanceRate = reviews.performanceRate.toString();
+    experienceRate = reviews.experienceRate.toString();
+    _controller.text = reviews.content ?? "";
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.center,
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: [
+        //Title (如果使用 Container 为最外层容器则默认为 match_parent 的效果,除非我们限制宽度和最大高度最小高度)
+        Container(
+          width: double.infinity,
+          decoration: BoxDecoration(
+            color: Colors.white,
+            borderRadius: const BorderRadius.all(Radius.circular(15)),
+          ),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Center(
+                child: MyTextView(
+                  "Remarks".tr,
+                  fontSize: 19,
+                  isFontMedium: true,
+                  textColor: ColorConstants.black,
+                  marginTop: 23,
+                  marginBottom: 19,
+                  marginLeft: 22,
+                  marginRight: 22,
+                ),
+              ),
+
+              MyTextView(
+                reviews.memberName ?? "",
+                isFontRegular: true,
+                textColor: Colors.black,
+                marginLeft: 22,
+                marginRight: 22,
+                fontSize: 17,
+              ),
+
+              //态度评分
+              Row(
+                children: [
+                  MyTextView(
+                    "Attitude".tr,
+                    isFontRegular: true,
+                    textColor: ColorConstants.black66,
+                    fontSize: 15,
+                  ).expanded(),
+                  RatingWidget(
+                    nomalImage: Assets.baseServiceRatingUnselected,
+                    selectImage: Assets.baseServiceRatingSelected,
+                    size: 21,
+                    padding: 5,
+                    selectAble: Utils.isEmpty(reviews.content),
+                    integerOnly: true,
+                    value: reviews.attitudeRate,
+                    onRatingUpdate: (value) {
+                      attitudeRate = value;
+                    },
+                  )
+                ],
+              ).marginOnly(top: 15, left: 22, right: 22),
+
+              //表现评分
+              Row(
+                children: [
+                  MyTextView(
+                    "Performance".tr,
+                    isFontRegular: true,
+                    textColor: ColorConstants.black66,
+                    fontSize: 15,
+                  ).expanded(),
+                  RatingWidget(
+                    nomalImage: Assets.baseServiceRatingUnselected,
+                    selectImage: Assets.baseServiceRatingSelected,
+                    size: 21,
+                    padding: 5,
+                    selectAble: Utils.isEmpty(reviews.content),
+                    integerOnly: true,
+                    value: reviews.performanceRate,
+                    onRatingUpdate: (value) {
+                      performanceRate = value;
+                    },
+                  )
+                ],
+              ).marginOnly(top: 15, left: 22, right: 22),
+
+              //经验评分
+              Row(
+                children: [
+                  MyTextView(
+                    "Experience".tr,
+                    isFontRegular: true,
+                    textColor: ColorConstants.black66,
+                    fontSize: 15,
+                  ).expanded(),
+                  RatingWidget(
+                    nomalImage: Assets.baseServiceRatingUnselected,
+                    selectImage: Assets.baseServiceRatingSelected,
+                    size: 21,
+                    padding: 5,
+                    selectAble: Utils.isEmpty(reviews.content),
+                    integerOnly: true,
+                    value: reviews.experienceRate,
+                    onRatingUpdate: (value) {
+                      experienceRate = value;
+                    },
+                  )
+                ],
+              ).marginOnly(top: 15, left: 22, right: 22),
+
+              //着装评分
+              Row(
+                children: [
+                  MyTextView(
+                    "Grooming".tr,
+                    isFontRegular: true,
+                    textColor: ColorConstants.black66,
+                    fontSize: 15,
+                  ).expanded(),
+                  RatingWidget(
+                    nomalImage: Assets.baseServiceRatingUnselected,
+                    selectImage: Assets.baseServiceRatingSelected,
+                    size: 21,
+                    padding: 5,
+                    selectAble: Utils.isEmpty(reviews.content),
+                    integerOnly: true,
+                    value: reviews.groomingRate,
+                    onRatingUpdate: (value) {
+                      groomingRate = value;
+                    },
+                  )
+                ],
+              ).marginOnly(top: 15, left: 22, right: 22),
+
+              IgnoreKeyboardDismiss(
+                child: Container(
+                  height: 130,
+                  margin: EdgeInsets.symmetric(vertical: 19, horizontal: 22),
+                  padding: EdgeInsets.symmetric(vertical: 15, horizontal: 15),
+                  decoration: BoxDecoration(
+                    color: Color(0xFFF0F0F0),
+                    border: Border.all(
+                      color: Color(0xFFD8D8D8),
+                      width: 0.5,
+                    ),
+                  ),
+                  child: TextField(
+                    cursorColor: ColorConstants.black66,
+                    cursorWidth: 1.5,
+                    autofocus: false,
+                    enabled: Utils.isEmpty(reviews.content),
+                    focusNode: _focusNode,
+                    controller: _controller,
+                    // 装饰
+                    decoration: InputDecoration(
+                      isDense: true,
+                      isCollapsed: true,
+                      border: InputBorder.none,
+                      hintText: "Enter...".tr,
+                      hintStyle: TextStyle(
+                        color: ColorConstants.black66,
+                        fontSize: 15.0,
+                        fontWeight: FontWeight.w400,
+                      ),
+                    ),
+                    style: TextStyle(
+                      color: ColorConstants.black,
+                      fontSize: 15.0,
+                      fontWeight: FontWeight.w400,
+                    ),
+                    // 键盘动作右下角图标
+                    textInputAction: TextInputAction.done,
+                    onSubmitted: (value) {
+                      doCallbackAction();
+                    },
+                  ),
+                ),
+              ),
+
+              // 分割线
+              Container(
+                color: Color(0XFFCECECE),
+                height: 0.5,
+              ),
+
+              //按钮组
+              Row(
+                children: [
+                  Expanded(
+                      flex: 1,
+                      child: InkWell(
+                        onTap: () {
+                          onCancel();
+                        },
+                        child: MyTextView(
+                          "Cancel".tr,
+                          fontSize: 17.5,
+                          isFontMedium: true,
+                          textAlign: TextAlign.center,
+                          textColor: Color(0XFF0085C4),
+                          cornerRadius: 3,
+                          borderWidth: 1,
+                        ),
+                      )),
+                  Container(
+                    color: Color(0xff09141F).withOpacity(0.13),
+                    width: 0.5,
+                  ),
+                  Expanded(
+                      flex: 1,
+                      child: InkWell(
+                        onTap: () {
+                          doCallbackAction();
+                        },
+                        child: MyTextView(
+                          "Submit".tr,
+                          marginLeft: 10,
+                          fontSize: 17.5,
+                          isFontMedium: true,
+                          textAlign: TextAlign.center,
+                          textColor: Color(0XFF0085C4),
+                          cornerRadius: 3,
+                        ),
+                      )),
+                ],
+              ).constrained(height: 46),
+            ],
+          ),
+        ),
+      ],
+    ).constrained(width: 285);
+  }
+
+  //取消弹框
+  void onCancel() async {
+    SmartDialog.dismiss();
+  }
+
+  //执行回调
+  void doCallbackAction() {
+    _focusNode.unfocus();
+
+    content = _controller.text.toString();
+
+    if (attitudeRate == "0" || experienceRate == "0" || performanceRate == "0" || groomingRate == "0") {
+      ToastEngine.show("Rate First");
+      return;
+    }
+
+    if (Utils.isEmpty(content)) {
+      ToastEngine.show("Please Enter Remark".tr);
+      return;
+    }
+
+    onCancel();
+
+    widget.confirmAction?.call(attitudeRate, performanceRate, experienceRate, groomingRate, content);
+  }
+}

+ 22 - 22
packages/cs_domain/lib/entity/response/job_list_remark_view_entity.dart

@@ -5,29 +5,29 @@ export 'package:domain/generated/json/job_list_remark_view_entity.g.dart';
 
 @JsonSerializable()
 class JobListRemarkViewEntity {
-	@JSONField(name: "applied_id")
-	int appliedId = 0;
-	@JSONField(name: "member_name")
-	String? memberName = null;
-	@JSONField(name: "attitude_rate")
-	int attitudeRate = 0;
-	@JSONField(name: "grooming_rate")
-	int groomingRate = 0;
-	@JSONField(name: "performance_rate")
-	int performanceRate = 0;
-	@JSONField(name: "experience_rate")
-	int experienceRate = 0;
-	String? content = null;
-	String? disabled = null;
+  @JSONField(name: "applied_id")
+  int appliedId = 0;
+  @JSONField(name: "member_name")
+  String? memberName = null;
+  @JSONField(name: "attitude_rate")
+  double attitudeRate = 5.0;
+  @JSONField(name: "grooming_rate")
+  double groomingRate = 5.0;
+  @JSONField(name: "performance_rate")
+  double performanceRate = 5.0;
+  @JSONField(name: "experience_rate")
+  double experienceRate = 5.0;
+  String? content = null;
+  String? disabled = null;
 
-	JobListRemarkViewEntity();
+  JobListRemarkViewEntity();
 
-	factory JobListRemarkViewEntity.fromJson(Map<String, dynamic> json) => $JobListRemarkViewEntityFromJson(json);
+  factory JobListRemarkViewEntity.fromJson(Map<String, dynamic> json) => $JobListRemarkViewEntityFromJson(json);
 
-	Map<String, dynamic> toJson() => $JobListRemarkViewEntityToJson(this);
+  Map<String, dynamic> toJson() => $JobListRemarkViewEntityToJson(this);
 
-	@override
-	String toString() {
-		return jsonEncode(this);
-	}
-}
+  @override
+  String toString() {
+    return jsonEncode(this);
+  }
+}

+ 8 - 8
packages/cs_domain/lib/generated/json/job_list_remark_view_entity.g.dart

@@ -11,19 +11,19 @@ JobListRemarkViewEntity $JobListRemarkViewEntityFromJson(Map<String, dynamic> js
   if (memberName != null) {
     jobListRemarkViewEntity.memberName = memberName;
   }
-  final int? attitudeRate = jsonConvert.convert<int>(json['attitude_rate']);
+  final double? attitudeRate = jsonConvert.convert<double>(json['attitude_rate']);
   if (attitudeRate != null) {
     jobListRemarkViewEntity.attitudeRate = attitudeRate;
   }
-  final int? groomingRate = jsonConvert.convert<int>(json['grooming_rate']);
+  final double? groomingRate = jsonConvert.convert<double>(json['grooming_rate']);
   if (groomingRate != null) {
     jobListRemarkViewEntity.groomingRate = groomingRate;
   }
-  final int? performanceRate = jsonConvert.convert<int>(json['performance_rate']);
+  final double? performanceRate = jsonConvert.convert<double>(json['performance_rate']);
   if (performanceRate != null) {
     jobListRemarkViewEntity.performanceRate = performanceRate;
   }
-  final int? experienceRate = jsonConvert.convert<int>(json['experience_rate']);
+  final double? experienceRate = jsonConvert.convert<double>(json['experience_rate']);
   if (experienceRate != null) {
     jobListRemarkViewEntity.experienceRate = experienceRate;
   }
@@ -55,10 +55,10 @@ extension JobListRemarkViewEntityExtension on JobListRemarkViewEntity {
   JobListRemarkViewEntity copyWith({
     int? appliedId,
     String? memberName,
-    int? attitudeRate,
-    int? groomingRate,
-    int? performanceRate,
-    int? experienceRate,
+    double? attitudeRate,
+    double? groomingRate,
+    double? performanceRate,
+    double? experienceRate,
     String? content,
     String? disabled,
   }) {

BIN
packages/cs_resources/assets/base_service/rating_selected.webp


BIN
packages/cs_resources/assets/base_service/rating_unselected.webp


+ 1 - 0
packages/cs_resources/lib/constants/color_constants.dart

@@ -20,6 +20,7 @@ class ColorConstants {
   static const Color appBlue = Color(0xff0689fb);
   static const Color blue1578fe = Color(0xFF1578FE);
   static const Color black33 = Color(0xff333333);
+  static const Color black66 = Color(0xff666666);
   static const Color black404A5B = Color(0xff404A5B);
   static const Color whitefe = Color(0xffFEFEFE);
   static const Color greye0 = Color(0xffE0E0E0);

+ 2 - 0
packages/cs_resources/lib/generated/assets.dart

@@ -13,6 +13,8 @@ class Assets {
   static const String baseServiceItemUnselectedIcon = 'assets/base_service/item_unselected_icon.webp';
   static const String baseServicePageLoadError = 'assets/base_service/page_load_error.webp';
   static const String baseServicePageNoData = 'assets/base_service/page_no_data.webp';
+  static const String baseServiceRatingSelected = 'assets/base_service/rating_selected.webp';
+  static const String baseServiceRatingUnselected = 'assets/base_service/rating_unselected.webp';
   static const String baseServiceTitleBarFilterIcon = 'assets/base_service/title_bar_filter_icon.webp';
   static const String baseServiceTriangleDropDownIcon = 'assets/base_service/triangle_drop_down_icon.webp';
   static const String cptAuthLoginRadioChecked = 'assets/cpt_auth/login_radio_checked.webp';

+ 7 - 0
packages/cs_resources/lib/local/language/en_US.dart

@@ -118,6 +118,13 @@ const Map<String, String> en_US = {
   'Operation Approve': 'Operation Approve',
   'Batch Modify': 'Batch Modify',
   'Please select the applied record': 'Please select the applied record',
+  'Are you sure you want to setting approved?': 'Are you sure you want to setting approved?',
+  'Attitude': 'Attitude',
+  'Performance': 'Performance',
+  'Experience': 'Experience',
+  'Grooming': 'Grooming',
+  'Enter...': 'Enter...',
+  'Please Enter Remark': 'Please Enter Remark',
 
   //插件的国际化
   'Pull to refresh': 'Pull to refresh',

+ 7 - 0
packages/cs_resources/lib/local/language/zh_CN.dart

@@ -118,6 +118,13 @@ const Map<String, String> zh_CN = {
   'Operation Approve': '操作批准',
   'Batch Modify': '批量修改',
   'Please select the applied record': '请选择申请记录',
+  'Are you sure you want to setting approved?': '你确定要设置为批准状态吗?',
+  'Attitude': '态度',
+  'Performance': '表现',
+  'Experience': '经验',
+  'Grooming': '着装',
+  'Enter...': '请输入...',
+  'Please Enter Remark': '请输入评论内容',
 
   //插件的国际化
   'Pull to refresh': '下拉刷新',

+ 207 - 0
packages/cs_widgets/lib/shatter/rating_widget.dart

@@ -0,0 +1,207 @@
+import 'package:flutter/material.dart';
+import 'package:widgets/my_load_image.dart';
+
+class RatingWidget extends StatefulWidget {
+  /// 星星数量
+  final int count;
+
+  /// 最大评分值
+  final double maxRating;
+
+  /// 当前选中的评分值
+  final double value;
+
+  /// 没一颗星的大小
+  final double size;
+
+  /// 每个星之间的间距
+  final double padding;
+
+  /// 空星的图片资源
+  final String nomalImage;
+
+  /// 满星的图片资源
+  final String selectImage;
+
+  /// 是否可以操作选择
+  final bool selectAble;
+
+  /// 选中评分的回调
+  final ValueChanged<String> onRatingUpdate;
+
+  /// 是否只显示整数的分数
+  final bool integerOnly;
+
+  RatingWidget({
+    required this.nomalImage,
+    required this.selectImage,
+    required this.onRatingUpdate,
+    this.count = 5,
+    this.value = 5.0,
+    this.maxRating = 5.0,
+    this.size = 20,
+    this.padding = 0,
+    this.selectAble = false,
+    this.integerOnly = false,
+  });
+
+  @override
+  _RatingWidgetState createState() => _RatingWidgetState();
+}
+
+class _RatingWidgetState extends State<RatingWidget> {
+  late double value;
+
+  @override
+  Widget build(BuildContext context) {
+    return Listener(
+      child: buildRowRating(),
+      onPointerDown: (PointerDownEvent event) {
+        double x = event.localPosition.dx;
+        if (x < 0) x = 0;
+        pointValue(x);
+      },
+      onPointerMove: (PointerMoveEvent event) {
+        double x = event.localPosition.dx;
+        if (x < 0) x = 0;
+        pointValue(x);
+      },
+      onPointerUp: (_) {},
+      behavior: HitTestBehavior.deferToChild,
+    );
+  }
+
+  pointValue(double dx) {
+    if (!widget.selectAble) {
+      return;
+    }
+    if (dx >= widget.size * widget.count + widget.padding * (widget.count - 1)) {
+      value = widget.maxRating;
+    } else {
+      for (double i = 1; i < widget.count + 1; i++) {
+        if (dx > widget.size * i + widget.padding * (i - 1) && dx < widget.size * i + widget.padding * i) {
+          value = i * (widget.maxRating / widget.count);
+          break;
+        } else if (dx > widget.size * (i - 1) + widget.padding * (i - 1) && dx < widget.size * i + widget.padding * i) {
+          value = (dx - widget.padding * (i - 1)) / (widget.size * widget.count) * widget.maxRating;
+          break;
+        }
+      }
+    }
+
+    if (widget.integerOnly) {
+      value = value.ceilToDouble(); // 直接向上取整
+    }
+
+    setState(() {
+      widget.onRatingUpdate(value.toStringAsFixed(1));
+    });
+  }
+
+  int fullStars() {
+    if (value != null) {
+      return (value / (widget.maxRating / widget.count)).ceil(); // 使用向上取整
+    }
+    return 0;
+  }
+
+  double star() {
+    if (widget.integerOnly) {
+      return 0;
+    }
+    if (value != null) {
+      if (widget.count / fullStars() == widget.maxRating / value) {
+        return 0;
+      }
+      return (value % (widget.maxRating / widget.count)) / (widget.maxRating / widget.count);
+    }
+    return 0;
+  }
+
+  List<Widget> buildRow() {
+    int full = fullStars();
+    List<Widget> children = [];
+    for (int i = 0; i < full; i++) {
+      children.add(MyAssetImage(
+        widget.selectImage,
+        width: widget.size,
+        height: widget.size,
+      ));
+
+      if (i < widget.count - 1) {
+        children.add(
+          SizedBox(
+            width: widget.padding,
+          ),
+        );
+      }
+    }
+    if (full < widget.count) {
+      children.add(ClipRect(
+        clipper: SMClipper(rating: star() * widget.size),
+        child: MyAssetImage(
+          widget.selectImage,
+          width: widget.size,
+          height: widget.size,
+        ),
+      ));
+    }
+
+    return children;
+  }
+
+  List<Widget> buildNomalRow() {
+    List<Widget> children = [];
+    for (int i = 0; i < widget.count; i++) {
+      children.add(MyAssetImage(
+        widget.nomalImage,
+        width: widget.size,
+        height: widget.size,
+      ));
+      if (i < widget.count - 1) {
+        children.add(SizedBox(
+          width: widget.padding,
+        ));
+      }
+    }
+    return children;
+  }
+
+  Widget buildRowRating() {
+    return Container(
+      child: Stack(
+        children: <Widget>[
+          Row(
+            children: buildNomalRow(),
+          ),
+          Row(
+            children: buildRow(),
+          )
+        ],
+      ),
+    );
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    value = widget.value;
+  }
+}
+
+class SMClipper extends CustomClipper<Rect> {
+  final double rating;
+
+  SMClipper({required this.rating});
+
+  @override
+  Rect getClip(Size size) {
+    return Rect.fromLTRB(0.0, 0.0, rating, size.height);
+  }
+
+  @override
+  bool shouldReclip(SMClipper oldClipper) {
+    return rating != oldClipper.rating;
+  }
+}
+